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, KEYSYM_NO_FLAGS}; use smithay::input::keyboard::Keysym; use crate::recent_windows::{MruDirection, MruFilter, MruScope}; use crate::utils::{expect_only_children, MergeWith}; #[derive(Debug, Default, PartialEq)] pub struct Binds(pub Vec); #[derive(Debug, Clone, PartialEq)] pub struct Bind { pub key: Key, pub action: Action, pub repeat: bool, pub cooldown: Option, pub allow_when_locked: bool, pub allow_inhibiting: bool, pub hotkey_overlay_title: Option>, } #[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, #[knuffel(child)] pub lid_close: Option, #[knuffel(child)] pub tablet_mode_on: Option, #[knuffel(child)] pub tablet_mode_off: Option, } impl MergeWith for SwitchBinds { fn merge_with(&mut self, part: &SwitchBinds) { merge_clone_opt!( (self, part), lid_open, lid_close, tablet_mode_on, tablet_mode_off, ); } } #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct SwitchAction { #[knuffel(child, unwrap(arguments))] pub spawn: Vec, } // 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), SpawnSh(#[knuffel(argument)] String), DoScreenTransition(#[knuffel(property(name = "delay-ms"))] Option), #[knuffel(skip)] ConfirmScreenshot { write_to_disk: bool, }, #[knuffel(skip)] CancelScreenshot, #[knuffel(skip)] ScreenshotTogglePointer, Screenshot( #[knuffel(property(name = "show-pointer"), default = true)] bool, // Path; not settable from knuffel Option, ), ScreenshotScreen( #[knuffel(property(name = "write-to-disk"), default = true)] bool, #[knuffel(property(name = "show-pointer"), default = true)] bool, // Path; not settable from knuffel Option, ), ScreenshotWindow( #[knuffel(property(name = "write-to-disk"), default = true)] bool, // Path; not settable from knuffel Option, ), #[knuffel(skip)] ScreenshotWindowById { id: u64, write_to_disk: bool, path: Option, }, 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, SwitchPresetColumnWidthBack, SwitchPresetWindowWidth, SwitchPresetWindowWidthBack, #[knuffel(skip)] SwitchPresetWindowWidthById(u64), #[knuffel(skip)] SwitchPresetWindowWidthBackById(u64), SwitchPresetWindowHeight, SwitchPresetWindowHeightBack, #[knuffel(skip)] SwitchPresetWindowHeightById(u64), #[knuffel(skip)] SwitchPresetWindowHeightBackById(u64), MaximizeColumn, MaximizeWindowToEdges, #[knuffel(skip)] MaximizeWindowToEdgesById(u64), 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, x: PositionChange, y: PositionChange, }, ToggleWindowRuleOpacity, #[knuffel(skip)] ToggleWindowRuleOpacityById(u64), SetDynamicCastWindow, #[knuffel(skip)] SetDynamicCastWindowById(u64), SetDynamicCastMonitor(#[knuffel(argument)] Option), ClearDynamicCastTarget, ToggleOverview, OpenOverview, CloseOverview, #[knuffel(skip)] ToggleWindowUrgent(u64), #[knuffel(skip)] SetWindowUrgent(u64), #[knuffel(skip)] UnsetWindowUrgent(u64), #[knuffel(skip)] LoadConfigFile, #[knuffel(skip)] MruAdvance { direction: MruDirection, scope: Option, filter: Option, }, #[knuffel(skip)] MruConfirm, #[knuffel(skip)] MruCancel, #[knuffel(skip)] MruCloseCurrentWindow, #[knuffel(skip)] MruFirst, #[knuffel(skip)] MruLast, #[knuffel(skip)] MruSetScope(MruScope), #[knuffel(skip)] MruCycleScope, } impl From 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, path } => { Self::Screenshot(show_pointer, path) } niri_ipc::Action::ScreenshotScreen { write_to_disk, show_pointer, path, } => Self::ScreenshotScreen(write_to_disk, show_pointer, path), niri_ipc::Action::ScreenshotWindow { id: None, write_to_disk, path, } => Self::ScreenshotWindow(write_to_disk, path), niri_ipc::Action::ScreenshotWindow { id: Some(id), write_to_disk, path, } => Self::ScreenshotWindowById { id, write_to_disk, path, }, 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::SwitchPresetColumnWidthBack {} => Self::SwitchPresetColumnWidthBack, niri_ipc::Action::SwitchPresetWindowWidth { id: None } => Self::SwitchPresetWindowWidth, niri_ipc::Action::SwitchPresetWindowWidthBack { id: None } => { Self::SwitchPresetWindowWidthBack } niri_ipc::Action::SwitchPresetWindowWidth { id: Some(id) } => { Self::SwitchPresetWindowWidthById(id) } niri_ipc::Action::SwitchPresetWindowWidthBack { id: Some(id) } => { Self::SwitchPresetWindowWidthBackById(id) } niri_ipc::Action::SwitchPresetWindowHeight { id: None } => { Self::SwitchPresetWindowHeight } niri_ipc::Action::SwitchPresetWindowHeightBack { id: None } => { Self::SwitchPresetWindowHeightBack } niri_ipc::Action::SwitchPresetWindowHeight { id: Some(id) } => { Self::SwitchPresetWindowHeightById(id) } niri_ipc::Action::SwitchPresetWindowHeightBack { id: Some(id) } => { Self::SwitchPresetWindowHeightBackById(id) } niri_ipc::Action::MaximizeColumn {} => Self::MaximizeColumn, niri_ipc::Action::MaximizeWindowToEdges { id: None } => Self::MaximizeWindowToEdges, niri_ipc::Action::MaximizeWindowToEdges { id: Some(id) } => { Self::MaximizeWindowToEdgesById(id) } 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 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 knuffel::DecodeScalar for WorkspaceReference { fn type_check( type_name: &Option>, ctx: &mut knuffel::decode::Context, ) { 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, ctx: &mut knuffel::decode::Context, ) -> Result> { 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 knuffel::Decode for Binds where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { 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 knuffel::Decode for Bind where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { 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::() .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 { 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("WheelScrollDown") { Trigger::WheelScrollDown } else if key.eq_ignore_ascii_case("WheelScrollUp") { Trigger::WheelScrollUp } else if key.eq_ignore_ascii_case("WheelScrollLeft") { Trigger::WheelScrollLeft } else if key.eq_ignore_ascii_case("WheelScrollRight") { Trigger::WheelScrollRight } else if key.eq_ignore_ascii_case("TouchpadScrollDown") { Trigger::TouchpadScrollDown } else if key.eq_ignore_ascii_case("TouchpadScrollUp") { Trigger::TouchpadScrollUp } else if key.eq_ignore_ascii_case("TouchpadScrollLeft") { Trigger::TouchpadScrollLeft } else if key.eq_ignore_ascii_case("TouchpadScrollRight") { Trigger::TouchpadScrollRight } else { let mut keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE); // The keyboard event handling code can receive either // XF86ScreenSaver or XF86Screensaver, because there is no // case mapping defined between these keysyms. If we just // use the case-insensitive version of keysym_from_name it // is not possible to bind the uppercase version, because the // case-insensitive match prefers the lowercase version when // there is a choice. // // Therefore, when we match this key with the initial // case-insensitive match we try a further case-sensitive match // (so that either key can be bound). If that fails, we change // to the uppercase version because: // // - A comment in xkb_keysym_from_name (in libxkbcommon) tells us that the uppercase // version is the "best" of the two. [0] // - The xkbcommon crate only has a constant for ScreenSaver. [1] // // [0]: https://github.com/xkbcommon/libxkbcommon/blob/45a118d5325b051343b4b174f60c1434196fa7d4/src/keysym.c#L276 // [1]: https://docs.rs/xkbcommon/latest/xkbcommon/xkb/keysyms/index.html#:~:text=KEY%5FXF86ScreenSaver // // See https://github.com/YaLTeR/niri/issues/1969 if keysym == Keysym::XF86_Screensaver { keysym = keysym_from_name(key, KEYSYM_NO_FLAGS); if keysym.raw() == KEY_NoSymbol { keysym = Keysym::XF86_ScreenSaver; } } if keysym.raw() == KEY_NoSymbol { return Err(miette!("invalid key: {key}")); } Trigger::Keysym(keysym) }; Ok(Key { trigger, modifiers }) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_xf86_screensaver() { assert_eq!( "XF86ScreenSaver".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::XF86_ScreenSaver), modifiers: Modifiers::empty(), }, ); assert_eq!( "XF86Screensaver".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::XF86_Screensaver), modifiers: Modifiers::empty(), } ); assert_eq!( "xf86screensaver".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::XF86_ScreenSaver), modifiers: Modifiers::empty(), } ); } #[test] fn parse_iso_level_shifts() { assert_eq!( "ISO_Level3_Shift+A".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::a), modifiers: Modifiers::ISO_LEVEL3_SHIFT }, ); assert_eq!( "Mod5+A".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::a), modifiers: Modifiers::ISO_LEVEL3_SHIFT }, ); assert_eq!( "ISO_Level5_Shift+A".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::a), modifiers: Modifiers::ISO_LEVEL5_SHIFT }, ); assert_eq!( "Mod3+A".parse::().unwrap(), Key { trigger: Trigger::Keysym(Keysym::a), modifiers: Modifiers::ISO_LEVEL5_SHIFT }, ); } }