config: Extract binds

This commit is contained in:
Ivan Molodetskikh
2025-08-27 10:54:41 +03:00
parent 758a4c5e65
commit a03a7bd1a3
3 changed files with 979 additions and 966 deletions

975
niri-config/src/binds.rs Normal file
View File

@@ -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("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 keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
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_iso_level_shifts() {
assert_eq!(
"ISO_Level3_Shift+A".parse::<Key>().unwrap(),
Key {
trigger: Trigger::Keysym(Keysym::a),
modifiers: Modifiers::ISO_LEVEL3_SHIFT
},
);
assert_eq!(
"Mod5+A".parse::<Key>().unwrap(),
Key {
trigger: Trigger::Keysym(Keysym::a),
modifiers: Modifiers::ISO_LEVEL3_SHIFT
},
);
assert_eq!(
"ISO_Level5_Shift+A".parse::<Key>().unwrap(),
Key {
trigger: Trigger::Keysym(Keysym::a),
modifiers: Modifiers::ISO_LEVEL5_SHIFT
},
);
assert_eq!(
"Mod3+A".parse::<Key>().unwrap(),
Key {
trigger: Trigger::Keysym(Keysym::a),
modifiers: Modifiers::ISO_LEVEL5_SHIFT
},
);
}
}

View File

@@ -4,7 +4,8 @@ use miette::miette;
use smithay::input::keyboard::XkbConfig;
use smithay::reexports::input;
use crate::{FloatOrInt, Modifiers, Percent};
use crate::binds::Modifiers;
use crate::{FloatOrInt, Percent};
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Input {

File diff suppressed because it is too large Load Diff