aboutsummaryrefslogtreecommitdiff
path: root/niri-config/src/lib.rs
diff options
context:
space:
mode:
authorGergely Nagy <niri@gergo.csillger.hu>2024-05-11 22:40:30 +0200
committerIvan Molodetskikh <yalterz@gmail.com>2024-05-16 01:24:34 -0700
commiteb9bbe3352820754a4ee3c19f15cff690d1c193d (patch)
tree65264aa9d03cc1ed63b4a540d0bca438ba75624b /niri-config/src/lib.rs
parent229ca905071d837dbe6cfd9383e51cbb3c4634d7 (diff)
downloadniri-eb9bbe3352820754a4ee3c19f15cff690d1c193d.tar.gz
niri-eb9bbe3352820754a4ee3c19f15cff690d1c193d.tar.bz2
niri-eb9bbe3352820754a4ee3c19f15cff690d1c193d.zip
Implement named workspaces
This is an implementation of named, pre-declared workspaces. With this implementation, workspaces can be declared in the configuration file by name: ``` workspace "name" { open-on-output "winit" } ``` The `open-on-output` property is optional, and can be skipped, in which case the workspace will open on the primary output. All actions that were able to target a workspace by index can now target them by either an index, or a name. In case of the command line, where we do not have types available, this means that workspace names that also pass as `u8` cannot be switched to by name, only by index. Unlike dynamic workspaces, named workspaces do not close when they are empty, they remain static. Like dynamic workspaces, named workspaces are bound to a particular output. Switching to a named workspace, or moving a window or column to one will also switch to, or move the thing in question to the output of the workspace. When reloading the configuration, newly added named workspaces will be created, and removed ones will lose their name. If any such orphaned workspace was empty, they will be removed. If they weren't, they'll remain as a dynamic workspace, without a name. Re-declaring a workspace with the same name later will create a new one. Additionally, this also implements a `open-on-workspace "<name>"` window rule. Matching windows will open on the given workspace (or the current one, if the named workspace does not exist). Signed-off-by: Gergely Nagy <niri@gergo.csillger.hu>
Diffstat (limited to 'niri-config/src/lib.rs')
-rw-r--r--niri-config/src/lib.rs170
1 files changed, 162 insertions, 8 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,
},