diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-01-07 09:07:22 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-01-07 09:28:14 +0400 |
| commit | 64c41fa2c8853aefc8f62bf9492043a6c25b8c8f (patch) | |
| tree | ce7e5d6dfd02b844e0c2d8f91e13348ea8e4c3ae /niri-config/src | |
| parent | 4e0aa391137a53180783ab3d2d0ff0cc6311b23b (diff) | |
| download | niri-64c41fa2c8853aefc8f62bf9492043a6c25b8c8f.tar.gz niri-64c41fa2c8853aefc8f62bf9492043a6c25b8c8f.tar.bz2 niri-64c41fa2c8853aefc8f62bf9492043a6c25b8c8f.zip | |
Move config into a separate crate
Get miette and knuffel deps contained within.
Diffstat (limited to 'niri-config/src')
| -rw-r--r-- | niri-config/src/lib.rs | 889 |
1 files changed, 889 insertions, 0 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs new file mode 100644 index 00000000..19f6d036 --- /dev/null +++ b/niri-config/src/lib.rs @@ -0,0 +1,889 @@ +#[macro_use] +extern crate tracing; + +use std::path::PathBuf; +use std::str::FromStr; + +use bitflags::bitflags; +use directories::ProjectDirs; +use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler}; +use smithay::input::keyboard::keysyms::KEY_NoSymbol; +use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE}; +use smithay::input::keyboard::{Keysym, XkbConfig}; + +#[derive(knuffel::Decode, Debug, PartialEq)] +pub struct Config { + #[knuffel(child, default)] + pub input: Input, + #[knuffel(children(name = "output"))] + pub outputs: Vec<Output>, + #[knuffel(children(name = "spawn-at-startup"))] + pub spawn_at_startup: Vec<SpawnAtStartup>, + #[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<String>, + #[knuffel(child, default)] + pub binds: Binds, + #[knuffel(child, default)] + pub debug: DebugConfig, +} + +// FIXME: Add other devices. +#[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 tablet: Tablet, + #[knuffel(child)] + pub disable_power_key_handling: bool, +} + +#[derive(knuffel::Decode, Debug, Default, 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 = 600)] + pub repeat_delay: u16, + #[knuffel(child, unwrap(argument), default = 25)] + pub repeat_rate: u8, + #[knuffel(child, unwrap(argument), default)] + pub track_layout: TrackLayout, +} + +#[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))] + pub layout: Option<String>, + #[knuffel(child, unwrap(argument), default)] + pub variant: String, + #[knuffel(child, unwrap(argument))] + pub options: Option<String>, +} + +impl Xkb { + pub fn to_xkb_config(&self) -> XkbConfig { + XkbConfig { + rules: &self.rules, + model: &self.model, + layout: self.layout.as_deref().unwrap_or("us"), + variant: &self.variant, + options: self.options.clone(), + } + } +} + +#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)] +pub enum TrackLayout { + /// The layout change is global. + #[default] + Global, + /// The layout change is window local. + Window, +} + +// FIXME: Add the rest of the settings. +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct Touchpad { + #[knuffel(child)] + pub tap: bool, + #[knuffel(child)] + pub natural_scroll: bool, + #[knuffel(child, unwrap(argument), default)] + pub accel_speed: f64, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct Tablet { + #[knuffel(child, unwrap(argument))] + pub map_to_output: Option<String>, +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +pub struct Output { + #[knuffel(child)] + pub off: bool, + #[knuffel(argument)] + pub name: String, + #[knuffel(child, unwrap(argument), default = 1.)] + pub scale: f64, + #[knuffel(child)] + pub position: Option<Position>, + #[knuffel(child, unwrap(argument, str))] + pub mode: Option<Mode>, +} + +impl Default for Output { + fn default() -> Self { + Self { + off: false, + name: String::new(), + scale: 1., + position: None, + mode: None, + } + } +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +pub struct Position { + #[knuffel(property)] + pub x: i32, + #[knuffel(property)] + pub y: i32, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Mode { + pub width: u16, + pub height: u16, + pub refresh: Option<f64>, +} + +#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)] +pub struct Layout { + #[knuffel(child, default)] + pub focus_ring: FocusRing, + #[knuffel(child, default = default_border())] + pub border: FocusRing, + #[knuffel(child, unwrap(children), default)] + pub preset_column_widths: Vec<PresetWidth>, + #[knuffel(child)] + pub default_column_width: Option<DefaultColumnWidth>, + #[knuffel(child, unwrap(argument), default = 16)] + pub gaps: u16, + #[knuffel(child, default)] + pub struts: Struts, +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] +pub struct SpawnAtStartup { + #[knuffel(arguments)] + pub command: Vec<String>, +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] +pub struct FocusRing { + #[knuffel(child)] + pub off: bool, + #[knuffel(child, unwrap(argument), default = 4)] + pub width: u16, + #[knuffel(child, default = Color::new(127, 200, 255, 255))] + pub active_color: Color, + #[knuffel(child, default = Color::new(80, 80, 80, 255))] + pub inactive_color: Color, +} + +impl Default for FocusRing { + fn default() -> Self { + Self { + off: false, + width: 4, + active_color: Color::new(127, 200, 255, 255), + inactive_color: Color::new(80, 80, 80, 255), + } + } +} + +pub const fn default_border() -> FocusRing { + FocusRing { + off: true, + width: 4, + active_color: Color::new(255, 200, 127, 255), + inactive_color: Color::new(80, 80, 80, 255), + } +} + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Color { + #[knuffel(argument)] + pub r: u8, + #[knuffel(argument)] + pub g: u8, + #[knuffel(argument)] + pub b: u8, + #[knuffel(argument)] + pub a: u8, +} + +impl Color { + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } +} + +impl From<Color> for [f32; 4] { + fn from(c: Color) -> Self { + [c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.) + } +} + +#[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(knuffel::Decode, Debug, Clone, PartialEq)] +pub struct DefaultColumnWidth(#[knuffel(children)] pub Vec<PresetWidth>); + +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Struts { + #[knuffel(child, unwrap(argument), default)] + pub left: u16, + #[knuffel(child, unwrap(argument), default)] + pub right: u16, + #[knuffel(child, unwrap(argument), default)] + pub top: u16, + #[knuffel(child, unwrap(argument), default)] + pub bottom: u16, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq)] +pub struct Binds(#[knuffel(children)] pub Vec<Bind>); + +#[derive(knuffel::Decode, Debug, PartialEq)] +pub struct Bind { + #[knuffel(node_name)] + pub key: Key, + #[knuffel(children)] + pub actions: Vec<Action>, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Key { + pub keysym: Keysym, + pub modifiers: Modifiers, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct Modifiers : u8 { + const CTRL = 1; + const SHIFT = 2; + const ALT = 4; + const SUPER = 8; + const COMPOSITOR = 16; + } +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +pub enum Action { + Quit, + #[knuffel(skip)] + ChangeVt(i32), + Suspend, + PowerOffMonitors, + ToggleDebugTint, + Spawn(#[knuffel(arguments)] Vec<String>), + #[knuffel(skip)] + ConfirmScreenshot, + #[knuffel(skip)] + CancelScreenshot, + Screenshot, + ScreenshotScreen, + ScreenshotWindow, + CloseWindow, + FullscreenWindow, + FocusColumnLeft, + FocusColumnRight, + FocusColumnFirst, + FocusColumnLast, + FocusWindowDown, + FocusWindowUp, + FocusWindowOrWorkspaceDown, + FocusWindowOrWorkspaceUp, + MoveColumnLeft, + MoveColumnRight, + MoveColumnToFirst, + MoveColumnToLast, + MoveWindowDown, + MoveWindowUp, + MoveWindowDownOrToWorkspaceDown, + MoveWindowUpOrToWorkspaceUp, + ConsumeWindowIntoColumn, + ExpelWindowFromColumn, + CenterColumn, + FocusWorkspaceDown, + FocusWorkspaceUp, + FocusWorkspace(#[knuffel(argument)] u8), + MoveWindowToWorkspaceDown, + MoveWindowToWorkspaceUp, + MoveWindowToWorkspace(#[knuffel(argument)] u8), + MoveWorkspaceDown, + MoveWorkspaceUp, + FocusMonitorLeft, + FocusMonitorRight, + FocusMonitorDown, + FocusMonitorUp, + MoveWindowToMonitorLeft, + MoveWindowToMonitorRight, + MoveWindowToMonitorDown, + MoveWindowToMonitorUp, + SetWindowHeight(#[knuffel(argument, str)] SizeChange), + SwitchPresetColumnWidth, + MaximizeColumn, + SetColumnWidth(#[knuffel(argument, str)] SizeChange), + SwitchLayout(#[knuffel(argument)] LayoutAction), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SizeChange { + SetFixed(i32), + SetProportion(f64), + AdjustFixed(i32), + AdjustProportion(f64), +} + +#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq)] +pub enum LayoutAction { + Next, + Prev, +} + +#[derive(knuffel::Decode, Debug, PartialEq)] +pub struct DebugConfig { + #[knuffel(child, unwrap(argument), default = 1.)] + pub animation_slowdown: f64, + #[knuffel(child)] + pub dbus_interfaces_in_non_session_instances: bool, + #[knuffel(child)] + pub wait_for_frame_completion_before_queueing: bool, + #[knuffel(child)] + pub enable_color_transformations_capability: bool, + #[knuffel(child)] + pub enable_overlay_planes: bool, + #[knuffel(child)] + pub disable_cursor_plane: bool, + #[knuffel(child, unwrap(argument))] + pub render_drm_device: Option<PathBuf>, +} + +impl Default for DebugConfig { + fn default() -> Self { + Self { + animation_slowdown: 1., + dbus_interfaces_in_non_session_instances: false, + wait_for_frame_completion_before_queueing: false, + enable_color_transformations_capability: false, + enable_overlay_planes: false, + disable_cursor_plane: false, + render_drm_device: None, + } + } +} + +impl Config { + pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> { + Self::load_internal(path).context("error loading config") + } + + fn load_internal(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> { + let path = if let Some(path) = path { + path + } else { + let mut path = ProjectDirs::from("", "", "niri") + .ok_or_else(|| miette!("error retrieving home directory"))? + .config_dir() + .to_owned(); + path.push("config.kdl"); + path + }; + + let contents = std::fs::read_to_string(&path) + .into_diagnostic() + .with_context(|| format!("error reading {path:?}"))?; + + let config = Self::parse("config.kdl", &contents).context("error parsing")?; + debug!("loaded config from {path:?}"); + Ok((config, path)) + } + + pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> { + knuffel::parse(filename, text) + } +} + +impl Default for Config { + fn default() -> Self { + Config::parse( + "default-config.kdl", + include_str!("../../resources/default-config.kdl"), + ) + .unwrap() + } +} + +impl FromStr for Mode { + type Err = miette::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let Some((width, rest)) = s.split_once('x') else { + return Err(miette!("no 'x' separator found")); + }; + + let (height, refresh) = match rest.split_once('@') { + Some((height, refresh)) => (height, Some(refresh)), + None => (rest, None), + }; + + let width = width + .parse() + .into_diagnostic() + .context("error parsing width")?; + let height = height + .parse() + .into_diagnostic() + .context("error parsing height")?; + let refresh = refresh + .map(str::parse) + .transpose() + .into_diagnostic() + .context("error parsing refresh rate")?; + + Ok(Self { + width, + height, + refresh, + }) + } +} + +impl FromStr for Key { + type Err = miette::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut modifiers = Modifiers::empty(); + + let mut split = s.split('+'); + let key = split.next_back().unwrap(); + + for part in split { + let part = part.trim(); + if part.eq_ignore_ascii_case("mod") { + modifiers |= Modifiers::COMPOSITOR + } else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") { + modifiers |= Modifiers::CTRL; + } else if part.eq_ignore_ascii_case("shift") { + modifiers |= Modifiers::SHIFT; + } else if part.eq_ignore_ascii_case("alt") { + modifiers |= Modifiers::ALT; + } else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") { + modifiers |= Modifiers::SUPER; + } else { + return Err(miette!("invalid modifier: {part}")); + } + } + + let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE); + if keysym.raw() == KEY_NoSymbol { + return Err(miette!("invalid key: {key}")); + } + + Ok(Key { keysym, modifiers }) + } +} + +impl FromStr for SizeChange { + type Err = miette::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.split_once('%') { + Some((value, empty)) => { + if !empty.is_empty() { + return Err(miette!("trailing characters after '%' are not allowed")); + } + + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::AdjustProportion(value)) + } + Some(_) => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::SetProportion(value)) + } + None => Err(miette!("value is missing")), + } + } + None => { + let value = s; + match value.bytes().next() { + Some(b'-' | b'+') => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::AdjustFixed(value)) + } + Some(_) => { + let value = value + .parse() + .into_diagnostic() + .context("error parsing value")?; + Ok(Self::SetFixed(value)) + } + None => Err(miette!("value is missing")), + } + } + } + } +} + +pub fn set_miette_hook() -> Result<(), miette::InstallError> { + miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))) +} + +#[cfg(test)] +mod tests { + use miette::NarratableReportHandler; + + use super::*; + + #[track_caller] + fn check(text: &str, expected: Config) { + let _ = miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))); + + 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 + accel-speed 0.2 + } + + tablet { + map-to-output "eDP-1" + } + + disable-power-key-handling + } + + output "eDP-1" { + scale 2.0 + position x=10 y=20 + mode "1920x1080@144" + } + + layout { + focus-ring { + width 5 + active-color 0 100 200 255 + inactive-color 255 200 100 0 + } + + border { + width 3 + active-color 0 100 200 255 + inactive-color 255 200 100 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 + } + } + + spawn-at-startup "alacritty" "-e" "fish" + + prefer-no-csd + + cursor { + xcursor-theme "breeze_cursors" + xcursor-size 16 + } + + screenshot-path "~/Screenshots/screenshot.png" + + binds { + Mod+T { 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;} + } + + debug { + animation-slowdown 2.0 + render-drm-device "/dev/dri/renderD129" + } + "#, + Config { + input: Input { + keyboard: Keyboard { + xkb: Xkb { + layout: Some("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 { + tap: true, + natural_scroll: false, + accel_speed: 0.2, + }, + tablet: Tablet { + map_to_output: Some("eDP-1".to_owned()), + }, + disable_power_key_handling: true, + }, + outputs: vec![Output { + off: false, + name: "eDP-1".to_owned(), + scale: 2., + position: Some(Position { x: 10, y: 20 }), + mode: Some(Mode { + width: 1920, + height: 1080, + refresh: Some(144.), + }), + }], + layout: Layout { + focus_ring: FocusRing { + off: false, + width: 5, + active_color: Color { + r: 0, + g: 100, + b: 200, + a: 255, + }, + inactive_color: Color { + r: 255, + g: 200, + b: 100, + a: 0, + }, + }, + border: FocusRing { + off: false, + width: 3, + active_color: Color { + r: 0, + g: 100, + b: 200, + a: 255, + }, + inactive_color: Color { + r: 255, + g: 200, + b: 100, + a: 0, + }, + }, + preset_column_widths: vec![ + PresetWidth::Proportion(0.25), + PresetWidth::Proportion(0.5), + PresetWidth::Fixed(960), + PresetWidth::Fixed(1280), + ], + default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion( + 0.25, + )])), + gaps: 8, + struts: Struts { + left: 1, + right: 2, + top: 3, + bottom: 0, + }, + }, + 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")), + binds: Binds(vec![ + Bind { + key: Key { + keysym: Keysym::t, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::Spawn(vec!["alacritty".to_owned()])], + }, + Bind { + key: Key { + keysym: Keysym::q, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::CloseWindow], + }, + Bind { + key: Key { + keysym: Keysym::h, + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + actions: vec![Action::FocusMonitorLeft], + }, + Bind { + key: Key { + keysym: Keysym::l, + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, + }, + actions: vec![Action::MoveWindowToMonitorRight], + }, + Bind { + key: Key { + keysym: Keysym::comma, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::ConsumeWindowIntoColumn], + }, + Bind { + key: Key { + keysym: Keysym::_1, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::FocusWorkspace(1)], + }, + ]), + debug: DebugConfig { + animation_slowdown: 2., + 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::<Mode>().unwrap(), + Mode { + width: 2560, + height: 1600, + refresh: Some(165.004), + }, + ); + + assert_eq!( + "1920x1080".parse::<Mode>().unwrap(), + Mode { + width: 1920, + height: 1080, + refresh: None, + }, + ); + + assert!("1920".parse::<Mode>().is_err()); + assert!("1920x".parse::<Mode>().is_err()); + assert!("1920x1080@".parse::<Mode>().is_err()); + assert!("1920x1080@60Hz".parse::<Mode>().is_err()); + } + + #[test] + fn parse_size_change() { + assert_eq!( + "10".parse::<SizeChange>().unwrap(), + SizeChange::SetFixed(10), + ); + assert_eq!( + "+10".parse::<SizeChange>().unwrap(), + SizeChange::AdjustFixed(10), + ); + assert_eq!( + "-10".parse::<SizeChange>().unwrap(), + SizeChange::AdjustFixed(-10), + ); + assert_eq!( + "10%".parse::<SizeChange>().unwrap(), + SizeChange::SetProportion(10.), + ); + assert_eq!( + "+10%".parse::<SizeChange>().unwrap(), + SizeChange::AdjustProportion(10.), + ); + assert_eq!( + "-10%".parse::<SizeChange>().unwrap(), + SizeChange::AdjustProportion(-10.), + ); + + assert!("-".parse::<SizeChange>().is_err()); + assert!("10% ".parse::<SizeChange>().is_err()); + } +} |
