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.
This commit is contained in:
1 parent
058ec5c98d
commit
f4b8b5dd50
4 files changed
+66
-37
No files matched your search
@@ -1,5 +1,40 @@
|
|||||||
# Decisions
|
# 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
|
## 2026-08-24 – power-profiles-daemon replaces auto-cpufreq
|
||||||
|
|
||||||
- **Who**: Dominik, ClaudeCode
|
- **Who**: Dominik, ClaudeCode
|
||||||
|
|||||||
@@ -257,11 +257,13 @@ NixOS module owns the destination:
|
|||||||
| `waypaper/config.ini` | wallpaper path, rewritten and seeded into `$HOME` |
|
| `waypaper/config.ini` | wallpaper path, rewritten and seeded into `$HOME` |
|
||||||
| fontconfig defaults | `/etc/fonts/conf.d` is owned by the NixOS module |
|
| 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
|
`kanshi/config` is not deployed and kanshi is not installed: niri configures its
|
||||||
monitor profiles are machine specific — they belong in
|
outputs itself, and `nwg-displays` writes the layout it is given to
|
||||||
`~/.config/kanshi/config`. A missing config file is what kanshi treats as an
|
`~/.config/niri/monitor.kdl`. niri prefers a home config over the deployed one
|
||||||
error (`failed to parse config file`), which with `Restart=on-failure` would
|
and does not merge them, so `~/.config/niri/config.kdl` is seeded with an
|
||||||
spin, so the service starts only once that file exists.
|
`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
|
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
|
copy seeded through `systemd.user.tmpfiles` with the `C` directive, which never
|
||||||
|
|||||||
+18
-7
@@ -36,6 +36,20 @@ let
|
|||||||
--replace-fail /usr/share/moonarch/wallpaper-blur.jpg ${wallpaperBlur}
|
--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
|
# PipeWire searches $PIPEWIRE_CONFIG_DIR, then $XDG_CONFIG_HOME/pipewire, then
|
||||||
# its compiled-in /etc/pipewire — never /etc/xdg (verified in PipeWire's
|
# 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
|
# 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/quickshell/moonarch".source = "${xdg}/quickshell/moonarch";
|
||||||
"xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini";
|
"xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini";
|
||||||
"xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc";
|
"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/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig";
|
||||||
"xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf";
|
"xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf";
|
||||||
"xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini";
|
"xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini";
|
||||||
@@ -286,7 +295,6 @@ in
|
|||||||
|
|
||||||
# Compositor extras
|
# Compositor extras
|
||||||
xwayland-satellite
|
xwayland-satellite
|
||||||
kanshi
|
|
||||||
|
|
||||||
# Bar, notifications, clipboard.
|
# Bar, notifications, clipboard.
|
||||||
# Neither waybar nor swaync are installed: the Quickshell bar owns both
|
# Neither waybar nor swaync are installed: the Quickshell bar owns both
|
||||||
@@ -361,7 +369,7 @@ in
|
|||||||
fwupd
|
fwupd
|
||||||
upower
|
upower
|
||||||
polkit_gnome
|
polkit_gnome
|
||||||
wdisplays
|
nwg-displays
|
||||||
wl-color-picker
|
wl-color-picker
|
||||||
|
|
||||||
stasis
|
stasis
|
||||||
@@ -399,6 +407,9 @@ in
|
|||||||
# exists, so the moonarch config has to be in place before it starts.
|
# 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/stasis/stasis.rune 0644 - - - ${xdg}/stasis/stasis.rune"
|
||||||
"C %h/.config/swaylock/config 0644 - - - ${swaylockConfig}"
|
"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}"
|
"C %h/.zshrc 0644 - - - ${userZshrc}"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
+6
-25
@@ -112,30 +112,6 @@ in
|
|||||||
# --- Firewall, replaces the ufw calls in post-install.sh ---
|
# --- Firewall, replaces the ufw calls in post-install.sh ---
|
||||||
networking.firewall.enable = true;
|
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/ ---
|
# --- System services from defaults/etc/systemd/system/ ---
|
||||||
#
|
#
|
||||||
# Rebuilt rather than copied, same reason as the user units: the originals
|
# Rebuilt rather than copied, same reason as the user units: the originals
|
||||||
@@ -166,6 +142,11 @@ in
|
|||||||
} // sysfsWriterHardening;
|
} // 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
|
# 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.
|
# a crash does not leave the session without a bar until the next login.
|
||||||
systemd.user.services.quickshell = {
|
systemd.user.services.quickshell = {
|
||||||
@@ -262,7 +243,7 @@ in
|
|||||||
systemd.user.services.wlsunset = {
|
systemd.user.services.wlsunset = {
|
||||||
description = "Wlsunset night light (blue light filter)";
|
description = "Wlsunset night light (blue light filter)";
|
||||||
partOf = [ "graphical-session.target" ];
|
partOf = [ "graphical-session.target" ];
|
||||||
after = [ "graphical-session.target" "kanshi.service" ];
|
after = [ "graphical-session.target" ];
|
||||||
wantedBy = [ ];
|
wantedBy = [ ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
|
|||||||
Reference in new issue
Block a user