diff options
| -rw-r--r-- | Cargo.lock | 268 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | resources/default-config.kdl | 88 | ||||
| -rw-r--r-- | src/config.rs | 297 | ||||
| -rw-r--r-- | src/input.rs | 190 | ||||
| -rw-r--r-- | src/main.rs | 22 | ||||
| -rw-r--r-- | src/niri.rs | 27 |
8 files changed, 781 insertions, 128 deletions
@@ -3,12 +3,32 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -245,6 +265,36 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -378,6 +428,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -845,6 +904,12 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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" @@ -965,6 +1036,23 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1010,6 +1098,33 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1160,6 +1275,38 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1226,9 +1373,12 @@ dependencies = [ "directories", "image", "keyframe", + "knuffel", "logind-zbus", + "miette", "profiling", "sd-notify", + "serde", "smithay", "smithay-drm-extras", "time", @@ -1406,6 +1556,15 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1443,6 +1602,12 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1518,6 +1683,30 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1674,6 +1863,12 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1825,6 +2020,12 @@ 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" source = "git+https://github.com/Smithay/smithay.git#04f63f30caca9d7af1059fc4d72edeea47ac79ca" @@ -1918,6 +2119,34 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1953,6 +2182,27 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2145,6 +2395,24 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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"] } @@ -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 <kbd>Super</kbd>. When running in a window, the Mod key is <kbd>Alt</kbd>. @@ -60,11 +60,9 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb | Hotkey | Description | | ------ | ----------- | | <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` | -| <kbd>Mod</kbd><kbd>D</kbd> | Spawn `fuzzel` | -| <kbd>Mod</kbd><kbd>N</kbd> | Spawn `nautilus` | | <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window | -| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the window to the left | -| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the window to the right | +| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>←</kbd> | Focus the column to the left | +| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>→</kbd> | Focus the column to the right | | <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>↓</kbd> | Focus the window below in a column | | <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>↑</kbd> | Focus the window above in a column | | <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>←</kbd> | Move the focused column to the left | @@ -86,6 +84,12 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb | <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>T</kbd> | Toggle debug tinting of rendered elements | | <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | 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<String>, + #[knuffel(child, unwrap(argument), default)] + pub variant: String, + #[knuffel(child, unwrap(argument))] + pub options: Option<String>, +} + +// 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<Bind>); + +#[derive(knuffel::Decode, Debug, PartialEq, Eq)] +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, Eq)] +pub enum Action { + #[knuffel(skip)] + None, + Quit, + #[knuffel(skip)] + ChangeVt(i32), + Suspend, + ToggleDebugTint, + Spawn(#[knuffel(arguments)] Vec<String>), + 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<PathBuf>) -> miette::Result<Self> { + 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<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 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 == 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<Action> for FilterResult<Action> { } } -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. |
