aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-08-27 10:54:41 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-08-27 10:55:31 +0300
commita03a7bd1a321ccb6eab91d1279abdb55ee1814f9 (patch)
tree09f447d3a3fe948ca26ef176625036a3b6db7a28
parent758a4c5e6572302d50c412040f358a7791c1912e (diff)
downloadniri-a03a7bd1a321ccb6eab91d1279abdb55ee1814f9.tar.gz
niri-a03a7bd1a321ccb6eab91d1279abdb55ee1814f9.tar.bz2
niri-a03a7bd1a321ccb6eab91d1279abdb55ee1814f9.zip
config: Extract binds
-rw-r--r--niri-config/src/binds.rs975
-rw-r--r--niri-config/src/input.rs3
-rw-r--r--niri-config/src/lib.rs967
3 files changed, 979 insertions, 966 deletions
diff --git a/niri-config/src/binds.rs b/niri-config/src/binds.rs
new file mode 100644
index 00000000..2cdc96cd
--- /dev/null
+++ b/niri-config/src/binds.rs
@@ -0,0 +1,975 @@
+use std::collections::HashSet;
+use std::str::FromStr;
+use std::time::Duration;
+
+use bitflags::bitflags;
+use knuffel::errors::DecodeError;
+use miette::miette;
+use niri_ipc::{
+ ColumnDisplay, LayoutSwitchTarget, PositionChange, SizeChange, WorkspaceReferenceArg,
+};
+use smithay::input::keyboard::keysyms::KEY_NoSymbol;
+use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
+use smithay::input::keyboard::Keysym;
+
+use crate::expect_only_children;
+
+#[derive(Debug, Default, PartialEq)]
+pub struct Binds(pub Vec<Bind>);
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Bind {
+ pub key: Key,
+ pub action: Action,
+ pub repeat: bool,
+ pub cooldown: Option<Duration>,
+ pub allow_when_locked: bool,
+ pub allow_inhibiting: bool,
+ pub hotkey_overlay_title: Option<Option<String>>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+pub struct Key {
+ pub trigger: Trigger,
+ pub modifiers: Modifiers,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
+pub enum Trigger {
+ Keysym(Keysym),
+ MouseLeft,
+ MouseRight,
+ MouseMiddle,
+ MouseBack,
+ MouseForward,
+ WheelScrollDown,
+ WheelScrollUp,
+ WheelScrollLeft,
+ WheelScrollRight,
+ TouchpadScrollDown,
+ TouchpadScrollUp,
+ TouchpadScrollLeft,
+ TouchpadScrollRight,
+}
+
+bitflags! {
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+ pub struct Modifiers : u8 {
+ const CTRL = 1;
+ const SHIFT = 1 << 1;
+ const ALT = 1 << 2;
+ const SUPER = 1 << 3;
+ const ISO_LEVEL3_SHIFT = 1 << 4;
+ const ISO_LEVEL5_SHIFT = 1 << 5;
+ const COMPOSITOR = 1 << 6;
+ }
+}
+
+#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+pub struct SwitchBinds {
+ #[knuffel(child)]
+ pub lid_open: Option<SwitchAction>,
+ #[knuffel(child)]
+ pub lid_close: Option<SwitchAction>,
+ #[knuffel(child)]
+ pub tablet_mode_on: Option<SwitchAction>,
+ #[knuffel(child)]
+ pub tablet_mode_off: Option<SwitchAction>,
+}
+
+#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
+pub struct SwitchAction {
+ #[knuffel(child, unwrap(arguments))]
+ pub spawn: Vec<String>,
+}
+
+// Remember to add new actions to the CLI enum too.
+#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
+pub enum Action {
+ Quit(#[knuffel(property(name = "skip-confirmation"), default)] bool),
+ #[knuffel(skip)]
+ ChangeVt(i32),
+ Suspend,
+ PowerOffMonitors,
+ PowerOnMonitors,
+ ToggleDebugTint,
+ DebugToggleOpaqueRegions,
+ DebugToggleDamage,
+ Spawn(#[knuffel(arguments)] Vec<String>),
+ SpawnSh(#[knuffel(argument)] String),
+ DoScreenTransition(#[knuffel(property(name = "delay-ms"))] Option<u16>),
+ #[knuffel(skip)]
+ ConfirmScreenshot {
+ write_to_disk: bool,
+ },
+ #[knuffel(skip)]
+ CancelScreenshot,
+ #[knuffel(skip)]
+ ScreenshotTogglePointer,
+ Screenshot(#[knuffel(property(name = "show-pointer"), default = true)] bool),
+ ScreenshotScreen(
+ #[knuffel(property(name = "write-to-disk"), default = true)] bool,
+ #[knuffel(property(name = "show-pointer"), default = true)] bool,
+ ),
+ ScreenshotWindow(#[knuffel(property(name = "write-to-disk"), default = true)] bool),
+ #[knuffel(skip)]
+ ScreenshotWindowById {
+ id: u64,
+ write_to_disk: bool,
+ },
+ ToggleKeyboardShortcutsInhibit,
+ CloseWindow,
+ #[knuffel(skip)]
+ CloseWindowById(u64),
+ FullscreenWindow,
+ #[knuffel(skip)]
+ FullscreenWindowById(u64),
+ ToggleWindowedFullscreen,
+ #[knuffel(skip)]
+ ToggleWindowedFullscreenById(u64),
+ #[knuffel(skip)]
+ FocusWindow(u64),
+ FocusWindowInColumn(#[knuffel(argument)] u8),
+ FocusWindowPrevious,
+ FocusColumnLeft,
+ #[knuffel(skip)]
+ FocusColumnLeftUnderMouse,
+ FocusColumnRight,
+ #[knuffel(skip)]
+ FocusColumnRightUnderMouse,
+ FocusColumnFirst,
+ FocusColumnLast,
+ FocusColumnRightOrFirst,
+ FocusColumnLeftOrLast,
+ FocusColumn(#[knuffel(argument)] usize),
+ FocusWindowOrMonitorUp,
+ FocusWindowOrMonitorDown,
+ FocusColumnOrMonitorLeft,
+ FocusColumnOrMonitorRight,
+ FocusWindowDown,
+ FocusWindowUp,
+ FocusWindowDownOrColumnLeft,
+ FocusWindowDownOrColumnRight,
+ FocusWindowUpOrColumnLeft,
+ FocusWindowUpOrColumnRight,
+ FocusWindowOrWorkspaceDown,
+ FocusWindowOrWorkspaceUp,
+ FocusWindowTop,
+ FocusWindowBottom,
+ FocusWindowDownOrTop,
+ FocusWindowUpOrBottom,
+ MoveColumnLeft,
+ MoveColumnRight,
+ MoveColumnToFirst,
+ MoveColumnToLast,
+ MoveColumnLeftOrToMonitorLeft,
+ MoveColumnRightOrToMonitorRight,
+ MoveColumnToIndex(#[knuffel(argument)] usize),
+ MoveWindowDown,
+ MoveWindowUp,
+ MoveWindowDownOrToWorkspaceDown,
+ MoveWindowUpOrToWorkspaceUp,
+ ConsumeOrExpelWindowLeft,
+ #[knuffel(skip)]
+ ConsumeOrExpelWindowLeftById(u64),
+ ConsumeOrExpelWindowRight,
+ #[knuffel(skip)]
+ ConsumeOrExpelWindowRightById(u64),
+ ConsumeWindowIntoColumn,
+ ExpelWindowFromColumn,
+ SwapWindowLeft,
+ SwapWindowRight,
+ ToggleColumnTabbedDisplay,
+ SetColumnDisplay(#[knuffel(argument, str)] ColumnDisplay),
+ CenterColumn,
+ CenterWindow,
+ #[knuffel(skip)]
+ CenterWindowById(u64),
+ CenterVisibleColumns,
+ FocusWorkspaceDown,
+ #[knuffel(skip)]
+ FocusWorkspaceDownUnderMouse,
+ FocusWorkspaceUp,
+ #[knuffel(skip)]
+ FocusWorkspaceUpUnderMouse,
+ FocusWorkspace(#[knuffel(argument)] WorkspaceReference),
+ FocusWorkspacePrevious,
+ MoveWindowToWorkspaceDown(#[knuffel(property(name = "focus"), default = true)] bool),
+ MoveWindowToWorkspaceUp(#[knuffel(property(name = "focus"), default = true)] bool),
+ MoveWindowToWorkspace(
+ #[knuffel(argument)] WorkspaceReference,
+ #[knuffel(property(name = "focus"), default = true)] bool,
+ ),
+ #[knuffel(skip)]
+ MoveWindowToWorkspaceById {
+ window_id: u64,
+ reference: WorkspaceReference,
+ focus: bool,
+ },
+ MoveColumnToWorkspaceDown(#[knuffel(property(name = "focus"), default = true)] bool),
+ MoveColumnToWorkspaceUp(#[knuffel(property(name = "focus"), default = true)] bool),
+ MoveColumnToWorkspace(
+ #[knuffel(argument)] WorkspaceReference,
+ #[knuffel(property(name = "focus"), default = true)] bool,
+ ),
+ MoveWorkspaceDown,
+ MoveWorkspaceUp,
+ MoveWorkspaceToIndex(#[knuffel(argument)] usize),
+ #[knuffel(skip)]
+ MoveWorkspaceToIndexByRef {
+ new_idx: usize,
+ reference: WorkspaceReference,
+ },
+ #[knuffel(skip)]
+ MoveWorkspaceToMonitorByRef {
+ output_name: String,
+ reference: WorkspaceReference,
+ },
+ MoveWorkspaceToMonitor(#[knuffel(argument)] String),
+ SetWorkspaceName(#[knuffel(argument)] String),
+ #[knuffel(skip)]
+ SetWorkspaceNameByRef {
+ name: String,
+ reference: WorkspaceReference,
+ },
+ UnsetWorkspaceName,
+ #[knuffel(skip)]
+ UnsetWorkSpaceNameByRef(#[knuffel(argument)] WorkspaceReference),
+ FocusMonitorLeft,
+ FocusMonitorRight,
+ FocusMonitorDown,
+ FocusMonitorUp,
+ FocusMonitorPrevious,
+ FocusMonitorNext,
+ FocusMonitor(#[knuffel(argument)] String),
+ MoveWindowToMonitorLeft,
+ MoveWindowToMonitorRight,
+ MoveWindowToMonitorDown,
+ MoveWindowToMonitorUp,
+ MoveWindowToMonitorPrevious,
+ MoveWindowToMonitorNext,
+ MoveWindowToMonitor(#[knuffel(argument)] String),
+ #[knuffel(skip)]
+ MoveWindowToMonitorById {
+ id: u64,
+ output: String,
+ },
+ MoveColumnToMonitorLeft,
+ MoveColumnToMonitorRight,
+ MoveColumnToMonitorDown,
+ MoveColumnToMonitorUp,
+ MoveColumnToMonitorPrevious,
+ MoveColumnToMonitorNext,
+ MoveColumnToMonitor(#[knuffel(argument)] String),
+ SetWindowWidth(#[knuffel(argument, str)] SizeChange),
+ #[knuffel(skip)]
+ SetWindowWidthById {
+ id: u64,
+ change: SizeChange,
+ },
+ SetWindowHeight(#[knuffel(argument, str)] SizeChange),
+ #[knuffel(skip)]
+ SetWindowHeightById {
+ id: u64,
+ change: SizeChange,
+ },
+ ResetWindowHeight,
+ #[knuffel(skip)]
+ ResetWindowHeightById(u64),
+ SwitchPresetColumnWidth,
+ SwitchPresetWindowWidth,
+ #[knuffel(skip)]
+ SwitchPresetWindowWidthById(u64),
+ SwitchPresetWindowHeight,
+ #[knuffel(skip)]
+ SwitchPresetWindowHeightById(u64),
+ MaximizeColumn,
+ SetColumnWidth(#[knuffel(argument, str)] SizeChange),
+ ExpandColumnToAvailableWidth,
+ SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget),
+ ShowHotkeyOverlay,
+ MoveWorkspaceToMonitorLeft,
+ MoveWorkspaceToMonitorRight,
+ MoveWorkspaceToMonitorDown,
+ MoveWorkspaceToMonitorUp,
+ MoveWorkspaceToMonitorPrevious,
+ MoveWorkspaceToMonitorNext,
+ ToggleWindowFloating,
+ #[knuffel(skip)]
+ ToggleWindowFloatingById(u64),
+ MoveWindowToFloating,
+ #[knuffel(skip)]
+ MoveWindowToFloatingById(u64),
+ MoveWindowToTiling,
+ #[knuffel(skip)]
+ MoveWindowToTilingById(u64),
+ FocusFloating,
+ FocusTiling,
+ SwitchFocusBetweenFloatingAndTiling,
+ #[knuffel(skip)]
+ MoveFloatingWindowById {
+ id: Option<u64>,
+ x: PositionChange,
+ y: PositionChange,
+ },
+ ToggleWindowRuleOpacity,
+ #[knuffel(skip)]
+ ToggleWindowRuleOpacityById(u64),
+ SetDynamicCastWindow,
+ #[knuffel(skip)]
+ SetDynamicCastWindowById(u64),
+ SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
+ ClearDynamicCastTarget,
+ ToggleOverview,
+ OpenOverview,
+ CloseOverview,
+ #[knuffel(skip)]
+ ToggleWindowUrgent(u64),
+ #[knuffel(skip)]
+ SetWindowUrgent(u64),
+ #[knuffel(skip)]
+ UnsetWindowUrgent(u64),
+ #[knuffel(skip)]
+ LoadConfigFile,
+}
+
+impl From<niri_ipc::Action> for Action {
+ fn from(value: niri_ipc::Action) -> Self {
+ match value {
+ niri_ipc::Action::Quit { skip_confirmation } => Self::Quit(skip_confirmation),
+ niri_ipc::Action::PowerOffMonitors {} => Self::PowerOffMonitors,
+ niri_ipc::Action::PowerOnMonitors {} => Self::PowerOnMonitors,
+ niri_ipc::Action::Spawn { command } => Self::Spawn(command),
+ niri_ipc::Action::SpawnSh { command } => Self::SpawnSh(command),
+ niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms),
+ niri_ipc::Action::Screenshot { show_pointer } => Self::Screenshot(show_pointer),
+ niri_ipc::Action::ScreenshotScreen {
+ write_to_disk,
+ show_pointer,
+ } => Self::ScreenshotScreen(write_to_disk, show_pointer),
+ niri_ipc::Action::ScreenshotWindow {
+ id: None,
+ write_to_disk,
+ } => Self::ScreenshotWindow(write_to_disk),
+ niri_ipc::Action::ScreenshotWindow {
+ id: Some(id),
+ write_to_disk,
+ } => Self::ScreenshotWindowById { id, write_to_disk },
+ niri_ipc::Action::ToggleKeyboardShortcutsInhibit {} => {
+ Self::ToggleKeyboardShortcutsInhibit
+ }
+ niri_ipc::Action::CloseWindow { id: None } => Self::CloseWindow,
+ niri_ipc::Action::CloseWindow { id: Some(id) } => Self::CloseWindowById(id),
+ niri_ipc::Action::FullscreenWindow { id: None } => Self::FullscreenWindow,
+ niri_ipc::Action::FullscreenWindow { id: Some(id) } => Self::FullscreenWindowById(id),
+ niri_ipc::Action::ToggleWindowedFullscreen { id: None } => {
+ Self::ToggleWindowedFullscreen
+ }
+ niri_ipc::Action::ToggleWindowedFullscreen { id: Some(id) } => {
+ Self::ToggleWindowedFullscreenById(id)
+ }
+ niri_ipc::Action::FocusWindow { id } => Self::FocusWindow(id),
+ niri_ipc::Action::FocusWindowInColumn { index } => Self::FocusWindowInColumn(index),
+ niri_ipc::Action::FocusWindowPrevious {} => Self::FocusWindowPrevious,
+ niri_ipc::Action::FocusColumnLeft {} => Self::FocusColumnLeft,
+ niri_ipc::Action::FocusColumnRight {} => Self::FocusColumnRight,
+ niri_ipc::Action::FocusColumnFirst {} => Self::FocusColumnFirst,
+ niri_ipc::Action::FocusColumnLast {} => Self::FocusColumnLast,
+ niri_ipc::Action::FocusColumnRightOrFirst {} => Self::FocusColumnRightOrFirst,
+ niri_ipc::Action::FocusColumnLeftOrLast {} => Self::FocusColumnLeftOrLast,
+ niri_ipc::Action::FocusColumn { index } => Self::FocusColumn(index),
+ niri_ipc::Action::FocusWindowOrMonitorUp {} => Self::FocusWindowOrMonitorUp,
+ niri_ipc::Action::FocusWindowOrMonitorDown {} => Self::FocusWindowOrMonitorDown,
+ niri_ipc::Action::FocusColumnOrMonitorLeft {} => Self::FocusColumnOrMonitorLeft,
+ niri_ipc::Action::FocusColumnOrMonitorRight {} => Self::FocusColumnOrMonitorRight,
+ niri_ipc::Action::FocusWindowDown {} => Self::FocusWindowDown,
+ niri_ipc::Action::FocusWindowUp {} => Self::FocusWindowUp,
+ niri_ipc::Action::FocusWindowDownOrColumnLeft {} => Self::FocusWindowDownOrColumnLeft,
+ niri_ipc::Action::FocusWindowDownOrColumnRight {} => Self::FocusWindowDownOrColumnRight,
+ niri_ipc::Action::FocusWindowUpOrColumnLeft {} => Self::FocusWindowUpOrColumnLeft,
+ niri_ipc::Action::FocusWindowUpOrColumnRight {} => Self::FocusWindowUpOrColumnRight,
+ niri_ipc::Action::FocusWindowOrWorkspaceDown {} => Self::FocusWindowOrWorkspaceDown,
+ niri_ipc::Action::FocusWindowOrWorkspaceUp {} => Self::FocusWindowOrWorkspaceUp,
+ niri_ipc::Action::FocusWindowTop {} => Self::FocusWindowTop,
+ niri_ipc::Action::FocusWindowBottom {} => Self::FocusWindowBottom,
+ niri_ipc::Action::FocusWindowDownOrTop {} => Self::FocusWindowDownOrTop,
+ niri_ipc::Action::FocusWindowUpOrBottom {} => Self::FocusWindowUpOrBottom,
+ niri_ipc::Action::MoveColumnLeft {} => Self::MoveColumnLeft,
+ niri_ipc::Action::MoveColumnRight {} => Self::MoveColumnRight,
+ niri_ipc::Action::MoveColumnToFirst {} => Self::MoveColumnToFirst,
+ niri_ipc::Action::MoveColumnToLast {} => Self::MoveColumnToLast,
+ niri_ipc::Action::MoveColumnToIndex { index } => Self::MoveColumnToIndex(index),
+ niri_ipc::Action::MoveColumnLeftOrToMonitorLeft {} => {
+ Self::MoveColumnLeftOrToMonitorLeft
+ }
+ niri_ipc::Action::MoveColumnRightOrToMonitorRight {} => {
+ Self::MoveColumnRightOrToMonitorRight
+ }
+ niri_ipc::Action::MoveWindowDown {} => Self::MoveWindowDown,
+ niri_ipc::Action::MoveWindowUp {} => Self::MoveWindowUp,
+ niri_ipc::Action::MoveWindowDownOrToWorkspaceDown {} => {
+ Self::MoveWindowDownOrToWorkspaceDown
+ }
+ niri_ipc::Action::MoveWindowUpOrToWorkspaceUp {} => Self::MoveWindowUpOrToWorkspaceUp,
+ niri_ipc::Action::ConsumeOrExpelWindowLeft { id: None } => {
+ Self::ConsumeOrExpelWindowLeft
+ }
+ niri_ipc::Action::ConsumeOrExpelWindowLeft { id: Some(id) } => {
+ Self::ConsumeOrExpelWindowLeftById(id)
+ }
+ niri_ipc::Action::ConsumeOrExpelWindowRight { id: None } => {
+ Self::ConsumeOrExpelWindowRight
+ }
+ niri_ipc::Action::ConsumeOrExpelWindowRight { id: Some(id) } => {
+ Self::ConsumeOrExpelWindowRightById(id)
+ }
+ niri_ipc::Action::ConsumeWindowIntoColumn {} => Self::ConsumeWindowIntoColumn,
+ niri_ipc::Action::ExpelWindowFromColumn {} => Self::ExpelWindowFromColumn,
+ niri_ipc::Action::SwapWindowRight {} => Self::SwapWindowRight,
+ niri_ipc::Action::SwapWindowLeft {} => Self::SwapWindowLeft,
+ niri_ipc::Action::ToggleColumnTabbedDisplay {} => Self::ToggleColumnTabbedDisplay,
+ niri_ipc::Action::SetColumnDisplay { display } => Self::SetColumnDisplay(display),
+ niri_ipc::Action::CenterColumn {} => Self::CenterColumn,
+ niri_ipc::Action::CenterWindow { id: None } => Self::CenterWindow,
+ niri_ipc::Action::CenterWindow { id: Some(id) } => Self::CenterWindowById(id),
+ niri_ipc::Action::CenterVisibleColumns {} => Self::CenterVisibleColumns,
+ niri_ipc::Action::FocusWorkspaceDown {} => Self::FocusWorkspaceDown,
+ niri_ipc::Action::FocusWorkspaceUp {} => Self::FocusWorkspaceUp,
+ niri_ipc::Action::FocusWorkspace { reference } => {
+ Self::FocusWorkspace(WorkspaceReference::from(reference))
+ }
+ niri_ipc::Action::FocusWorkspacePrevious {} => Self::FocusWorkspacePrevious,
+ niri_ipc::Action::MoveWindowToWorkspaceDown { focus } => {
+ Self::MoveWindowToWorkspaceDown(focus)
+ }
+ niri_ipc::Action::MoveWindowToWorkspaceUp { focus } => {
+ Self::MoveWindowToWorkspaceUp(focus)
+ }
+ niri_ipc::Action::MoveWindowToWorkspace {
+ window_id: None,
+ reference,
+ focus,
+ } => Self::MoveWindowToWorkspace(WorkspaceReference::from(reference), focus),
+ niri_ipc::Action::MoveWindowToWorkspace {
+ window_id: Some(window_id),
+ reference,
+ focus,
+ } => Self::MoveWindowToWorkspaceById {
+ window_id,
+ reference: WorkspaceReference::from(reference),
+ focus,
+ },
+ niri_ipc::Action::MoveColumnToWorkspaceDown { focus } => {
+ Self::MoveColumnToWorkspaceDown(focus)
+ }
+ niri_ipc::Action::MoveColumnToWorkspaceUp { focus } => {
+ Self::MoveColumnToWorkspaceUp(focus)
+ }
+ niri_ipc::Action::MoveColumnToWorkspace { reference, focus } => {
+ Self::MoveColumnToWorkspace(WorkspaceReference::from(reference), focus)
+ }
+ niri_ipc::Action::MoveWorkspaceDown {} => Self::MoveWorkspaceDown,
+ niri_ipc::Action::MoveWorkspaceUp {} => Self::MoveWorkspaceUp,
+ niri_ipc::Action::SetWorkspaceName {
+ name,
+ workspace: None,
+ } => Self::SetWorkspaceName(name),
+ niri_ipc::Action::SetWorkspaceName {
+ name,
+ workspace: Some(reference),
+ } => Self::SetWorkspaceNameByRef {
+ name,
+ reference: WorkspaceReference::from(reference),
+ },
+ niri_ipc::Action::UnsetWorkspaceName { reference: None } => Self::UnsetWorkspaceName,
+ niri_ipc::Action::UnsetWorkspaceName {
+ reference: Some(reference),
+ } => Self::UnsetWorkSpaceNameByRef(WorkspaceReference::from(reference)),
+ niri_ipc::Action::FocusMonitorLeft {} => Self::FocusMonitorLeft,
+ niri_ipc::Action::FocusMonitorRight {} => Self::FocusMonitorRight,
+ niri_ipc::Action::FocusMonitorDown {} => Self::FocusMonitorDown,
+ niri_ipc::Action::FocusMonitorUp {} => Self::FocusMonitorUp,
+ niri_ipc::Action::FocusMonitorPrevious {} => Self::FocusMonitorPrevious,
+ niri_ipc::Action::FocusMonitorNext {} => Self::FocusMonitorNext,
+ niri_ipc::Action::FocusMonitor { output } => Self::FocusMonitor(output),
+ niri_ipc::Action::MoveWindowToMonitorLeft {} => Self::MoveWindowToMonitorLeft,
+ niri_ipc::Action::MoveWindowToMonitorRight {} => Self::MoveWindowToMonitorRight,
+ niri_ipc::Action::MoveWindowToMonitorDown {} => Self::MoveWindowToMonitorDown,
+ niri_ipc::Action::MoveWindowToMonitorUp {} => Self::MoveWindowToMonitorUp,
+ niri_ipc::Action::MoveWindowToMonitorPrevious {} => Self::MoveWindowToMonitorPrevious,
+ niri_ipc::Action::MoveWindowToMonitorNext {} => Self::MoveWindowToMonitorNext,
+ niri_ipc::Action::MoveWindowToMonitor { id: None, output } => {
+ Self::MoveWindowToMonitor(output)
+ }
+ niri_ipc::Action::MoveWindowToMonitor {
+ id: Some(id),
+ output,
+ } => Self::MoveWindowToMonitorById { id, output },
+ niri_ipc::Action::MoveColumnToMonitorLeft {} => Self::MoveColumnToMonitorLeft,
+ niri_ipc::Action::MoveColumnToMonitorRight {} => Self::MoveColumnToMonitorRight,
+ niri_ipc::Action::MoveColumnToMonitorDown {} => Self::MoveColumnToMonitorDown,
+ niri_ipc::Action::MoveColumnToMonitorUp {} => Self::MoveColumnToMonitorUp,
+ niri_ipc::Action::MoveColumnToMonitorPrevious {} => Self::MoveColumnToMonitorPrevious,
+ niri_ipc::Action::MoveColumnToMonitorNext {} => Self::MoveColumnToMonitorNext,
+ niri_ipc::Action::MoveColumnToMonitor { output } => Self::MoveColumnToMonitor(output),
+ niri_ipc::Action::SetWindowWidth { id: None, change } => Self::SetWindowWidth(change),
+ niri_ipc::Action::SetWindowWidth {
+ id: Some(id),
+ change,
+ } => Self::SetWindowWidthById { id, change },
+ niri_ipc::Action::SetWindowHeight { id: None, change } => Self::SetWindowHeight(change),
+ niri_ipc::Action::SetWindowHeight {
+ id: Some(id),
+ change,
+ } => Self::SetWindowHeightById { id, change },
+ niri_ipc::Action::ResetWindowHeight { id: None } => Self::ResetWindowHeight,
+ niri_ipc::Action::ResetWindowHeight { id: Some(id) } => Self::ResetWindowHeightById(id),
+ niri_ipc::Action::SwitchPresetColumnWidth {} => Self::SwitchPresetColumnWidth,
+ niri_ipc::Action::SwitchPresetWindowWidth { id: None } => Self::SwitchPresetWindowWidth,
+ niri_ipc::Action::SwitchPresetWindowWidth { id: Some(id) } => {
+ Self::SwitchPresetWindowWidthById(id)
+ }
+ niri_ipc::Action::SwitchPresetWindowHeight { id: None } => {
+ Self::SwitchPresetWindowHeight
+ }
+ niri_ipc::Action::SwitchPresetWindowHeight { id: Some(id) } => {
+ Self::SwitchPresetWindowHeightById(id)
+ }
+ niri_ipc::Action::MaximizeColumn {} => Self::MaximizeColumn,
+ niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change),
+ niri_ipc::Action::ExpandColumnToAvailableWidth {} => Self::ExpandColumnToAvailableWidth,
+ niri_ipc::Action::SwitchLayout { layout } => Self::SwitchLayout(layout),
+ niri_ipc::Action::ShowHotkeyOverlay {} => Self::ShowHotkeyOverlay,
+ niri_ipc::Action::MoveWorkspaceToMonitorLeft {} => Self::MoveWorkspaceToMonitorLeft,
+ niri_ipc::Action::MoveWorkspaceToMonitorRight {} => Self::MoveWorkspaceToMonitorRight,
+ niri_ipc::Action::MoveWorkspaceToMonitorDown {} => Self::MoveWorkspaceToMonitorDown,
+ niri_ipc::Action::MoveWorkspaceToMonitorUp {} => Self::MoveWorkspaceToMonitorUp,
+ niri_ipc::Action::MoveWorkspaceToMonitorPrevious {} => {
+ Self::MoveWorkspaceToMonitorPrevious
+ }
+ niri_ipc::Action::MoveWorkspaceToIndex {
+ index,
+ reference: Some(reference),
+ } => Self::MoveWorkspaceToIndexByRef {
+ new_idx: index,
+ reference: WorkspaceReference::from(reference),
+ },
+ niri_ipc::Action::MoveWorkspaceToIndex {
+ index,
+ reference: None,
+ } => Self::MoveWorkspaceToIndex(index),
+ niri_ipc::Action::MoveWorkspaceToMonitor {
+ output,
+ reference: Some(reference),
+ } => Self::MoveWorkspaceToMonitorByRef {
+ output_name: output,
+ reference: WorkspaceReference::from(reference),
+ },
+ niri_ipc::Action::MoveWorkspaceToMonitor {
+ output,
+ reference: None,
+ } => Self::MoveWorkspaceToMonitor(output),
+ niri_ipc::Action::MoveWorkspaceToMonitorNext {} => Self::MoveWorkspaceToMonitorNext,
+ niri_ipc::Action::ToggleDebugTint {} => Self::ToggleDebugTint,
+ niri_ipc::Action::DebugToggleOpaqueRegions {} => Self::DebugToggleOpaqueRegions,
+ niri_ipc::Action::DebugToggleDamage {} => Self::DebugToggleDamage,
+ niri_ipc::Action::ToggleWindowFloating { id: None } => Self::ToggleWindowFloating,
+ niri_ipc::Action::ToggleWindowFloating { id: Some(id) } => {
+ Self::ToggleWindowFloatingById(id)
+ }
+ niri_ipc::Action::MoveWindowToFloating { id: None } => Self::MoveWindowToFloating,
+ niri_ipc::Action::MoveWindowToFloating { id: Some(id) } => {
+ Self::MoveWindowToFloatingById(id)
+ }
+ niri_ipc::Action::MoveWindowToTiling { id: None } => Self::MoveWindowToTiling,
+ niri_ipc::Action::MoveWindowToTiling { id: Some(id) } => {
+ Self::MoveWindowToTilingById(id)
+ }
+ niri_ipc::Action::FocusFloating {} => Self::FocusFloating,
+ niri_ipc::Action::FocusTiling {} => Self::FocusTiling,
+ niri_ipc::Action::SwitchFocusBetweenFloatingAndTiling {} => {
+ Self::SwitchFocusBetweenFloatingAndTiling
+ }
+ niri_ipc::Action::MoveFloatingWindow { id, x, y } => {
+ Self::MoveFloatingWindowById { id, x, y }
+ }
+ niri_ipc::Action::ToggleWindowRuleOpacity { id: None } => Self::ToggleWindowRuleOpacity,
+ niri_ipc::Action::ToggleWindowRuleOpacity { id: Some(id) } => {
+ Self::ToggleWindowRuleOpacityById(id)
+ }
+ niri_ipc::Action::SetDynamicCastWindow { id: None } => Self::SetDynamicCastWindow,
+ niri_ipc::Action::SetDynamicCastWindow { id: Some(id) } => {
+ Self::SetDynamicCastWindowById(id)
+ }
+ niri_ipc::Action::SetDynamicCastMonitor { output } => {
+ Self::SetDynamicCastMonitor(output)
+ }
+ niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
+ niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
+ niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
+ niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
+ niri_ipc::Action::ToggleWindowUrgent { id } => Self::ToggleWindowUrgent(id),
+ niri_ipc::Action::SetWindowUrgent { id } => Self::SetWindowUrgent(id),
+ niri_ipc::Action::UnsetWindowUrgent { id } => Self::UnsetWindowUrgent(id),
+ niri_ipc::Action::LoadConfigFile {} => Self::LoadConfigFile,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum WorkspaceReference {
+ Id(u64),
+ Index(u8),
+ Name(String),
+}
+
+impl From<WorkspaceReferenceArg> for WorkspaceReference {
+ fn from(reference: WorkspaceReferenceArg) -> WorkspaceReference {
+ match reference {
+ WorkspaceReferenceArg::Id(id) => Self::Id(id),
+ 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))
+ }
+ }
+ }
+}
+
+impl<S> knuffel::Decode<S> for Binds
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ expect_only_children(node, ctx);
+
+ let mut seen_keys = HashSet::new();
+
+ let mut binds = Vec::new();
+
+ for child in node.children() {
+ match Bind::decode_node(child, ctx) {
+ Err(e) => {
+ ctx.emit_error(e);
+ }
+ Ok(bind) => {
+ if seen_keys.insert(bind.key) {
+ binds.push(bind);
+ } else {
+ // ideally, this error should point to the previous instance of this keybind
+ //
+ // i (sodiboo) have tried to implement this in various ways:
+ // miette!(), #[derive(Diagnostic)]
+ // DecodeError::Custom, DecodeError::Conversion
+ // nothing seems to work, and i suspect it's not possible.
+ //
+ // DecodeError is fairly restrictive.
+ // even DecodeError::Custom just wraps a std::error::Error
+ // and this erases all rich information from miette. (why???)
+ //
+ // why does knuffel do this?
+ // from what i can tell, it doesn't even use DecodeError for much.
+ // it only ever converts them to a Report anyways!
+ // https://github.com/tailhook/knuffel/blob/c44c6b0c0f31ea6d1174d5d2ed41064922ea44ca/src/wrappers.rs#L55-L58
+ //
+ // besides like, allowing downstream users (such as us!)
+ // to match on parse failure, i don't understand why
+ // it doesn't just use a generic error type
+ //
+ // even the matching isn't consistent,
+ // because errors can also be omitted as ctx.emit_error.
+ // why does *that one* especially, require a DecodeError?
+ //
+ // anyways if you can make it format nicely, definitely do fix this
+ ctx.emit_error(DecodeError::unexpected(
+ &child.node_name,
+ "keybind",
+ "duplicate keybind",
+ ));
+ }
+ }
+ }
+ }
+
+ Ok(Self(binds))
+ }
+}
+
+impl<S> knuffel::Decode<S> for Bind
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_node(
+ node: &knuffel::ast::SpannedNode<S>,
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ if let Some(type_name) = &node.type_name {
+ ctx.emit_error(DecodeError::unexpected(
+ type_name,
+ "type name",
+ "no type name expected for this node",
+ ));
+ }
+
+ for val in node.arguments.iter() {
+ ctx.emit_error(DecodeError::unexpected(
+ &val.literal,
+ "argument",
+ "no arguments expected for this node",
+ ));
+ }
+
+ let key = node
+ .node_name
+ .parse::<Key>()
+ .map_err(|e| DecodeError::conversion(&node.node_name, e.wrap_err("invalid keybind")))?;
+
+ let mut repeat = true;
+ let mut cooldown = None;
+ let mut allow_when_locked = false;
+ let mut allow_when_locked_node = None;
+ let mut allow_inhibiting = true;
+ let mut hotkey_overlay_title = None;
+ for (name, val) in &node.properties {
+ match &***name {
+ "repeat" => {
+ repeat = knuffel::traits::DecodeScalar::decode(val, ctx)?;
+ }
+ "cooldown-ms" => {
+ cooldown = Some(Duration::from_millis(
+ knuffel::traits::DecodeScalar::decode(val, ctx)?,
+ ));
+ }
+ "allow-when-locked" => {
+ allow_when_locked = knuffel::traits::DecodeScalar::decode(val, ctx)?;
+ allow_when_locked_node = Some(name);
+ }
+ "allow-inhibiting" => {
+ allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?;
+ }
+ "hotkey-overlay-title" => {
+ hotkey_overlay_title = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?);
+ }
+ name_str => {
+ ctx.emit_error(DecodeError::unexpected(
+ name,
+ "property",
+ format!("unexpected property `{}`", name_str.escape_default()),
+ ));
+ }
+ }
+ }
+
+ let mut children = node.children();
+
+ // If the action is invalid but the key is fine, we still want to return something.
+ // That way, the parent can handle the existence of duplicate keybinds,
+ // even if their contents are not valid.
+ let dummy = Self {
+ key,
+ action: Action::Spawn(vec![]),
+ repeat: true,
+ cooldown: None,
+ allow_when_locked: false,
+ allow_inhibiting: true,
+ hotkey_overlay_title: None,
+ };
+
+ if let Some(child) = children.next() {
+ for unwanted_child in children {
+ ctx.emit_error(DecodeError::unexpected(
+ unwanted_child,
+ "node",
+ "only one action is allowed per keybind",
+ ));
+ }
+ match Action::decode_node(child, ctx) {
+ Ok(action) => {
+ if !matches!(action, Action::Spawn(_) | Action::SpawnSh(_)) {
+ if let Some(node) = allow_when_locked_node {
+ ctx.emit_error(DecodeError::unexpected(
+ node,
+ "property",
+ "allow-when-locked can only be set on spawn binds",
+ ));
+ }
+ }
+
+ // The toggle-inhibit action must always be uninhibitable.
+ // Otherwise, it would be impossible to trigger it.
+ if matches!(action, Action::ToggleKeyboardShortcutsInhibit) {
+ allow_inhibiting = false;
+ }
+
+ Ok(Self {
+ key,
+ action,
+ repeat,
+ cooldown,
+ allow_when_locked,
+ allow_inhibiting,
+ hotkey_overlay_title,
+ })
+ }
+ Err(e) => {
+ ctx.emit_error(e);
+ Ok(dummy)
+ }
+ }
+ } else {
+ ctx.emit_error(DecodeError::missing(
+ node,
+ "expected an action for this keybind",
+ ));
+ Ok(dummy)
+ }
+ }
+}
+
+impl FromStr for Key {
+ type Err = miette::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut modifiers = Modifiers::empty();
+
+ let mut split = s.split('+');
+ let key = split.next_back().unwrap();
+
+ for part in split {
+ let part = part.trim();
+ if part.eq_ignore_ascii_case("mod") {
+ modifiers |= Modifiers::COMPOSITOR
+ } else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") {
+ modifiers |= Modifiers::CTRL;
+ } else if part.eq_ignore_ascii_case("shift") {
+ modifiers |= Modifiers::SHIFT;
+ } else if part.eq_ignore_ascii_case("alt") {
+ modifiers |= Modifiers::ALT;
+ } else if part.eq_ignore_ascii_case("super") || par