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;
+ }
+
+ if bind.key.modifiers | comp_mod == modifiers {
+ return bind.actions.first().cloned().unwrap_or(Action::None);
+ }
}
- // 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,
- }
+ 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();