#[macro_use] extern crate tracing; use std::collections::HashSet; use std::ffi::OsStr; 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 miette::{miette, Context, IntoDiagnostic, NarratableReportHandler}; use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg}; use regex::Regex; 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.2, 0.2, 0.2, 1.]); #[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(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 hotkey_overlay: HotkeyOverlay, #[knuffel(child, default)] pub animations: Animations, #[knuffel(child, default)] pub environment: Environment, #[knuffel(children(name = "window-rule"))] pub window_rules: Vec, #[knuffel(child, default)] pub binds: Binds, #[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 tablet: Tablet, #[knuffel(child, default)] pub touch: Touch, #[knuffel(child)] pub disable_power_key_handling: bool, #[knuffel(child)] pub warp_mouse_to_focus: bool, #[knuffel(child)] pub focus_follows_mouse: Option, #[knuffel(child)] pub workspace_auto_back_and_forth: bool, } #[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, } impl Default for Keyboard { fn default() -> Self { Self { xkb: Default::default(), repeat_delay: 600, repeat_rate: 25, track_layout: 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, } 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, 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)] pub natural_scroll: bool, #[knuffel(child, unwrap(argument, str))] pub click_method: Option, #[knuffel(child, unwrap(argument), default)] pub accel_speed: f64, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[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, } #[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: f64, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[knuffel(child)] pub left_handed: bool, #[knuffel(child)] pub middle_emulation: bool, } #[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: f64, #[knuffel(child, unwrap(argument, str))] pub accel_profile: Option, #[knuffel(child, unwrap(argument, str))] pub scroll_method: Option, #[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(argument))] pub map_to_output: Option, #[knuffel(child)] pub left_handed: bool, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Touch { #[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(Debug, Clone, Copy, PartialEq)] pub struct Percent(pub f64); #[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: bool, #[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)] pub background_color: Color, } impl Default for Output { fn default() -> Self { Self { off: false, name: String::new(), scale: None, transform: Transform::Normal, position: None, mode: None, variable_refresh_rate: false, background_color: DEFAULT_BACKGROUND_COLOR, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] pub struct Position { #[knuffel(property)] pub x: i32, #[knuffel(property)] pub y: i32, } // 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, unwrap(children), default)] pub preset_column_widths: Vec, #[knuffel(child)] pub default_column_width: Option, #[knuffel(child, unwrap(argument), default)] pub center_focused_column: CenterFocusedColumn, #[knuffel(child, unwrap(argument), default = Self::default().gaps)] pub gaps: FloatOrInt<0, 65535>, #[knuffel(child, default)] pub struts: Struts, } impl Default for Layout { fn default() -> Self { Self { focus_ring: Default::default(), border: Default::default(), preset_column_widths: Default::default(), default_column_width: Default::default(), center_focused_column: Default::default(), gaps: FloatOrInt(16.), struts: Default::default(), } } } #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct SpawnAtStartup { #[knuffel(arguments)] pub command: Vec, } #[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)] pub active_gradient: Option, #[knuffel(child)] pub inactive_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), active_gradient: None, inactive_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, } #[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)] pub active_gradient: Option, #[knuffel(child)] pub inactive_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), active_gradient: None, inactive_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, active_gradient: value.active_gradient, inactive_gradient: value.inactive_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, active_gradient: value.active_gradient, inactive_gradient: value.inactive_gradient, } } } /// 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 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] } } #[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, } impl Default for Cursor { fn default() -> Self { Self { xcursor_theme: String::from("default"), xcursor_size: 24, } } } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] pub enum PresetWidth { Proportion(#[knuffel(argument)] f64), Fixed(#[knuffel(argument)] i32), } #[derive(Debug, Clone, PartialEq)] pub struct DefaultColumnWidth(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, } #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Animations { #[knuffel(child)] pub off: bool, #[knuffel(child, unwrap(argument), default = 1.)] pub slowdown: f64, #[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, } impl Default for Animations { fn default() -> Self { Self { off: false, slowdown: 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(), } } } #[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 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, 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 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, 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, // 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, 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 block_out_from: Option, } // Remember to update the PartialEq impl when adding fields! #[derive(knuffel::Decode, Debug, Default, Clone)] 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 at_startup: Option, } impl PartialEq for Match { fn eq(&self, other: &Self) -> bool { self.is_active == other.is_active && self.is_focused == other.is_focused && self.is_active_in_column == other.is_active_in_column && self.at_startup == other.at_startup && self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str) && self.title.as_ref().map(Regex::as_str) == other.title.as_ref().map(Regex::as_str) } } #[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, ] } } #[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 active_gradient: Option, #[knuffel(child)] pub inactive_gradient: Option, } #[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, } #[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), 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; } } // 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, ToggleDebugTint, DebugToggleOpaqueRegions, DebugToggleDamage, Spawn(#[knuffel(arguments)] Vec), DoScreenTransition(#[knuffel(property(name = "delay-ms"))] Option), #[knuffel(skip)] ConfirmScreenshot, #[knuffel(skip)] CancelScreenshot, #[knuffel(skip)] ScreenshotTogglePointer, Screenshot, ScreenshotScreen, ScreenshotWindow, CloseWindow, FullscreenWindow, FocusColumnLeft, FocusColumnRight, FocusColumnFirst, FocusColumnLast, FocusColumnRightOrFirst, FocusColumnLeftOrLast, FocusWindowOrMonitorUp, FocusWindowOrMonitorDown, FocusColumnOrMonitorLeft, FocusColumnOrMonitorRight, FocusWindowDown, FocusWindowUp, FocusWindowDownOrColumnLeft, FocusWindowDownOrColumnRight, FocusWindowUpOrColumnLeft, FocusWindowUpOrColumnRight, FocusWindowOrWorkspaceDown, FocusWindowOrWorkspaceUp, MoveColumnLeft, MoveColumnRight, MoveColumnToFirst, MoveColumnToLast, MoveColumnLeftOrToMonitorLeft, MoveColumnRightOrToMonitorRight, MoveWindowDown, MoveWindowUp, MoveWindowDownOrToWorkspaceDown, MoveWindowUpOrToWorkspaceUp, ConsumeOrExpelWindowLeft, ConsumeOrExpelWindowRight, ConsumeWindowIntoColumn, ExpelWindowFromColumn, CenterColumn, FocusWorkspaceDown, FocusWorkspaceUp, FocusWorkspace(#[knuffel(argument)] WorkspaceReference), FocusWorkspacePrevious, MoveWindowToWorkspaceDown, MoveWindowToWorkspaceUp, MoveWindowToWorkspace(#[knuffel(argument)] WorkspaceReference), MoveColumnToWorkspaceDown, MoveColumnToWorkspaceUp, MoveColumnToWorkspace(#[knuffel(argument)] WorkspaceReference), MoveWorkspaceDown, MoveWorkspaceUp, FocusMonitorLeft, FocusMonitorRight, FocusMonitorDown, FocusMonitorUp, MoveWindowToMonitorLeft, MoveWindowToMonitorRight, MoveWindowToMonitorDown, MoveWindowToMonitorUp, MoveColumnToMonitorLeft, MoveColumnToMonitorRight, MoveColumnToMonitorDown, MoveColumnToMonitorUp, SetWindowHeight(#[knuffel(argument, str)] SizeChange), ResetWindowHeight, SwitchPresetColumnWidth, MaximizeColumn, SetColumnWidth(#[knuffel(argument, str)] SizeChange), SwitchLayout(#[knuffel(argument, str)] LayoutSwitchTarget), ShowHotkeyOverlay, MoveWorkspaceToMonitorLeft, MoveWorkspaceToMonitorRight, MoveWorkspaceToMonitorDown, MoveWorkspaceToMonitorUp, } 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::Spawn { command } => Self::Spawn(command), niri_ipc::Action::DoScreenTransition { delay_ms } => Self::DoScreenTransition(delay_ms), niri_ipc::Action::Screenshot => Self::Screenshot, niri_ipc::Action::ScreenshotScreen => Self::ScreenshotScreen, niri_ipc::Action::ScreenshotWindow => Self::ScreenshotWindow, niri_ipc::Action::CloseWindow => Self::CloseWindow, niri_ipc::Action::FullscreenWindow => Self::FullscreenWindow, 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::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::MoveColumnLeft => Self::MoveColumnLeft, niri_ipc::Action::MoveColumnRight => Self::MoveColumnRight, niri_ipc::Action::MoveColumnToFirst => Self::MoveColumnToFirst, niri_ipc::Action::MoveColumnToLast => Self::MoveColumnToLast, 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 => Self::ConsumeOrExpelWindowLeft, niri_ipc::Action::ConsumeOrExpelWindowRight => Self::ConsumeOrExpelWindowRight, niri_ipc::Action::ConsumeWindowIntoColumn => Self::ConsumeWindowIntoColumn, niri_ipc::Action::ExpelWindowFromColumn => Self::ExpelWindowFromColumn, niri_ipc::Action::CenterColumn => Self::CenterColumn, 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 { reference } => { Self::MoveWindowToWorkspace(WorkspaceReference::from(reference)) } niri_ipc::Action::MoveColumnToWorkspaceDown => Self::MoveColumnToWorkspaceDown, niri_ipc::Action::MoveColumnToWorkspaceUp => Self::MoveColumnToWorkspaceUp, niri_ipc::Action::MoveColumnToWorkspace { reference } => { Self::MoveColumnToWorkspace(WorkspaceReference::from(reference)) } niri_ipc::Action::MoveWorkspaceDown => Self::MoveWorkspaceDown, niri_ipc::Action::MoveWorkspaceUp => Self::MoveWorkspaceUp, 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::MoveWindowToMonitorLeft => Self::MoveWindowToMonitorLeft, niri_ipc::Action::MoveWindowToMonitorRight => Self::MoveWindowToMonitorRight, niri_ipc::Action::MoveWindowToMonitorDown => Self::MoveWindowToMonitorDown, niri_ipc::Action::MoveWindowToMonitorUp => Self::MoveWindowToMonitorUp, 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::SetWindowHeight { change } => Self::SetWindowHeight(change), niri_ipc::Action::ResetWindowHeight => Self::ResetWindowHeight, niri_ipc::Action::SwitchPresetColumnWidth => Self::SwitchPresetColumnWidth, niri_ipc::Action::MaximizeColumn => Self::MaximizeColumn, niri_ipc::Action::SetColumnWidth { change } => Self::SetColumnWidth(change), 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::ToggleDebugTint => Self::ToggleDebugTint, niri_ipc::Action::DebugToggleOpaqueRegions => Self::DebugToggleOpaqueRegions, niri_ipc::Action::DebugToggleDamage => Self::DebugToggleDamage, } } } #[derive(Debug, PartialEq, Eq, Clone)] pub enum WorkspaceReference { Index(u8), Name(String), } impl From for WorkspaceReference { fn from(reference: WorkspaceReferenceArg) -> WorkspaceReference { match reference { 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, unwrap(argument))] pub render_drm_device: Option, #[knuffel(child)] pub emulate_zero_presentation_time: bool, } #[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)] pub enum PreviewRender { Screencast, ScreenCapture, } impl Config { pub fn load(path: &Path) -> miette::Result { let _span = tracy_client::span!("Config::load"); Self::load_internal(path).context("error loading config") } fn load_internal(path: &Path) -> miette::Result { let contents = std::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) { self.off |= other.off; self.on |= other.on; 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.active_gradient { self.active_gradient = Some(x); } if let Some(x) = other.inactive_gradient { self.inactive_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.active_gradient { config.active_gradient = Some(x); } if let Some(x) = self.inactive_gradient { config.inactive_gradient = Some(x); } config } } 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 { 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; } 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()?.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: &str) -> Option<&Output> { self.0.iter().find(|o| o.name.eq_ignore_ascii_case(name)) } pub fn find_mut(&mut self, name: &str) -> Option<&mut Output> { self.0 .iter_mut() .find(|o| o.name.eq_ignore_ascii_case(name)) } } impl knuffel::Decode for DefaultColumnWidth 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", )); } PresetWidth::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 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; 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); } 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, }; 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(_)) { 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", )); } } Ok(Self { key, action, repeat, cooldown, allow_when_locked, }) } Err(e) => { ctx.emit_error(e); Ok(dummy) } } } else { ctx.emit_error(DecodeError::missing( node, "expected an action for this keybind", )); Ok(dummy) } } } impl FromStr for Key { type Err = miette::Error; fn from_str(s: &str) -> Result { let mut modifiers = Modifiers::empty(); let mut split = s.split('+'); let key = split.next_back().unwrap(); for part in split { let part = part.trim(); if part.eq_ignore_ascii_case("mod") { modifiers |= Modifiers::COMPOSITOR } else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") { modifiers |= Modifiers::CTRL; } else if part.eq_ignore_ascii_case("shift") { modifiers |= Modifiers::SHIFT; } else if part.eq_ignore_ascii_case("alt") { modifiers |= Modifiers::ALT; } else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") { modifiers |= Modifiers::SUPER; } else if part.eq_ignore_ascii_case("iso_level3_shift") || part.eq_ignore_ascii_case("mod5") { modifiers |= Modifiers::ISO_LEVEL3_SHIFT; } else if part.eq_ignore_ascii_case("iso_level5_shift") || part.eq_ignore_ascii_case("mod3") { modifiers |= Modifiers::ISO_LEVEL5_SHIFT; } else { return Err(miette!("invalid modifier: {part}")); } } let trigger = if key.eq_ignore_ascii_case("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.)) } } pub fn set_miette_hook() -> Result<(), miette::InstallError> { miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))) } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use super::*; #[track_caller] fn check(text: &str, expected: Config) { let _ = set_miette_hook(); let parsed = Config::parse("test.kdl", text) .map_err(miette::Report::new) .unwrap(); assert_eq!(parsed, expected); } #[test] fn parse() { check( 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 click-method "clickfinger" accel-speed 0.2 accel-profile "flat" scroll-method "two-finger" tap-button-map "left-middle-right" disabled-on-external-mouse } mouse { natural-scroll accel-speed 0.4 accel-profile "flat" scroll-method "no-scroll" middle-emulation } trackpoint { off natural-scroll accel-speed 0.0 accel-profile "flat" scroll-method "on-button-down" } tablet { map-to-output "eDP-1" } touch { map-to-output "eDP-1" } disable-power-key-handling warp-mouse-to-focus focus-follows-mouse workspace-auto-back-and-forth } output "eDP-1" { scale 2 transform "flipped-90" position x=10 y=20 mode "1920x1080@144" variable-refresh-rate 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)" } preset-column-widths { 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" } spawn-at-startup "alacritty" "-e" "fish" prefer-no-csd cursor { xcursor-theme "breeze_cursors" xcursor-size 16 } screenshot-path "~/Screenshots/screenshot.png" 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; } } 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 focus-ring { off width 3 } border { on width 8.5 } } binds { Mod+T allow-when-locked=true { spawn "alacritty"; } Mod+Q { close-window; } Mod+Shift+H { focus-monitor-left; } Mod+Ctrl+Shift+L { move-window-to-monitor-right; } Mod+Comma { consume-window-into-column; } Mod+1 { focus-workspace 1; } Mod+Shift+1 { focus-workspace "workspace-1"; } Mod+Shift+E { quit skip-confirmation=true; } Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } } debug { render-drm-device "/dev/dri/renderD129" } workspace "workspace-1" { open-on-output "eDP-1" } workspace "workspace-2" workspace "workspace-3" "##, Config { input: Input { keyboard: Keyboard { xkb: Xkb { layout: "us,ru".to_owned(), options: Some("grp:win_space_toggle".to_owned()), ..Default::default() }, repeat_delay: 600, repeat_rate: 25, track_layout: TrackLayout::Window, }, touchpad: Touchpad { off: false, tap: true, dwt: true, dwtp: true, click_method: Some(ClickMethod::Clickfinger), natural_scroll: false, accel_speed: 0.2, accel_profile: Some(AccelProfile::Flat), scroll_method: Some(ScrollMethod::TwoFinger), tap_button_map: Some(TapButtonMap::LeftMiddleRight), left_handed: false, disabled_on_external_mouse: true, middle_emulation: false, }, mouse: Mouse { off: false, natural_scroll: true, accel_speed: 0.4, accel_profile: Some(AccelProfile::Flat), scroll_method: Some(ScrollMethod::NoScroll), left_handed: false, middle_emulation: true, }, trackpoint: Trackpoint { off: true, natural_scroll: true, accel_speed: 0.0, accel_profile: Some(AccelProfile::Flat), scroll_method: Some(ScrollMethod::OnButtonDown), middle_emulation: false, }, tablet: Tablet { off: false, map_to_output: Some("eDP-1".to_owned()), left_handed: false, }, touch: Touch { map_to_output: Some("eDP-1".to_owned()), }, disable_power_key_handling: true, warp_mouse_to_focus: true, focus_follows_mouse: Some(FocusFollowsMouse { max_scroll_amount: None, }), workspace_auto_back_and_forth: true, }, outputs: Outputs(vec![Output { off: false, name: "eDP-1".to_owned(), scale: Some(FloatOrInt(2.)), transform: Transform::Flipped90, position: Some(Position { x: 10, y: 20 }), mode: Some(ConfiguredMode { width: 1920, height: 1080, refresh: Some(144.), }), variable_refresh_rate: true, background_color: Color::from_rgba8_unpremul(25, 25, 102, 255), }]), layout: Layout { focus_ring: FocusRing { off: false, width: FloatOrInt(5.), active_color: Color::from_rgba8_unpremul(0, 100, 200, 255), inactive_color: Color::from_rgba8_unpremul(255, 200, 100, 0), active_gradient: Some(Gradient { from: Color::from_rgba8_unpremul(10, 20, 30, 255), to: Color::from_rgba8_unpremul(0, 128, 255, 255), angle: 180, relative_to: GradientRelativeTo::WorkspaceView, in_: GradientInterpolation { color_space: GradientColorSpace::Srgb, hue_interpolation: HueInterpolation::Shorter, }, }), inactive_gradient: None, }, border: Border { off: false, width: FloatOrInt(3.), active_color: Color::from_rgba8_unpremul(255, 200, 127, 255), inactive_color: Color::from_rgba8_unpremul(255, 200, 100, 0), active_gradient: None, inactive_gradient: None, }, preset_column_widths: vec![ PresetWidth::Proportion(0.25), PresetWidth::Proportion(0.5), PresetWidth::Fixed(960), PresetWidth::Fixed(1280), ], default_column_width: Some(DefaultColumnWidth(Some(PresetWidth::Proportion( 0.25, )))), gaps: FloatOrInt(8.), struts: Struts { left: FloatOrInt(1.), right: FloatOrInt(2.), top: FloatOrInt(3.), bottom: FloatOrInt(0.), }, center_focused_column: CenterFocusedColumn::OnOverflow, }, spawn_at_startup: vec![SpawnAtStartup { command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()], }], prefer_no_csd: true, cursor: Cursor { xcursor_theme: String::from("breeze_cursors"), xcursor_size: 16, }, screenshot_path: Some(String::from("~/Screenshots/screenshot.png")), hotkey_overlay: HotkeyOverlay { skip_at_startup: true, }, animations: Animations { slowdown: 2., workspace_switch: WorkspaceSwitchAnim(Animation { off: false, kind: AnimationKind::Spring(SpringParams { damping_ratio: 1., stiffness: 1000, epsilon: 0.0001, }), }), horizontal_view_movement: HorizontalViewMovementAnim(Animation { off: false, kind: AnimationKind::Easing(EasingParams { duration_ms: 100, curve: AnimationCurve::EaseOutExpo, }), }), window_open: WindowOpenAnim { anim: Animation { off: true, ..WindowOpenAnim::default().anim }, custom_shader: None, }, ..Default::default() }, environment: Environment(vec![ EnvironmentVariable { name: String::from("QT_QPA_PLATFORM"), value: Some(String::from("wayland")), }, EnvironmentVariable { name: String::from("DISPLAY"), value: None, }, ]), window_rules: vec![WindowRule { matches: vec![Match { app_id: Some(Regex::new(".*alacritty").unwrap()), title: None, is_active: None, is_focused: None, is_active_in_column: None, at_startup: None, }], excludes: vec![ Match { app_id: None, title: Some(Regex::new("~").unwrap()), is_active: None, is_focused: None, is_active_in_column: None, at_startup: None, }, Match { app_id: None, title: None, is_active: Some(true), is_focused: Some(false), is_active_in_column: None, at_startup: None, }, ], open_on_output: Some("eDP-1".to_owned()), open_maximized: Some(true), open_fullscreen: Some(false), focus_ring: BorderRule { off: true, width: Some(FloatOrInt(3.)), ..Default::default() }, border: BorderRule { on: true, width: Some(FloatOrInt(8.5)), ..Default::default() }, ..Default::default() }], workspaces: vec![ Workspace { name: WorkspaceName("workspace-1".to_string()), open_on_output: Some("eDP-1".to_string()), }, Workspace { name: WorkspaceName("workspace-2".to_string()), open_on_output: None, }, Workspace { name: WorkspaceName("workspace-3".to_string()), open_on_output: None, }, ], binds: Binds(vec![ Bind { key: Key { trigger: Trigger::Keysym(Keysym::t), modifiers: Modifiers::COMPOSITOR, }, action: Action::Spawn(vec!["alacritty".to_owned()]), repeat: true, cooldown: None, allow_when_locked: true, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::q), modifiers: Modifiers::COMPOSITOR, }, action: Action::CloseWindow, repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::h), modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::FocusMonitorLeft, repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::l), modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, }, action: Action::MoveWindowToMonitorRight, repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::comma), modifiers: Modifiers::COMPOSITOR, }, action: Action::ConsumeWindowIntoColumn, repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::_1), modifiers: Modifiers::COMPOSITOR, }, action: Action::FocusWorkspace(WorkspaceReference::Index(1)), repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::_1), modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::FocusWorkspace(WorkspaceReference::Name( "workspace-1".to_string(), )), repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::e), modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, }, action: Action::Quit(true), repeat: true, cooldown: None, allow_when_locked: false, }, Bind { key: Key { trigger: Trigger::WheelScrollDown, modifiers: Modifiers::COMPOSITOR, }, action: Action::FocusWorkspaceDown, repeat: true, cooldown: Some(Duration::from_millis(150)), allow_when_locked: false, }, ]), debug: DebugConfig { render_drm_device: Some(PathBuf::from("/dev/dri/renderD129")), ..Default::default() }, }, ); } #[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_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); } }