From 5225bc9e558cd87ed54271e47dcddaac2d5bcf62 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 5 Sep 2023 12:58:51 +0400 Subject: Add configuration file --- Cargo.lock | 268 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + README.md | 14 +- resources/default-config.kdl | 88 +++++++++++++ src/config.rs | 297 +++++++++++++++++++++++++++++++++++++++++++ src/input.rs | 190 +++++++++++---------------- src/main.rs | 22 +++- src/niri.rs | 27 +++- 8 files changed, 781 insertions(+), 128 deletions(-) create mode 100644 resources/default-config.kdl create mode 100644 src/config.rs diff --git a/Cargo.lock b/Cargo.lock index d42787f7..d35694a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,32 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -244,6 +264,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" + [[package]] name = "bitflags" version = "1.3.2" @@ -377,6 +427,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "chumsky" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23170228b96236b5a7299057ac284a321457700bc8c41a4476052f0f4ba5349d" +dependencies = [ + "hashbrown 0.12.3", +] + [[package]] name = "clap" version = "4.4.2" @@ -844,6 +903,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "gl_generator" version = "0.14.0" @@ -860,6 +925,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" @@ -872,6 +940,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -964,6 +1035,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.11", + "windows-sys 0.48.0", +] + +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itoa" version = "1.0.9" @@ -1009,6 +1097,33 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "knuffel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04bee6ddc6071011314b1ce4f7705fef6c009401dba4fd22cb0009db6a177413" +dependencies = [ + "base64", + "chumsky", + "knuffel-derive", + "miette", + "thiserror", + "unicode-width", +] + +[[package]] +name = "knuffel-derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91977f56c49cfb961e3d840e2e7c6e4a56bde7283898cf606861f1421348283d" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1159,6 +1274,38 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "backtrace", + "backtrace-ext", + "is-terminal", + "miette-derive", + "once_cell", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1226,9 +1373,12 @@ dependencies = [ "directories", "image", "keyframe", + "knuffel", "logind-zbus", + "miette", "profiling", "sd-notify", + "serde", "smithay", "smithay-drm-extras", "time", @@ -1405,6 +1555,15 @@ dependencies = [ "objc-sys", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -1442,6 +1601,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking" version = "2.1.0" @@ -1517,6 +1682,30 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -1673,6 +1862,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustix" version = "0.37.23" @@ -1824,6 +2019,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "smithay" version = "0.3.0" @@ -1917,6 +2118,34 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "supports-color" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4950e7174bffabe99455511c39707310e7e9b440364a2fcb1cc21521be57b354" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" +dependencies = [ + "is-terminal", +] + +[[package]] +name = "supports-unicode" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6c2cb240ab5dd21ed4906895ee23fe5a48acdbd15a3ce388e7b62a9b66baf7" +dependencies = [ + "is-terminal", +] + [[package]] name = "syn" version = "1.0.109" @@ -1952,6 +2181,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.48" @@ -2144,6 +2394,24 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 38ea6da4..165dfde7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,12 @@ clap = { version = "4.3.21", features = ["derive"] } directories = "5.0.1" image = { version = "0.24.7", default-features = false, features = ["png"] } keyframe = { version = "1.1.1", default-features = false } +knuffel = "3.2.0" logind-zbus = "3.1.2" +miette = { version = "5.10.0", features = ["fancy"] } profiling = "1.0.9" sd-notify = "0.4.1" +serde = { version = "1.0.188", features = ["derive"] } time = { version = "0.3.28", features = ["formatting", "local-offset", "macros"] } tracing = { version = "0.1.37", features = ["max_level_trace", "release_max_level_debug"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/README.md b/README.md index e7050be2..25f46605 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ You can also autostart systemd services like [mako] by symlinking them into `$HO Niri also somewhat-works with xdg-desktop-portal-gnome for Flatpak apps. -## Hotkeys +## Default Hotkeys When running on a TTY, the Mod key is Super. When running in a window, the Mod key is Alt. @@ -60,11 +60,9 @@ The general system is: if a hotkey switches somewhere, then adding CtrlModT | Spawn `alacritty` | -| ModD | Spawn `fuzzel` | -| ModN | Spawn `nautilus` | | ModQ | Close the focused window | -| ModH or Mod | Focus the window to the left | -| ModL or Mod | Focus the window to the right | +| ModH or Mod | Focus the column to the left | +| ModL or Mod | Focus the column to the right | | ModJ or Mod | Focus the window below in a column | | ModK or Mod | Focus the window above in a column | | ModCtrlH or ModCtrl | Move the focused column to the left | @@ -86,6 +84,12 @@ The general system is: if a hotkey switches somewhere, then adding CtrlModCtrlShiftT | Toggle debug tinting of rendered elements | | ModShiftE | Exit niri | +## Configuration + +Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`. +If this fails, it will load [the default configuration file](resources/default-config.kdl). +Please use the default configuration file as the starting point for your custom configuration. + [PaperWM]: https://github.com/paperwm/PaperWM [mako]: https://github.com/emersion/mako diff --git a/resources/default-config.kdl b/resources/default-config.kdl new file mode 100644 index 00000000..00a73451 --- /dev/null +++ b/resources/default-config.kdl @@ -0,0 +1,88 @@ +// This config is in the KDL format: https://kdl.dev +// "/-" comments out the following node. + +input { + keyboard { + xkb { + // You can set rules, model, layout, variant and options. + // For more information, see xkeyboard-config(7). + + // For example: + /-layout "us,ru" + /-options "grp:win_space_toggle,compose:ralt,ctrl:nocaps" + } + } + + // Next sections contain libinput settings. + // Omitting settings disables them, or leaves them at their default values. + touchpad { + tap + natural-scroll + /-accel-speed 0.2 + } +} + +binds { + // Keys consist of modifiers separated by + signs, followed by an XKB key name + // in the end. To find an XKB name for a particular key, you may use a program + // like wev. + // + // "Mod" is a special modifier equal to Super when running on a TTY, and to Alt + // when running as a winit window. + + Mod+T { spawn "alacritty"; } + Mod+Q { close-window; } + + Mod+H { focus-column-left; } + Mod+J { focus-window-down; } + Mod+K { focus-window-up; } + Mod+L { focus-column-right; } + Mod+Left { focus-column-left; } + Mod+Down { focus-window-down; } + Mod+Up { focus-window-up; } + Mod+Right { focus-column-right; } + + Mod+Ctrl+H { move-column-left; } + Mod+Ctrl+J { move-window-down; } + Mod+Ctrl+K { move-window-up; } + Mod+Ctrl+L { move-column-right; } + Mod+Ctrl+Left { move-column-left; } + Mod+Ctrl+Down { move-window-down; } + Mod+Ctrl+Up { move-window-up; } + Mod+Ctrl+Right { move-column-right; } + + Mod+Shift+H { focus-monitor-left; } + Mod+Shift+J { focus-monitor-down; } + Mod+Shift+K { focus-monitor-up; } + Mod+Shift+L { focus-monitor-right; } + Mod+Shift+Left { focus-monitor-left; } + Mod+Shift+Down { focus-monitor-down; } + Mod+Shift+Up { focus-monitor-up; } + Mod+Shift+Right { focus-monitor-right; } + + Mod+Shift+Ctrl+H { move-window-to-monitor-left; } + Mod+Shift+Ctrl+J { move-window-to-monitor-down; } + Mod+Shift+Ctrl+K { move-window-to-monitor-up; } + Mod+Shift+Ctrl+L { move-window-to-monitor-right; } + Mod+Shift+Ctrl+Left { move-window-to-monitor-left; } + Mod+Shift+Ctrl+Down { move-window-to-monitor-down; } + Mod+Shift+Ctrl+Up { move-window-to-monitor-up; } + Mod+Shift+Ctrl+Right { move-window-to-monitor-right; } + + Mod+U { focus-workspace-down; } + Mod+I { focus-workspace-up; } + Mod+Ctrl+U { move-window-to-workspace-down; } + Mod+Ctrl+I { move-window-to-workspace-up; } + + Mod+Comma { consume-window-into-column; } + Mod+Period { expel-window-from-column; } + + Mod+R { switch-preset-column-width; } + Mod+F { maximize-column; } + Mod+Shift+F { fullscreen-window; } + + Print { screenshot; } + Mod+Shift+E { quit; } + + Mod+Shift+Ctrl+T { toggle-debug-tint; } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..a7d6fd7d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,297 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use bitflags::bitflags; +use directories::ProjectDirs; +use miette::{miette, Context, IntoDiagnostic}; +use smithay::input::keyboard::xkb::{keysym_from_name, KEY_NoSymbol, KEYSYM_CASE_INSENSITIVE}; +use smithay::input::keyboard::Keysym; + +#[derive(knuffel::Decode, Debug, PartialEq)] +pub struct Config { + #[knuffel(child, default)] + pub input: Input, + #[knuffel(child, default)] + pub binds: Binds, +} + +// 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, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)] +pub struct Keyboard { + #[knuffel(child, default)] + pub xkb: Xkb, +} + +#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)] +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, + #[knuffel(child, unwrap(argument), default)] + pub variant: String, + #[knuffel(child, unwrap(argument))] + pub options: Option, +} + +// 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, Eq)] +pub struct Binds(#[knuffel(children)] pub Vec); + +#[derive(knuffel::Decode, Debug, PartialEq, Eq)] +pub struct Bind { + #[knuffel(node_name)] + pub key: Key, + #[knuffel(children)] + pub actions: Vec, +} + +#[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, Eq)] +pub enum Action { + #[knuffel(skip)] + None, + Quit, + #[knuffel(skip)] + ChangeVt(i32), + Suspend, + ToggleDebugTint, + Spawn(#[knuffel(arguments)] Vec), + Screenshot, + CloseWindow, + FullscreenWindow, + FocusColumnLeft, + FocusColumnRight, + FocusWindowDown, + FocusWindowUp, + MoveColumnLeft, + MoveColumnRight, + MoveWindowDown, + MoveWindowUp, + ConsumeWindowIntoColumn, + ExpelWindowFromColumn, + FocusWorkspaceDown, + FocusWorkspaceUp, + MoveWindowToWorkspaceDown, + MoveWindowToWorkspaceUp, + FocusMonitorLeft, + FocusMonitorRight, + FocusMonitorDown, + FocusMonitorUp, + MoveWindowToMonitorLeft, + MoveWindowToMonitorRight, + MoveWindowToMonitorDown, + MoveWindowToMonitorUp, + SwitchPresetColumnWidth, + MaximizeColumn, +} + +impl Config { + pub fn load(path: Option) -> miette::Result { + 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) + } + + pub fn parse(filename: &str, text: &str) -> Result { + 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 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 { + return Err(miette!("invalid modifier: {part}")); + } + } + + let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE); + if keysym == KEY_NoSymbol { + return Err(miette!("invalid key: {key}")); + } + + Ok(Key { keysym, modifiers }) + } +} + +#[cfg(test)] +mod tests { + use smithay::input::keyboard::xkb::keysyms::*; + + use super::*; + + #[track_caller] + fn check(text: &str, expected: Config) { + let parsed = Config::parse("test.kdl", text) + .map_err(miette::Report::new) + .unwrap(); + assert_eq!(parsed, expected); + } + + #[test] + fn parse() { + check( + r#" + input { + keyboard { + xkb { + layout "us,ru" + options "grp:win_space_toggle" + } + } + + touchpad { + tap + accel-speed 0.2 + } + } + + 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; } + } + "#, + Config { + input: Input { + keyboard: Keyboard { + xkb: Xkb { + layout: Some("us,ru".to_owned()), + options: Some("grp:win_space_toggle".to_owned()), + ..Default::default() + }, + }, + touchpad: Touchpad { + tap: true, + natural_scroll: false, + accel_speed: 0.2, + }, + }, + binds: Binds(vec![ + Bind { + key: Key { + keysym: KEY_t, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::Spawn(vec!["alacritty".to_owned()])], + }, + Bind { + key: Key { + keysym: KEY_q, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::CloseWindow], + }, + Bind { + key: Key { + keysym: KEY_h, + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + actions: vec![Action::FocusMonitorLeft], + }, + Bind { + key: Key { + keysym: KEY_l, + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL, + }, + actions: vec![Action::MoveWindowToMonitorRight], + }, + Bind { + key: Key { + keysym: KEY_comma, + modifiers: Modifiers::COMPOSITOR, + }, + actions: vec![Action::ConsumeWindowIntoColumn], + }, + ]), + }, + ); + } + + #[test] + fn can_create_default_config() { + let _ = Config::default(); + } +} diff --git a/src/input.rs b/src/input.rs index 514dcd2c..ad83d331 100644 --- a/src/input.rs +++ b/src/input.rs @@ -17,45 +17,10 @@ use smithay::input::pointer::{ use smithay::utils::SERIAL_COUNTER; use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; +use crate::config::{Action, Config, Modifiers}; use crate::niri::State; use crate::utils::get_monotonic_time; -enum Action { - None, - Quit, - ChangeVt(i32), - Suspend, - ToggleDebugTint, - Spawn(String), - Screenshot, - CloseWindow, - ToggleFullscreen, - FocusLeft, - FocusRight, - FocusDown, - FocusUp, - MoveLeft, - MoveRight, - MoveDown, - MoveUp, - ConsumeIntoColumn, - ExpelFromColumn, - SwitchWorkspaceDown, - SwitchWorkspaceUp, - MoveToWorkspaceDown, - MoveToWorkspaceUp, - FocusMonitorLeft, - FocusMonitorRight, - FocusMonitorDown, - FocusMonitorUp, - MoveToMonitorLeft, - MoveToMonitorRight, - MoveToMonitorDown, - MoveToMonitorUp, - ToggleWidth, - ToggleFullWidth, -} - pub enum CompositorMod { Super, Alt, @@ -70,13 +35,17 @@ impl From for FilterResult { } } -fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) -> Action { +fn action( + config: &Config, + comp_mod: CompositorMod, + keysym: KeysymHandle, + mods: ModifiersState, +) -> Action { use keysyms::*; - let modified = keysym.modified_sym(); - + // Handle hardcoded binds. #[allow(non_upper_case_globals)] // wat - match modified { + match keysym.modified_sym() { modified @ KEY_XF86Switch_VT_1..=KEY_XF86Switch_VT_12 => { let vt = (modified - KEY_XF86Switch_VT_1 + 1) as i32; return Action::ChangeVt(vt); @@ -85,59 +54,45 @@ fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) - _ => (), } - let mod_down = match comp_mod { - CompositorMod::Super => mods.logo, - CompositorMod::Alt => mods.alt, + // Handle configured binds. + let mut modifiers = Modifiers::empty(); + if mods.ctrl { + modifiers |= Modifiers::CTRL; + } + if mods.shift { + modifiers |= Modifiers::SHIFT; + } + if mods.alt { + modifiers |= Modifiers::ALT; + } + if mods.logo { + modifiers |= Modifiers::SUPER; + } + + let (mod_down, mut comp_mod) = match comp_mod { + CompositorMod::Super => (mods.logo, Modifiers::SUPER), + CompositorMod::Alt => (mods.alt, Modifiers::ALT), }; + if mod_down { + modifiers |= Modifiers::COMPOSITOR; + } else { + comp_mod = Modifiers::empty(); + } - if !mod_down { + let Some(&raw) = keysym.raw_syms().first() else { return Action::None; - } + }; + for bind in &config.binds.0 { + if bind.key.keysym != raw { + continue; + } - // FIXME: these don't work in the Russian layout. I guess I'll need to - // find a US keymap, then map keys somehow. - #[allow(non_upper_case_globals)] // wat - match modified { - KEY_E => Action::Quit, - KEY_t => Action::Spawn("alacritty".to_owned()), - KEY_d => Action::Spawn("fuzzel".to_owned()), - KEY_n => Action::Spawn("nautilus".to_owned()), - // Alt + PrtSc = SysRq - KEY_Sys_Req | KEY_Print => Action::Screenshot, - KEY_T if mods.shift && mods.ctrl => Action::ToggleDebugTint, - KEY_q => Action::CloseWindow, - KEY_F => Action::ToggleFullscreen, - KEY_comma => Action::ConsumeIntoColumn, - KEY_period => Action::ExpelFromColumn, - KEY_r => Action::ToggleWidth, - KEY_f => Action::ToggleFullWidth, - // Move to monitor. - KEY_H | KEY_Left if mods.shift && mods.ctrl => Action::MoveToMonitorLeft, - KEY_L | KEY_Right if mods.shift && mods.ctrl => Action::MoveToMonitorRight, - KEY_J | KEY_Down if mods.shift && mods.ctrl => Action::MoveToMonitorDown, - KEY_K | KEY_Up if mods.shift && mods.ctrl => Action::MoveToMonitorUp, - // Focus monitor. - KEY_H | KEY_Left if mods.shift => Action::FocusMonitorLeft, - KEY_L | KEY_Right if mods.shift => Action::FocusMonitorRight, - KEY_J | KEY_Down if mods.shift => Action::FocusMonitorDown, - KEY_K | KEY_Up if mods.shift => Action::FocusMonitorUp, - // Move. - KEY_h | KEY_Left if mods.ctrl => Action::MoveLeft, - KEY_l | KEY_Right if mods.ctrl => Action::MoveRight, - KEY_j | KEY_Down if mods.ctrl => Action::MoveDown, - KEY_k | KEY_Up if mods.ctrl => Action::MoveUp, - // Focus. - KEY_h | KEY_Left => Action::FocusLeft, - KEY_l | KEY_Right => Action::FocusRight, - KEY_j | KEY_Down => Action::FocusDown, - KEY_k | KEY_Up => Action::FocusUp, - // Workspaces. - KEY_u if mods.ctrl => Action::MoveToWorkspaceDown, - KEY_i if mods.ctrl => Action::MoveToWorkspaceUp, - KEY_u => Action::SwitchWorkspaceDown, - KEY_i => Action::SwitchWorkspaceUp, - _ => Action::None, + if bind.key.modifiers | comp_mod == modifiers { + return bind.actions.first().cloned().unwrap_or(Action::None); + } } + + Action::None } impl State { @@ -166,9 +121,9 @@ impl State { event.state(), serial, time, - |_, mods, keysym| { + |self_, mods, keysym| { if event.state() == KeyState::Pressed { - action(comp_mod, keysym, *mods).into() + action(&self_.config, comp_mod, keysym, *mods).into() } else { FilterResult::Forward } @@ -192,8 +147,10 @@ impl State { self.backend.toggle_debug_tint(); } Action::Spawn(command) => { - if let Err(err) = Command::new(command).spawn() { - warn!("error spawning alacritty: {err}"); + if let Some((command, args)) = command.split_first() { + if let Err(err) = Command::new(command).args(args).spawn() { + warn!("error spawning {command}: {err}"); + } } } Action::Screenshot => { @@ -211,78 +168,78 @@ impl State { window.toplevel().send_close(); } } - Action::ToggleFullscreen => { + Action::FullscreenWindow => { let focus = self.niri.monitor_set.focus().cloned(); if let Some(window) = focus { self.niri.monitor_set.toggle_fullscreen(&window); } } - Action::MoveLeft => { + Action::MoveColumnLeft => { self.niri.monitor_set.move_left(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveRight => { + Action::MoveColumnRight => { self.niri.monitor_set.move_right(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveDown => { + Action::MoveWindowDown => { self.niri.monitor_set.move_down(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveUp => { + Action::MoveWindowUp => { self.niri.monitor_set.move_up(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::FocusLeft => { + Action::FocusColumnLeft => { self.niri.monitor_set.focus_left(); } - Action::FocusRight => { + Action::FocusColumnRight => { self.niri.monitor_set.focus_right(); } - Action::FocusDown => { + Action::FocusWindowDown => { self.niri.monitor_set.focus_down(); } - Action::FocusUp => { + Action::FocusWindowUp => { self.niri.monitor_set.focus_up(); } - Action::MoveToWorkspaceDown => { + Action::MoveWindowToWorkspaceDown => { self.niri.monitor_set.move_to_workspace_down(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::MoveToWorkspaceUp => { + Action::MoveWindowToWorkspaceUp => { self.niri.monitor_set.move_to_workspace_up(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::SwitchWorkspaceDown => { + Action::FocusWorkspaceDown => { self.niri.monitor_set.switch_workspace_down(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::SwitchWorkspaceUp => { + Action::FocusWorkspaceUp => { self.niri.monitor_set.switch_workspace_up(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::ConsumeIntoColumn => { + Action::ConsumeWindowIntoColumn => { self.niri.monitor_set.consume_into_column(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::ExpelFromColumn => { + Action::ExpelWindowFromColumn => { self.niri.monitor_set.expel_from_column(); // FIXME: granular self.niri.queue_redraw_all(); } - Action::ToggleWidth => { + Action::SwitchPresetColumnWidth => { self.niri.monitor_set.toggle_width(); } - Action::ToggleFullWidth => { + Action::MaximizeColumn => { self.niri.monitor_set.toggle_full_width(); } Action::FocusMonitorLeft => { @@ -309,25 +266,25 @@ impl State { self.move_cursor_to_output(&output); } } - Action::MoveToMonitorLeft => { + Action::MoveWindowToMonitorLeft => { if let Some(output) = self.niri.output_left() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); } } - Action::MoveToMonitorRight => { + Action::MoveWindowToMonitorRight => { if let Some(output) = self.niri.output_right() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); } } - Action::MoveToMonitorDown => { + Action::MoveWindowToMonitorDown => { if let Some(output) = self.niri.output_down() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); } } - Action::MoveToMonitorUp => { + Action::MoveWindowToMonitorUp => { if let Some(output) = self.niri.output_up() { self.niri.monitor_set.move_to_output(&output); self.move_cursor_to_output(&output); @@ -740,9 +697,10 @@ impl State { // According to Mutter code, this setting is specific to touchpads. let is_touchpad = device.config_tap_finger_count() > 0; if is_touchpad { - let _ = device.config_tap_set_enabled(true); - let _ = device.config_scroll_set_natural_scroll_enabled(true); - let _ = device.config_accel_set_speed(0.2); + let c = &self.config.input.touchpad; + let _ = device.config_tap_set_enabled(c.tap); + let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll); + let _ = device.config_accel_set_speed(c.accel_speed); } } } diff --git a/src/main.rs b/src/main.rs index 9af30d43..f71a959a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ extern crate tracing; mod animation; mod backend; +mod config; mod dbus; mod frame_clock; mod handlers; @@ -13,8 +14,11 @@ mod utils; use std::env; use std::ffi::OsString; +use std::path::PathBuf; use clap::Parser; +use config::Config; +use miette::Context; use niri::{Niri, State}; use smithay::reexports::calloop::EventLoop; use smithay::reexports::wayland_server::Display; @@ -23,6 +27,9 @@ use tracing_subscriber::EnvFilter; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { + /// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`). + #[arg(short, long)] + config: Option, /// Command to run upon compositor startup. #[arg(last = true)] command: Vec, @@ -47,9 +54,22 @@ fn main() { let _client = tracy_client::Client::start(); + let config = match Config::load(cli.config).context("error loading config") { + Ok(config) => config, + Err(err) => { + warn!("{err:?}"); + Config::default() + } + }; + let mut event_loop = EventLoop::try_new().unwrap(); let mut display = Display::new().unwrap(); - let state = State::new(event_loop.handle(), event_loop.get_signal(), &mut display); + let state = State::new( + config, + event_loop.handle(), + event_loop.get_signal(), + &mut display, + ); let mut data = LoopData { display, state }; if let Some((command, args)) = cli.command.split_first() { diff --git a/src/niri.rs b/src/niri.rs index 88a11e8f..23b8f3b1 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -56,6 +56,7 @@ use smithay::wayland::tablet_manager::TabletManagerState; use time::OffsetDateTime; use crate::backend::{Backend, Tty, Winit}; +use crate::config::Config; use crate::dbus::mutter_service_channel::ServiceChannel; use crate::frame_clock::FrameClock; use crate::layout::{MonitorRenderElement, MonitorSet}; @@ -115,12 +116,14 @@ pub struct OutputState { } pub struct State { + pub config: Config, pub backend: Backend, pub niri: Niri, } impl State { pub fn new( + config: Config, event_loop: LoopHandle<'static, LoopData>, stop_signal: LoopSignal, display: &mut Display, @@ -134,10 +137,20 @@ impl State { Backend::Tty(Tty::new(event_loop.clone())) }; - let mut niri = Niri::new(event_loop, stop_signal, display, backend.seat_name()); + let mut niri = Niri::new( + &config, + event_loop, + stop_signal, + display, + backend.seat_name(), + ); backend.init(&mut niri); - Self { backend, niri } + Self { + config, + backend, + niri, + } } pub fn move_cursor(&mut self, location: Point) { @@ -178,6 +191,7 @@ impl State { impl Niri { pub fn new( + config: &Config, event_loop: LoopHandle<'static, LoopData>, stop_signal: LoopSignal, display: &mut Display, @@ -202,11 +216,12 @@ impl Niri { PresentationState::new::(&display_handle, CLOCK_MONOTONIC as u32); let mut seat: Seat = seat_state.new_wl_seat(&display_handle, seat_name); - // FIXME: get Xkb and repeat interval from GNOME dconf. let xkb = XkbConfig { - layout: "us,ru", - options: Some("grp:win_space_toggle,compose:ralt,ctrl:nocaps".to_owned()), - ..Default::default() + rules: &config.input.keyboard.xkb.rules, + model: &config.input.keyboard.xkb.model, + layout: &config.input.keyboard.xkb.layout.as_deref().unwrap_or("us"), + variant: &config.input.keyboard.xkb.variant, + options: config.input.keyboard.xkb.options.clone(), }; seat.add_keyboard(xkb, 400, 30).unwrap(); seat.add_pointer(); -- cgit