From f4b8b5dd5026377eb5f9e73798e2ba52aded0062 Mon Sep 17 00:00:00 2001 From: nevaforget <> Date: Tue, 8 Sep 2026 16:47:21 +0200 Subject: [PATCH] feat(desktop): nwg-displays writes niri outputs, kanshi dropped The port swapped moonarch's wdisplays-persistent for the nixpkgs wdisplays, which cannot save -- so the kanshi config stayed empty and its unit was skipped on every boot. nwg-displays supports niri and writes the layout to ~/.config/niri/monitor.kdl, which niri applies itself. niri prefers a home config and does not merge, so config.kdl is seeded with an include of /etc/niri/config.kdl, like moonarch does for stasis.rune. --- DECISIONS.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 12 +++++++----- modules/desktop.nix | 25 ++++++++++++++++++------- modules/services.nix | 31 ++++++------------------------- 4 files changed, 66 insertions(+), 37 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 09c3e9c..89f2571 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,40 @@ # Decisions +## 2026-09-08 – nwg-displays replaces wdisplays; kanshi dropped for niri's own outputs + +- **Who**: Dominik, ClaudeCode +- **Why**: Monitors could not be set persistently at all. moonarch pairs + `wdisplays-persistent` (AUR fork `sfs-pra/wdisplays`, see moonarch `README.md:25` and + its `DECISIONS.md:178`) with kanshi: the fork saves straight into the kanshi config. + The port took the nixpkgs `wdisplays` instead — upstream, with no save function — and + never recorded the difference. The result was a GUI that applies a layout and forgets + it, an always-empty `~/.config/kanshi/config`, and a `kanshi.service` whose + `ConditionPathExists` therefore skipped it on every boot. +- **Tradeoffs**: Packaging the fork would match moonarch exactly and keeps kanshi's + per-combination profiles, but it is unmaintained and would be ours to carry. + `nwg-displays` is in nixpkgs, actively developed, and supports niri as a first-class + target alongside sway and Hyprland; it writes `~/.config/niri/monitor.kdl` and adds the + `include` for it. That makes kanshi redundant — niri applies `output` blocks itself, on + hotplug and with live reload — so it goes, together with the hardening set it had just + been given. What is given up is kanshi's profiles *per attached combination*: niri has + one setting per output. Fixed monitors do not care; several docks with different + arrangements would, and then kanshi has to come back. + The seed is the same mechanism moonarch chose for stasis (its `DECISIONS.md`, + 2026-04-22): niri has no config hierarchy either, so system-default-plus-user-override + cannot be modelled — the home file is seeded once and then belongs to the user. +- **How**: `modules/desktop.nix` — `wdisplays` → `nwg-displays`, `kanshi` dropped from + `environment.systemPackages`, new `niriUserConfig` in the `let` block holding + `include "/etc/niri/config.kdl"`, seeded through the existing + `systemd.user.tmpfiles.rules` block with `C` so later edits survive. The stale comment + about the undeployed kanshi config is gone from `environment.etc`, the paragraph in + `README.md` rewritten. `modules/services.nix` — `systemd.user.services.kanshi` removed + and `wlsunset`'s `after` reduced to `graphical-session.target`. + Verified: niri 26.04 does load `include` (an include pointing at a deliberately broken + file makes `niri validate` fail on that file, so it is parsed, not ignored); + `nwg-displays` 0.4.4 logs `Running on niri` and sees both outputs; without + `~/.config/niri/` it exits 0 with no window, and it creates `monitor.kdl` but no + `config.kdl` — which is what the seed is for. + ## 2026-08-24 – power-profiles-daemon replaces auto-cpufreq - **Who**: Dominik, ClaudeCode diff --git a/README.md b/README.md index 555f35a..4bd3a3a 100644 --- a/README.md +++ b/README.md @@ -257,11 +257,13 @@ NixOS module owns the destination: | `waypaper/config.ini` | wallpaper path, rewritten and seeded into `$HOME` | | fontconfig defaults | `/etc/fonts/conf.d` is owned by the NixOS module | -`kanshi/config` is not deployed: the file in the config source is empty, and -monitor profiles are machine specific — they belong in -`~/.config/kanshi/config`. A missing config file is what kanshi treats as an -error (`failed to parse config file`), which with `Restart=on-failure` would -spin, so the service starts only once that file exists. +`kanshi/config` is not deployed and kanshi is not installed: niri configures its +outputs itself, and `nwg-displays` writes the layout it is given to +`~/.config/niri/monitor.kdl`. niri prefers a home config over the deployed one +and does not merge them, so `~/.config/niri/config.kdl` is seeded with an +`include` of `/etc/niri/config.kdl` — the home config then adds the monitor +layout instead of replacing everything. nwg-displays also refuses to start +without that file. Programs that read only from `$HOME` and have no system-wide fallback get a copy seeded through `systemd.user.tmpfiles` with the `C` directive, which never diff --git a/modules/desktop.nix b/modules/desktop.nix index 0fb052a..2a8ee91 100644 --- a/modules/desktop.nix +++ b/modules/desktop.nix @@ -36,6 +36,20 @@ let --replace-fail /usr/share/moonarch/wallpaper-blur.jpg ${wallpaperBlur} ''; + # niri prefers ~/.config/niri/config.kdl over /etc/niri/config.kdl and does not + # merge the two, so a home config would otherwise replace the whole deployed + # one. This seed pulls the system config back in through `include`, which niri + # 26.04 really does load (verified by pointing an include at a broken file and + # watching `niri validate` fail on it). + # + # It exists for nwg-displays, which writes the output layout to + # ~/.config/niri/monitor.kdl and adds an include for it here. Without a + # config.kdl in the home directory nwg-displays refuses to start at all + # ("Couldn't find niri config directory", exit 0, no window). + niriUserConfig = pkgs.writeText "niri-config.kdl" '' + include "/etc/niri/config.kdl" + ''; + # PipeWire searches $PIPEWIRE_CONFIG_DIR, then $XDG_CONFIG_HOME/pipewire, then # its compiled-in /etc/pipewire — never /etc/xdg (verified in PipeWire's # src/pipewire/conf.c). The shared file therefore cannot be deployed the way @@ -93,11 +107,6 @@ in "xdg/quickshell/moonarch".source = "${xdg}/quickshell/moonarch"; "xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini"; "xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc"; - # defaults/xdg/kanshi/config is an empty placeholder. Not - # deployed: monitor profiles are machine specific and belong in - # ~/.config/kanshi/config. An empty file parses fine; a missing one is what - # kanshi reports as "failed to parse config file", which is why the unit in - # modules/services.nix waits for the home config to exist. "xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig"; "xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf"; "xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini"; @@ -286,7 +295,6 @@ in # Compositor extras xwayland-satellite - kanshi # Bar, notifications, clipboard. # Neither waybar nor swaync are installed: the Quickshell bar owns both @@ -361,7 +369,7 @@ in fwupd upower polkit_gnome - wdisplays + nwg-displays wl-color-picker stasis @@ -399,6 +407,9 @@ in # exists, so the moonarch config has to be in place before it starts. "C %h/.config/stasis/stasis.rune 0644 - - - ${xdg}/stasis/stasis.rune" "C %h/.config/swaylock/config 0644 - - - ${swaylockConfig}" + # nwg-displays needs this file to exist before it will start, and niri would + # otherwise ignore the monitor.kdl it writes next to it. + "C %h/.config/niri/config.kdl 0644 - - - ${niriUserConfig}" "C %h/.zshrc 0644 - - - ${userZshrc}" ]; } diff --git a/modules/services.nix b/modules/services.nix index 8649df5..049b176 100644 --- a/modules/services.nix +++ b/modules/services.nix @@ -112,30 +112,6 @@ in # --- Firewall, replaces the ufw calls in post-install.sh --- networking.firewall.enable = true; - # --- Session services --- - # - # Rebuilt rather than copied: the upstream units reference /usr/bin/, - # which does not exist under Nix. - - systemd.user.services.kanshi = { - description = "Dynamic display configuration"; - partOf = [ "graphical-session.target" ]; - after = [ "graphical-session.target" ]; - wantedBy = [ "graphical-session.target" ]; - # Without a config kanshi exits with a parse error and, with - # Restart=on-failure, spins. Monitor profiles are per machine. - unitConfig.ConditionPathExists = "%h/.config/kanshi/config"; - # The config is read, never written, so ProtectSystem=strict is enough and - # no exception for %h is needed. Not exercised at runtime here: this host - # has no kanshi config, so the condition skips the unit. - serviceConfig = { - Type = "simple"; - ExecStart = lib.getExe pkgs.kanshi; - Restart = "on-failure"; - RestartSec = 3; - } // waylandClientHardening; - }; - # --- System services from defaults/etc/systemd/system/ --- # # Rebuilt rather than copied, same reason as the user units: the originals @@ -166,6 +142,11 @@ in } // sysfsWriterHardening; }; + # --- Session services --- + # + # Rebuilt rather than copied: the upstream units reference /usr/bin/, + # which does not exist under Nix. + # The bar, launcher and every popout. A unit rather than a spawn-at-startup so # a crash does not leave the session without a bar until the next login. systemd.user.services.quickshell = { @@ -262,7 +243,7 @@ in systemd.user.services.wlsunset = { description = "Wlsunset night light (blue light filter)"; partOf = [ "graphical-session.target" ]; - after = [ "graphical-session.target" "kanshi.service" ]; + after = [ "graphical-session.target" ]; wantedBy = [ ]; serviceConfig = { Type = "simple";