This commit is contained in:
Gergely Nagy 2024-05-15 17:57:41 +02:00 committed by GitHub
commit a4e044caae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 639 additions and 83 deletions

View File

@ -11,7 +11,7 @@ use bitflags::bitflags;
use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform};
use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg};
use regex::Regex;
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
@ -52,6 +52,8 @@ pub struct Config {
pub binds: Binds,
#[knuffel(child, default)]
pub debug: DebugConfig,
#[knuffel(children(name = "workspace"))]
pub workspaces: Vec<Workspace>,
}
// FIXME: Add other devices.
@ -687,6 +689,17 @@ pub struct EnvironmentVariable {
pub value: Option<String>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct Workspace {
#[knuffel(argument)]
pub name: WorkspaceName,
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceName(pub String);
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct WindowRule {
#[knuffel(children(name = "match"))]
@ -700,6 +713,8 @@ pub struct WindowRule {
#[knuffel(child, unwrap(argument))]
pub open_on_output: Option<String>,
#[knuffel(child, unwrap(argument))]
pub open_on_workspace: Option<String>,
#[knuffel(child, unwrap(argument))]
pub open_maximized: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub open_fullscreen: Option<bool>,
@ -884,14 +899,14 @@ pub enum Action {
CenterColumn,
FocusWorkspaceDown,
FocusWorkspaceUp,
FocusWorkspace(#[knuffel(argument)] u8),
FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
FocusWorkspacePrevious,
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
MoveWindowToWorkspace(#[knuffel(argument)] u8),
MoveWindowToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveColumnToWorkspaceDown,
MoveColumnToWorkspaceUp,
MoveColumnToWorkspace(#[knuffel(argument)] u8),
MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference),
MoveWorkspaceDown,
MoveWorkspaceUp,
FocusMonitorLeft,
@ -956,14 +971,20 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::CenterColumn => Self::CenterColumn,
niri_ipc::Action::FocusWorkspaceDown => Self::FocusWorkspaceDown,
niri_ipc::Action::FocusWorkspaceUp => Self::FocusWorkspaceUp,
niri_ipc::Action::FocusWorkspace { index } => Self::FocusWorkspace(index),
niri_ipc::Action::FocusWorkspace { reference } => {
Self::FocusWorkspace(WorkspaceReference::from(reference))
}
niri_ipc::Action::FocusWorkspacePrevious => Self::FocusWorkspacePrevious,
niri_ipc::Action::MoveWindowToWorkspaceDown => Self::MoveWindowToWorkspaceDown,
niri_ipc::Action::MoveWindowToWorkspaceUp => Self::MoveWindowToWorkspaceUp,
niri_ipc::Action::MoveWindowToWorkspace { index } => Self::MoveWindowToWorkspace(index),
niri_ipc::Action::MoveWindowToWorkspace { reference } => {
Self::MoveWindowToWorkspace(WorkspaceReference::from(reference))
}
niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown,
niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp,
niri_ipc::Action::MoveColumnToWorkspace { index } => Self::MoveColumnToWorkspace(index),
niri_ipc::Action::MoveColumnToWorkspace { reference } => {
Self::MoveColumnToWorkspace(WorkspaceReference::from(reference))
}
niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown,
niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp,
niri_ipc::Action::FocusMonitorLeft => Self::FocusMonitorLeft,
@ -996,6 +1017,59 @@ impl From<niri_ipc::Action> for Action {
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum WorkspaceReference {
Index(u8),
Name(String),
}
impl From<WorkspaceReferenceArg> for WorkspaceReference {
fn from(reference: WorkspaceReferenceArg) -> WorkspaceReference {
match reference {
WorkspaceReferenceArg::Index(i) => Self::Index(i),
WorkspaceReferenceArg::Name(n) => Self::Name(n),
}
}
}
impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceReference {
fn type_check(
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
ctx: &mut knuffel::decode::Context<S>,
) {
if let Some(type_name) = &type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
}
fn raw_decode(
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<WorkspaceReference, DecodeError<S>> {
match &**val {
knuffel::ast::Literal::String(ref s) => Ok(WorkspaceReference::Name(s.clone().into())),
knuffel::ast::Literal::Int(ref value) => match value.try_into() {
Ok(v) => Ok(WorkspaceReference::Index(v)),
Err(e) => {
ctx.emit_error(DecodeError::conversion(val, e));
Ok(WorkspaceReference::Index(0))
}
},
_ => {
ctx.emit_error(DecodeError::unsupported(
val,
"Unsupported value, only numbers and strings are recognized",
));
Ok(WorkspaceReference::Index(0))
}
}
}
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct DebugConfig {
#[knuffel(child, unwrap(argument))]
@ -1403,6 +1477,54 @@ where
}
}
impl<S: knuffel::traits::ErrorSpan> knuffel::DecodeScalar<S> for WorkspaceName {
fn type_check(
type_name: &Option<knuffel::span::Spanned<knuffel::ast::TypeName, S>>,
ctx: &mut knuffel::decode::Context<S>,
) {
if let Some(type_name) = &type_name {
ctx.emit_error(DecodeError::unexpected(
type_name,
"type name",
"no type name expected for this node",
));
}
}
fn raw_decode(
val: &knuffel::span::Spanned<knuffel::ast::Literal, S>,
ctx: &mut knuffel::decode::Context<S>,
) -> Result<WorkspaceName, DecodeError<S>> {
#[derive(Debug)]
struct WorkspaceNameSet(HashSet<String>);
match &**val {
knuffel::ast::Literal::String(ref s) => {
let mut name_set: HashSet<String> = match ctx.get::<WorkspaceNameSet>() {
Some(h) => h.0.clone(),
None => HashSet::new(),
};
if !name_set.insert(s.clone().to_string()) {
ctx.emit_error(DecodeError::unexpected(
&val,
"named workspace",
format!("duplicate named workspace: {}", s),
));
return Ok(Self(String::new()));
}
ctx.set(WorkspaceNameSet(name_set));
Ok(Self(s.clone().into()))
}
_ => {
ctx.emit_error(DecodeError::unsupported(
val,
"workspace names must be strings",
));
Ok(Self(String::new()))
}
}
}
}
impl<S> knuffel::Decode<S> for WindowOpenAnim
where
S: knuffel::traits::ErrorSpan,
@ -2261,6 +2383,7 @@ mod tests {
Mod+Ctrl+Shift+L { move-window-to-monitor-right; }
Mod+Comma { consume-window-into-column; }
Mod+1 { focus-workspace 1; }
Mod+Shift+1 { focus-workspace "workspace-1"; }
Mod+Shift+E { quit skip-confirmation=true; }
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
}
@ -2268,6 +2391,12 @@ mod tests {
debug {
render-drm-device "/dev/dri/renderD129"
}
workspace "workspace-1" {
open-on-output "eDP-1"
}
workspace "workspace-2"
workspace "workspace-3"
"##,
Config {
input: Input {
@ -2469,6 +2598,20 @@ mod tests {
},
..Default::default()
}],
workspaces: vec![
Workspace {
name: WorkspaceName("workspace-1".to_string()),
open_on_output: Some("eDP-1".to_string()),
},
Workspace {
name: WorkspaceName("workspace-2".to_string()),
open_on_output: None,
},
Workspace {
name: WorkspaceName("workspace-3".to_string()),
open_on_output: None,
},
],
binds: Binds(vec![
Bind {
key: Key {
@ -2520,7 +2663,16 @@ mod tests {
trigger: Trigger::Keysym(Keysym::_1),
modifiers: Modifiers::COMPOSITOR,
},
action: Action::FocusWorkspace(1),
action: Action::FocusWorkspace(WorkspaceReference::Index(1)),
cooldown: None,
allow_when_locked: false,
},
Bind {
key: Key {
trigger: Trigger::Keysym(Keysym::_1),
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
action: Action::FocusWorkspace(WorkspaceReference::Name("workspace-1".to_string())),
cooldown: None,
allow_when_locked: false,
},

View File

@ -146,11 +146,11 @@ pub enum Action {
FocusWorkspaceDown,
/// Focus the workspace above.
FocusWorkspaceUp,
/// Focus a workspace by index.
/// Focus a workspace by reference (index or name).
FocusWorkspace {
/// Index of the workspace to focus.
/// Reference (index or name) of the workspace to focus.
#[cfg_attr(feature = "clap", arg())]
index: u8,
reference: WorkspaceReferenceArg,
},
/// Focus the previous workspace.
FocusWorkspacePrevious,
@ -158,21 +158,21 @@ pub enum Action {
MoveWindowToWorkspaceDown,
/// Move the focused window to the workspace above.
MoveWindowToWorkspaceUp,
/// Move the focused window to a workspace by index.
/// Move the focused window to a workspace by reference (index or name).
MoveWindowToWorkspace {
/// Index of the target workspace.
/// Reference (index or name) of the workspace to move the window to.
#[cfg_attr(feature = "clap", arg())]
index: u8,
reference: WorkspaceReferenceArg,
},
/// Move the focused column to the workspace below.
MoveColumnToWorkspaceDown,
/// Move the focused column to the workspace above.
MoveColumnToWorkspaceUp,
/// Move the focused column to a workspace by index.
/// Move the focused column to a workspace by reference (index or name).
MoveColumnToWorkspace {
/// Index of the target workspace.
/// Reference (index or name) of the workspace to move the column to.
#[cfg_attr(feature = "clap", arg())]
index: u8,
reference: WorkspaceReferenceArg,
},
/// Move the focused workspace down.
MoveWorkspaceDown,
@ -257,6 +257,15 @@ pub enum SizeChange {
AdjustProportion(f64),
}
/// Workspace reference (index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceReferenceArg {
/// Index of the workspace.
Index(u8),
/// Name of the workspace.
Name(String),
}
/// Layout to switch to.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutSwitchTarget {
@ -475,6 +484,24 @@ pub enum OutputConfigChanged {
OutputWasMissing,
}
impl FromStr for WorkspaceReferenceArg {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let reference = if let Ok(index) = s.parse::<i32>() {
if let Ok(idx) = u8::try_from(index) {
Self::Index(idx)
} else {
return Err("workspace indexes must be between 0 and 255");
}
} else {
Self::Name(s.to_string())
};
Ok(reference)
}
}
impl FromStr for SizeChange {
type Err = &'static str;

View File

@ -119,22 +119,23 @@ impl CompositorHandler for State {
let toplevel = window.toplevel().expect("no X11 support");
let (rules, width, is_full_width, output) =
let (rules, width, is_full_width, output, workspace_name) =
if let InitialConfigureState::Configured {
rules,
width,
is_full_width,
output,
workspace_name,
} = state
{
// Check that the output is still connected.
let output =
output.filter(|o| self.niri.layout.monitor_for_output(o).is_some());
(rules, width, is_full_width, output)
(rules, width, is_full_width, output, workspace_name)
} else {
error!("window map must happen after initial configure");
(ResolvedWindowRules::empty(), None, false, None)
(ResolvedWindowRules::empty(), None, false, None, None)
};
let parent = toplevel
@ -160,6 +161,16 @@ impl CompositorHandler for State {
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
} else if let (Some(workspace_name), Some(output)) = (&workspace_name, &output)
{
self.niri.layout.add_window_to_named_workspace(
output,
workspace_name,
mapped,
width,
is_full_width,
);
Some(output)
} else if let Some(output) = &output {
self.niri
.layout

View File

@ -369,38 +369,52 @@ impl XdgShellHandler for State {
width,
is_full_width,
output,
workspace_name,
} => {
// Figure out the monitor following a similar logic to initial configure.
// FIXME: deduplicate.
let mon = output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent))
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
});
let mon = workspace_name
.as_deref()
.and_then(|name| self.niri.layout.monitor_for_workspace(name))
.map(|mon| (mon, false));
let mon = mon.or_else(|| {
output
.as_ref()
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, false))
// If not, check if we have a parent with a monitor.
.or_else(|| {
toplevel
.parent()
.and_then(|parent| {
self.niri.layout.find_window_and_output(&parent)
})
.map(|(_win, output)| output)
.and_then(|o| self.niri.layout.monitor_for_output(o))
.map(|mon| (mon, true))
})
// If not, fall back to the active monitor.
.or_else(|| {
self.niri
.layout
.active_monitor_ref()
.map(|mon| (mon, false))
})
});
*output = mon
.filter(|(_, parent)| !parent)
.map(|(mon, _)| mon.output.clone());
let mon = mon.map(|(mon, _)| mon);
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
let ws = workspace_name
.as_deref()
.and_then(|name| mon.map(|mon| mon.workspace_ref(name)))
.unwrap_or_else(|| {
mon.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace())
});
if let Some(ws) = ws {
toplevel.with_pending_state(|state| {
@ -577,12 +591,20 @@ impl State {
return;
};
// Pick the target monitor. First, check if we had an output set in the window rules.
// Pick the target monitor. First, check if we had a workspace set in the window rules.
let mon = rules
.open_on_output
.open_on_workspace
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|o| self.niri.layout.monitor_for_output(o));
.and_then(|name| self.niri.layout.monitor_for_workspace(name));
// If not, check if we had an output set in the window rules.
let mon = mon.or_else(|| {
rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|o| self.niri.layout.monitor_for_output(o))
});
// If not, check if the window requested one for fullscreen.
let mon = mon.or_else(|| {
@ -622,9 +644,14 @@ impl State {
let is_full_width = rules.open_maximized.unwrap_or(false);
// Tell the surface the preferred size and bounds for its likely output.
let ws = mon
.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace());
let ws = rules
.open_on_workspace
.as_deref()
.and_then(|name| mon.map(|mon| mon.workspace_ref(name)))
.unwrap_or_else(|| {
mon.map(|mon| mon.active_workspace_ref())
.or_else(|| self.niri.layout.active_workspace())
});
if let Some(ws) = ws {
// Set a fullscreen state based on window request and window rule.
@ -663,6 +690,7 @@ impl State {
width,
is_full_width,
output,
workspace_name: ws.and_then(|w| w.name.clone()),
};
toplevel.send_configure();

View File

@ -587,12 +587,22 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.move_to_workspace(idx);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
Action::MoveWindowToWorkspace(reference) => {
if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference)
{
if let Some(output) = output {
self.niri.layout.move_to_workspace_on_output(&output, index);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
} else {
self.niri.layout.move_to_workspace(index);
self.maybe_warp_cursor_to_focus();
}
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::MoveColumnToWorkspaceDown => {
self.niri.layout.move_column_to_workspace_down();
@ -606,12 +616,24 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.move_column_to_workspace(idx);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
Action::MoveColumnToWorkspace(reference) => {
if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference)
{
if let Some(output) = output {
self.niri
.layout
.move_column_to_workspace_on_output(&output, index);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
} else {
self.niri.layout.move_column_to_workspace(index);
self.maybe_warp_cursor_to_focus();
}
// FIXME: granular
self.niri.queue_redraw_all();
}
}
Action::FocusWorkspaceDown => {
self.niri.layout.switch_workspace_down();
@ -625,19 +647,28 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
Action::FocusWorkspace(reference) => {
if let Some((output, index)) = self.niri.find_output_and_workspace_index(reference)
{
if let Some(output) = output {
self.niri.layout.focus_output(&output);
self.niri.layout.switch_workspace(index);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
} else {
let config = &self.niri.config;
if config.borrow().input.workspace_auto_back_and_forth {
self.niri.layout.switch_workspace_auto_back_and_forth(index);
} else {
self.niri.layout.switch_workspace(index);
}
self.maybe_warp_cursor_to_focus();
}
let config = &self.niri.config;
if config.borrow().input.workspace_auto_back_and_forth {
self.niri.layout.switch_workspace_auto_back_and_forth(idx);
} else {
self.niri.layout.switch_workspace(idx);
// FIXME: granular
self.niri.queue_redraw_all();
}
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspacePrevious => {
self.niri.layout.switch_workspace_previous();

View File

@ -34,7 +34,7 @@ use std::mem;
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, Config, Struts};
use niri_config::{CenterFocusedColumn, Config, Struts, Workspace as WorkspaceConfig};
use niri_ipc::SizeChange;
use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
@ -278,7 +278,7 @@ impl Options {
impl<W: LayoutElement> Layout<W> {
pub fn new(config: &Config) -> Self {
Self::with_options(Options::from_config(config))
Self::with_options_and_workspaces(&config, Options::from_config(config))
}
pub fn with_options(options: Options) -> Self {
@ -288,6 +288,20 @@ impl<W: LayoutElement> Layout<W> {
}
}
fn with_options_and_workspaces(config: &Config, options: Options) -> Self {
let opts = Rc::new(options);
let workspaces = config.workspaces
.iter()
.map(|ws| Workspace::new_with_config_no_outputs(Some(ws.clone()), opts.clone()))
.collect();
Self {
monitor_set: MonitorSet::NoOutputs { workspaces },
options: opts,
}
}
pub fn add_output(&mut self, output: Output) {
let id = OutputId::new(&output);
@ -317,7 +331,7 @@ impl<W: LayoutElement> Layout<W> {
// The user could've closed a window while remaining on this workspace, on
// another monitor. However, we will add an empty workspace in the end
// instead.
if ws.has_windows() {
if ws.has_windows() || ws.name.is_some() {
workspaces.push(ws);
}
@ -462,6 +476,56 @@ impl<W: LayoutElement> Layout<W> {
}
}
/// Adds a new window to the layout on a specific output & workspace.
pub fn add_window_to_named_workspace(
&mut self,
output: &Output,
workspace_name: &str,
window: W,
width: Option<ColumnWidth>,
is_full_width: bool,
) {
let mut width = width.unwrap_or_else(|| ColumnWidth::Fixed(window.size().w));
if let ColumnWidth::Fixed(w) = &mut width {
let rules = window.rules();
let border_config = rules.border.resolve_against(self.options.border);
if !border_config.off {
*w += border_config.width as i32 * 2;
}
}
let MonitorSet::Normal {
monitors,
active_monitor_idx,
..
} = &mut self.monitor_set
else {
panic!()
};
let (mon_idx, mon) = monitors
.iter_mut()
.enumerate()
.find(|(_, mon)| mon.output == *output)
.unwrap();
let ws_idx = &mon
.find_workspace_index(workspace_name)
.unwrap();
// Don't steal focus from an active fullscreen window.
let mut activate = true;
let ws = &mon.workspaces[*ws_idx];
if mon_idx == *active_monitor_idx
&& !ws.columns.is_empty()
&& ws.columns[ws.active_column_idx].is_fullscreen
{
activate = false;
}
mon.add_window(*ws_idx, window, activate, width, is_full_width);
}
pub fn add_column_by_idx(
&mut self,
monitor_idx: usize,
@ -648,6 +712,7 @@ impl<W: LayoutElement> Layout<W> {
&& idx != mon.active_workspace_idx
&& idx != mon.workspaces.len() - 1
&& mon.workspace_switch.is_none()
&& mon.workspaces[idx].name.is_none()
{
mon.workspaces.remove(idx);
@ -667,7 +732,7 @@ impl<W: LayoutElement> Layout<W> {
rv = Some(ws.remove_window(window));
// Clean up empty workspaces.
if !ws.has_windows() {
if !ws.has_windows() && workspaces[idx].name.is_none() {
workspaces.remove(idx);
}
@ -717,6 +782,35 @@ impl<W: LayoutElement> Layout<W> {
None
}
pub fn find_workspace_by_name(&self, workspace_name: &str) -> Option<(usize, &Workspace<W>)> {
if let MonitorSet::Normal { ref monitors, .. } = &self.monitor_set {
for mon in monitors {
if let Some((index, workspace)) = mon
.workspaces
.iter()
.enumerate()
.find(|(_, w)| w.name.as_deref() == Some(workspace_name))
{
return Some((index, workspace));
}
}
}
None
}
pub fn unname_workspace(&mut self, workspace_name: &str) {
if let MonitorSet::Normal {
ref mut monitors, ..
} = &mut self.monitor_set
{
for mon in monitors {
mon.unname_workspace(workspace_name);
mon.clean_up_workspaces();
}
}
}
pub fn find_window_and_output_mut(
&mut self,
wl_surface: &WlSurface,
@ -969,6 +1063,19 @@ impl<W: LayoutElement> Layout<W> {
monitors.iter().find(|monitor| &monitor.output == output)
}
pub fn monitor_for_workspace(&self, workspace_name: &str) -> Option<&Monitor<W>> {
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
return None;
};
monitors.iter().find(|monitor| {
monitor
.workspaces
.iter()
.any(|ws| ws.name.as_deref() == Some(workspace_name))
})
}
pub fn outputs(&self) -> impl Iterator<Item = &Output> + '_ {
let monitors = if let MonitorSet::Normal { monitors, .. } = &self.monitor_set {
&monitors[..]
@ -1126,6 +1233,12 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_to_workspace(idx);
}
pub fn move_to_workspace_on_output(&mut self, output: &Output, idx: usize) {
self.move_to_output(&output);
self.focus_output(&output);
self.move_to_workspace(idx);
}
pub fn move_column_to_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@ -1147,6 +1260,12 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_column_to_workspace(idx);
}
pub fn move_column_to_workspace_on_output(&mut self, output: &Output, idx: usize) {
self.move_to_output(&output);
self.focus_output(&output);
self.move_column_to_workspace(idx);
}
pub fn switch_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@ -1256,6 +1375,7 @@ impl<W: LayoutElement> Layout<W> {
use crate::layout::monitor::WorkspaceSwitch;
let mut seen_workspace_id = HashSet::new();
let mut seen_workspace_name = HashSet::new();
let (monitors, &primary_idx, &active_monitor_idx) = match &self.monitor_set {
MonitorSet::Normal {
@ -1280,6 +1400,13 @@ impl<W: LayoutElement> Layout<W> {
"workspace id must be unique"
);
if let Some(name) = &workspace.name {
assert!(
seen_workspace_name.insert(name),
"workspace name must be unique"
);
}
workspace.verify_invariants();
}
@ -1342,6 +1469,11 @@ impl<W: LayoutElement> Layout<W> {
"monitor must have an empty workspace in the end"
);
assert!(
monitor.workspaces.last().unwrap().name.is_none(),
"monitor must have an unnamed workspace in the end"
);
// If there's no workspace switch in progress, there can't be any non-last non-active
// empty workspaces.
if monitor.workspace_switch.is_none() {
@ -1369,6 +1501,13 @@ impl<W: LayoutElement> Layout<W> {
"workspace id must be unique"
);
if let Some(name) = &workspace.name {
assert!(
seen_workspace_name.insert(name),
"workspace name must be unique"
);
}
workspace.verify_invariants();
}
}
@ -1429,6 +1568,29 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn new_workspace_for_output(
&mut self,
workspace_config: WorkspaceConfig,
output: Option<Output>,
config: &Config,
) {
let options = Rc::new(Options::from_config(config));
let output = output.or(self.active_output().cloned());
let ws = if let Some(output) = output {
Workspace::new_with_config(output, Some(workspace_config), options)
} else {
Workspace::new_with_config_no_outputs(Some(workspace_config.clone()), options)
};
let Some(mon) = self.active_monitor() else {
return;
};
mon.workspaces.push(ws);
}
pub fn update_config(&mut self, config: &Config) {
let options = Rc::new(Options::from_config(config));

View File

@ -103,6 +103,18 @@ impl<W: LayoutElement> Monitor<W> {
&self.workspaces[self.active_workspace_idx]
}
pub fn workspace_ref(&self, workspace_name: &str) -> Option<&Workspace<W>> {
self.workspaces
.iter()
.find(|w| w.name.as_deref() == Some(workspace_name))
}
pub fn find_workspace_index(&self, workspace_name: &str) -> Option<usize> {
self.workspaces
.iter()
.position(|w| w.name.as_deref() == Some(workspace_name))
}
pub fn active_workspace(&mut self) -> &mut Workspace<W> {
&mut self.workspaces[self.active_workspace_idx]
}
@ -204,7 +216,7 @@ impl<W: LayoutElement> Monitor<W> {
continue;
}
if !self.workspaces[idx].has_windows() {
if !self.workspaces[idx].has_windows() && self.workspaces[idx].name.is_none() {
self.workspaces.remove(idx);
if self.active_workspace_idx > idx {
self.active_workspace_idx -= 1;
@ -213,6 +225,17 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn unname_workspace(&mut self, workspace_name: &str) {
for idx in 0..self.workspaces.len() - 1 {
if let Some(name) = &self.workspaces[idx].name {
if name == workspace_name {
self.workspaces[idx].unname();
return;
}
}
}
}
pub fn move_left(&mut self) {
self.active_workspace().move_left();
}

View File

@ -3,7 +3,7 @@ use std::iter::{self, zip};
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, Struts};
use niri_config::{CenterFocusedColumn, PresetWidth, Struts, Workspace as WorkspaceConfig};
use niri_ipc::SizeChange;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
@ -94,6 +94,9 @@ pub struct Workspace<W: LayoutElement> {
/// Configurable properties of the layout.
pub options: Rc<Options>,
/// Optional name of this workspace.
pub name: Option<String>,
/// Unique ID of this workspace.
id: WorkspaceId,
}
@ -313,9 +316,23 @@ impl TileData {
impl<W: LayoutElement> Workspace<W> {
pub fn new(output: Output, options: Rc<Options>) -> Self {
Self::new_with_config(output, None, options)
}
pub fn new_with_config(
output: Output,
config: Option<WorkspaceConfig>,
options: Rc<Options>,
) -> Self {
let original_output = config
.clone()
.and_then(|c| c.open_on_output.clone())
.and_then(|c| Some(OutputId(c)))
.unwrap_or(OutputId::new(&output));
let working_area = compute_working_area(&output, options.struts);
Self {
original_output: OutputId::new(&output),
original_output,
view_size: output_size(&output),
working_area,
output: Some(output),
@ -329,14 +346,24 @@ impl<W: LayoutElement> Workspace<W> {
view_offset_before_fullscreen: None,
closing_windows: vec![],
options,
name: config.and_then(|c| Some(c.name.clone().0)),
id: WorkspaceId::next(),
}
}
pub fn new_no_outputs(options: Rc<Options>) -> Self {
pub fn new_with_config_no_outputs(
config: Option<WorkspaceConfig>,
options: Rc<Options>,
) -> Self {
let original_output = OutputId(
config
.clone()
.and_then(|c| c.open_on_output)
.unwrap_or(String::new()),
);
Self {
output: None,
original_output: OutputId(String::new()),
original_output,
view_size: Size::from((1280, 720)),
working_area: Rectangle::from_loc_and_size((0, 0), (1280, 720)),
columns: vec![],
@ -349,14 +376,23 @@ impl<W: LayoutElement> Workspace<W> {
view_offset_before_fullscreen: None,
closing_windows: vec![],
options,
name: config.and_then(|c| Some(c.name.0)),
id: WorkspaceId::next(),
}
}
pub fn new_no_outputs(options: Rc<Options>) -> Self {
Self::new_with_config_no_outputs(None, options)
}
pub fn id(&self) -> WorkspaceId {
self.id
}
pub fn unname(&mut self) {
self.name = None;
}
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
anim.set_current_time(current_time);
@ -435,6 +471,10 @@ impl<W: LayoutElement> Workspace<W> {
.map(Tile::window_mut)
}
pub fn get_output_name(&self) -> Option<String> {
self.output.as_ref().and_then(|o| Some(o.name()))
}
pub fn set_output(&mut self, output: Option<Output>) {
if self.output == output {
return;

View File

@ -11,7 +11,7 @@ use std::{env, mem, thread};
use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as KdeDecorationsMode;
use anyhow::{ensure, Context};
use calloop::futures::Scheduler;
use niri_config::{Config, Key, Modifiers, PreviewRender, TrackLayout};
use niri_config::{Config, Key, Modifiers, PreviewRender, TrackLayout, WorkspaceReference};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
@ -857,6 +857,35 @@ impl State {
self.niri.config_error_notification.hide();
// Create new named workspaces
for ws in &config.workspaces {
if self
.niri
.layout
.find_workspace_by_name(&ws.name.0)
.is_none()
{
let output = ws
.open_on_output
.clone()
.and_then(|name| self.niri.output_by_name(Some(&name)));
self.niri
.layout
.new_workspace_for_output(ws.clone(), output, &config);
}
}
// Find & orphan removed named workspaces
let mut removed_workspaces: Vec<String> = vec![];
for ws in &self.niri.config.borrow().workspaces {
if !config.workspaces.iter().any(|w| w.name == ws.name) {
removed_workspaces.push(ws.name.0.clone());
}
}
for name in removed_workspaces {
self.niri.layout.unname_workspace(&name);
}
self.niri.layout.update_config(&config);
let slowdown = if config.animations.off {
@ -2087,6 +2116,46 @@ impl Niri {
.cloned()
}
pub fn output_by_name(&self, name: Option<&str>) -> Option<Output> {
let Some(name) = name else {
return None;
};
self.global_space
.outputs()
.find(|output| output.name().eq_ignore_ascii_case(&name))
.cloned()
}
pub fn find_output_and_workspace_index(
&self,
workspace_reference: WorkspaceReference,
) -> Option<(Option<Output>, usize)> {
let workspace_name = match workspace_reference {
WorkspaceReference::Index(index) => {
return Some((None, index.saturating_sub(1) as usize));
}
WorkspaceReference::Name(name) => name,
};
let (target_workspace_index, target_workspace) =
self.layout.find_workspace_by_name(&workspace_name)?;
let Some(active_workspace) = self.layout.active_workspace() else {
return None;
};
if target_workspace.get_output_name() == active_workspace.get_output_name() {
return Some((None, target_workspace_index));
}
let Some(target_output) =
self.output_by_name(target_workspace.get_output_name().as_deref())
else {
return None;
};
Some((Some(target_output), target_workspace_index))
}
pub fn output_down(&self) -> Option<Output> {
let active = self.layout.active_output()?;
let active_geo = self.global_space.output_geometry(active).unwrap();

View File

@ -33,6 +33,9 @@ pub struct ResolvedWindowRules {
/// Output to open this window on.
pub open_on_output: Option<String>,
/// Workspace to open this window on.
pub open_on_workspace: Option<String>,
/// Whether the window should open full-width.
pub open_maximized: Option<bool>,
@ -99,6 +102,7 @@ impl ResolvedWindowRules {
Self {
default_width: None,
open_on_output: None,
open_on_workspace: None,
open_maximized: None,
open_fullscreen: None,
min_width: None,
@ -151,6 +155,7 @@ impl ResolvedWindowRules {
}
let mut open_on_output = None;
let mut open_on_workspace = None;
for rule in rules {
let matches = |m| window_matches(window, &role, m);
@ -175,6 +180,10 @@ impl ResolvedWindowRules {
open_on_output = Some(x);
}
if let Some(x) = rule.open_on_workspace.as_deref() {
open_on_workspace = Some(x);
}
if let Some(x) = rule.open_maximized {
resolved.open_maximized = Some(x);
}
@ -217,6 +226,7 @@ impl ResolvedWindowRules {
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
resolved.open_on_workspace = open_on_workspace.map(|x| x.to_owned());
});
resolved

View File

@ -42,6 +42,9 @@ pub enum InitialConfigureState {
/// - This is a dialog with a parent, and there was no explicit output set, so this dialog
/// should fetch the parent's current output again upon mapping.
output: Option<Output>,
/// Workspace to open this window on.
workspace_name: Option<String>,
},
}