aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--niri-config/src/lib.rs170
-rw-r--r--niri-ipc/src/lib.rs45
-rw-r--r--src/handlers/compositor.rs18
-rw-r--r--src/handlers/xdg_shell.rs88
-rw-r--r--src/input/mod.rs77
-rw-r--r--src/layout/mod.rs349
-rw-r--r--src/layout/monitor.rs24
-rw-r--r--src/layout/workspace.rs48
-rw-r--r--src/niri.rs50
-rw-r--r--src/window/mod.rs10
-rw-r--r--src/window/unmapped.rs4
11 files changed, 784 insertions, 99 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index be93f9a7..355007f6 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -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.
@@ -693,6 +695,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"))]
@@ -706,6 +719,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>,
@@ -890,14 +905,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,
@@ -962,14 +977,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,
@@ -1002,6 +1023,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))]
@@ -1409,6 +1483,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,
@@ -2278,6 +2400,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; }
}
@@ -2285,6 +2408,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 {
@@ -2489,6 +2618,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 {
@@ -2540,7 +2683,18 @@ 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,
},
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index beabfcbc..b0f124f9 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -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;
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs
index 7d3f0419..2afb8fe8 100644
--- a/src/handlers/compositor.rs
+++ b/src/handlers/compositor.rs
@@ -119,22 +119,27 @@ 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)
+ // Chech that the workspace still exists.
+ let workspace_name = workspace_name
+ .filter(|n| self.niri.layout.find_workspace_by_name(n).is_some());
+
+ (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 +165,13 @@ impl CompositorHandler for State {
self.niri
.layout
.add_window_right_of(&p, mapped, width, is_full_width)
+ } else if let Some(workspace_name) = &workspace_name {
+ self.niri.layout.add_window_to_named_workspace(
+ workspace_name,
+ mapped,
+ width,
+ is_full_width,
+ )
} else if let Some(output) = &output {
self.niri
.layout
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 44f07839..30df1d5f 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -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.find_named_workspace(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.find_named_workspace(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();
diff --git a/src/input/mod.rs b/src/input/mod.rs
index 8206890c..1984b23c 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -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();
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index d091c024..0eb451e6 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -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;
@@ -279,7 +279,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 {
@@ -289,6 +289,21 @@ 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);
@@ -318,7 +333,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);
}
@@ -463,6 +478,67 @@ impl<W: LayoutElement> Layout<W> {
}
}
+ /// Adds a new window to the layout on a specific workspace.
+ pub fn add_window_to_named_workspace(
+ &mut self,
+ workspace_name: &str,
+ window: W,
+ width: Option<ColumnWidth>,
+ is_full_width: bool,
+ ) -> Option<&Output> {
+ 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;
+ }
+ }
+
+ match &mut self.monitor_set {
+ MonitorSet::Normal {
+ monitors,
+ active_monitor_idx,
+ ..
+ } => {
+ let (mon_idx, mon, ws_idx) = monitors
+ .iter_mut()
+ .enumerate()
+ .find_map(|(mon_idx, mon)| {
+ mon.find_named_workspace_index(workspace_name)
+ .map(move |ws_idx| (mon_idx, mon, ws_idx))
+ })
+ .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;
+ }
+
+ // Don't activate if on a different workspace.
+ if mon.active_workspace_idx != ws_idx {
+ activate = false;
+ }
+
+ mon.add_window(ws_idx, window, activate, width, is_full_width);
+ Some(&mon.output)
+ }
+ MonitorSet::NoOutputs { workspaces } => {
+ let ws = workspaces
+ .iter_mut()
+ .find(|ws| ws.name.as_deref() == Some(workspace_name))
+ .unwrap();
+ ws.add_window(window, true, width, is_full_width);
+ None
+ }
+ }
+ }
+
pub fn add_column_by_idx(
&mut self,
monitor_idx: usize,
@@ -649,6 +725,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);
@@ -668,7 +745,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);
}
@@ -718,6 +795,63 @@ impl<W: LayoutElement> Layout<W> {
None
}
+ pub fn find_workspace_by_name(&self, workspace_name: &str) -> Option<(usize, &Workspace<W>)> {
+ match &self.monitor_set {
+ MonitorSet::Normal { ref monitors, .. } => {
+ 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));
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces } => {
+ if let Some((index, workspace)) = 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) {
+ match &mut self.monitor_set {
+ MonitorSet::Normal { monitors, .. } => {
+ for mon in monitors {
+ if mon.unname_workspace(workspace_name) {
+ if mon.workspace_switch.is_none() {
+ mon.clean_up_workspaces();
+ }
+ return;
+ }
+ }
+ }
+ MonitorSet::NoOutputs { workspaces } => {
+ for (idx, ws) in workspaces.iter_mut().enumerate() {
+ if ws.name.as_deref() == Some(workspace_name) {
+ ws.unname();
+
+ // Clean up empty workspaces.
+ if !ws.has_windows() {
+ workspaces.remove(idx);
+ }
+
+ return;
+ }
+ }
+ }
+ }
+ }
+
pub fn find_window_and_output_mut(
&mut self,
wl_surface: &WlSurface,
@@ -970,6 +1104,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[..]
@@ -1127,6 +1274,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;
@@ -1148,6 +1301,12 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_column_to_workspace(idx);
}
+ pub fn move_column_to_workspace_on_output(&mut self, out