diff options
| -rw-r--r-- | niri-config/src/binds.rs | 975 | ||||
| -rw-r--r-- | niri-config/src/input.rs | 3 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 967 |
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") || part.eq_ignore_ascii_case("win") { + modifiers |= Modifiers::SUPER; + } else if part.eq_ignore_ascii_case("iso_level3_shift") + || part.eq_ignore_ascii_case("mod5") + { + modifiers |= Modifiers::ISO_LEVEL3_SHIFT; + } else if part.eq_ignore_ascii_case("iso_level5_shift") + || part.eq_ignore_ascii_case("mod3") + { + modifiers |= Modifiers::ISO_LEVEL5_SHIFT; + } else { + return Err(miette!("invalid modifier: {part}")); + } + } + + let trigger = if key.eq_ignore_ascii_case("MouseLeft") { + Trigger::MouseLeft + } else if key.eq_ignore_ascii_case("MouseRight") { + Trigger::MouseRight + } else if key.eq_ignore_ascii_case("MouseMiddle") { + Trigger::MouseMiddle + } else if key.eq_ignore_ascii_case("MouseBack") { + Trigger::MouseBack + } else if key.eq_ignore_ascii_case("MouseForward") { + Trigger::MouseForward + } else if key.eq_ignore_ascii_case("WheelScrollDow |
