diff --git a/README.md b/README.md index f6da37d..aa746a9 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,75 @@ # rwm rust window manager ported from [dwm](https://dwm.suckless.org/) -For now this is a line-for-line port of dwm 6.4 with tons of unsafe code. Now -that it's working in this state, I'll start moving toward a safe Rust version -where possible. Linked lists are pretty cool, though. +rwm was originally a line-for-line port of dwm 6.4 with tons of unsafe code. +There's now a little bit less unsafe code, with some parts moved to safe Rust, +but there are now some functional differences from dwm too: +* Lua-based config file +* Several dwm patches applied + * [systray](https://dwm.suckless.org/patches/systray/) for a system tray + * [swallow](https://dwm.suckless.org/patches/swallow/) for window swallowing + * [pertag](https://dwm.suckless.org/patches/pertag/) for per-tag layouts and other settings + * [scratchpad](https://dwm.suckless.org/patches/scratchpad/) for a persistent + but hidable scratch terminal + * [stacker](https://dwm.suckless.org/patches/stacker/) (partial) for moving + windows up and down the stack + +## Configuration +The default config file for rwm is `$XDG_CONFIG_HOME/rwm/config.lua` or +`$HOME/.config/rwm/config.lua` if `XDG_CONFIG_HOME` is not set. As inspired by +[ghostty](https://ghostty.org/docs)'s "Zero configuration" approach, the +default, empty config provides the normal dwm bindings out of the box. A warning +will be emitted if no config file is found, but rwm will substitute the default +value and run like normal. + +The default config options are specified in [src/config.lua](src/config.lua), +with the settings ultimately ready from the `rwm` table therein. To override +settings in your own config file, just set the corresponding options on the +`rwm` table. For example, to enable `resize_hints` and change your default font, +add this code to your own `config.lua` file: + +``` lua +rwm.resize_hints = true +rwm.fonts = {"monospace:size=12"} +``` + +Everything in `src/config.lua` is in scope when your config is read, so you can +use the `key`, `button`, and `rule` constructor functions, and the `tagkeys` +function for generating the repetitive Mod+1, ..., Mod+9 bindings for +manipulating tags. + +### Key bindings +The code below is from my personal config but arguably should be included in the +repo. It handles overwriting existing keys when conflicts occur in your custom +bindings. If you don't locate existing keys and overwrite them, rwm will run all +bindings associated with the key, not just the newest one. + +``` lua +-- Look for key collisions and replace any conflicts instead of appending. If +-- you actually want multiple behaviors on the same key, just call +-- `table.insert` directly +function locate_key (o) + for i, k in ipairs(rwm.keys) do + if k.keysym == o.keysym and k.mod_ == o.mod_ then + return i + end + end + return false +end + +for _, key in ipairs(keys) do + idx = locate_key(key) + if idx then + rwm.keys[idx] = key + else + table.insert(rwm.keys, key) + end +end +``` + +Just combine this with a custom `keys` table like the one defined in +`src/config.lua`. Of course, if you'd rather include a full key table, you can +simply override `rwm.keys` directly. ## Screenshot As you can see, it looks just like dwm, with the addition of a simple bar from @@ -25,9 +91,9 @@ make install This defers to `cargo install` and thus will place the resulting binary in `~/.cargo/bin` by default. -I've been building with a Rust 1.81.0 nightly compiler from 2024-06-11 and test -in CI with nightly, but I don't use any nightly features, so it will likely -build with a stable toolchain too. +I've most recently built rwm with a stable 1.82 compiler, but I also test in CI +on nightly, so anything after 1.82 should work well. At least 1.82 is required +because I use the new `&raw` syntax from that release. You'll also need the X11, Xft, Xinerama, and fontconfig libraries installed on your system where rustc can find them, but the `x11` and `fontconfig-sys` crates @@ -46,7 +112,7 @@ done ``` Wrapping it with this loop allows smoother restarting, but you can also use the -simpler `exec rwm` if you prefer. +simpler `exec rwm` if you prefer. You can optionally set the log level with the `RUST_LOG` environment variable. I think I only used `log::trace!`, so you'll need `RUST_LOG=trace` if you need to diff --git a/example.fig b/example.fig deleted file mode 100644 index 573ed40..0000000 --- a/example.fig +++ /dev/null @@ -1,136 +0,0 @@ -let borderpx = 3; -let snap = 32; -let showbar = true; -let topbar = true; -let mfact = 0.5; -let nmaster = 1; -let resize_hints = false; -let lock_fullscreen = true; - -let swallowfloating = false; -let systraypinning = 0; -let systrayonleft = false; -let systrayspacing = 2; -let systraypinningfailfirst = true; -let showsystray = true; - -let scratchpadname = "scratchpad"; -let scratchpadcmd = ["st", "-t", scratchpadname, "-g", "120x34"]; - -let fonts = ["monospace:size=10"]; -let tags = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; - -let gray1 = "#222222"; -let gray2 = "#444444"; -let gray3 = "#bbbbbb"; -let gray4 = "#eeeeee"; -let cyan = "#005577"; - -let colors = {SchemeNorm: [gray3, gray1, gray2], SchemeSel: [gray4, cyan, cyan]}; - -# class, instance, title, tags, isfloating, isterminal, noswallow, monitor -let rules = [ - ["st-256color", nil, nil, 0, false, true, false, -1], -]; - -let dmenufont = "monospace:size=12"; -let dmenucmd = [ - "dmenu_run", - "-fn", dmenufont, - "-nb", gray1, - "-nf", gray3, - "-sb", cyan, - "-sf", gray4, -]; - -let termcmd = ["st", "-e", "zsh"]; - -let MODKEY = Mod4Mask; -let S_MOD = ShiftMask | MODKEY; - -let keys = [ - [MODKEY, XK_p, spawn, {v: dmenucmd}], - [S_MOD, XK_Return, spawn, {v: termcmd}], - [MODKEY, XK_grave, togglescratch, {v: scratchpadcmd}], - [MODKEY, XK_b, togglebar, {i: 0}], - [MODKEY, XK_j, focusstack, {i: 1}], - [MODKEY, XK_k, focusstack, {i: -1}], - [S_MOD, XK_j, pushstack, {i: 1}], - [S_MOD, XK_k, pushstack, {i: -1}], - [MODKEY, XK_i, incnmaster, {i: 1}], - [MODKEY, XK_d, incnmaster, {i: -1}], - [MODKEY, XK_h, setmfact, {f: -0.05}], - [MODKEY, XK_l, setmfact, {f: 0.05}], - [MODKEY, XK_Return, zoom, {i: 0}], - [MODKEY, XK_Tab, view, {ui: 0}], - [S_MOD, XK_c, killclient, {i: 0}], - [MODKEY, XK_t, setlayout, {l: 0}], - [MODKEY, XK_f, setlayout, {l: 1}], - [MODKEY, XK_m, setlayout, {l: 2}], - [MODKEY, XK_space, setlayout, {l: "none"}], - [S_MOD, XK_space, togglefloating, {i: 0}], - [MODKEY, XK_0, view, {ui: !0}], - [S_MOD, XK_0, tag, {ui: !0}], - [MODKEY, XK_comma, focusmon, {i: -1}], - [MODKEY, XK_period, focusmon, {i: 1}], - [S_MOD, XK_comma, tagmon, {i: -1}], - [S_MOD, XK_period, tagmon, {i: 1}], - [MODKEY, XK_1, view, {ui: 1 << 0}], - [MODKEY | ControlMask, XK_1, toggleview, {ui: 1 << 0}], - [S_MOD, XK_1, tag, {ui: 1 << 0}], - [S_MOD | ControlMask, XK_1, toggletag, {ui: 1 << 0}], - [MODKEY, XK_2, view, {ui: 1 << 1}], - [MODKEY | ControlMask, XK_2, toggleview, {ui: 1 << 1}], - [S_MOD, XK_2, tag, {ui: 1 << 1}], - [S_MOD | ControlMask, XK_2, toggletag, {ui: 1 << 1}], - [MODKEY, XK_3, view, {ui: 1 << 2}], - [MODKEY | ControlMask, XK_3, toggleview, {ui: 1 << 2}], - [S_MOD, XK_3, tag, {ui: 1 << 2}], - [S_MOD | ControlMask, XK_3, toggletag, {ui: 1 << 2}], - [MODKEY, XK_4, view, {ui: 1 << 3}], - [MODKEY | ControlMask, XK_4, toggleview, {ui: 1 << 3}], - [S_MOD, XK_4, tag, {ui: 1 << 3}], - [S_MOD | ControlMask, XK_4, toggletag, {ui: 1 << 3}], - [MODKEY, XK_5, view, {ui: 1 << 4}], - [MODKEY | ControlMask, XK_5, toggleview, {ui: 1 << 4}], - [S_MOD, XK_5, tag, {ui: 1 << 4}], - [S_MOD | ControlMask, XK_5, toggletag, {ui: 1 << 4}], - [MODKEY, XK_6, view, {ui: 1 << 5}], - [MODKEY | ControlMask, XK_6, toggleview, {ui: 1 << 5}], - [S_MOD, XK_6, tag, {ui: 1 << 5}], - [S_MOD | ControlMask, XK_6, toggletag, {ui: 1 << 5}], - [MODKEY, XK_7, view, {ui: 1 << 6}], - [MODKEY | ControlMask, XK_7, toggleview, {ui: 1 << 6}], - [S_MOD, XK_7, tag, {ui: 1 << 6}], - [S_MOD | ControlMask, XK_7, toggletag, {ui: 1 << 6}], - [MODKEY, XK_8, view, {ui: 1 << 7}], - [MODKEY | ControlMask, XK_8, toggleview, {ui: 1 << 7}], - [S_MOD, XK_8, tag, {ui: 1 << 7}], - [S_MOD | ControlMask, XK_8, toggletag, {ui: 1 << 7}], - [MODKEY, XK_9, view, {ui: 1 << 8}], - [MODKEY | ControlMask, XK_9, toggleview, {ui: 1 << 8}], - [S_MOD, XK_9, tag, {ui: 1 << 8}], - [S_MOD | ControlMask, XK_9, toggletag, {ui: 1 << 8}], - [S_MOD, XK_q, quit, {i: 0}], -]; - -let buttons = [ - [ClkLtSymbol, 0, Button1, setlayout, {l: nil}], - [ClkLtSymbol, 0, Button3, setlayout, {l: 2}], - [ClkWinTitle, 0, Button2, zoom, {i: 0}], - [ClkStatusText, 0, Button2, spawn, {v: termcmd}], - [ClkClientWin, MODKEY, Button1, movemouse, {i: 0}], - [ClkClientWin, MODKEY, Button2, togglefloating, {i: 0}], - [ClkClientWin, MODKEY, Button3, resizemouse, {i: 0}], - [ClkTagBar, 0, Button1, view, {i: 0}], - [ClkTagBar, 0, Button3, toggleview, {i: 0}], - [ClkTagBar, MODKEY, Button1, tag, {i: 0}], - [ClkTagBar, MODKEY, Button3, toggletag, {i: 0}], -]; - -let layouts = [ - ["[]=", tile], - ["><>", nil], - ["[M]", monocle], -]; - diff --git a/src/config.rs b/src/config.rs index 3c3467e..0039a4c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::{ error::Error, ffi::{c_float, c_int, c_uint, CString}, fs::read_to_string, - path::Path, + path::{Path, PathBuf}, }; use env::{CLICKS, HANDLERS, KEYS, XKEYS}; @@ -171,18 +171,16 @@ impl Config { /// Attempt to load a config file on first usage from `$XDG_CONFIG_HOME`, /// then `$HOME`, before falling back to the default config. pub fn load_home() -> Self { - let mut home = std::env::var("XDG_CONFIG_HOME"); - if home.is_err() { - home = std::env::var("HOME"); - } - if home.is_err() { + let base = if let Ok(xdg_home) = std::env::var("XDG_CONFIG_HOME") { + PathBuf::from(xdg_home) + } else if let Ok(home) = std::env::var("HOME") { + PathBuf::from(home).join(".config") + } else { log::warn!("unable to determine config directory"); return Config::default(); - } - let config_path = Path::new(&home.unwrap()) - .join(".config") - .join("rwm") - .join("config.lua"); + }; + + let config_path = base.join("rwm").join("config.lua"); Config::from_lua(config_path).unwrap_or_else(|e| { log::error!("failed to read config file: {e:?}"); @@ -199,7 +197,7 @@ mod tests { #[test] fn from_lua() { - let got = Config::from_lua("example.lua").unwrap(); + let got = Config::from_lua("testfiles/config.lua").unwrap(); assert_debug_snapshot!(got) } } diff --git a/example.lua b/testfiles/config.lua similarity index 100% rename from example.lua rename to testfiles/config.lua