#[macro_use] extern crate tracing; use std::collections::HashSet; use std::ffi::OsStr; use std::fs::{self, File}; use std::io::Write; use std::ops::{Mul, MulAssign}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::Duration; use bitflags::bitflags; use knuffel::errors::DecodeError; use knuffel::Decode as _; use layer_rule::LayerRule; use miette::{miette, Context, IntoDiagnostic}; use niri_ipc::{ ColumnDisplay, ConfiguredMode, LayoutSwitchTarget, PositionChange, SizeChange, Transform, WorkspaceReferenceArg, }; use smithay::backend::renderer::Color32F; use smithay::input::keyboard::keysyms::KEY_NoSymbol; use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE}; use smithay::input::keyboard::{Keysym, XkbConfig}; use smithay::reexports::input; pub const DEFAULT_BACKGROUND_COLOR: Color = Color::from_array_unpremul([0.25, 0.25, 0.25, 1.]); pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15, 0.15, 1.]); pub mod layer_rule; mod utils; pub use utils::RegexEq; #[derive(knuffel::Decode, Debug, PartialEq)] pub struct Config { #[knuffel(child, default)] pub input: Input, #[knuffel(children(name = "output"))] pub outputs: Outputs, #[knuffel(children(name = "spawn-at-startup"))] pub spawn_at_startup: Vec, #[knuffel(children(name = "spawn-at-startup-sh"))] pub spawn_at_startup_sh: Vec, #[knuffel(child, default)] pub layout: Layout, #[knuffel(child, default)] pub prefer_no_csd: bool, #[knuffel(child, default)] pub cursor: Cursor, #[knuffel( child, unwrap(argument), default = Some(String::from( "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" ))) ] pub screenshot_path: Option, #[knuffel(child, default)] pub clipboard: Clipboard, #[knuffel(child, default)] pub hotkey_overlay: HotkeyOverlay, #[knuffel(child, default)] pub config_notification: ConfigNotification, #[knuffel(child, default)] pub animations: Animations, #[knuffel(child, default)] pub gestures: Gestures, #[knuffel(child, default)] pub overview: Overview, #[knuffel(child, default)] pub environment: Environment, #[knuffel(child, default)] pub xwayland_satellite: XwaylandSatellite, #[knuffel(children(name = "window-rule"))] pub window_rules: Vec, #[knuffel(children(name = "layer-rule"))] pub layer_rules: Vec, #[knuffel(child, default)] pub binds: Binds, #[knuffel(child, default)] pub switch_events: SwitchBinds, #[knuffel(child, default)] pub debug: DebugConfig, #[knuffel(children(name = "workspace"))] pub workspaces: Vec, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Input { #[knuffel(child, default)] pub keyboard: Keyboard, #[knuffel(child, default)] pub touchpad: Touchpad, #[knuffel(child, default)] pub mouse: Mouse, #[knuffel(child, default)] pub trackpoint: Trackpoint, #[knuffel(child, default)] pub trackball: Trackball, #[knuffel(child, default)] pub tablet: Tablet, #[knuffel(child, default)] pub touch: Touch, #[knuffel(child)] pub disable_power_key_handling: bool, #[knuffel(child)] pub warp_mouse_to_focus: Option, #[knuffel(child)] pub focus_follows_mouse: Option, #[knuffel(child)] pub workspace_auto_back_and_forth: bool, #[knuffel(child, unwrap(argument, str))] pub mod_key: Option, #[knuffel(child, unwrap(argument, str))] pub mod_key_nested: Option, } #[derive(knuffel::Decode, Debug, PartialEq, Eq)] pub struct Keyboard { #[knuffel(child, default)] pub xkb: Xkb, // The defaults were chosen to match wlroots and sway. #[knuffel(child, unwrap(argument), default = Self::default().repeat_delay)] pub repeat_delay: u16, #[knuffel(child, unwrap(argument), default = Self::default().repeat_rate)] pub repeat_rate: u8, #[knuffel(child, unwrap(argument), default)] pub track_layout: TrackLayout, #[knuffel(child)] pub numlock: bool, } impl Default for Keyboard { fn default() -> Self { Self { xkb: Default::default(), repeat_delay: 600, repeat_rate: 25, track_layout: Default::default(), numlock: Default::default(), } } } #[derive(knuffel::Decode, Debug, Default, PartialEq, Eq, Clone)] pub struct Xkb { #[knuffel(child, unwrap(argument), default)] pub rules: String, #[knuffel(child, unwrap(argument), default)] pub model: String, #[knuffel(child, unwrap(argument), default)] pub layout: String, #[knuffel(child, unwrap(argument), default)] pub variant: String, #[knuffel(child, unwrap(argument))] pub options: Option, #[knuffel(child, unwrap(argument))] pub file: Option, } impl Xkb { pub fn to_xkb_config(&self) -> XkbConfig<'_> { XkbConfig { rules: &self.rules, model: &self.model, layout: &self.layout, variant: &self.variant, options: self.options.clone(), } } } #[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq, Clone, Copy)] pub enum CenterFocusedColumn { /// Focusing a column will not center the column. #[default] Never, /// The focused column will always be centered. Always, /// Focusing a column will center it if it doesn't fit on the screen together with the /// previously focused column. OnOverflow, } #[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)] pub enum TrackLayout { /// The layout change is global. #[default] Global, /// The layout change is window local. Window, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct ScrollFactor { #[knuffel(argument)] pub base: Option>, #[knuffel(property)] pub horizontal: Option>, #[knuffel(property)] pub vertical: Option>, } impl ScrollFactor { pub fn h_v_factors(&self) -> (f64, f64) { let base_value = self.base.map(|f| f.0).unwrap_or(1.0); let h = self.horizontal.map(|f| f.0).unwrap_or(base_value); let v = self.vertical.map(|f| f.0).unwrap_or(base_value); (h, v) } } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Touchpad { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub tap: bool, #[knuffel(child)] pub dwt: bool, #[knuffel(child)] pub dwtp: bool, #[knuffel(child, unwrap(argument))] pub drag: Option, #[knuffel(child)] pub drag_lock: bool, #[knuffel(child)] pub natural_scroll: bool, #[knuffel(child, unwrap(argument, str))] pub click_method: Option, #[knuffel(child, unwrap(argument), default)] pub accel_speed: FloatOrInt<-1, 1>, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[knuffel(child, unwrap(argument))] pub scroll_button: Option, #[knuffel(child)] pub scroll_button_lock: bool, #[knuffel(child, unwrap(argument, str))] pub tap_button_map: Option, #[knuffel(child)] pub left_handed: bool, #[knuffel(child)] pub disabled_on_external_mouse: bool, #[knuffel(child)] pub middle_emulation: bool, #[knuffel(child)] pub scroll_factor: Option, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Mouse { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub natural_scroll: bool, #[knuffel(child, unwrap(argument), default)] pub accel_speed: FloatOrInt<-1, 1>, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[knuffel(child, unwrap(argument))] pub scroll_button: Option, #[knuffel(child)] pub scroll_button_lock: bool, #[knuffel(child)] pub left_handed: bool, #[knuffel(child)] pub middle_emulation: bool, #[knuffel(child)] pub scroll_factor: Option, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Trackpoint { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub natural_scroll: bool, #[knuffel(child, unwrap(argument), default)] pub accel_speed: FloatOrInt<-1, 1>, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[knuffel(child, unwrap(argument))] pub scroll_button: Option, #[knuffel(child)] pub scroll_button_lock: bool, #[knuffel(child)] pub left_handed: bool, #[knuffel(child)] pub middle_emulation: bool, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Trackball { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub natural_scroll: bool, #[knuffel(child, unwrap(argument), default)] pub accel_speed: FloatOrInt<-1, 1>, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[knuffel(child, unwrap(argument))] pub scroll_button: Option, #[knuffel(child)] pub scroll_button_lock: bool, #[knuffel(child)] pub left_handed: bool, #[knuffel(child)] pub middle_emulation: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ClickMethod { Clickfinger, ButtonAreas, } impl From for input::ClickMethod { fn from(value: ClickMethod) -> Self { match value { ClickMethod::Clickfinger => Self::Clickfinger, ClickMethod::ButtonAreas => Self::ButtonAreas, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccelProfile { Adaptive, Flat, } impl From for input::AccelProfile { fn from(value: AccelProfile) -> Self { match value { AccelProfile::Adaptive => Self::Adaptive, AccelProfile::Flat => Self::Flat, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ScrollMethod { NoScroll, TwoFinger, Edge, OnButtonDown, } impl From for input::ScrollMethod { fn from(value: ScrollMethod) -> Self { match value { ScrollMethod::NoScroll => Self::NoScroll, ScrollMethod::TwoFinger => Self::TwoFinger, ScrollMethod::Edge => Self::Edge, ScrollMethod::OnButtonDown => Self::OnButtonDown, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TapButtonMap { LeftRightMiddle, LeftMiddleRight, } impl From for input::TapButtonMap { fn from(value: TapButtonMap) -> Self { match value { TapButtonMap::LeftRightMiddle => Self::LeftRightMiddle, TapButtonMap::LeftMiddleRight => Self::LeftMiddleRight, } } } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Tablet { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(arguments))] pub calibration_matrix: Option>, #[knuffel(child, unwrap(argument))] pub map_to_output: Option, #[knuffel(child)] pub left_handed: bool, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Touch { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument))] pub map_to_output: Option, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct FocusFollowsMouse { #[knuffel(property, str)] pub max_scroll_amount: Option, } #[derive(knuffel::Decode, Debug, PartialEq, Eq, Clone, Copy)] pub struct WarpMouseToFocus { #[knuffel(property, str)] pub mode: Option, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum WarpMouseToFocusMode { CenterXy, CenterXyAlways, } impl FromStr for WarpMouseToFocusMode { type Err = miette::Error; fn from_str(s: &str) -> Result { match s { "center-xy" => Ok(Self::CenterXy), "center-xy-always" => Ok(Self::CenterXyAlways), _ => Err(miette!( r#"invalid mode for warp-mouse-to-focus, can be "center-xy" or "center-xy-always" (or leave unset for separate centering)"# )), } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct Percent(pub f64); #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ModKey { Ctrl, Shift, Alt, Super, IsoLevel3Shift, IsoLevel5Shift, } impl ModKey { pub fn to_modifiers(&self) -> Modifiers { match self { ModKey::Ctrl => Modifiers::CTRL, ModKey::Shift => Modifiers::SHIFT, ModKey::Alt => Modifiers::ALT, ModKey::Super => Modifiers::SUPER, ModKey::IsoLevel3Shift => Modifiers::ISO_LEVEL3_SHIFT, ModKey::IsoLevel5Shift => Modifiers::ISO_LEVEL5_SHIFT, } } } #[derive(Debug, Default, Clone, PartialEq)] pub struct Outputs(pub Vec); #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Output { #[knuffel(child)] pub off: bool, #[knuffel(argument)] pub name: String, #[knuffel(child, unwrap(argument))] pub scale: Option>, #[knuffel(child, unwrap(argument, str), default = Transform::Normal)] pub transform: Transform, #[knuffel(child)] pub position: Option, #[knuffel(child, unwrap(argument, str))] pub mode: Option, #[knuffel(child)] pub variable_refresh_rate: Option, #[knuffel(child)] pub focus_at_startup: bool, #[knuffel(child)] pub background_color: Option, #[knuffel(child)] pub backdrop_color: Option, } impl Output { pub fn is_vrr_always_on(&self) -> bool { self.variable_refresh_rate == Some(Vrr { on_demand: false }) } pub fn is_vrr_on_demand(&self) -> bool { self.variable_refresh_rate == Some(Vrr { on_demand: true }) } pub fn is_vrr_always_off(&self) -> bool { self.variable_refresh_rate.is_none() } } impl Default for Output { fn default() -> Self { Self { off: false, focus_at_startup: false, name: String::new(), scale: None, transform: Transform::Normal, position: None, mode: None, variable_refresh_rate: None, background_color: None, backdrop_color: None, } } } #[derive(Debug, Clone)] pub struct OutputName { pub connector: String, pub make: Option, pub model: Option, pub serial: Option, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] pub struct Position { #[knuffel(property)] pub x: i32, #[knuffel(property)] pub y: i32, } #[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)] pub struct Vrr { #[knuffel(property, default = false)] pub on_demand: bool, } // MIN and MAX generics are only used during parsing to check the value. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct FloatOrInt(pub f64); #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Layout { #[knuffel(child, default)] pub focus_ring: FocusRing, #[knuffel(child, default)] pub border: Border, #[knuffel(child, default)] pub shadow: Shadow, #[knuffel(child, default)] pub tab_indicator: TabIndicator, #[knuffel(child, default)] pub insert_hint: InsertHint, #[knuffel(child, unwrap(children), default)] pub preset_column_widths: Vec, #[knuffel(child)] pub default_column_width: Option, #[knuffel(child, unwrap(children), default)] pub preset_window_heights: Vec, #[knuffel(child, unwrap(argument), default)] pub center_focused_column: CenterFocusedColumn, #[knuffel(child)] pub always_center_single_column: bool, #[knuffel(child)] pub empty_workspace_above_first: bool, #[knuffel(child, unwrap(argument, str), default = Self::default().default_column_display)] pub default_column_display: ColumnDisplay, #[knuffel(child, unwrap(argument), default = Self::default().gaps)] pub gaps: FloatOrInt<0, 65535>, #[knuffel(child, default)] pub struts: Struts, #[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)] pub background_color: Color, } impl Default for Layout { fn default() -> Self { Self { focus_ring: Default::default(), border: Default::default(), shadow: Default::default(), tab_indicator: Default::default(), insert_hint: Default::default(), preset_column_widths: Default::default(), default_column_width: Default::default(), center_focused_column: Default::default(), always_center_single_column: false, empty_workspace_above_first: false, default_column_display: ColumnDisplay::Normal, gaps: FloatOrInt(16.), struts: Default::default(), preset_window_heights: Default::default(), background_color: DEFAULT_BACKGROUND_COLOR, } } } #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct SpawnAtStartup { #[knuffel(arguments)] pub command: Vec, } #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct SpawnAtStartupSh { #[knuffel(argument)] pub command: String, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct FocusRing { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = Self::default().width)] pub width: FloatOrInt<0, 65535>, #[knuffel(child, default = Self::default().active_color)] pub active_color: Color, #[knuffel(child, default = Self::default().inactive_color)] pub inactive_color: Color, #[knuffel(child, default = Self::default().urgent_color)] pub urgent_color: Color, #[knuffel(child)] pub active_gradient: Option, #[knuffel(child)] pub inactive_gradient: Option, #[knuffel(child)] pub urgent_gradient: Option, } impl Default for FocusRing { fn default() -> Self { Self { off: false, width: FloatOrInt(4.), active_color: Color::from_rgba8_unpremul(127, 200, 255, 255), inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255), urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255), active_gradient: None, inactive_gradient: None, urgent_gradient: None, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct Gradient { #[knuffel(property, str)] pub from: Color, #[knuffel(property, str)] pub to: Color, #[knuffel(property, default = 180)] pub angle: i16, #[knuffel(property, default)] pub relative_to: GradientRelativeTo, #[knuffel(property(name = "in"), str, default)] pub in_: GradientInterpolation, } impl From for Gradient { fn from(value: Color) -> Self { Self { from: value, to: value, angle: 0, relative_to: GradientRelativeTo::Window, in_: GradientInterpolation::default(), } } } #[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum GradientRelativeTo { #[default] Window, WorkspaceView, } #[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct GradientInterpolation { pub color_space: GradientColorSpace, pub hue_interpolation: HueInterpolation, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum GradientColorSpace { #[default] Srgb, SrgbLinear, Oklab, Oklch, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum HueInterpolation { #[default] Shorter, Longer, Increasing, Decreasing, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct Border { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = Self::default().width)] pub width: FloatOrInt<0, 65535>, #[knuffel(child, default = Self::default().active_color)] pub active_color: Color, #[knuffel(child, default = Self::default().inactive_color)] pub inactive_color: Color, #[knuffel(child, default = Self::default().urgent_color)] pub urgent_color: Color, #[knuffel(child)] pub active_gradient: Option, #[knuffel(child)] pub inactive_gradient: Option, #[knuffel(child)] pub urgent_gradient: Option, } impl Default for Border { fn default() -> Self { Self { off: true, width: FloatOrInt(4.), active_color: Color::from_rgba8_unpremul(255, 200, 127, 255), inactive_color: Color::from_rgba8_unpremul(80, 80, 80, 255), urgent_color: Color::from_rgba8_unpremul(155, 0, 0, 255), active_gradient: None, inactive_gradient: None, urgent_gradient: None, } } } impl From for FocusRing { fn from(value: Border) -> Self { Self { off: value.off, width: value.width, active_color: value.active_color, inactive_color: value.inactive_color, urgent_color: value.urgent_color, active_gradient: value.active_gradient, inactive_gradient: value.inactive_gradient, urgent_gradient: value.urgent_gradient, } } } impl From for Border { fn from(value: FocusRing) -> Self { Self { off: value.off, width: value.width, active_color: value.active_color, inactive_color: value.inactive_color, urgent_color: value.urgent_color, active_gradient: value.active_gradient, inactive_gradient: value.inactive_gradient, urgent_gradient: value.urgent_gradient, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct Shadow { #[knuffel(child)] pub on: bool, #[knuffel(child, default = Self::default().offset)] pub offset: ShadowOffset, #[knuffel(child, unwrap(argument), default = Self::default().softness)] pub softness: FloatOrInt<0, 1024>, #[knuffel(child, unwrap(argument), default = Self::default().spread)] pub spread: FloatOrInt<-1024, 1024>, #[knuffel(child, unwrap(argument), default = Self::default().draw_behind_window)] pub draw_behind_window: bool, #[knuffel(child, default = Self::default().color)] pub color: Color, #[knuffel(child)] pub inactive_color: Option, } impl Default for Shadow { fn default() -> Self { Self { on: false, offset: ShadowOffset { x: FloatOrInt(0.), y: FloatOrInt(5.), }, softness: FloatOrInt(30.), spread: FloatOrInt(5.), draw_behind_window: false, color: Color::from_rgba8_unpremul(0, 0, 0, 0x70), inactive_color: None, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct ShadowOffset { #[knuffel(property, default)] pub x: FloatOrInt<-65535, 65535>, #[knuffel(property, default)] pub y: FloatOrInt<-65535, 65535>, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct WorkspaceShadow { #[knuffel(child)] pub off: bool, #[knuffel(child, default = Self::default().offset)] pub offset: ShadowOffset, #[knuffel(child, unwrap(argument), default = Self::default().softness)] pub softness: FloatOrInt<0, 1024>, #[knuffel(child, unwrap(argument), default = Self::default().spread)] pub spread: FloatOrInt<-1024, 1024>, #[knuffel(child, default = Self::default().color)] pub color: Color, } impl Default for WorkspaceShadow { fn default() -> Self { Self { off: false, offset: ShadowOffset { x: FloatOrInt(0.), y: FloatOrInt(10.), }, softness: FloatOrInt(40.), spread: FloatOrInt(10.), color: Color::from_rgba8_unpremul(0, 0, 0, 0x50), } } } impl From for Shadow { fn from(value: WorkspaceShadow) -> Self { Self { on: !value.off, offset: value.offset, softness: value.softness, spread: value.spread, draw_behind_window: false, color: value.color, inactive_color: None, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct TabIndicator { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub hide_when_single_tab: bool, #[knuffel(child)] pub place_within_column: bool, #[knuffel(child, unwrap(argument), default = Self::default().gap)] pub gap: FloatOrInt<-65535, 65535>, #[knuffel(child, unwrap(argument), default = Self::default().width)] pub width: FloatOrInt<0, 65535>, #[knuffel(child, default = Self::default().length)] pub length: TabIndicatorLength, #[knuffel(child, unwrap(argument), default = Self::default().position)] pub position: TabIndicatorPosition, #[knuffel(child, unwrap(argument), default = Self::default().gaps_between_tabs)] pub gaps_between_tabs: FloatOrInt<0, 65535>, #[knuffel(child, unwrap(argument), default = Self::default().corner_radius)] pub corner_radius: FloatOrInt<0, 65535>, #[knuffel(child)] pub active_color: Option, #[knuffel(child)] pub inactive_color: Option, #[knuffel(child)] pub urgent_color: Option, #[knuffel(child)] pub active_gradient: Option, #[knuffel(child)] pub inactive_gradient: Option, #[knuffel(child)] pub urgent_gradient: Option, } impl Default for TabIndicator { fn default() -> Self { Self { off: false, hide_when_single_tab: false, place_within_column: false, gap: FloatOrInt(5.), width: FloatOrInt(4.), length: TabIndicatorLength { total_proportion: Some(0.5), }, position: TabIndicatorPosition::Left, gaps_between_tabs: FloatOrInt(0.), corner_radius: FloatOrInt(0.), active_color: None, inactive_color: None, urgent_color: None, active_gradient: None, inactive_gradient: None, urgent_gradient: None, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct TabIndicatorLength { #[knuffel(property)] pub total_proportion: Option, } #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] pub enum TabIndicatorPosition { Left, Right, Top, Bottom, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct InsertHint { #[knuffel(child)] pub off: bool, #[knuffel(child, default = Self::default().color)] pub color: Color, #[knuffel(child)] pub gradient: Option, } impl Default for InsertHint { fn default() -> Self { Self { off: false, color: Color::from_rgba8_unpremul(127, 200, 255, 128), gradient: None, } } } /// RGB color in [0, 1] with unpremultiplied alpha. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Color { pub r: f32, pub g: f32, pub b: f32, pub a: f32, } impl Color { pub const fn new_unpremul(r: f32, g: f32, b: f32, a: f32) -> Self { Self { r, g, b, a } } pub fn from_rgba8_unpremul(r: u8, g: u8, b: u8, a: u8) -> Self { Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) } pub fn from_array_premul([r, g, b, a]: [f32; 4]) -> Self { let a = a.clamp(0., 1.); if a == 0. { Self::new_unpremul(0., 0., 0., 0.) } else { Self { r: (r / a).clamp(0., 1.), g: (g / a).clamp(0., 1.), b: (b / a).clamp(0., 1.), a, } } } pub const fn from_array_unpremul([r, g, b, a]: [f32; 4]) -> Self { Self { r, g, b, a } } pub fn from_color32f(color: Color32F) -> Self { Self::from_array_premul(color.components()) } pub fn to_array_unpremul(self) -> [f32; 4] { [self.r, self.g, self.b, self.a] } pub fn to_array_premul(self) -> [f32; 4] { let [r, g, b, a] = [self.r, self.g, self.b, self.a]; [r * a, g * a, b * a, a] } } impl Mul for Color { type Output = Self; fn mul(mut self, rhs: f32) -> Self::Output { self.a *= rhs; self } } impl MulAssign for Color { fn mul_assign(&mut self, rhs: f32) { self.a *= rhs; } } #[derive(knuffel::Decode, Debug, PartialEq)] pub struct Cursor { #[knuffel(child, unwrap(argument), default = String::from("default"))] pub xcursor_theme: String, #[knuffel(child, unwrap(argument), default = 24)] pub xcursor_size: u8, #[knuffel(child)] pub hide_when_typing: bool, #[knuffel(child, unwrap(argument))] pub hide_after_inactive_ms: Option, } impl Default for Cursor { fn default() -> Self { Self { xcursor_theme: String::from("default"), xcursor_size: 24, hide_when_typing: false, hide_after_inactive_ms: None, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub enum PresetSize { Proportion(#[knuffel(argument)] f64), Fixed(#[knuffel(argument)] i32), } impl From for SizeChange { fn from(value: PresetSize) -> Self { match value { PresetSize::Proportion(prop) => SizeChange::SetProportion(prop * 100.), PresetSize::Fixed(fixed) => SizeChange::SetFixed(fixed), } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct DefaultPresetSize(pub Option); #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct Struts { #[knuffel(child, unwrap(argument), default)] pub left: FloatOrInt<-65535, 65535>, #[knuffel(child, unwrap(argument), default)] pub right: FloatOrInt<-65535, 65535>, #[knuffel(child, unwrap(argument), default)] pub top: FloatOrInt<-65535, 65535>, #[knuffel(child, unwrap(argument), default)] pub bottom: FloatOrInt<-65535, 65535>, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct HotkeyOverlay { #[knuffel(child)] pub skip_at_startup: bool, #[knuffel(child)] pub hide_not_bound: bool, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct ConfigNotification { #[knuffel(child)] pub disable_failed: bool, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Clipboard { #[knuffel(child)] pub disable_primary: bool, } #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Animations { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = FloatOrInt(1.))] pub slowdown: FloatOrInt<0, { i32::MAX }>, #[knuffel(child, default)] pub workspace_switch: WorkspaceSwitchAnim, #[knuffel(child, default)] pub window_open: WindowOpenAnim, #[knuffel(child, default)] pub window_close: WindowCloseAnim, #[knuffel(child, default)] pub horizontal_view_movement: HorizontalViewMovementAnim, #[knuffel(child, default)] pub window_movement: WindowMovementAnim, #[knuffel(child, default)] pub window_resize: WindowResizeAnim, #[knuffel(child, default)] pub config_notification_open_close: ConfigNotificationOpenCloseAnim, #[knuffel(child, default)] pub screenshot_ui_open: ScreenshotUiOpenAnim, #[knuffel(child, default)] pub overview_open_close: OverviewOpenCloseAnim, } impl Default for Animations { fn default() -> Self { Self { off: false, slowdown: FloatOrInt(1.), workspace_switch: Default::default(), horizontal_view_movement: Default::default(), window_movement: Default::default(), window_open: Default::default(), window_close: Default::default(), window_resize: Default::default(), config_notification_open_close: Default::default(), screenshot_ui_open: Default::default(), overview_open_close: Default::default(), } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct WorkspaceSwitchAnim(pub Animation); impl Default for WorkspaceSwitchAnim { fn default() -> Self { Self(Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 1., stiffness: 1000, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, PartialEq)] pub struct WindowOpenAnim { pub anim: Animation, pub custom_shader: Option, } impl Default for WindowOpenAnim { fn default() -> Self { Self { anim: Animation { off: false, kind: AnimationKind::Easing(EasingParams { duration_ms: 150, curve: AnimationCurve::EaseOutExpo, }), }, custom_shader: None, } } } #[derive(Debug, Clone, PartialEq)] pub struct WindowCloseAnim { pub anim: Animation, pub custom_shader: Option, } impl Default for WindowCloseAnim { fn default() -> Self { Self { anim: Animation { off: false, kind: AnimationKind::Easing(EasingParams { duration_ms: 150, curve: AnimationCurve::EaseOutQuad, }), }, custom_shader: None, } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct HorizontalViewMovementAnim(pub Animation); impl Default for HorizontalViewMovementAnim { fn default() -> Self { Self(Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct WindowMovementAnim(pub Animation); impl Default for WindowMovementAnim { fn default() -> Self { Self(Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, PartialEq)] pub struct WindowResizeAnim { pub anim: Animation, pub custom_shader: Option, } impl Default for WindowResizeAnim { fn default() -> Self { Self { anim: Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }, custom_shader: None, } } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct ConfigNotificationOpenCloseAnim(pub Animation); impl Default for ConfigNotificationOpenCloseAnim { fn default() -> Self { Self(Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 0.6, stiffness: 1000, epsilon: 0.001, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct ScreenshotUiOpenAnim(pub Animation); impl Default for ScreenshotUiOpenAnim { fn default() -> Self { Self(Animation { off: false, kind: AnimationKind::Easing(EasingParams { duration_ms: 200, curve: AnimationCurve::EaseOutQuad, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct OverviewOpenCloseAnim(pub Animation); impl Default for OverviewOpenCloseAnim { fn default() -> Self { Self(Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 1., stiffness: 800, epsilon: 0.0001, }), }) } } #[derive(Debug, Clone, Copy, PartialEq)] pub struct Animation { pub off: bool, pub kind: AnimationKind, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum AnimationKind { Easing(EasingParams), Spring(SpringParams), } #[derive(Debug, Clone, Copy, PartialEq)] pub struct EasingParams { pub duration_ms: u32, pub curve: AnimationCurve, } #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] pub enum AnimationCurve { Linear, EaseOutQuad, EaseOutCubic, EaseOutExpo, } #[derive(Debug, Clone, Copy, PartialEq)] pub struct SpringParams { pub damping_ratio: f64, pub stiffness: u32, pub epsilon: f64, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct Gestures { #[knuffel(child, default)] pub dnd_edge_view_scroll: DndEdgeViewScroll, #[knuffel(child, default)] pub dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch, #[knuffel(child, default)] pub hot_corners: HotCorners, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct DndEdgeViewScroll { #[knuffel(child, unwrap(argument), default = Self::default().trigger_width)] pub trigger_width: FloatOrInt<0, 65535>, #[knuffel(child, unwrap(argument), default = Self::default().delay_ms)] pub delay_ms: u16, #[knuffel(child, unwrap(argument), default = Self::default().max_speed)] pub max_speed: FloatOrInt<0, 1_000_000>, } impl Default for DndEdgeViewScroll { fn default() -> Self { Self { trigger_width: FloatOrInt(30.), // Taken from GTK 4. delay_ms: 100, max_speed: FloatOrInt(1500.), } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct DndEdgeWorkspaceSwitch { #[knuffel(child, unwrap(argument), default = Self::default().trigger_height)] pub trigger_height: FloatOrInt<0, 65535>, #[knuffel(child, unwrap(argument), default = Self::default().delay_ms)] pub delay_ms: u16, #[knuffel(child, unwrap(argument), default = Self::default().max_speed)] pub max_speed: FloatOrInt<0, 1_000_000>, } impl Default for DndEdgeWorkspaceSwitch { fn default() -> Self { Self { trigger_height: FloatOrInt(50.), delay_ms: 100, max_speed: FloatOrInt(1500.), } } } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct HotCorners { #[knuffel(child)] pub off: bool, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct Overview { #[knuffel(child, unwrap(argument), default = Self::default().zoom)] pub zoom: FloatOrInt<0, 1>, #[knuffel(child, default = Self::default().backdrop_color)] pub backdrop_color: Color, #[knuffel(child, default)] pub workspace_shadow: WorkspaceShadow, } impl Default for Overview { fn default() -> Self { Self { zoom: FloatOrInt(0.5), backdrop_color: DEFAULT_BACKDROP_COLOR, workspace_shadow: WorkspaceShadow::default(), } } } #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)] pub struct Environment(#[knuffel(children)] pub Vec); #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct EnvironmentVariable { #[knuffel(node_name)] pub name: String, #[knuffel(argument)] pub value: Option, } #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct XwaylandSatellite { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = Self::default().path)] pub path: String, } impl Default for XwaylandSatellite { fn default() -> Self { Self { off: false, path: String::from("xwayland-satellite"), } } } #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct Workspace { #[knuffel(argument)] pub name: WorkspaceName, #[knuffel(child, unwrap(argument))] pub open_on_output: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct WorkspaceName(pub String); #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] pub struct WindowRule { #[knuffel(children(name = "match"))] pub matches: Vec, #[knuffel(children(name = "exclude"))] pub excludes: Vec, // Rules applied at initial configure. #[knuffel(child)] pub default_column_width: Option, #[knuffel(child)] pub default_window_height: Option, #[knuffel(child, unwrap(argument))] pub open_on_output: Option, #[knuffel(child, unwrap(argument))] pub open_on_workspace: Option, #[knuffel(child, unwrap(argument))] pub open_maximized: Option, #[knuffel(child, unwrap(argument))] pub open_fullscreen: Option, #[knuffel(child, unwrap(argument))] pub open_floating: Option, #[knuffel(child, unwrap(argument))] pub open_focused: Option, // Rules applied dynamically. #[knuffel(child, unwrap(argument))] pub min_width: Option, #[knuffel(child, unwrap(argument))] pub min_height: Option, #[knuffel(child, unwrap(argument))] pub max_width: Option, #[knuffel(child, unwrap(argument))] pub max_height: Option, #[knuffel(child, default)] pub focus_ring: BorderRule, #[knuffel(child, default)] pub border: BorderRule, #[knuffel(child, default)] pub shadow: ShadowRule, #[knuffel(child, default)] pub tab_indicator: TabIndicatorRule, #[knuffel(child, unwrap(argument))] pub draw_border_with_background: Option, #[knuffel(child, unwrap(argument))] pub opacity: Option, #[knuffel(child)] pub geometry_corner_radius: Option, #[knuffel(child, unwrap(argument))] pub clip_to_geometry: Option, #[knuffel(child, unwrap(argument))] pub baba_is_float: Option, #[knuffel(child, unwrap(argument))] pub block_out_from: Option, #[knuffel(child, unwrap(argument))] pub variable_refresh_rate: Option, #[knuffel(child, unwrap(argument, str))] pub default_column_display: Option, #[knuffel(child)] pub default_floating_position: Option, #[knuffel(child, unwrap(argument))] pub scroll_factor: Option>, #[knuffel(child, unwrap(argument))] pub tiled_state: Option, } #[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] pub struct Match { #[knuffel(property, str)] pub app_id: Option, #[knuffel(property, str)] pub title: Option, #[knuffel(property)] pub is_active: Option, #[knuffel(property)] pub is_focused: Option, #[knuffel(property)] pub is_active_in_column: Option, #[knuffel(property)] pub is_floating: Option, #[knuffel(property)] pub is_window_cast_target: Option, #[knuffel(property)] pub is_urgent: Option, #[knuffel(property)] pub at_startup: Option, } #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct CornerRadius { pub top_left: f32, pub top_right: f32, pub bottom_right: f32, pub bottom_left: f32, } impl From for [f32; 4] { fn from(value: CornerRadius) -> Self { [ value.top_left, value.top_right, value.bottom_right, value.bottom_left, ] } } impl From for CornerRadius { fn from(value: f32) -> Self { Self { top_left: value, top_right: value, bottom_right: value, bottom_left: value, } } } #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)] pub enum BlockOutFrom { Screencast, ScreenCapture, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct BorderRule { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub on: bool, #[knuffel(child, unwrap(argument))] pub width: Option>, #[knuffel(child)] pub active_color: Option, #[knuffel(child)] pub inactive_color: Option, #[knuffel(child)] pub urgent_color: Option, #[knuffel(child)] pub active_gradient: Option, #[knuffel(child)] pub inactive_gradient: Option, #[knuffel(child)] pub urgent_gradient: Option, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct ShadowRule { #[knuffel(child)] pub off: bool, #[knuffel(child)] pub on: bool, #[knuffel(child)] pub offset: Option, #[knuffel(child, unwrap(argument))] pub softness: Option>, #[knuffel(child, unwrap(argument))] pub spread: Option>, #[knuffel(child, unwrap(argument))] pub draw_behind_window: Option, #[knuffel(child)] pub color: Option, #[knuffel(child)] pub inactive_color: Option, } #[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] pub struct TabIndicatorRule { #[knuffel(child)] pub active_color: Option, #[knuffel(child)] pub inactive_color: Option, #[knuffel(child)] pub urgent_color: Option, #[knuffel(child)] pub active_gradient: Option, #[knuffel(child)] pub inactive_gradient: Option, #[knuffel(child)] pub urgent_gradient: Option, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub struct FloatingPosition { #[knuffel(property)] pub x: FloatOrInt<-65535, 65535>, #[knuffel(property)] pub y: FloatOrInt<-65535, 65535>, #[knuffel(property, default)] pub relative_to: RelativeTo, } #[derive(knuffel::DecodeScalar, Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum RelativeTo { #[default] TopLeft, TopRight, BottomLeft, BottomRight, Top, Bottom, Left, Right, } #[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, } #[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), 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, MoveWindowToWorkspaceUp, 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, 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, } 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 } => 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 {} => Self::MoveWindowToWorkspaceDown, niri_ipc::Action::MoveWindowToWorkspaceUp {} => Self::MoveWindowToWorkspaceUp, 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 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::DecodeScalar for FloatOrInt { 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::Int(ref value) => match value.try_into() { Ok(v) => { if (MIN..=MAX).contains(&v) { Ok(FloatOrInt(f64::from(v))) } else { ctx.emit_error(DecodeError::conversion( val, format!("value must be between {MIN} and {MAX}"), )); Ok(FloatOrInt::default()) } } Err(e) => { ctx.emit_error(DecodeError::conversion(val, e)); Ok(FloatOrInt::default()) } }, knuffel::ast::Literal::Decimal(ref value) => match value.try_into() { Ok(v) => { if (f64::from(MIN)..=f64::from(MAX)).contains(&v) { Ok(FloatOrInt(v)) } else { ctx.emit_error(DecodeError::conversion( val, format!("value must be between {MIN} and {MAX}"), )); Ok(FloatOrInt::default()) } } Err(e) => { ctx.emit_error(DecodeError::conversion(val, e)); Ok(FloatOrInt::default()) } }, _ => { ctx.emit_error(DecodeError::unsupported( val, "Unsupported value, only numbers are recognized", )); Ok(FloatOrInt::default()) } } } } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct DebugConfig { #[knuffel(child, unwrap(argument))] pub preview_render: Option, #[knuffel(child)] pub dbus_interfaces_in_non_session_instances: bool, #[knuffel(child)] pub wait_for_frame_completion_before_queueing: bool, #[knuffel(child)] pub enable_overlay_planes: bool, #[knuffel(child)] pub disable_cursor_plane: bool, #[knuffel(child)] pub disable_direct_scanout: bool, #[knuffel(child)] pub keep_max_bpc_unchanged: bool, #[knuffel(child)] pub restrict_primary_scanout_to_matching_format: bool, #[knuffel(child, unwrap(argument))] pub render_drm_device: Option, #[knuffel(child)] pub force_pipewire_invalid_modifier: bool, #[knuffel(child)] pub emulate_zero_presentation_time: bool, #[knuffel(child)] pub disable_resize_throttling: bool, #[knuffel(child)] pub disable_transactions: bool, #[knuffel(child)] pub keep_laptop_panel_on_when_lid_is_closed: bool, #[knuffel(child)] pub disable_monitor_names: bool, #[knuffel(child)] pub strict_new_window_focus_policy: bool, #[knuffel(child)] pub honor_xdg_activation_with_invalid_serial: bool, #[knuffel(child)] pub deactivate_unfocused_windows: bool, #[knuffel(child)] pub skip_cursor_only_updates_during_vrr: bool, } #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)] pub enum PreviewRender { Screencast, ScreenCapture, } #[derive(Debug, Clone)] pub enum ConfigPath { /// Explicitly set config path. /// /// Load the config only from this path, never create it. Explicit(PathBuf), /// Default config path. /// /// Prioritize the user path, fallback to the system path, fallback to creating the user path /// at compositor startup. Regular { /// User config path, usually `$XDG_CONFIG_HOME/niri/config.kdl`. user_path: PathBuf, /// System config path, usually `/etc/niri/config.kdl`. system_path: PathBuf, }, } impl ConfigPath { /// Load the config, or return an error if it doesn't exist. pub fn load(&self) -> miette::Result { let _span = tracy_client::span!("ConfigPath::load"); self.load_inner(|user_path, system_path| { Err(miette::miette!( "no config file found; create one at {user_path:?} or {system_path:?}", )) }) .context("error loading config") } /// Load the config, or create it if it doesn't exist. /// /// Returns a tuple containing the path that was created, if any, and the loaded config. /// /// If the config was created, but for some reason could not be read afterwards, /// this may return `(Some(_), Err(_))`. pub fn load_or_create(&self) -> (Option<&Path>, miette::Result) { let _span = tracy_client::span!("ConfigPath::load_or_create"); let mut created_at = None; let result = self .load_inner(|user_path, _| { Self::create(user_path, &mut created_at) .map(|()| user_path) .with_context(|| format!("error creating config at {user_path:?}")) }) .context("error loading config"); (created_at, result) } fn load_inner<'a>( &'a self, maybe_create: impl FnOnce(&'a Path, &'a Path) -> miette::Result<&'a Path>, ) -> miette::Result { let path = match self { ConfigPath::Explicit(path) => path.as_path(), ConfigPath::Regular { user_path, system_path, } => { if user_path.exists() { user_path.as_path() } else if system_path.exists() { system_path.as_path() } else { maybe_create(user_path.as_path(), system_path.as_path())? } } }; Config::load(path) } fn create<'a>(path: &'a Path, created_at: &mut Option<&'a Path>) -> miette::Result<()> { if let Some(default_parent) = path.parent() { fs::create_dir_all(default_parent) .into_diagnostic() .with_context(|| format!("error creating config directory {default_parent:?}"))?; } // Create the config and fill it with the default config if it doesn't exist. let mut new_file = match File::options() .read(true) .write(true) .create_new(true) .open(path) { Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => return Ok(()), res => res, } .into_diagnostic() .with_context(|| format!("error opening config file at {path:?}"))?; *created_at = Some(path); let default = include_bytes!("../../resources/default-config.kdl"); new_file .write_all(default) .into_diagnostic() .with_context(|| format!("error writing default config to {path:?}"))?; Ok(()) } } impl Config { pub fn load(path: &Path) -> miette::Result { let contents = fs::read_to_string(path) .into_diagnostic() .with_context(|| format!("error reading {path:?}"))?; let config = Self::parse( path.file_name() .and_then(OsStr::to_str) .unwrap_or("config.kdl"), &contents, ) .context("error parsing")?; debug!("loaded config from {path:?}"); Ok(config) } pub fn parse(filename: &str, text: &str) -> Result { let _span = tracy_client::span!("Config::parse"); knuffel::parse(filename, text) } } impl Default for Config { fn default() -> Self { Config::parse( "default-config.kdl", include_str!("../../resources/default-config.kdl"), ) .unwrap() } } impl BorderRule { pub fn merge_with(&mut self, other: &Self) { if other.off { self.off = true; self.on = false; } if other.on { self.off = false; self.on = true; } if let Some(x) = other.width { self.width = Some(x); } if let Some(x) = other.active_color { self.active_color = Some(x); } if let Some(x) = other.inactive_color { self.inactive_color = Some(x); } if let Some(x) = other.urgent_color { self.urgent_color = Some(x); } if let Some(x) = other.active_gradient { self.active_gradient = Some(x); } if let Some(x) = other.inactive_gradient { self.inactive_gradient = Some(x); } if let Some(x) = other.urgent_gradient { self.urgent_gradient = Some(x); } } pub fn resolve_against(&self, mut config: Border) -> Border { config.off |= self.off; if self.on { config.off = false; } if let Some(x) = self.width { config.width = x; } if let Some(x) = self.active_color { config.active_color = x; config.active_gradient = None; } if let Some(x) = self.inactive_color { config.inactive_color = x; config.inactive_gradient = None; } if let Some(x) = self.urgent_color { config.urgent_color = x; config.urgent_gradient = None; } if let Some(x) = self.active_gradient { config.active_gradient = Some(x); } if let Some(x) = self.inactive_gradient { config.inactive_gradient = Some(x); } if let Some(x) = self.urgent_gradient { config.urgent_gradient = Some(x); } config } } impl ShadowRule { pub fn merge_with(&mut self, other: &Self) { if other.off { self.off = true; self.on = false; } if other.on { self.off = false; self.on = true; } if let Some(x) = other.offset { self.offset = Some(x); } if let Some(x) = other.softness { self.softness = Some(x); } if let Some(x) = other.spread { self.spread = Some(x); } if let Some(x) = other.draw_behind_window { self.draw_behind_window = Some(x); } if let Some(x) = other.color { self.color = Some(x); } if let Some(x) = other.inactive_color { self.inactive_color = Some(x); } } pub fn resolve_against(&self, mut config: Shadow) -> Shadow { config.on |= self.on; if self.off { config.on = false; } if let Some(x) = self.offset { config.offset = x; } if let Some(x) = self.softness { config.softness = x; } if let Some(x) = self.spread { config.spread = x; } if let Some(x) = self.draw_behind_window { config.draw_behind_window = x; } if let Some(x) = self.color { config.color = x; } if let Some(x) = self.inactive_color { config.inactive_color = Some(x); } config } } impl TabIndicatorRule { pub fn merge_with(&mut self, other: &Self) { if let Some(x) = other.active_color { self.active_color = Some(x); } if let Some(x) = other.inactive_color { self.inactive_color = Some(x); } if let Some(x) = other.urgent_color { self.urgent_color = Some(x); } if let Some(x) = other.active_gradient { self.active_gradient = Some(x); } if let Some(x) = other.inactive_gradient { self.inactive_gradient = Some(x); } if let Some(x) = other.urgent_gradient { self.urgent_gradient = Some(x); } } } impl CornerRadius { pub fn fit_to(self, width: f32, height: f32) -> Self { // Like in CSS: https://drafts.csswg.org/css-backgrounds/#corner-overlap let reduction = f32::min( f32::min( width / (self.top_left + self.top_right), width / (self.bottom_left + self.bottom_right), ), f32::min( height / (self.top_left + self.bottom_left), height / (self.top_right + self.bottom_right), ), ); let reduction = f32::min(1., reduction); Self { top_left: self.top_left * reduction, top_right: self.top_right * reduction, bottom_right: self.bottom_right * reduction, bottom_left: self.bottom_left * reduction, } } pub fn expanded_by(mut self, width: f32) -> Self { // Radius = 0 is preserved, so that square corners remain square. if self.top_left > 0. { self.top_left += width; } if self.top_right > 0. { self.top_right += width; } if self.bottom_right > 0. { self.bottom_right += width; } if self.bottom_left > 0. { self.bottom_left += width; } if width < 0. { self.top_left = self.top_left.max(0.); self.top_right = self.top_right.max(0.); self.bottom_left = self.bottom_left.max(0.); self.bottom_right = self.bottom_right.max(0.); } self } pub fn scaled_by(self, scale: f32) -> Self { Self { top_left: self.top_left * scale, top_right: self.top_right * scale, bottom_right: self.bottom_right * scale, bottom_left: self.bottom_left * scale, } } } impl FromStr for GradientInterpolation { type Err = miette::Error; fn from_str(s: &str) -> Result { let mut iter = s.split_whitespace(); let in_part1 = iter.next(); let in_part2 = iter.next(); let in_part3 = iter.next(); let Some(in_part1) = in_part1 else { return Err(miette!("missing color space")); }; let color = match in_part1 { "srgb" => GradientColorSpace::Srgb, "srgb-linear" => GradientColorSpace::SrgbLinear, "oklab" => GradientColorSpace::Oklab, "oklch" => GradientColorSpace::Oklch, x => { return Err(miette!( "invalid color space {x}; can be srgb, srgb-linear, oklab or oklch" )) } }; let interpolation = if let Some(in_part2) = in_part2 { if color != GradientColorSpace::Oklch { return Err(miette!("only oklch color space can have hue interpolation")); } if in_part3 != Some("hue") { return Err(miette!( "interpolation must end with \"hue\", like \"oklch shorter hue\"" )); } else if iter.next().is_some() { return Err(miette!("unexpected text after hue interpolation")); } else { match in_part2 { "shorter" => HueInterpolation::Shorter, "longer" => HueInterpolation::Longer, "increasing" => HueInterpolation::Increasing, "decreasing" => HueInterpolation::Decreasing, x => { return Err(miette!( "invalid hue interpolation {x}; \ can be shorter, longer, increasing, decreasing" )) } } } } else { HueInterpolation::default() }; Ok(Self { color_space: color, hue_interpolation: interpolation, }) } } impl FromStr for Color { type Err = miette::Error; fn from_str(s: &str) -> Result { let color = csscolorparser::parse(s) .into_diagnostic()? .clamp() .to_array(); Ok(Self::from_array_unpremul(color)) } } #[derive(knuffel::Decode)] struct ColorRgba { #[knuffel(argument)] r: u8, #[knuffel(argument)] g: u8, #[knuffel(argument)] b: u8, #[knuffel(argument)] a: u8, } impl From for Color { fn from(value: ColorRgba) -> Self { let ColorRgba { r, g, b, a } = value; Self::from_array_unpremul([r, g, b, a].map(|x| x as f32 / 255.)) } } // Manual impl to allow both one-argument string and 4-argument RGBA forms. impl knuffel::Decode for Color where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { // Check for unexpected type name. if let Some(type_name) = &node.type_name { ctx.emit_error(DecodeError::unexpected( type_name, "type name", "no type name expected for this node", )); } // Get the first argument. let mut iter_args = node.arguments.iter(); let val = iter_args .next() .ok_or_else(|| DecodeError::missing(node, "additional argument is required"))?; // Check for unexpected type name. if let Some(typ) = &val.type_name { ctx.emit_error(DecodeError::TypeName { span: typ.span().clone(), found: Some((**typ).clone()), expected: knuffel::errors::ExpectedType::no_type(), rust_type: "str", }); } // Check the argument type. let rv = match *val.literal { // If it's a string, use FromStr. knuffel::ast::Literal::String(ref s) => { Color::from_str(s).map_err(|e| DecodeError::conversion(&val.literal, e)) } // Otherwise, fall back to the 4-argument RGBA form. _ => return ColorRgba::decode_node(node, ctx).map(Color::from), }?; // Check for unexpected following arguments. if let Some(val) = iter_args.next() { ctx.emit_error(DecodeError::unexpected( &val.literal, "argument", "unexpected argument", )); } // Check for unexpected properties and children. for name in node.properties.keys() { ctx.emit_error(DecodeError::unexpected( name, "property", format!("unexpected property `{}`", name.escape_default()), )); } for child in node.children.as_ref().map(|lst| &lst[..]).unwrap_or(&[]) { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", child.node_name.escape_default()), )); } Ok(rv) } } fn expect_only_children( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) where S: knuffel::traits::ErrorSpan, { 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", )) } for name in node.properties.keys() { ctx.emit_error(DecodeError::unexpected( name, "property", "no properties expected for this node", )) } } impl FromIterator for Outputs { fn from_iter>(iter: T) -> Self { Self(Vec::from_iter(iter)) } } impl Outputs { pub fn find(&self, name: &OutputName) -> Option<&Output> { self.0.iter().find(|o| name.matches(&o.name)) } pub fn find_mut(&mut self, name: &OutputName) -> Option<&mut Output> { self.0.iter_mut().find(|o| name.matches(&o.name)) } } impl OutputName { pub fn from_ipc_output(output: &niri_ipc::Output) -> Self { Self { connector: output.name.clone(), make: (output.make != "Unknown").then(|| output.make.clone()), model: (output.model != "Unknown").then(|| output.model.clone()), serial: output.serial.clone(), } } /// Returns an output description matching what Smithay's `Output::new()` does. pub fn format_description(&self) -> String { format!( "{} - {} - {}", self.make.as_deref().unwrap_or("Unknown"), self.model.as_deref().unwrap_or("Unknown"), self.connector, ) } /// Returns an output name that will match by make/model/serial or, if they are missing, by /// connector. pub fn format_make_model_serial_or_connector(&self) -> String { if self.make.is_none() && self.model.is_none() && self.serial.is_none() { self.connector.to_string() } else { self.format_make_model_serial() } } pub fn format_make_model_serial(&self) -> String { let make = self.make.as_deref().unwrap_or("Unknown"); let model = self.model.as_deref().unwrap_or("Unknown"); let serial = self.serial.as_deref().unwrap_or("Unknown"); format!("{make} {model} {serial}") } pub fn matches(&self, target: &str) -> bool { // Match by connector. if target.eq_ignore_ascii_case(&self.connector) { return true; } // If no other fields are available, don't try to match by them. // // This is used by niri msg output. if self.make.is_none() && self.model.is_none() && self.serial.is_none() { return false; } // Match by "make model serial" with Unknown if something is missing. let make = self.make.as_deref().unwrap_or("Unknown"); let model = self.model.as_deref().unwrap_or("Unknown"); let serial = self.serial.as_deref().unwrap_or("Unknown"); let Some(target_make) = target.get(..make.len()) else { return false; }; let rest = &target[make.len()..]; if !target_make.eq_ignore_ascii_case(make) { return false; } if !rest.starts_with(' ') { return false; } let rest = &rest[1..]; let Some(target_model) = rest.get(..model.len()) else { return false; }; let rest = &rest[model.len()..]; if !target_model.eq_ignore_ascii_case(model) { return false; } if !rest.starts_with(' ') { return false; } let rest = &rest[1..]; if !rest.eq_ignore_ascii_case(serial) { return false; } true } // Similar in spirit to Ord, but I don't want to derive Eq to avoid mistakes (you should use // `Self::match`, not Eq). pub fn compare(&self, other: &Self) -> std::cmp::Ordering { let self_missing_mms = self.make.is_none() && self.model.is_none() && self.serial.is_none(); let other_missing_mms = other.make.is_none() && other.model.is_none() && other.serial.is_none(); match (self_missing_mms, other_missing_mms) { (true, true) => self.connector.cmp(&other.connector), (true, false) => std::cmp::Ordering::Greater, (false, true) => std::cmp::Ordering::Less, (false, false) => self .make .cmp(&other.make) .then_with(|| self.model.cmp(&other.model)) .then_with(|| self.serial.cmp(&other.serial)) .then_with(|| self.connector.cmp(&other.connector)), } } } impl knuffel::Decode for DefaultPresetSize 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 children = node.children(); if let Some(child) = children.next() { if let Some(unwanted_child) = children.next() { ctx.emit_error(DecodeError::unexpected( unwanted_child, "node", "expected no more than one child", )); } PresetSize::decode_node(child, ctx).map(Some).map(Self) } else { Ok(Self(None)) } } } fn parse_arg_node>( name: &str, node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let mut iter_args = node.arguments.iter(); let val = iter_args.next().ok_or_else(|| { DecodeError::missing(node, format!("additional argument `{name}` is required")) })?; let value = knuffel::traits::DecodeScalar::decode(val, ctx)?; if let Some(val) = iter_args.next() { ctx.emit_error(DecodeError::unexpected( &val.literal, "argument", "unexpected argument", )); } for name in node.properties.keys() { ctx.emit_error(DecodeError::unexpected( name, "property", format!("unexpected property `{}`", name.escape_default()), )); } for child in node.children() { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", child.node_name.escape_default()), )); } Ok(value) } impl knuffel::Decode for WorkspaceSwitchAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for HorizontalViewMovementAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for WindowMovementAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::DecodeScalar for WorkspaceName { 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> { #[derive(Debug)] struct WorkspaceNameSet(Vec); match &**val { knuffel::ast::Literal::String(ref s) => { let mut name_set: Vec = match ctx.get::() { Some(h) => h.0.clone(), None => Vec::new(), }; if name_set.iter().any(|name| name.eq_ignore_ascii_case(s)) { ctx.emit_error(DecodeError::unexpected( val, "named workspace", format!("duplicate named workspace: {s}"), )); return Ok(Self(String::new())); } name_set.push(s.to_string()); ctx.set(WorkspaceNameSet(name_set)); Ok(Self(s.clone().into())) } _ => { ctx.emit_error(DecodeError::unsupported( val, "workspace names must be strings", )); Ok(Self(String::new())) } } } } impl knuffel::Decode for WindowOpenAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().anim; let mut custom_shader = None; let anim = Animation::decode_node(node, ctx, default, |child, ctx| { if &**child.node_name == "custom-shader" { custom_shader = parse_arg_node("custom-shader", child, ctx)?; Ok(true) } else { Ok(false) } })?; Ok(Self { anim, custom_shader, }) } } impl knuffel::Decode for WindowCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().anim; let mut custom_shader = None; let anim = Animation::decode_node(node, ctx, default, |child, ctx| { if &**child.node_name == "custom-shader" { custom_shader = parse_arg_node("custom-shader", child, ctx)?; Ok(true) } else { Ok(false) } })?; Ok(Self { anim, custom_shader, }) } } impl knuffel::Decode for WindowResizeAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().anim; let mut custom_shader = None; let anim = Animation::decode_node(node, ctx, default, |child, ctx| { if &**child.node_name == "custom-shader" { custom_shader = parse_arg_node("custom-shader", child, ctx)?; Ok(true) } else { Ok(false) } })?; Ok(Self { anim, custom_shader, }) } } impl knuffel::Decode for ConfigNotificationOpenCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for ScreenshotUiOpenAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl knuffel::Decode for OverviewOpenCloseAnim where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { let default = Self::default().0; Ok(Self(Animation::decode_node(node, ctx, default, |_, _| { Ok(false) })?)) } } impl Animation { pub fn new_off() -> Self { Self { off: true, kind: AnimationKind::Easing(EasingParams { duration_ms: 0, curve: AnimationCurve::Linear, }), } } fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, default: Self, mut process_children: impl FnMut( &knuffel::ast::SpannedNode, &mut knuffel::decode::Context, ) -> Result>, ) -> Result> { #[derive(Default, PartialEq)] struct OptionalEasingParams { duration_ms: Option, curve: Option, } expect_only_children(node, ctx); let mut off = false; let mut easing_params = OptionalEasingParams::default(); let mut spring_params = None; for child in node.children() { match &**child.node_name { "off" => { knuffel::decode::check_flag_node(child, ctx); if off { ctx.emit_error(DecodeError::unexpected( &child.node_name, "node", "duplicate node `off`, single node expected", )); } else { off = true; } } "spring" => { if easing_params != OptionalEasingParams::default() { ctx.emit_error(DecodeError::unexpected( child, "node", "cannot set both spring and easing parameters at once", )); } if spring_params.is_some() { ctx.emit_error(DecodeError::unexpected( &child.node_name, "node", "duplicate node `spring`, single node expected", )); } spring_params = Some(SpringParams::decode_node(child, ctx)?); } "duration-ms" => { if spring_params.is_some() { ctx.emit_error(DecodeError::unexpected( child, "node", "cannot set both spring and easing parameters at once", )); } if easing_params.duration_ms.is_some() { ctx.emit_error(DecodeError::unexpected( &child.node_name, "node", "duplicate node `duration-ms`, single node expected", )); } easing_params.duration_ms = Some(parse_arg_node("duration-ms", child, ctx)?); } "curve" => { if spring_params.is_some() { ctx.emit_error(DecodeError::unexpected( child, "node", "cannot set both spring and easing parameters at once", )); } if easing_params.curve.is_some() { ctx.emit_error(DecodeError::unexpected( &child.node_name, "node", "duplicate node `curve`, single node expected", )); } easing_params.curve = Some(parse_arg_node("curve", child, ctx)?); } name_str => { if !process_children(child, ctx)? { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", name_str.escape_default()), )); } } } } let kind = if let Some(spring_params) = spring_params { // Configured spring. AnimationKind::Spring(spring_params) } else if easing_params == OptionalEasingParams::default() { // Did not configure anything. default.kind } else { // Configured easing. let default = if let AnimationKind::Easing(easing) = default.kind { easing } else { // Generic fallback values for when the default animation is spring, but the user // configured an easing animation. EasingParams { duration_ms: 250, curve: AnimationCurve::EaseOutCubic, } }; AnimationKind::Easing(EasingParams { duration_ms: easing_params.duration_ms.unwrap_or(default.duration_ms), curve: easing_params.curve.unwrap_or(default.curve), }) }; Ok(Self { off, kind }) } } impl knuffel::Decode for SpringParams 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", )); } if let Some(val) = node.arguments.first() { ctx.emit_error(DecodeError::unexpected( &val.literal, "argument", "unexpected argument", )); } for child in node.children() { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", child.node_name.escape_default()), )); } let mut damping_ratio = None; let mut stiffness = None; let mut epsilon = None; for (name, val) in &node.properties { match &***name { "damping-ratio" => { damping_ratio = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); } "stiffness" => { stiffness = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); } "epsilon" => { epsilon = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); } name_str => { ctx.emit_error(DecodeError::unexpected( name, "property", format!("unexpected property `{}`", name_str.escape_default()), )); } } } let damping_ratio = damping_ratio .ok_or_else(|| DecodeError::missing(node, "property `damping-ratio` is required"))?; let stiffness = stiffness .ok_or_else(|| DecodeError::missing(node, "property `stiffness` is required"))?; let epsilon = epsilon.ok_or_else(|| DecodeError::missing(node, "property `epsilon` is required"))?; if !(0.1..=10.).contains(&damping_ratio) { ctx.emit_error(DecodeError::conversion( node, "damping-ratio must be between 0.1 and 10.0", )); } if stiffness < 1 { ctx.emit_error(DecodeError::conversion(node, "stiffness must be >= 1")); } if !(0.00001..=0.1).contains(&epsilon) { ctx.emit_error(DecodeError::conversion( node, "epsilon must be between 0.00001 and 0.1", )); } Ok(SpringParams { damping_ratio, stiffness, epsilon, }) } } impl knuffel::Decode for CornerRadius where S: knuffel::traits::ErrorSpan, { fn decode_node( node: &knuffel::ast::SpannedNode, ctx: &mut knuffel::decode::Context, ) -> Result> { // Check for unexpected type name. if let Some(type_name) = &node.type_name { ctx.emit_error(DecodeError::unexpected( type_name, "type name", "no type name expected for this node", )); } let decode_radius = |ctx: &mut knuffel::decode::Context, val: &knuffel::ast::Value| { // Check for unexpected type name. if let Some(typ) = &val.type_name { ctx.emit_error(DecodeError::TypeName { span: typ.span().clone(), found: Some((**typ).clone()), expected: knuffel::errors::ExpectedType::no_type(), rust_type: "str", }); } // Decode both integers and floats. let radius = match *val.literal { knuffel::ast::Literal::Int(ref x) => f32::from(match x.try_into() { Ok(x) => x, Err(err) => { ctx.emit_error(DecodeError::conversion(&val.literal, err)); 0i16 } }), knuffel::ast::Literal::Decimal(ref x) => match x.try_into() { Ok(x) => x, Err(err) => { ctx.emit_error(DecodeError::conversion(&val.literal, err)); 0. } }, _ => { ctx.emit_error(DecodeError::scalar_kind( knuffel::decode::Kind::Int, &val.literal, )); 0. } }; if radius < 0. { ctx.emit_error(DecodeError::conversion(&val.literal, "radius must be >= 0")); } radius }; // Get the first argument. let mut iter_args = node.arguments.iter(); let val = iter_args .next() .ok_or_else(|| DecodeError::missing(node, "additional argument is required"))?; let top_left = decode_radius(ctx, val); let mut rv = CornerRadius { top_left, top_right: top_left, bottom_right: top_left, bottom_left: top_left, }; if let Some(val) = iter_args.next() { rv.top_right = decode_radius(ctx, val); let val = iter_args.next().ok_or_else(|| { DecodeError::missing(node, "either 1 or 4 arguments are required") })?; rv.bottom_right = decode_radius(ctx, val); let val = iter_args.next().ok_or_else(|| { DecodeError::missing(node, "either 1 or 4 arguments are required") })?; rv.bottom_left = decode_radius(ctx, val); // Check for unexpected following arguments. if let Some(val) = iter_args.next() { ctx.emit_error(DecodeError::unexpected( &val.literal, "argument", "unexpected argument", )); } } // Check for unexpected properties and children. for name in node.properties.keys() { ctx.emit_error(DecodeError::unexpected( name, "property", format!("unexpected property `{}`", name.escape_default()), )); } for child in node.children.as_ref().map(|lst| &lst[..]).unwrap_or(&[]) { ctx.emit_error(DecodeError::unexpected( child, "node", format!("unexpected node `{}`", child.node_name.escape_default()), )); } Ok(rv) } } 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 ModKey { type Err = miette::Error; fn from_str(s: &str) -> Result { match &*s.to_ascii_lowercase() { "ctrl" | "control" => Ok(Self::Ctrl), "shift" => Ok(Self::Shift), "alt" => Ok(Self::Alt), "super" | "win" => Ok(Self::Super), "iso_level3_shift" | "mod5" => Ok(Self::IsoLevel3Shift), "iso_level5_shift" | "mod3" => Ok(Self::IsoLevel5Shift), _ => Err(miette!("invalid Mod key: {s}")), } } } 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 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 }) } } impl FromStr for ClickMethod { type Err = miette::Error; fn from_str(s: &str) -> Result { match s { "clickfinger" => Ok(Self::Clickfinger), "button-areas" => Ok(Self::ButtonAreas), _ => Err(miette!( r#"invalid click method, can be "button-areas" or "clickfinger""# )), } } } impl FromStr for AccelProfile { type Err = miette::Error; fn from_str(s: &str) -> Result { match s { "adaptive" => Ok(Self::Adaptive), "flat" => Ok(Self::Flat), _ => Err(miette!( r#"invalid accel profile, can be "adaptive" or "flat""# )), } } } impl FromStr for ScrollMethod { type Err = miette::Error; fn from_str(s: &str) -> Result { match s { "no-scroll" => Ok(Self::NoScroll), "two-finger" => Ok(Self::TwoFinger), "edge" => Ok(Self::Edge), "on-button-down" => Ok(Self::OnButtonDown), _ => Err(miette!( r#"invalid scroll method, can be "no-scroll", "two-finger", "edge", or "on-button-down""# )), } } } impl FromStr for TapButtonMap { type Err = miette::Error; fn from_str(s: &str) -> Result { match s { "left-right-middle" => Ok(Self::LeftRightMiddle), "left-middle-right" => Ok(Self::LeftMiddleRight), _ => Err(miette!( r#"invalid tap button map, can be "left-right-middle" or "left-middle-right""# )), } } } impl FromStr for Percent { type Err = miette::Error; fn from_str(s: &str) -> Result { let Some((value, empty)) = s.split_once('%') else { return Err(miette!("value must end with '%'")); }; if !empty.is_empty() { return Err(miette!("trailing characters after '%' are not allowed")); } let value: f64 = value.parse().map_err(|_| miette!("error parsing value"))?; Ok(Percent(value / 100.)) } } #[cfg(test)] mod tests { use insta::{assert_debug_snapshot, assert_snapshot}; use niri_ipc::PositionChange; use pretty_assertions::assert_eq; use super::*; #[track_caller] fn do_parse(text: &str) -> Config { Config::parse("test.kdl", text) .map_err(miette::Report::new) .unwrap() } #[test] fn parse_scroll_factor_combined() { // Test combined scroll-factor syntax let parsed = do_parse( r#" input { mouse { scroll-factor 2.0 } touchpad { scroll-factor 1.5 } } "#, ); assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" Some( ScrollFactor { base: Some( FloatOrInt( 2.0, ), ), horizontal: None, vertical: None, }, ) "#); assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" Some( ScrollFactor { base: Some( FloatOrInt( 1.5, ), ), horizontal: None, vertical: None, }, ) "#); } #[test] fn parse_scroll_factor_split() { // Test split horizontal/vertical syntax let parsed = do_parse( r#" input { mouse { scroll-factor horizontal=2.0 vertical=-1.0 } touchpad { scroll-factor horizontal=-1.5 vertical=0.5 } } "#, ); assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" Some( ScrollFactor { base: None, horizontal: Some( FloatOrInt( 2.0, ), ), vertical: Some( FloatOrInt( -1.0, ), ), }, ) "#); assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" Some( ScrollFactor { base: None, horizontal: Some( FloatOrInt( -1.5, ), ), vertical: Some( FloatOrInt( 0.5, ), ), }, ) "#); } #[test] fn parse_scroll_factor_partial() { // Test partial specification (only one axis) let parsed = do_parse( r#" input { mouse { scroll-factor horizontal=2.0 } touchpad { scroll-factor vertical=-1.5 } } "#, ); assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" Some( ScrollFactor { base: None, horizontal: Some( FloatOrInt( 2.0, ), ), vertical: None, }, ) "#); assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" Some( ScrollFactor { base: None, horizontal: None, vertical: Some( FloatOrInt( -1.5, ), ), }, ) "#); } #[test] fn parse_scroll_factor_mixed() { // Test mixed base + override syntax let parsed = do_parse( r#" input { mouse { scroll-factor 2 vertical=-1 } touchpad { scroll-factor 1.5 horizontal=3 } } "#, ); assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" Some( ScrollFactor { base: Some( FloatOrInt( 2.0, ), ), horizontal: None, vertical: Some( FloatOrInt( -1.0, ), ), }, ) "#); assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" Some( ScrollFactor { base: Some( FloatOrInt( 1.5, ), ), horizontal: Some( FloatOrInt( 3.0, ), ), vertical: None, }, ) "#); } #[test] fn scroll_factor_h_v_factors() { let sf = ScrollFactor { base: Some(FloatOrInt(2.0)), horizontal: None, vertical: None, }; assert_debug_snapshot!(sf.h_v_factors(), @r#" ( 2.0, 2.0, ) "#); let sf = ScrollFactor { base: None, horizontal: Some(FloatOrInt(3.0)), vertical: Some(FloatOrInt(-1.0)), }; assert_debug_snapshot!(sf.h_v_factors(), @r#" ( 3.0, -1.0, ) "#); let sf = ScrollFactor { base: Some(FloatOrInt(2.0)), horizontal: Some(FloatOrInt(1.0)), vertical: None, }; assert_debug_snapshot!(sf.h_v_factors(), @r" ( 1.0, 2.0, ) "); } #[test] fn parse() { let parsed = do_parse( r##" input { keyboard { repeat-delay 600 repeat-rate 25 track-layout "window" xkb { layout "us,ru" options "grp:win_space_toggle" } } touchpad { tap dwt dwtp drag true click-method "clickfinger" accel-speed 0.2 accel-profile "flat" scroll-method "two-finger" scroll-button 272 scroll-button-lock tap-button-map "left-middle-right" disabled-on-external-mouse scroll-factor 0.9 } mouse { natural-scroll accel-speed 0.4 accel-profile "flat" scroll-method "no-scroll" scroll-button 273 middle-emulation scroll-factor 0.2 } trackpoint { off natural-scroll accel-speed 0.0 accel-profile "flat" scroll-method "on-button-down" scroll-button 274 } trackball { off natural-scroll accel-speed 0.0 accel-profile "flat" scroll-method "edge" scroll-button 275 scroll-button-lock left-handed middle-emulation } tablet { map-to-output "eDP-1" calibration-matrix 1.0 2.0 3.0 \ 4.0 5.0 6.0 } touch { map-to-output "eDP-1" } disable-power-key-handling warp-mouse-to-focus focus-follows-mouse workspace-auto-back-and-forth mod-key "Mod5" mod-key-nested "Super" } output "eDP-1" { focus-at-startup scale 2 transform "flipped-90" position x=10 y=20 mode "1920x1080@144" variable-refresh-rate on-demand=true background-color "rgba(25, 25, 102, 1.0)" } layout { focus-ring { width 5 active-color 0 100 200 255 inactive-color 255 200 100 0 active-gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view" } border { width 3 inactive-color "rgba(255, 200, 100, 0.0)" } shadow { offset x=10 y=-20 } tab-indicator { width 10 position "top" } preset-column-widths { proportion 0.25 proportion 0.5 fixed 960 fixed 1280 } preset-window-heights { proportion 0.25 proportion 0.5 fixed 960 fixed 1280 } default-column-width { proportion 0.25; } gaps 8 struts { left 1 right 2 top 3 } center-focused-column "on-overflow" default-column-display "tabbed" insert-hint { color "rgb(255, 200, 127)" gradient from="rgba(10, 20, 30, 1.0)" to="#0080ffff" relative-to="workspace-view" } } spawn-at-startup "alacritty" "-e" "fish" spawn-at-startup-sh "qs -c ~/source/qs/MyAwesomeShell" prefer-no-csd cursor { xcursor-theme "breeze_cursors" xcursor-size 16 hide-when-typing hide-after-inactive-ms 3000 } screenshot-path "~/Screenshots/screenshot.png" clipboard { disable-primary } hotkey-overlay { skip-at-startup } animations { slowdown 2.0 workspace-switch { spring damping-ratio=1.0 stiffness=1000 epsilon=0.0001 } horizontal-view-movement { duration-ms 100 curve "ease-out-expo" } window-open { off; } } gestures { dnd-edge-view-scroll { trigger-width 10 max-speed 50 } } environment { QT_QPA_PLATFORM "wayland" DISPLAY null } window-rule { match app-id=".*alacritty" exclude title="~" exclude is-active=true is-focused=false open-on-output "eDP-1" open-maximized true open-fullscreen false open-floating false open-focused true default-window-height { fixed 500; } default-column-display "tabbed" default-floating-position x=100 y=-200 relative-to="bottom-left" focus-ring { off width 3 } border { on width 8.5 } tab-indicator { active-color "#f00" } } layer-rule { match namespace="^notifications$" block-out-from "screencast" } binds { Mod+Escape hotkey-overlay-title="Inhibit" { toggle-keyboard-shortcuts-inhibit; } Mod+Shift+Escape allow-inhibiting=true { toggle-keyboard-shortcuts-inhibit; } Mod+T allow-when-locked=true { spawn "alacritty"; } Mod+Q hotkey-overlay-title=null { close-window; } Mod+Shift+H { focus-monitor-left; } Mod+Shift+O { focus-monitor "eDP-1"; } Mod+Ctrl+Shift+L { move-window-to-monitor-right; } Mod+Ctrl+Alt+O { move-window-to-monitor "eDP-1"; } Mod+Ctrl+Alt+P { move-column-to-monitor "DP-1"; } Mod+Comma { consume-window-into-column; } Mod+1 { focus-workspace 1; } Mod+Shift+1 { focus-workspace "workspace-1"; } Mod+Shift+E allow-inhibiting=false { quit skip-confirmation=true; } Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } Super+Alt+S allow-when-locked=true { spawn-sh "pkill orca || exec orca"; } } switch-events { tablet-mode-on { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true"; } tablet-mode-off { spawn "bash" "-c" "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false"; } } debug { render-drm-device "/dev/dri/renderD129" } workspace "workspace-1" { open-on-output "eDP-1" } workspace "workspace-2" workspace "workspace-3" "##, ); assert_debug_snapshot!(parsed, @r#" Config { input: Input { keyboard: Keyboard { xkb: Xkb { rules: "", model: "", layout: "us,ru", variant: "", options: Some( "grp:win_space_toggle", ), file: None, }, repeat_delay: 600, repeat_rate: 25, track_layout: Window, numlock: false, }, touchpad: Touchpad { off: false, tap: true, dwt: true, dwtp: true, drag: Some( true, ), drag_lock: false, natural_scroll: false, click_method: Some( Clickfinger, ), accel_speed: FloatOrInt( 0.2, ), accel_profile: Some( Flat, ), scroll_method: Some( TwoFinger, ), scroll_button: Some( 272, ), scroll_button_lock: true, tap_button_map: Some( LeftMiddleRight, ), left_handed: false, disabled_on_external_mouse: true, middle_emulation: false, scroll_factor: Some( ScrollFactor { base: Some( FloatOrInt( 0.9, ), ), horizontal: None, vertical: None, }, ), }, mouse: Mouse { off: false, natural_scroll: true, accel_speed: FloatOrInt( 0.4, ), accel_profile: Some( Flat, ), scroll_method: Some( NoScroll, ), scroll_button: Some( 273, ), scroll_button_lock: false, left_handed: false, middle_emulation: true, scroll_factor: Some( ScrollFactor { base: Some( FloatOrInt( 0.2, ), ), horizontal: None, vertical: None, }, ), }, trackpoint: Trackpoint { off: true, natural_scroll: true, accel_speed: FloatOrInt( 0.0, ), accel_profile: Some( Flat, ), scroll_method: Some( OnButtonDown, ), scroll_button: Some( 274, ), scroll_button_lock: false, left_handed: false, middle_emulation: false, }, trackball: Trackball { off: true, natural_scroll: true, accel_speed: FloatOrInt( 0.0, ), accel_profile: Some( Flat, ), scroll_method: Some( Edge, ), scroll_button: Some( 275, ), scroll_button_lock: true, left_handed: true, middle_emulation: true, }, tablet: Tablet { off: false, calibration_matrix: Some( [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, ], ), map_to_output: Some( "eDP-1", ), left_handed: false, }, touch: Touch { off: false, map_to_output: Some( "eDP-1", ), }, disable_power_key_handling: true, warp_mouse_to_focus: Some( WarpMouseToFocus { mode: None, }, ), focus_follows_mouse: Some( FocusFollowsMouse { max_scroll_amount: None, }, ), workspace_auto_back_and_forth: true, mod_key: Some( IsoLevel3Shift, ), mod_key_nested: Some( Super, ), }, outputs: Outputs( [ Output { off: false, name: "eDP-1", scale: Some( FloatOrInt( 2.0, ), ), transform: Flipped90, position: Some( Position { x: 10, y: 20, }, ), mode: Some( ConfiguredMode { width: 1920, height: 1080, refresh: Some( 144.0, ), }, ), variable_refresh_rate: Some( Vrr { on_demand: true, }, ), focus_at_startup: true, background_color: Some( Color { r: 0.09803922, g: 0.09803922, b: 0.4, a: 1.0, }, ), backdrop_color: None, }, ], ), spawn_at_startup: [ SpawnAtStartup { command: [ "alacritty", "-e", "fish", ], }, ], spawn_at_startup_sh: [ SpawnAtStartupSh { command: "qs -c ~/source/qs/MyAwesomeShell", }, ], layout: Layout { focus_ring: FocusRing { off: false, width: FloatOrInt( 5.0, ), active_color: Color { r: 0.0, g: 0.39215687, b: 0.78431374, a: 1.0, }, inactive_color: Color { r: 1.0, g: 0.78431374, b: 0.39215687, a: 0.0, }, urgent_color: Color { r: 0.60784316, g: 0.0, b: 0.0, a: 1.0, }, active_gradient: Some( Gradient { from: Color { r: 0.039215688, g: 0.078431375, b: 0.11764706, a: 1.0, }, to: Color { r: 0.0, g: 0.5019608, b: 1.0, a: 1.0, }, angle: 180, relative_to: WorkspaceView, in_: GradientInterpolation { color_space: Srgb, hue_interpolation: Shorter, }, }, ), inactive_gradient: None, urgent_gradient: None, }, border: Border { off: false, width: FloatOrInt( 3.0, ), active_color: Color { r: 1.0, g: 0.78431374, b: 0.49803922, a: 1.0, }, inactive_color: Color { r: 1.0, g: 0.78431374, b: 0.39215687, a: 0.0, }, urgent_color: Color { r: 0.60784316, g: 0.0, b: 0.0, a: 1.0, }, active_gradient: None, inactive_gradient: None, urgent_gradient: None, }, shadow: Shadow { on: false, offset: ShadowOffset { x: FloatOrInt( 10.0, ), y: FloatOrInt( -20.0, ), }, softness: FloatOrInt( 30.0, ), spread: FloatOrInt( 5.0, ), draw_behind_window: false, color: Color { r: 0.0, g: 0.0, b: 0.0, a: 0.4392157, }, inactive_color: None, }, tab_indicator: TabIndicator { off: false, hide_when_single_tab: false, place_within_column: false, gap: FloatOrInt( 5.0, ), width: FloatOrInt( 10.0, ), length: TabIndicatorLength { total_proportion: Some( 0.5, ), }, position: Top, gaps_between_tabs: FloatOrInt( 0.0, ), corner_radius: FloatOrInt( 0.0, ), active_color: None, inactive_color: None, urgent_color: None, active_gradient: None, inactive_gradient: None, urgent_gradient: None, }, insert_hint: InsertHint { off: false, color: Color { r: 1.0, g: 0.78431374, b: 0.49803922, a: 1.0, }, gradient: Some( Gradient { from: Color { r: 0.039215688, g: 0.078431375, b: 0.11764706, a: 1.0, }, to: Color { r: 0.0, g: 0.5019608, b: 1.0, a: 1.0, }, angle: 180, relative_to: WorkspaceView, in_: GradientInterpolation { color_space: Srgb, hue_interpolation: Shorter, }, }, ), }, preset_column_widths: [ Proportion( 0.25, ), Proportion( 0.5, ), Fixed( 960, ), Fixed( 1280, ), ], default_column_width: Some( DefaultPresetSize( Some( Proportion( 0.25, ), ), ), ), preset_window_heights: [ Proportion( 0.25, ), Proportion( 0.5, ), Fixed( 960, ), Fixed( 1280, ), ], center_focused_column: OnOverflow, always_center_single_column: false, empty_workspace_above_first: false, default_column_display: Tabbed, gaps: FloatOrInt( 8.0, ), struts: Struts { left: FloatOrInt( 1.0, ), right: FloatOrInt( 2.0, ), top: FloatOrInt( 3.0, ), bottom: FloatOrInt( 0.0, ), }, background_color: Color { r: 0.25, g: 0.25, b: 0.25, a: 1.0, }, }, prefer_no_csd: true, cursor: Cursor { xcursor_theme: "breeze_cursors", xcursor_size: 16, hide_when_typing: true, hide_after_inactive_ms: Some( 3000, ), }, screenshot_path: Some( "~/Screenshots/screenshot.png", ), clipboard: Clipboard { disable_primary: true, }, hotkey_overlay: HotkeyOverlay { skip_at_startup: true, hide_not_bound: false, }, config_notification: ConfigNotification { disable_failed: false, }, animations: Animations { off: false, slowdown: FloatOrInt( 2.0, ), workspace_switch: WorkspaceSwitchAnim( Animation { off: false, kind: Spring( SpringParams { damping_ratio: 1.0, stiffness: 1000, epsilon: 0.0001, }, ), }, ), window_open: WindowOpenAnim { anim: Animation { off: true, kind: Easing( EasingParams { duration_ms: 150, curve: EaseOutExpo, }, ), }, custom_shader: None, }, window_close: WindowCloseAnim { anim: Animation { off: false, kind: Easing( EasingParams { duration_ms: 150, curve: EaseOutQuad, }, ), }, custom_shader: None, }, horizontal_view_movement: HorizontalViewMovementAnim( Animation { off: false, kind: Easing( EasingParams { duration_ms: 100, curve: EaseOutExpo, }, ), }, ), window_movement: WindowMovementAnim( Animation { off: false, kind: Spring( SpringParams { damping_ratio: 1.0, stiffness: 800, epsilon: 0.0001, }, ), }, ), window_resize: WindowResizeAnim { anim: Animation { off: false, kind: Spring( SpringParams { damping_ratio: 1.0, stiffness: 800, epsilon: 0.0001, }, ), }, custom_shader: None, }, config_notification_open_close: ConfigNotificationOpenCloseAnim( Animation { off: false, kind: Spring( SpringParams { damping_ratio: 0.6, stiffness: 1000, epsilon: 0.001, }, ), }, ), screenshot_ui_open: ScreenshotUiOpenAnim( Animation { off: false, kind: Easing( EasingParams { duration_ms: 200, curve: EaseOutQuad, }, ), }, ), overview_open_close: OverviewOpenCloseAnim( Animation { off: false, kind: Spring( SpringParams { damping_ratio: 1.0, stiffness: 800, epsilon: 0.0001, }, ), }, ), }, gestures: Gestures { dnd_edge_view_scroll: DndEdgeViewScroll { trigger_width: FloatOrInt( 10.0, ), delay_ms: 100, max_speed: FloatOrInt( 50.0, ), }, dnd_edge_workspace_switch: DndEdgeWorkspaceSwitch { trigger_height: FloatOrInt( 50.0, ), delay_ms: 100, max_speed: FloatOrInt( 1500.0, ), }, hot_corners: HotCorners { off: false, }, }, overview: Overview { zoom: FloatOrInt( 0.5, ), backdrop_color: Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0, }, workspace_shadow: WorkspaceShadow { off: false, offset: ShadowOffset { x: FloatOrInt( 0.0, ), y: FloatOrInt( 10.0, ), }, softness: FloatOrInt( 40.0, ), spread: FloatOrInt( 10.0, ), color: Color { r: 0.0, g: 0.0, b: 0.0, a: 0.3137255, }, }, }, environment: Environment( [ EnvironmentVariable { name: "QT_QPA_PLATFORM", value: Some( "wayland", ), }, EnvironmentVariable { name: "DISPLAY", value: None, }, ], ), xwayland_satellite: XwaylandSatellite { off: false, path: "xwayland-satellite", }, window_rules: [ WindowRule { matches: [ Match { app_id: Some( RegexEq( Regex( ".*alacritty", ), ), ), title: None, is_active: None, is_focused: None, is_active_in_column: None, is_floating: None, is_window_cast_target: None, is_urgent: None, at_startup: None, }, ], excludes: [ Match { app_id: None, title: Some( RegexEq( Regex( "~", ), ), ), is_active: None, is_focused: None, is_active_in_column: None, is_floating: None, is_window_cast_target: None, is_urgent: None, at_startup: None, }, Match { app_id: None, title: None, is_active: Some( true, ), is_focused: Some( false, ), is_active_in_column: None, is_floating: None, is_window_cast_target: None, is_urgent: None, at_startup: None, }, ], default_column_width: None, default_window_height: Some( DefaultPresetSize( Some( Fixed( 500, ), ), ), ), open_on_output: Some( "eDP-1", ), open_on_workspace: None, open_maximized: Some( true, ), open_fullscreen: Some( false, ), open_floating: Some( false, ), open_focused: Some( true, ), min_width: None, min_height: None, max_width: None, max_height: None, focus_ring: BorderRule { off: true, on: false, width: Some( FloatOrInt( 3.0, ), ), active_color: None, inactive_color: None, urgent_color: None, active_gradient: None, inactive_gradient: None, urgent_gradient: None, }, border: BorderRule { off: false, on: true, width: Some( FloatOrInt( 8.5, ), ), active_color: None, inactive_color: None, urgent_color: None, active_gradient: None, inactive_gradient: None, urgent_gradient: None, }, shadow: ShadowRule { off: false, on: false, offset: None, softness: None, spread: None, draw_behind_window: None, color: None, inactive_color: None, }, tab_indicator: TabIndicatorRule { active_color: Some( Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0, }, ), inactive_color: None, urgent_color: None, active_gradient: None, inactive_gradient: None, urgent_gradient: None, }, draw_border_with_background: None, opacity: None, geometry_corner_radius: None, clip_to_geometry: None, baba_is_float: None, block_out_from: None, variable_refresh_rate: None, default_column_display: Some( Tabbed, ), default_floating_position: Some( FloatingPosition { x: FloatOrInt( 100.0, ), y: FloatOrInt( -200.0, ), relative_to: BottomLeft, }, ), scroll_factor: None, tiled_state: None, }, ], layer_rules: [ LayerRule { matches: [ Match { namespace: Some( RegexEq( Regex( "^notifications$", ), ), ), at_startup: None, }, ], excludes: [], opacity: None, block_out_from: Some( Screencast, ), shadow: ShadowRule { off: false, on: false, offset: None, softness: None, spread: None, draw_behind_window: None, color: None, inactive_color: None, }, geometry_corner_radius: None, place_within_backdrop: None, baba_is_float: None, }, ], binds: Binds( [ Bind { key: Key { trigger: Keysym( XK_Escape, ), modifiers: Modifiers( COMPOSITOR, ), }, action: ToggleKeyboardShortcutsInhibit, repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: false, hotkey_overlay_title: Some( Some( "Inhibit", ), ), }, Bind { key: Key { trigger: Keysym( XK_Escape, ), modifiers: Modifiers( SHIFT | COMPOSITOR, ), }, action: ToggleKeyboardShortcutsInhibit, repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: false, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_t, ), modifiers: Modifiers( COMPOSITOR, ), }, action: Spawn( [ "alacritty", ], ), repeat: true, cooldown: None, allow_when_locked: true, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_q, ), modifiers: Modifiers( COMPOSITOR, ), }, action: CloseWindow, repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: Some( None, ), }, Bind { key: Key { trigger: Keysym( XK_h, ), modifiers: Modifiers( SHIFT | COMPOSITOR, ), }, action: FocusMonitorLeft, repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_o, ), modifiers: Modifiers( SHIFT | COMPOSITOR, ), }, action: FocusMonitor( "eDP-1", ), repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_l, ), modifiers: Modifiers( CTRL | SHIFT | COMPOSITOR, ), }, action: MoveWindowToMonitorRight, repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_o, ), modifiers: Modifiers( CTRL | ALT | COMPOSITOR, ), }, action: MoveWindowToMonitor( "eDP-1", ), repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_p, ), modifiers: Modifiers( CTRL | ALT | COMPOSITOR, ), }, action: MoveColumnToMonitor( "DP-1", ), repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_comma, ), modifiers: Modifiers( COMPOSITOR, ), }, action: ConsumeWindowIntoColumn, repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_1, ), modifiers: Modifiers( COMPOSITOR, ), }, action: FocusWorkspace( Index( 1, ), ), repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_1, ), modifiers: Modifiers( SHIFT | COMPOSITOR, ), }, action: FocusWorkspace( Name( "workspace-1", ), ), repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_e, ), modifiers: Modifiers( SHIFT | COMPOSITOR, ), }, action: Quit( true, ), repeat: true, cooldown: None, allow_when_locked: false, allow_inhibiting: false, hotkey_overlay_title: None, }, Bind { key: Key { trigger: WheelScrollDown, modifiers: Modifiers( COMPOSITOR, ), }, action: FocusWorkspaceDown, repeat: true, cooldown: Some( 150ms, ), allow_when_locked: false, allow_inhibiting: true, hotkey_overlay_title: None, }, Bind { key: Key { trigger: Keysym( XK_s, ), modifiers: Modifiers( ALT | SUPER, ), }, action: SpawnSh( "pkill orca || exec orca", ), repeat: true, cooldown: None, allow_when_locked: true, allow_inhibiting: true, hotkey_overlay_title: None, }, ], ), switch_events: SwitchBinds { lid_open: None, lid_close: None, tablet_mode_on: Some( SwitchAction { spawn: [ "bash", "-c", "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true", ], }, ), tablet_mode_off: Some( SwitchAction { spawn: [ "bash", "-c", "gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false", ], }, ), }, debug: DebugConfig { preview_render: None, dbus_interfaces_in_non_session_instances: false, wait_for_frame_completion_before_queueing: false, enable_overlay_planes: false, disable_cursor_plane: false, disable_direct_scanout: false, keep_max_bpc_unchanged: false, restrict_primary_scanout_to_matching_format: false, render_drm_device: Some( "/dev/dri/renderD129", ), force_pipewire_invalid_modifier: false, emulate_zero_presentation_time: false, disable_resize_throttling: false, disable_transactions: false, keep_laptop_panel_on_when_lid_is_closed: false, disable_monitor_names: false, strict_new_window_focus_policy: false, honor_xdg_activation_with_invalid_serial: false, deactivate_unfocused_windows: false, skip_cursor_only_updates_during_vrr: false, }, workspaces: [ Workspace { name: WorkspaceName( "workspace-1", ), open_on_output: Some( "eDP-1", ), }, Workspace { name: WorkspaceName( "workspace-2", ), open_on_output: None, }, Workspace { name: WorkspaceName( "workspace-3", ), open_on_output: None, }, ], } "#); } #[test] fn can_create_default_config() { let _ = Config::default(); } #[test] fn parse_mode() { assert_eq!( "2560x1600@165.004".parse::().unwrap(), ConfiguredMode { width: 2560, height: 1600, refresh: Some(165.004), }, ); assert_eq!( "1920x1080".parse::().unwrap(), ConfiguredMode { width: 1920, height: 1080, refresh: None, }, ); assert!("1920".parse::().is_err()); assert!("1920x".parse::().is_err()); assert!("1920x1080@".parse::().is_err()); assert!("1920x1080@60Hz".parse::().is_err()); } #[test] fn parse_size_change() { assert_eq!( "10".parse::().unwrap(), SizeChange::SetFixed(10), ); assert_eq!( "+10".parse::().unwrap(), SizeChange::AdjustFixed(10), ); assert_eq!( "-10".parse::().unwrap(), SizeChange::AdjustFixed(-10), ); assert_eq!( "10%".parse::().unwrap(), SizeChange::SetProportion(10.), ); assert_eq!( "+10%".parse::().unwrap(), SizeChange::AdjustProportion(10.), ); assert_eq!( "-10%".parse::().unwrap(), SizeChange::AdjustProportion(-10.), ); assert!("-".parse::().is_err()); assert!("10% ".parse::().is_err()); } #[test] fn parse_position_change() { assert_eq!( "10".parse::().unwrap(), PositionChange::SetFixed(10.), ); assert_eq!( "+10".parse::().unwrap(), PositionChange::AdjustFixed(10.), ); assert_eq!( "-10".parse::().unwrap(), PositionChange::AdjustFixed(-10.), ); assert!("10%".parse::().is_err()); assert!("+10%".parse::().is_err()); assert!("-10%".parse::().is_err()); assert!("-".parse::().is_err()); assert!("10% ".parse::().is_err()); } #[test] fn parse_gradient_interpolation() { assert_eq!( "srgb".parse::().unwrap(), GradientInterpolation { color_space: GradientColorSpace::Srgb, ..Default::default() } ); assert_eq!( "srgb-linear".parse::().unwrap(), GradientInterpolation { color_space: GradientColorSpace::SrgbLinear, ..Default::default() } ); assert_eq!( "oklab".parse::().unwrap(), GradientInterpolation { color_space: GradientColorSpace::Oklab, ..Default::default() } ); assert_eq!( "oklch".parse::().unwrap(), GradientInterpolation { color_space: GradientColorSpace::Oklch, ..Default::default() } ); assert_eq!( "oklch shorter hue" .parse::() .unwrap(), GradientInterpolation { color_space: GradientColorSpace::Oklch, hue_interpolation: HueInterpolation::Shorter, } ); assert_eq!( "oklch longer hue".parse::().unwrap(), GradientInterpolation { color_space: GradientColorSpace::Oklch, hue_interpolation: HueInterpolation::Longer, } ); assert_eq!( "oklch decreasing hue" .parse::() .unwrap(), GradientInterpolation { color_space: GradientColorSpace::Oklch, hue_interpolation: HueInterpolation::Decreasing, } ); assert_eq!( "oklch increasing hue" .parse::() .unwrap(), GradientInterpolation { color_space: GradientColorSpace::Oklch, hue_interpolation: HueInterpolation::Increasing, } ); assert!("".parse::().is_err()); assert!("srgb shorter hue".parse::().is_err()); assert!("oklch shorter".parse::().is_err()); assert!("oklch shorter h".parse::().is_err()); assert!("oklch a hue".parse::().is_err()); assert!("oklch shorter hue a" .parse::() .is_err()); } #[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 }, ); } #[test] fn default_repeat_params() { let config = Config::parse("config.kdl", "").unwrap(); assert_eq!(config.input.keyboard.repeat_delay, 600); assert_eq!(config.input.keyboard.repeat_rate, 25); } fn make_output_name( connector: &str, make: Option<&str>, model: Option<&str>, serial: Option<&str>, ) -> OutputName { OutputName { connector: connector.to_string(), make: make.map(|x| x.to_string()), model: model.map(|x| x.to_string()), serial: serial.map(|x| x.to_string()), } } #[test] fn test_output_name_match() { fn check( target: &str, connector: &str, make: Option<&str>, model: Option<&str>, serial: Option<&str>, ) -> bool { let name = make_output_name(connector, make, model, serial); name.matches(target) } assert!(check("dp-2", "DP-2", None, None, None)); assert!(!check("dp-1", "DP-2", None, None, None)); assert!(check("dp-2", "DP-2", Some("a"), Some("b"), Some("c"))); assert!(check( "some company some monitor 1234", "DP-2", Some("Some Company"), Some("Some Monitor"), Some("1234") )); assert!(!check( "some other company some monitor 1234", "DP-2", Some("Some Company"), Some("Some Monitor"), Some("1234") )); assert!(!check( "make model serial ", "DP-2", Some("make"), Some("model"), Some("serial") )); assert!(check( "make serial", "DP-2", Some("make"), Some(""), Some("serial") )); assert!(check( "make model unknown", "DP-2", Some("Make"), Some("Model"), None )); assert!(check( "unknown unknown serial", "DP-2", None, None, Some("Serial") )); assert!(!check("unknown unknown unknown", "DP-2", None, None, None)); } #[test] fn test_output_name_sorting() { let mut names = vec![ make_output_name("DP-2", None, None, None), make_output_name("DP-1", None, None, None), make_output_name("DP-3", Some("B"), Some("A"), Some("A")), make_output_name("DP-3", Some("A"), Some("B"), Some("A")), make_output_name("DP-3", Some("A"), Some("A"), Some("B")), make_output_name("DP-3", None, Some("A"), Some("A")), make_output_name("DP-3", Some("A"), None, Some("A")), make_output_name("DP-3", Some("A"), Some("A"), None), make_output_name("DP-5", Some("A"), Some("A"), Some("A")), make_output_name("DP-4", Some("A"), Some("A"), Some("A")), ]; names.sort_by(|a, b| a.compare(b)); let names = names .into_iter() .map(|name| { format!( "{} | {}", name.format_make_model_serial_or_connector(), name.connector, ) }) .collect::>(); assert_debug_snapshot!( names, @r#" [ "Unknown A A | DP-3", "A Unknown A | DP-3", "A A Unknown | DP-3", "A A A | DP-4", "A A A | DP-5", "A A B | DP-3", "A B A | DP-3", "B A A | DP-3", "DP-1 | DP-1", "DP-2 | DP-2", ] "# ); } #[test] fn test_border_rule_on_off_merging() { fn is_on(config: &str, rules: &[&str]) -> String { let mut resolved = BorderRule { off: false, on: false, width: None, active_color: None, inactive_color: None, urgent_color: None, active_gradient: None, inactive_gradient: None, urgent_gradient: None, }; for rule in rules.iter().copied() { let rule = BorderRule { off: rule == "off" || rule == "off,on", on: rule == "on" || rule == "off,on", ..Default::default() }; resolved.merge_with(&rule); } let config = Border { off: config == "off", ..Default::default() }; if resolved.resolve_against(config).off { "off" } else { "on" } .to_owned() } assert_snapshot!(is_on("off", &[]), @"off"); assert_snapshot!(is_on("off", &["off"]), @"off"); assert_snapshot!(is_on("off", &["on"]), @"on"); assert_snapshot!(is_on("off", &["off,on"]), @"on"); assert_snapshot!(is_on("on", &[]), @"on"); assert_snapshot!(is_on("on", &["off"]), @"off"); assert_snapshot!(is_on("on", &["on"]), @"on"); assert_snapshot!(is_on("on", &["off,on"]), @"on"); assert_snapshot!(is_on("off", &["off", "off"]), @"off"); assert_snapshot!(is_on("off", &["off", "on"]), @"on"); assert_snapshot!(is_on("off", &["on", "off"]), @"off"); assert_snapshot!(is_on("off", &["on", "on"]), @"on"); assert_snapshot!(is_on("on", &["off", "off"]), @"off"); assert_snapshot!(is_on("on", &["off", "on"]), @"on"); assert_snapshot!(is_on("on", &["on", "off"]), @"off"); assert_snapshot!(is_on("on", &["on", "on"]), @"on"); } }