# Roadmap & changelog Design/decision records and a running log of shipped work (items marked ✓). Split out of the README so that stays a focused entry point. > **Where to look** > > | Need | File | > |------|------| > | Product themes toward **v1.0** (not a queue) | [`docs/VISION.md`](VISION.md) | > | **What agents execute next** | [`agent/BACKLOG.md`](../agent/BACKLOG.md) | > | Docs map | [`docs/README.md`](README.md) | > | Agent loop protocol | [`agent/LOOP.md`](../agent/LOOP.md) | > > Backlog items may reference this file as **ROADMAP § \**. When > something ships, lasting design notes get a ✓ entry here; the BACKLOG > line is deleted. ## Roadmap - **Menu system** (apps launcher + theme switching + system actions), built on rofi 2.0 (native Wayland on 26.05) — its `.rasi` theme is baked from theme-state.json like every other app, with rich per-element styling: - ✓ shipped: `modules/home/rofi.nix` (per-element theme generated from the palette, `themes//rofi.rasi` whole-swap) and the `nomarchy-menu` dispatcher: root picker (no args) · `power` (lock/logout/suspend/hibernate/reboot/shutdown, SUPER+X) · `theme` (SUPER+T) · `clipboard` (cliphist, SUPER+CTRL+V) · `calc` (rofi-calc, live, SUPER+CTRL+C) · `files` (fd → xdg-open, SUPER+CTRL+F) · `emoji` (rofi-emoji, SUPER+CTRL+E) · `web` (Google). SUPER+Space is `rofi -show drun`. - ✓ shipped modules: `network` (nmtui in `$TERMINAL`) · `bluetooth` (blueman-manager) · `capture` (grim/slurp submenu: region/full → clipboard/file, saved to `~/Pictures/Screenshots`) · `keybinds` (the cheatsheet, see below) · `ask` (free-text → claude CLI in a terminal; auths via OAuth, no API key; pulled fresh via `npx @anthropic-ai/claude-code@latest` rather than the nixpkgs package, which lags model releases — `nodejs` is bundled for npx; REPL stays open) - ✓ keybindings cheatsheet: `modules/home/keybinds.nix` is the **single source** for both the Hyprland binds and the SUPER+? rofi list, so they can't drift; `nomarchy-menu keybinds` renders the padded two-column sheet (generated/mouse binds carried in its `extra` rows) - ✓ shipped binds: `SUPER+Space` → `rofi -show drun` (app launcher) · `SUPER+M` → `nomarchy-menu` (main menu) · `SUPER+?` → the cheatsheet (SUPER+D dropped — Space is the only launcher bind) - launcher icons: ✓ `show-icons` on, drawing from the theme's icon set (Papirus, via the icon-themes work below) - decision record: resolves the old Walker/Lua question — no GTK4 launcher, no second theming pipeline; the dispatcher owns the menu structure, so the renderer stays swappable (we moved fuzzel → rofi 2.0 once mainline gained native Wayland, for its richer theming) - ✓ **Organize the picker into category submenus:** the root picker had grown into a long flat list of 16 entries. It's now six: **Apps · Theme · Tools › · System › · Power · Keybindings**. `Tools` (Calculator, Clipboard, Emoji, Files, Web, Capture, Ask) and `System` (Network, Bluetooth, DND, and the self-gated Snapshots / Power-profile) are submenus the dispatcher routes via `nomarchy-menu tools|system`, each ending in a `← Back` entry that re-opens the root. The direct `SUPER+CTRL+` binds still hit the leaves straight (bypassing the menu), and the self-gated entries still hide when unavailable. Remaining (optional): a **Look & Feel** category once there are more appearance toggles to group with Theme (night-light, wallpaper). **General pattern (ongoing):** the grouping is a standing convention, not a one-off — any new feature that earns a menu entry must be placed in the right submenu (don't let the root creep back to a flat list), with its direct `SUPER+CTRL+` bind and self-gating as applicable. - ✓ **Back everywhere:** every list menu now ends with a `↩ Back` entry that returns one level up (power/theme/power-profile/clipboard/files/capture, not just Tools/System), so you never have to Esc out and reopen. A `back` helper + a shared `BACK` label keep it uniform; matched exactly so it can't collide with clipboard/filename content. Esc still quits instantly. - ✓ **Menu modules from rofi plugins:** the old `calc` flow committed the expression blind (result only in the *next* menu's `-mesg`) and `qalc -t` misparsed common phrasings (`15% of 200` → `rem(15, 1 B)`, the natural- language "of" tripping the CLI). Resolved by adopting purpose-built rofi modi (`programs.rofi.plugins`), all themed through the same `.rasi`: - **calc** → **rofi-calc**: live results as you type via libqalculate directly (dodging the qalc-CLI "of" bug), Enter copies, menu persists to chain calculations. - **emoji** → **rofi-emoji** (new module, SUPER+CTRL+E): glyph picker, copies via the plugin's Wayland clipboard adapter. - **files** stays the hand-rolled `fd` → `rofi -dmenu` → xdg-open fuzzy search: rofi-file-browser-extended was tried and dropped — its navigate-a-tree model felt worse than flat fuzzy-find for a quick launcher, and yazi (SUPER+E) already covers real browsing. - **More menu modules from rofi tools:** the script-based counterparts (run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate replacement of an existing flow. - ✓ **Network** (`networkmanager_dmenu`): a native rofi wifi/VPN picker replacing the old `nmtui`-in-terminal `network` flow. Configured (xdg `networkmanager-dmenu/config.ini`) to drive rofi with `rofi_highlight` for connected/available rows; editing a connection drops to nmtui in the terminal. Inherits the generated theme like every other module. - ✓ **Audio** (`rofi-pulse-select`): a PipeWire/pulse sink/source switcher under System → Audio (Output/Input), self-gated on the pulse socket. - Deferred: **rofi-rbw / rofi-pass** secrets module — no Bitwarden/`pass` setup here today (secrets are gpg/ssh + gnome-keyring, see `keys.nix`), so it'd mean adopting a new secret manager. Revisit if that changes. - Also: menu search is now **case-insensitive fuzzy** (`matching = fuzzy`, `sorting-method = fzf`) across every module + the launcher. - ✓ **Printer setup menu module:** a `nomarchy-menu printers` entry in the **System** submenu opens **system-config-printer** (the CUPS admin GUI — discovery, drivers/PPDs, options, test page), mirroring the bluetooth/blueman pattern: the printing service ships the package (`environment.systemPackages`) and the menu execs it, self-gated on the `system-config-printer` binary so the entry appears only when `nomarchy.services.printing` is on. No direct `SUPER+CTRL` bind — printer setup is a rare one-off, not a frequent utility (left out deliberately; easy to add). Chose the GUI over a rofi-native `lpadmin` flow (driver/PPD picking is impractical in rofi) and the CUPS web UI (an unthemed browser page). Validated: the generated menu script's `bash -n` build check passes, and an eval confirms printing-on puts system-config-printer in `systemPackages`. Pending an on-machine check. - ✓ **VPN setup & management menu:** a dedicated **System → VPN** flow (`nomarchy-vpn`, `modules/home/rofi.nix`) that goes past the Network module (`networkmanager_dmenu` only connects/disconnects *existing* VPNs) to a guided setup + management surface across the three common kinds: - **WireGuard:** import a `.conf` into NetworkManager (`nmcli connection import type wireguard file …` — NM handles wg tunnels natively, no plugin) and toggle it up/down. - **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`); the `networkmanager-openvpn` plugin ships system-side (`networking.networkmanager.plugins`, mkDefault) so the openvpn type is available — import type is chosen by file extension. - **Tailscale:** status (read-only) + `up`/`down` + **exit-node** selection. It lives outside NetworkManager, so the menu drives the `tailscale` CLI directly, **self-gated** on the CLI being present (= `nomarchy.services.tailscale`, which makes the login user the **operator** via `extraSetFlags`). So up/down/exit-node run **inline without sudo**, falling back to a sudo terminal only if the operator grant is absent; the first interactive login uses a terminal (the auth URL is visible). Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM VPN/WireGuard connections shown ● active / ○ inactive and toggled on select (networkmanager-group users need no sudo), Import via the Files/`fd` picker (`*.conf`/`*.ovpn`), the Tailscale block when present — ending in `↩ Back`. A self-gating Waybar **`custom/vpn`** shield (`nomarchy-vpn-status`: shown only while a NM tunnel or Tailscale is up; `@good` tone; click opens the submenu), wired into the generated bar **and the summer whole-swaps**. Secrets stay in the connection manager's own store (NetworkManager / Tailscale) — no new secret manager (cf. the deferred rofi-rbw/pass note above). **Decided: import-first** — a from-scratch WireGuard keypair/peer editor is too much rofi surface, so creation is deferred to `nm-connection-editor`. Eval + build green; **pending an on-machine check** (the nmcli import/up-down + Tailscale paths need a live session with real configs). Remaining (optional): richer exit-node display (country/city). - **Theme parity with legacy:** summer-day/night now carry their legacy bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3, logo button opens nomarchy-menu); the other four identity themes are palette recolors and already match. Remaining: a visual pass over all six on the live ISO - ✓ **Per-theme rofi identity:** the `themes//rofi.rasi` whole-swap ships, and all six identity themes now carry a designed `.rasi`. summer-day/ night keep their legacy ports (inverted window, green inputbar, yellow bottom-border); the other four were authored from each theme's character (no legacy layout to port — their waybar whole-swaps are palette-only): **nord** a soft rounded "frost panel" (frost border, brighter-frost selection, aurora-purple prompt); **retro-82** a sharp CRT terminal (square corners, amber-on-navy, a teal scanline underline, mono); **lumon** a clinical cyan "bezel" (thick cyan frame, a *framed*-not-filled readout inputbar, mono); **kanagawa** ink-and-paper (warm washi-paper frame, a deeper ink-well inputbar, crystal-blue wave reserved for the selection). Each is self-contained (it replaces the generated theme) and keeps the element structure the theme-grid picker's per-invocation `-theme-str` layers onto; all four parse clean under `rofi -dump-theme`. The generated palette theme stays the default for the other 15 presets. Remaining: a visual pass over the four on hardware (the parse check confirms syntax, not aesthetics). - ✓ **Visual theme picker (preview thumbnails):** `nomarchy-menu theme` is now a rofi **icon grid of real desktop previews** instead of a plain-text list — each theme a screenshot of its themed desktop (waybar + floating terminal), pretty name beneath, **grouped dark-first then light** in one scrollable grid (the previews make the mode obvious, so no light/dark submenu split), the active theme marked `✓`, ending in `↩ Back`. The grid, the Name→slug map and the active mark are all **generated at eval time** from the preset JSONs in `rofi.nix` (`builtins.readDir` + `fromJSON`); "active" is just `t.slug`, since every switch rebuilds the menu. Grid layout via a per-invocation `-theme-str` (`listview { columns: 3; flow: horizontal; }` so Down scrolls row-by-row, + vertical `element` cards, name centred below). **Sizing gotcha (learned the hard way):** rofi's `element-icon` `size` is a **single value → a square cell** (a two-value `WxH` is silently collapsed), and the icon is *contained* in that square. So a 16:9 preview letterboxes (theme-coloured bands top/ bottom), and a cell can't be shorter-than-square without the bands returning. The fix: a build-time imagemagick step **centre-crops each preview to a square** so it fills the cell edge-to-edge; the window width is derived so a column is exactly the icon side (no slack margins). One knob, `themeGridIconW` (240px), drives both icon and window. **Workflow:** Bernardo captures previews on real hardware and commits them as `themes//preview.png`, **already downscaled to 480×270** (~2.4 MB total for all 21, vs ~28 MB full-res) — the source stays 16:9 and untouched; only the *displayed* thumb is squared at build, so the crop is reversible. Graceful fallback when a theme has no `preview.png`: a plain-name row, so it degrades cleanly. (A headless VM-render route — `runNixOSTest` + software-GL Hyprland (`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP framebuffer dump — was prototyped 2026-06-19 and **works** (themed waybar rendered + captured); real-hardware capture was chosen for fidelity + simplicity, with the VM route as a documented fallback if hand-capturing all themes gets tedious.) - **Faster switches:** move `backgrounds/` out of the flake source. Diagnosis (confirmed): `themes/` is 86 MB and `backgrounds/` is **all** of it — the palette JSONs + per-theme overrides are only ~208 KB, and the wallpapers are **never read at Nix eval** (only the Python `swww` path uses them). But `theme-state.json` is git-tracked, so every `apply` rewrites it → the flake tree changes → Nix re-copies the whole 86 MB source before `home-manager switch` can evaluate. You pay an 86 MB copy to change a 1 KB file. **Decided approach (deferred — don't want the extra moving part yet):** Option 1, a **separate pinned wallpapers artifact** — a `Nomarchy-wallpapers` repo or release tarball, pulled once via a flake input / `fetchurl` (pinned by hash → content-addressed, never re-copied on a state write), with `nomarchy-theme-sync` reading wallpapers from that stable store path (the `NOMARCHY_DEFAULT_THEMES` env hook already anticipates external theme assets). Keeps eval **pure**; the live ISO still bakes them in (fetched at build). A state write then re-copies only ~208 KB. Rejected alternative: moving `theme-state.json` out to `~/.config` + `--impure` eval (one repo, no second artifact, but trades away the in-tree-pinned-state reproducibility). Follow-on if `home-manager switch` itself is still the bottleneck after the copy is gone: **pre-built theme variants** (build each theme's generation ahead of time so a switch just activates a cached one). - Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since v1: `nomarchy.system.plymouth.*`, background tinted from the state file) - Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy boot (v1 `nomarchy-install` is single-disk UEFI — see `pkgs/nomarchy-install`) - ✓ **Installer no-keyboard-variant safety (#91):** gum's display-only `(none)` row is normalized at the common interactive/unattended boundary, and the template patcher independently refuses to serialize that sentinel into either Home Manager or system XKB configuration. A permanent check patches the real downstream template for `us` + no variant and builds the resulting console keymap; the full offline installer VM also carries the literal sentinel through LUKS+swap installation and first boot. - ✓ **Catalog-only installer keyboard selection (#92):** the short default confirms US English with standard keys; non-default layouts and variants are searchable human-readable rows from the pinned XKB catalog. Gum runs in strict mode and a shared exact-membership boundary validates interactive and unattended values before hardware detection, disk changes, or generated Nix. The review panel shows the resolved layout and key behaviour. - launch-or-focus UX scripts (swayosd volume/brightness OSD ships since v1: `nomarchy.osd.*`, media keys drive `swayosd-client`, themed from the JSON) - **Distro branding, round 2:** `distroName = "Nomarchy"` ships (os-release `PRETTY_NAME`, systemd-boot entries, ISO menu label). ✓ tuigreet greeting (`Welcome to `) and a branded `users.motd` (doubling as a helper cheat sheet), both keyed off `distroName`. ✓ `isoImage.splashImage` — the vendored vector logo (`modules/nixos/branding/logo.svg`, from legacy) recolored to the palette accent on the theme base, built at ISO-build time (`hosts/live.nix`). ✓ **`isoImage.grubTheme` (UEFI boot matches BIOS):** `hosts/live.nix` now builds a `nomarchyGrubTheme` dir whose background is the *same* composed splash image as the isolinux splash (accent logo on base), with a palette-coloured boot menu in the lower third (clear of the centred logo) and an accent timeout bar. Derived from `nixos-grub2-theme` only to reuse its bundled DejaVu `.pf2` (grub `loadfont`s every `.pf2` in the dir); the stock NixOS `logo.png` is dropped since ours is in the background. Built + structure-verified (theme.txt palette colours, 1920×1080 background, font present). Remaining: a UEFI ISO-boot render check on hardware (the file wiring is confirmed; the visual is not CI-testable, same as the splash). ✓ **`distroId = "nomarchy"`:** os-release is now honest — `ID=nomarchy`, `ID_LIKE=nixos` (the standard derivative-distro lineage marker, cf. Ubuntu→debian), `DEFAULT_HOSTNAME=nomarchy`, lsb `DISTRIB_ID`/`CPE_NAME` follow. **Verified safe:** `switch-to-configuration` builds its "is this NixOS?" guard from the *configured* distroId (and `/etc/NIXOS` remains as the fallback), so rebuilds keep working — a new `checks.distro-id` VM-test boots such a system and runs `switch-to-configuration dry-activate` green; nixos-* CLI tools are package names, untouched. The one side effect (isNixos→false blanks the upstream nixos.org URLs) is handled by `extraOSReleaseArgs` restoring `HOME_URL`/`DOCUMENTATION_URL`/`SUPPORT_URL`/ `BUG_REPORT_URL` to the project. os-release output eval-verified from the real downstream config. - ✓ **fastfetch branding:** `modules/home/fastfetch.nix` (`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to the palette accent and rendered to compact block-art via chafa at build time (tracks the theme), fronting a curated module list. Replaces the oversized legacy ASCII with a themed, sized logo. - ✓ **Nomarchy logo font in Waybar:** vendored `Nomarchy.ttf` (`modules/nixos/branding/`), installed via `fonts.packages`, and the summer-day/night menu buttons now use its `U+F000` glyph with `font-family: Nomarchy` pinned in their CSS (Nerd Fonts also occupy U+F000, so the pin is required). The other themes have no logo button. - ✓ **Quality-of-life command aliases:** a curated set ships on by default in `modules/home/shell.nix` — navigation (`..`/`...`/`....`), a git block (`g`, `gst`, `ga`/`gaa`, `gc`/`gcm`, `gco`/`gsw`, `gb`, `gd`/`gds`, `gl`/`glg`, `gp`/`gpl`, `gf`), and a nix block (`ns`, `nr`, `nfu`, `nfc`, `nsearch`, `ngc`), plus `path` and `reload`. Scope decision: short where it helps but **never shadow a real binary** (same rule as rg/fd — the git block deliberately avoids `gs`/ghostscript); lifecycle is `nomarchy-pull` / `nomarchy-rebuild` / `nomarchy-home` (full names on PATH, not aliases). The set is documented in README §5 (Day-to-day), and `alias` lists shell shortcuts. - **Theme-switch feedback:** ✓ the "rebuilding…" notification is now persistent (timeout 0) and replaced in place by "applied ✓" / failure via a synchronous tag, so a multi-minute switch never reads as a failed selection. (An honest indeterminate indicator — HM gives no % — a literal progress widget would be cosmetic; revisit only if wanted.) - **Icon themes:** ✓ ships `papirus-icon-theme`; the resolved name lives on `nomarchy.theme.iconTheme` (the JSON's optional `icons` field, else Papirus-Dark/Light by `mode`) and feeds both Stylix's `gtk.iconTheme` (Thunar/GTK apps) and rofi's `show-icons`. ✓ **Opt-in cross-pack overrides (no default bloat):** `theme.nix` now resolves `nomarchy.theme.iconThemePackage` from a small pack registry (`iconPacks`: Papirus·Tela·Qogir·Reversal·Numix). Default themes name `Papirus-*`, so the resolved package is the *plain* `papirus-icon-theme` derivation — byte-identical closure, zero added MB. Only when a theme's `icons` names a set from another pack is that pack (and only it) symlinkJoined alongside papirus. Adding a pack = one `iconPacks` row. **Deliberately NOT shipping more packs by default** (Bernardo 2026-07-10): papirus alone is already ~1 GiB and each extra pack adds ~hundreds of MB, so variety is opt-in per install, not a default closure cost. - ✓ **Auto time-of-day theme (#79, `VISION § D`):** an automatic light/dark theme switch through the **same** engine as a manual apply — no second pipeline. `settings.autoTheme = {enable, day, night, sunrise, sunset}` drives `nomarchy-theme-sync auto`, which picks day vs night by the local clock and applies the preset **only if it differs** (idempotent). `modules/home/autotheme.nix` installs a user timer (`OnStartupSec` + `OnCalendar=*:0/15` + `Persistent`, gated on the state flag) that ticks `auto`; Look & Feel › Auto theme toggles it and picks the pair/times. `cmd_apply`'s core was factored into `apply_named` so `auto` reuses it verbatim. Periodic (not exact sunrise/sunset timers) for robustness to suspend/DST; the idempotent `auto` keeps ticks cheap. Enable rebuilds once (installs the timer + `auto --force`); disable is instant (the flag flips and `auto` self-gates). **V2:** `checks.auto-theme` moves the VM clock across a configured sunset/sunrise and asserts the theme flips day→night→day with the right rebuild count (idempotent between) — the `home-manager switch` itself is stubbed via `NOMARCHY_REBUILD` (that generic apply path is exercised by every manual theme change). The only on-hardware residual is the live user timer firing on its `OnCalendar` schedule (the schedule is `systemd-analyze`-validated). - ✓ **NixOS-side state bridges (#116):** `nomarchy.settings` is declared in exactly one place — `modules/home/options.nix`, the **Home Manager** side. On the NixOS side the attribute does not exist, so four options that defaulted from `config.nomarchy.settings.… or ` silently got the fallback on every machine ever built: `or` swallows the missing-attribute error, which makes a dead bridge and a live one look identical in the source. Two of them were **user-facing toggles that reported success and did nothing** — Control Center's Bluetooth and Printing wrote `settings.{bluetooth,printing}.enable` and printed "requires rebuild", and the rebuild changed nothing, for as long as those toggles had existed. **The pattern, now uniform:** the option declares a *static* default and the implementing module reads the state via `modules/theme-state-read.nix` (fails closed on bad JSON) and `mkDefault`s it — `lib.mkIf (stateX != null) (lib.mkDefault stateX)`, so an absent key leaves the option default as the single source of the fallback and a hand-set value in `system.nix` still pins it. Done for `greeter.autoLogin` (eb38008), `bluetooth.enable` (`modules/nixos/default.nix`) and `services.printing.enable` (`modules/nixos/services.nix`). `power.batteryChargeLimit` deliberately has **no** eval bridge: `power.nix`'s oneshot reads the key out of the live state with `jq` at *runtime* and prefers it over the baked value, which is why its menu worked all along — the phantom read was dead but harmless, and is simply gone. **The guard is the point:** nothing in a build fails when a bridge dies, so `checks.state-bridges` asserts at eval that a flipped state file reaches the config (and that a hand-set value still outranks it), and `checks.printing-from-state` boots a VM whose *only* input is the state file and waits for a running `cups.service`. Adding a bridge = copy the shape and add a case. **Lesson:** `or ` on a config read is an error-swallower — prefer an explicit `!= null` test over a fallback expression, and grep the whole of `modules/nixos/`, not just `options.nix` (the first pass at this item did the latter and undercounted). - **Nicer shell out of the box:** ✓ zsh is the default login shell, with a starship prompt themed from the JSON, autosuggestions + syntax highlighting, and modern-CLI ergonomics — `cat`→bat (theme "ansi", so it tracks the palette), `ls`→eza, `cd`→zoxide, plus `lt`/`tree`. `nomarchy.shell.enable`. (Deliberately did NOT alias grep→rg / find→fd: their flags differ enough to surprise; `rg`/`fd` ship as themselves.) - ✓ **Default application suite:** a starter complete-workstation set — `libreoffice-fresh`, `vscode`, `gimp`, `inkscape` — ships *active* in the downstream `templates/downstream/home.nix` `home.packages`, with the heavier opt-ins (`texliveFull` — multi-GB; `texliveMedium` is lighter — plus browser/email) commented just below — **Chromium is active by default** (2026-07-10); other browsers stay commented. **Decision: no `nomarchy.apps.*` option surface.** Unlike `nomarchy.services.*` (real config behind each toggle — systemd units, subuid ranges, the Flathub oneshot), a bare package install has nothing behind the toggle: `mkIf x { environment.systemPackages = [p]; }` just reinholds what a package list already gives, and the honest opt-out for a package is *deleting the line*. So the suite is the user's own curated list (extending the existing `# firefox` convention), not a distro-module feature — and it respects that editor/office/graphics are personal-taste choices, not defaults to impose. Unfree `vscode` evaluates fine — `mkFlake` and the system both set `allowUnfree = true`. **Amended 2026-07-14 (#103):** this design also kept the apps off the live ISO automatically (it never starts from the template) — which was read as a feature here, and was in fact the bug: the live session had no browser and no office at all. The live medium now names its own baseline set explicitly (see below); "the template is the app list" still holds for *installed* machines. - ✓ **Live-ISO baseline apps (#103):** the live session shipped the distro modules only — no browser, no office — for as long as it existed. That is the thing a user judges the distro by *before* installing, and what they boot to rescue a machine that will not start; it could do neither. The live HM user in `flake.nix` now names its own set: **Chromium, libreoffice-fresh, gnome-text-editor, amberol, snapshot** (Bernardo confirmed the gap on real hardware 2026-07-14). **Firefox deliberately excluded** (Bernardo's call): Chromium already owns the HTTP mime default and ships in the installed template, so it is the browser that matches what a user gets after installing — and it was the one item on #103's original list that would have cost a real closure. **The size finding, which inverted the trade:** `system.extraDependencies` already pins the template's HM closure into the ISO for offline installs, so chromium/libreoffice/amberol were *already in the image's store* — merely absent from the live user's profile, so nothing put them on PATH or in the launcher. Reusing the template's **exact** derivations (including `chromium.override { enableWideVine = true; }` — a plain `chromium` would be a second full browser build) makes those three cost **zero**. Only gnome-text-editor and snapshot are new: 9 store paths, 133 MiB uncompressed (99 MiB of it gst-plugins-rs, Snapshot's media stack). **Measured ISO delta: 8.038 → 8.078 GiB = +41.2 MiB (+0.50%)**, both built from the same tree. Guard: `checks.live-baseline-apps` asserts each app is on PATH *and* has a `.desktop` the launcher can see (a binary without one is invisible, which is the failure that matters), that HTTPS resolves to a chromium entry which is actually **present** — #94's exact trap, a mime default naming a package nothing ships — and that Firefox has not reappeared, since that is a size decision rather than a drive-by. Proved to fail: dropping snapshot makes it name the missing entry. **V3 pending** — that the apps *launch* needs real hardware (HARDWARE-QUEUE, Acer M5-481T). - ✓ **Plymouth logo contrast:** the shipped art was a fixed navy that vanished on dark bases. `modules/nixos/plymouth.nix` now recolors every element from the palette at build time (flat fill, alpha kept): logo/lock/bullet → text, entry/progress-box → surface, progress-bar → accent. Reads on light and dark; follows the theme as of the last system rebuild (like the background tint). - ✓ **Hibernation + zram by default (BACKLOG #76):** two memory layers ship on by default. `modules/nixos/oom.nix` enables `zramSwap` (zstd, 50% RAM, **priority 100**) as the compressed-RAM layer *before* earlyoom; the installer defaults the disk swap to a **RAM-sized** hibernation-ready swapfile on an `@swap` btrfs subvolume *inside* the encrypted volume, with `patch-template.py` wiring `swapDevices` + `boot.resumeDevice` + `resume_offset` (from `btrfs inspect-internal map-swapfile -r`). The priority split is the point: day-to-day paging stays in zram, so the disk swapfile is reserved for the hibernate image. `swapSize=0` stays a clean no-swap opt-out; the Power-menu Hibernate row is kept unconditional and **notifies on failure** (no-swap machines) rather than silently no-op'ing. Existing machines get an *enable-hibernation* runbook in `docs/MIGRATION.md` (no tool). Guarded by three VM checks — `checks.zram-swap` (device/algo/ priority), `checks.hibernate` (a full hibernate→resume→crash cycle on a swap *partition* that proves the resume mechanism + that zram at priority 100 doesn't swallow the image), and `checks.hibernate-swapfile` (the installer's btrfs `@swap` *swapfile* build — NOCOW `mkswapfile`, file-type swapon, a valid `map-swapfile -r` offset the kernel accepts, zram above it). A full hibernate from a *runtime* swapfile isn't headless-reachable (systemd `CanHibernate=no` for undeclared swap; and a runtime-computed `resume_offset` can't be fed into the boot cmdline), so the literal encrypted-swapfile power-cycle + LUKS-initrd unlock stayed a V3 laptop check — **V3 PASSED 2026-07-12** on TuringMachine (T14s, LUKS+`@swap` swapfile layout): hibernate → power off → single LUKS unlock → session restored. ✓ **Doctor hibernate/sleep section (#77, `VISION § C`):** `nomarchy-doctor` gained a read-only hibernate row group — resume device on the cmdline (+ `resume_offset` for a swapfile), disk swap ≥ RAM, zram active, and any suspend/hibernate error in the previous boot's kernel log. Self-gates (`skip` on `swap=0`); only `ok`/`warn`/`skip`, never fails the sheet. The `checks.doctor` VM test asserts the `hibernate` row renders. (Gotcha recorded: the script runs `set -euo pipefail`, so a no-match `grep` inside `$(…)` or a `cond && action` aborts it — use `|| true` and `if`.) ✓ **Doctor battery health (#80, `VISION § C`):** report-only `cycle_count` + retained capacity (`charge_full`/`charge_full_design` or `energy_*`) as `% of design`, system batteries only (not Device-scope peripherals). Self-gates when attrs missing; soft warn below 70% of design. `NOMARCHY_POWER_SUPPLY_ROOT` for the `checks.doctor` fixture (`test_power` has no cycle/design attrs). ✓ **First-boot welcome toast (#81, `VISION § B`):** oneshot user unit `nomarchy-first-boot` fires a dismissible “You're set” notify (SUPER+M / SUPER+T / SUPER+? + Network + Doctor pointer) once, then writes `settings.firstBootShown` into the flake checkout (`--no-switch`). Skips `hostname == nomarchy-live` (live ISO keeps its own welcome). Toggle: `nomarchy.firstBootWelcome.enable`. `checks.first-boot` gates toast / marker / re-run silence / live skip. ✓ **Generation readability (#82, `VISION § B`):** `nomarchy-what-changed` wraps nvd into plain-language counts (added/removed/updated); post-switch toast from `nomarchy-rebuild` / `nomarchy-home`; System › What changed? (toast + floating full report). `checks.what-changed` fixtures nvd via `NOMARCHY_NVD`. ✓ **Doctor first-boot pre-activate (#83, `VISION § A`):** if `/var/log/nomarchy-hm-preactivate.log` exists and no Home Manager generation is present, doctor ✖ with `home-manager switch --flake ~/.nomarchy -b bak`. Skips when log absent; ok once a generation exists. `checks.doctor` covers both arms. ✓ **Hardware hall of fame (#85, `VISION § F`):** HARDWARE.md §9 install-tested table (T14s Gen 4, Latitude 5410/5310) + “send your DMI line” PR invite; §10 points contributors at the table. ✓ **Clamshell logind (#86, `VISION § C`):** explicit `services.logind.settings.Login.HandleLidSwitchDocked = "ignore"` when power management is on (systemd docked = external display present). Undocked lid suspend left to upstream. `checks.clamshell-logind` asserts logind.conf + active logind. Real lid+external V3 in HARDWARE-QUEUE. ✓ **Dock audio sink priority (#87, `VISION § C`):** WirePlumber `90-nomarchy-dock-audio` raises `priority.session` for HDMI/DP and USB ALSA sinks so they become default when present; unplug falls back to built-in. Rules in `modules/nixos/dock-audio-rules.nix`; `checks.dock-audio` pure + conf-file VM. Real hotplug V3 in HARDWARE-QUEUE. ✓ **Dock audio round 2 — auto-follow watcher (2026-07-12, superseded by #100 below):** the T14s dock test showed priority rules alone can't deliver "just works": WirePlumber's *stored* default (default-nodes state, written by any past explicit device pick — every real machine has one) outranks `priority.session`, so the default never moves on plug. Fix: `nomarchy-dock-audio` (modules/home/dock-audio.nix, `nomarchy.dockAudio.enable`, default on) — initially a `pactl subscribe` watcher that selected new/card-changed dock-class sinks (regex data imported from dock-audio-rules.nix, single source) + toasts; startup sweep for login-while-docked (only when the default isn't already dock-class, so restarts don't override a manual pick). EasyEffects needs nothing: EE 8 `useDefaultOutputDevice` defaults true and verified live — with a stream playing, EE's output links moved to a fake dock-named null sink and back on unload. Unplug = WirePlumber priority fallback to built-in. Docked menu rows shipped alongside (the original panel-disable action is superseded by the hardware-QA correction below); Audio has a self-gated "Send output → " quick row plus the full output picker. ✓ **Undock blackout rescue (2026-07-12):** Bernardo asked whether the panel comes back if you undock while "Laptop screen off" — VM answer (tools/monitor-fallback.nix, full softGL desktop): **no**, Hyprland 0.55.4 leaves zero active outputs and never re-enables a soft-disabled monitor. Fix: `rescue_blackout` in the display hotplug watcher (hyprland.nix) — on monitorremoved with zero active outputs, immediately (and with retries) `keyword monitor ,preferred,auto, auto` every disabled output + toast; independent of the profile auto-switch gate (safety invariant, not a preference). Sharp edge found while testing: the zero-output state itself crashes Hyprland in the VM ~4/5 runs (ABRT in aquamarine CBackend::dispatchIdle, ~0.5s — faster than any userland rescue; control run with the panel active survives 5/5, so it's the state, not headless removal; monitor- disconnect crashes are a known upstream class). Posture: rescue ships as defense-in-depth (recovers every surviving case; worst case on real DRM is session-to-greeter on the re-lit panel, not a black brick). Also fixed in the rescue: `grep -c .` exits 1 at count 0 — an `||`-guard on that pipeline silently disabled the rescue exactly when it mattered. ✓ **Docking hardware-QA correction (2026-07-13, superseded by #100):** real clamshell QA confirmed every warned residual: Rofi's default `monitor=-5` followed the mouse pointer onto the hidden laptop workspace; the profile watcher (and blackout rescue) was not launched with default `displayProfiles = {}`; disabling eDP then unplugging failed to restore it, with application loss consistent with the known zero-output crash; audio did not arrive as a new sink; and the keyboard picker was disabled by an empty optional-candidate list. Correction: Rofi targets the focused output (`monitor=-1`); the first correction's interactive **Dock mode** moved eDP workspaces/focus but kept eDP logically active as a fallback; the reconnecting display watcher runs unconditionally and watches through the racy removal snapshot; keyboard hotplug runs by default, searches the pinned XKB catalog without D-Bus, persists per-device, and has a manual System › Keyboard path; that correction's dock audio reacted to new sinks and card/profile/port changes (replaced by #100's monitor-event boundary); and monitor-added re-applies the current awww wallpaper after the output settles (new outputs do not inherit the existing image). `checks.docking-ux` guards the generated default-session artifacts; the corrected real sequence remains V3. ✓ **Dock transition recovery (#100, 2026-07-13):** the second T14s pass exposed why keeping eDP active was not safe: Hyprland requires an active workspace on every active output, so one workspace necessarily dangled. Cable removal with the lid closed was independently proven in the previous boot journal to make logind enter s2idle, explaining the apparent crash. `nomarchy-display-transition` now follows the old working flake's ordering: dock is one batch (enable external, move all internal workspaces, focus external, disable internal); undock enables internal first, waits boundedly, then moves every workspace back. Named profiles that disable the internal panel use the same transition when they have an external target. `nomarchy-display-profile-watch` owns a block-mode, low-level `handle-lid-switch` inhibitor from external detection through internal restoration and physical lid-open; it uses process substitution so state stays in the parent shell, verifies acquisition through logind, kills a private process group on cleanup, and validates/cleans a stale runtime PID after an unclean exit. Normal undocked lid-close suspend resumes after release. Audio now uses `monitoradded` as the fresh-plug intent boundary: after a debounce and two-second hardware settle it restarts PipeWire, PipeWire Pulse, and WirePlumber, then explicitly selects the highest-priority available sink using the original WirePlumber regex data. Generic `change:sink`/card events never select, so a manual speaker choice sticks; the subscription loop reconnects after graph restarts. Journal lines record the trigger and selected/no-candidate result. `checks.docking-ux` syntax- and invariant-checks generated artifacts; `tools/monitor-fallback.nix` is now a bounded KVM test for workspace/output/inhibitor transitions and passed V2. QEMU's headless backend aborts Hyprland if its final active synthetic output is deleted while the DRM output is disabled, so the harness explicitly runs the internal-first recovery before deletion and leaves real cable-removal timing to V3. V3 is queued as round 4; no speculative audio power policy was added. ✓ **Docking rounds 4–5 — the hotplug event is not the truth (2026-07-14):** the dev-box dock pass failed on wallpaper, dock mode, docked audio and a four-times keyboard prompt. Three causes, and a process lesson. `socat -T 1` closed Hyprland's IPC socket after a second of *inactivity* — a dock is normally plugged into an idle desktop, so `monitoradded` was routinely lost in the reconnect gap, and wallpaper, profile and audio all hung off that one event. The connection is now long-lived with the tick coming from a 1s `read` timeout, and the *output set* is the source of truth: `reconcile` diffs it on ticks and events alike, so an event only makes the reaction instant. A newly-present external is as unambiguous a plug as the event, so it may still override a manual sink choice. Automatic dock mode never existed — the watcher auto-*undocked* but on plug only matched display profiles, so with none saved it did nothing while `Dock mode` stayed menu-only; the pair is now symmetric, with a matched profile winning as the more specific layout. Hyprland calls every key-capable evdev node a "keyboard" (lid/power buttons, a monitor's control channel, each extra HID collection), so udev's `ID_INPUT_KEYBOARD` is now the filter and the prompt is once per physical keyboard, applied to every node it brings. Audio to the BenQ was unreachable *by any means* because WirePlumber had the GPU's DP card pinned to `pro-audio`, whose raw `pro-output-N` sinks have no ports, no routing and no volume — and card profiles persist, so it survived forever; a plug that finds nothing routable now repairs cards carrying an *available* HDMI/DP port (the monitor asking for audio over the cable), which keeps the internal analog card and considered pro-audio setups untouched. On UCM cards headphones are a sink rather than a route, and a stored default outranks every priority rule, so the jack is now followed explicitly; unplug needs no undo since the pin goes stale by itself. The process lesson: both watchers are `exec-once`, so `nomarchy-home` leaves the *old* processes running and two of the four reports were never testing the fix — **relogin before testing a watcher**. V3 is queued as round 6. ✓ **Conditional layout indicator + swap parity (#109, 2026-07-14):** the indicator answers "which layout am I typing in?", so it earns bar space exactly when that has more than one answer — not when a device merely exists. Counting devices was the trap: the junk rows a keyboard's extra HID collections leave behind are all remembered at the session layout and added no second answer. The gate reads the runtime-remembered `settings.keyboard.devices` alongside the declared option, since the watcher writes them without a rebuild and they only graduate later — reading the option alone was wrong for exactly as long as the memory was fresh. `keyboard.layouts` is excluded on purpose: hyprland.nix keeps it as the picker's candidate pool and never merges it into `kb_layout`, so it cannot make the current layout ambiguous by itself. All four whole-swaps list the module unconditionally and so used to show what the generated bar hid; they are filtered through the same answer rather than re-deriving it by hand (the parity rule). Logic is pure in `modules/home/waybar-language.nix`; `checks.waybar-language` unit-tests both halves against boreal's real swap. ✓ **Parallel fingerprint-or-password (2026-07-12, Bernardo promoted from PROPOSED):** `nomarchy.hardware.fingerprint.parallel` (default **true** — the better UX is what opting into fingerprint PAM gets you; `false` restores stock sequential pam_fprintd). Implementation: packaged `pam-fprint-grosshack` v0.3.0 (pkgs/, GitLab pin — the field-standard fprintd fork; stock PAM cannot express parallel factors, linux-pam#301; the cleaner-concept `pam-any` stays the fallback candidate if grosshack fights greetd/hyprlock). Source reviewed before packaging: every failure path returns PAM_AUTHINFO_UNAVAIL (falls through to password) and a typed password is only ferried via PAM_AUTHTOK to the stock `auth sufficient pam_unix.so … try_first_pass` — the hack never validates passwords itself, so password login cannot be locked out by it. Wiring reuses stock fprintd's rule slot (mkForce'd modulePath) so ordering is inherited, not recomputed. checks.hardware-toggles grew to three nodes: parallel default (stack shape + password-works/ wrong-password-fails with no reader), parallel=false (stock pam_fprintd), pam=false (neither). Real-reader race → HARDWARE-QUEUE (AMD dev box). Accepted quirks (documented in the package): process- wide SIGUSR1 handler, pthread_cancel'd prompt on fingerprint win, prompt buffer not zeroized. ✓ **neon-glass finish (#88, `VISION § D`):** rewrote whole-swap `rofi.rasi` to rofi-native `@color` glass panel (cyan rim, soft selection wash — same hex-alpha glass pattern as Boreal); btop title/ selection in accent cyan; ui rounding/opacity tuned; `preview.png` regenerated from theme-shot. Bar remains generated from JSON (no waybar.css reintro). Contrast + theme-wholeswap green; V2 theme-shot neon-glass + boreal viewed. - ✓ **Hibernate double-unlock:** on resume from hibernate the LUKS passphrase already gates the machine, but locking hyprlock before sleep meant a second password. Fixed by *not locking* before an encrypted hibernate: a `nomarchy-lock-before-sleep` systemd unit (`modules/nixos/default.nix`) takes over from hypridle's `before_sleep_cmd` and locks on the RAM-resume sleeps (suspend / hybrid-sleep / suspend-then- hibernate) always, but skips `hibernate.target` when the disk is LUKS- encrypted. (A first attempt that dismissed hyprlock *after* resume was wrong — killing a Wayland session-lock client trips its "go to a tty" crash failsafe instead of unlocking, which is the error screen it caused.) - ✓ **Key agents & pinentry:** ships `modules/home/keys.nix` (`nomarchy.keys.enable`): one agent — `services.gpg-agent` with `enableSshSupport` fronts SSH, so a single `pinentry-qt` (native-Wayland, Stylix-themed Qt) handles GPG and SSH passphrases alike. `SSH_AUTH_SOCK` comes from the agent's zsh integration; cache TTLs (30 min / 2 h) govern re-prompting. gnome-keyring stays the Secret Service (modern versions run no SSH agent, so no socket contention); screen lock doesn't flush the cache. ✓ **session-level `SSH_AUTH_SOCK`:** besides the zsh integration, a `home.sessionVariables` export now covers GUI clients launched outside a shell (rofi launcher, autostarted apps) that never inherit the interactive shell's copy — resolved with `gpgconf --list-dirs agent-ssh-socket` at session-init (the same lookup the shell integration uses, so the two can't drift), reaching GUI apps via the login shell that starts Hyprland the same way `NIXOS_OZONE_WL` does. Verified by building the home generation and inspecting the rendered `hm-session-vars.sh` — it carries `export SSH_AUTH_SOCK="$(…/gpgconf --list-dirs agent-ssh-socket)"` with the command substitution intact (unescaped, resolved at session-init). Pending an on-machine check that a GUI git/ssh client picks up the agent. - **Sanitize & organize the repo:** a housekeeping pass for consistency and clarity. - ✓ pruned now-redundant config: the installer no longer writes `console.useXkbConfig` / `boot.initrd.systemd.enable` into `system.nix` (distro-wide defaults since the LUKS-keymap fix), nor the offline-pin test fixture in `flake.nix`. - ✓ reconciled the README option tables with the live `nomarchy.*` surface (added keys/fastfetch/snapper/greeter.autoLogin). - ✓ untracked `.claude/settings.local.json` (machine-local) + gitignored. - ✓ **branch/release model:** `main` is the development default; `v1` is the release pointer downstreams pin (`?ref=v1`), advanced to main once a batch is tested — rolling, no tags. Brought `main` current (it was a stale legacy-merge), and deleted the dead `refactor` / `wave/nixos-26.05` branches (kept `legacy` for history). See the comment in `flake.nix`. - ✓ re-checked that every file earns its place under the `modules`/`hosts`/`themes`/`pkgs`/`tools` rule of thumb: no orphaned modules (`keybinds.nix` is a *data* module `import`-ed by hyprland/rofi, so correctly absent from `default.nix`), no dead-code markers, no editor/backup cruft, each `themes/.json` palette pairs with its optional `/` overrides dir, and `hosts/{default,live}` are the documented reference + ISO hosts. Structure is clean as-is. - Open decision (deferred): the repo declares **no formatter** (no flake `formatter`, no `.editorconfig`); `.nix` files use deliberate aligned hand-formatting, so `nixfmt-rfc-style --check` flags ~33 files. Adopting a formatter would be a one-time repo-wide reformat that flattens that alignment — a maintainer call, not a silent cleanup, so left untouched. - ✓ **Full docs review & restructure** (completed 2026-07-04 in four slices): the roadmap/backlog split (this file + agent/BACKLOG.md); option tables reconciled with the live surface and guarded permanently by `checks.option-docs`; drift pass over OVERRIDES/TESTING/template README (claims held; two gaps fixed); the install/first-run story now hands off README §3 → template README explicitly; and a new **docs/RECOVERY.md** runbook (generations → journals → older boot generation → snapper → nixos-enter). Still open, deliberately: the docs-site-vs-Markdown call (BACKLOG § Decisions). - **Laptop power / battery management:** a real power story behind a `nomarchy.system.power.*` surface (`modules/nixos/power.nix`), replacing the old "only `services.upower` for Waybar reporting" baseline: - ✓ **profiles:** `power-profiles-daemon` ships by default (the upstream- aligned choice — power-saver/balanced/performance via `powerprofilesctl`, switched through polkit so no root prompt). A `power-profile` menu module (in the SUPER+M picker) and the first Waybar `custom/*` indicator (click to cycle) both **self-gate** on a battery being present + PPD running, so they hide on desktops and under TLP — no system→home wiring. **TLP** is the opt-in (`backend = "tlp"`) for deeper battery tuning; the two are mutually exclusive (asserted). - ✓ **thermal:** `thermald` behind `power.thermal.enable`; the installer turns it on for a `GenuineIntel` CPU. - ✓ **longevity:** `power.batteryChargeLimit` (e.g. 80) — a backend- independent sysfs oneshot writes `charge_control_end_threshold`. Off by default; the installer scaffolds it commented-out on laptops. ✓ the threshold is now **re-applied on AC state changes** (a `services.udev` rule on `SUBSYSTEM=="power_supply", ATTR{type}=="Mains"` starts the oneshot via `systemctl --no-block`), closing the firmware-resets-on- unplug gap — the match is by adapter *type*, not kernel name (AC/AC0/ADP1/ACAD vary). The successful oneshot returns inactive and repeated `start` requests coalesce while it is applying; this avoids a USB-C dock's event burst repeatedly terminating `restart` jobs and tripping systemd's start limit. Eval-verified both ways (rule present with charge limit on / absent off), and **VM-verified the trigger** (`checks.battery-charge-limit`: the `test_power` module fakes a Mains adapter, toggling `ac_online` emits a real `power_supply` uevent, and the udev rule starts the oneshot — a burst during an intentionally extended active run stays successful and a changed `InvocationID` proves reapply). The sysfs *write* itself needs a real `charge_control_end_threshold`, so a final on-hardware check (a `sudo nixos-rebuild` + a physical unplug) remains — the dev box has both an `AC` Mains adapter and a `BAT0` `charge_control_end_threshold`, so it can exercise it. - ✓ **installer:** writes `power.laptop = true` (battery probe) and `power.thermal.enable` (Intel) into the generated `system.nix`. - ✓ **idle cohesion:** `modules/home/idle.nix` now suspends only on battery (and sooner — 15 min vs the old fixed 30), gating the hypridle suspend listener on a `nomarchy-on-ac` probe so a plugged-in machine stays up mid-idle; lock/screen-off are unchanged on both sources. logind's lid handling is left at its defaults (suspend on lid close, ignore when docked) — an explicit "I'm done" that coheres with the idle behaviour. - ✓ **Waybar parity:** the `custom/powerprofile` indicator now shows in the summer-day/night whole-swap themes too. `powerProfileStatus`/`Cycle` are named `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the generated config and the static `waybar.jsonc` both exec them by bare name (the same parity the DND bell has). General pattern: any new generated-config module needs adding to the whole-swap themes too. - **Full hardware enablement (beyond nixos-hardware):** nixos-hardware profiles cover *basic* per-model functionality (quirks, drivers), but the *advanced* features of a machine — keyed more on the CPU/GPU generation than the model — are left for the user to wire up by hand. Close that gap without making it finnicky: extend the installer's existing autodetection (DMI → nixos-hardware profile today; add CPU vendor/arch, GPU, NPU, fingerprint reader, SSD) to switch on the *safe, broadly-beneficial* features automatically, and expose a small `nomarchy.hardware.*` surface (per-vendor where it helps, e.g. `nomarchy.hardware.amd.*`) for the heavy or experimental bits. Goal: full utilisation out of the box — opt-*out* for the safe defaults, opt-*in* for the multi-GB / experimental extras. Complements nixos-hardware (model quirks) rather than replacing it; keep the detection in the installer's `hardware-db` so an installed machine bakes the right defaults, all overridable through `nomarchy.hardware.*`. - ✓ **Mechanism + broad seed shipped** (`modules/nixos/hardware.nix`): a vendor-keyed `nomarchy.hardware.*` surface (`intel`, `amd`, `fingerprint`, `npu`), `hardware-db.sh` extended to detect Intel/AMD, a fingerprint reader (libfprint USB vendor IDs) and an NPU (Intel VPU / AMD XDNA PCI IDs), and the installer bakes the matching toggles into `system.nix` — safe defaults active (GuC/HuC, amd-pstate, AMD VA-API env, fprintd), heavy/experimental opt-ins commented (Intel compute-runtime, ROCm, fingerprint PAM, NPU driver). Audited against the nixos-hardware commons so it only fills the gap above them — microcode / the media-driver VA-API stack / weekly fstrim already come from `common-cpu-*` / `common-gpu-*` / `common-pc-ssd`. Exercised live on the dev machine's detection (an AMD Ryzen-AI laptop: AMD + fingerprint + NPU all detected) and a toggles-on build (`amd_pstate=active` + `i915.enable_guc=3` in kernel-params, `fprintd.service` present). **Remaining: on-hardware verification of the runtime bits (AMD/NPU/Intel GPU-compute) — none are testable in CI or on the Intel Latitudes for the AMD paths.** SSD TRIM is intentionally left to `common-pc-ssd`. - ✓ **Hardening (driver-gen + kernel awareness):** `intel.guc` emits the *i915* param, so the installer turns it off when the GPU is on the newer `xe` driver (Lunar Lake / Battlemage / Panther Lake — GuC is default-on there); the NPU detector matches the PCI *accelerator class* (not just a device-ID list) so new gens are caught without a code change; and a `nomarchy.hardware.latestKernel` escape hatch (+ a build-time warning when `npu.enable` predates the shipped kernel) covers very-new hardware whose drivers only just landed. A config-assertion VM test guards the wiring (`checks.hardware-toggles`: kernel cmdline + fprintd + PAM, booted in a VM). Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M): - **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB closure → opt-in (`nomarchy.hardware.amd.rocm.enable`); unlocks GPU PyTorch / Ollama. - **Ryzen AI NPU (XDNA):** the `amdxdna` kernel driver (mainline 6.14+) + the XRT/xdna userspace runtime + firmware. Niche/experimental → opt-in. - **AMD P-State EPP (Zen 4 power):** `amd_pstate=active` for the EPP governor, which power-profiles-daemon (already shipped, `power.nix`) drives per profile. Broadly beneficial on Zen 2+ → default on when the installer sees an AMD CPU. - **Hardware video acceleration (VA-API):** mesa's radeonsi exposes VA-API on AMD (`LIBVA_DRIVER_NAME=radeonsi`, libva + drivers in `hardware.graphics.extraPackages`); Intel uses `intel-media-driver`. Broadly beneficial → default on, vendor-detected. - **LVFS firmware & biometrics:** fwupd already ships (✓, `services.fwupd`). Add fingerprint: `services.fprintd` when a reader is detected, with opt-in PAM integration for login/sudo (password-only stays the default for the cautious). - **SSD periodic TRIM:** `services.fstrim.enable` (weekly) — safe and beneficial, so default on (trivial enough it may warrant being distro-wide regardless). Sub-items here can graduate into their own roadmap entries as they're scoped; the unifying work is the detection + `nomarchy.hardware.*` surface. - **Webcam support & tuning:** improve out-of-the-box webcam behaviour. Motivating case — on the ThinkPad T14s AMD Gen 4 the camera shows a dark, low-quality image. **Diagnosed on hardware (2026-06-26)**, and it's *not* what the first cut of this item guessed (UVC default-controls tuning vs a MIPI/ libcamera gap): - The camera is a **USB UVC** module (Chicony `04f2:b7c0`, `uvcvideo`) and is **dual-sensor** — `video0` Color (MJPG, up to 2592×1944 / 1080p) + `video2` **IR** (8-bit `GREY` only, the face-unlock sensor). The AMD IPU `[1022:1502]` is present on PCI but **unused** (the camera enumerates over USB, not the MIPI/ISP path), so the libcamera-software-ISP branch is moot on this machine. - The **raw color capture is fine**: at factory defaults with auto-exposure, `/dev/video0` measures luma ≈130/255 *from the first frame* (no AE ramp, not dark); forcing manual exposure made it *worse*. So there is **no v4l2 control default to bake** — the speculated `v4l2-ctl`-tuning / udev-oneshot fix is the wrong tree. - **Real cause is the consumption path.** PipeWire/WirePlumber exposes the device through **both** backends at once — two `[v4l2]` sources (node 144 = `/dev/video0` Color, node 146 = `/dev/video2` **IR**) **plus** two `[libcamera]` nodes (Color + IR) — and the IR sensor is presented as an **indistinguishable** "Integrated Camera". So an app's camera picker shows up to *four* identical entries, and choosing the IR one yields a black/dark monochrome frame; the libcamera path can also negotiate the low-res `YUYV` 640×480 mode ("bad quality"). The default source is the color node, so apps that don't let you choose are fine — the breakage is selecting (or an app auto-selecting) the wrong node. - **Fix — validated live on hardware (2026-06-26).** Two WirePlumber 0.5 drop-ins collapse the four entries to one clean color camera, confirmed via `wpctl status` (before: 2 v4l2 + 2 libcamera incl. both IR nodes → after: **1** v4l2 source = `/dev/video0` color, **0** libcamera): 1. **Hide the IR node** — `monitor.v4l2.rules` matching the IR sensor → `node.disabled = true`. (Tested by card name `~.*Integrated I`; the **shipping** match should key on the more robust, vendor-neutral heuristic of a **`GREY`-only / no-color-format** node, since other vendors' IR cards are named differently.) 2. **Drop the duplicate backend** — `wireplumber.profiles.main.monitor.libcamera = disabled`, since a plain UVC cam is fully covered by v4l2 (also kills the libcamera low-res-`YUYV` path). **Drawbacks / design constraints for the module:** - Rule 2 (libcamera off) is only safe **when a UVC camera exists** — on a MIPI/IPU-only machine (no UVC fallback) it would kill the camera entirely, so it **must be conditional on detection**, not blanket. Rule 1 (IR-hide) is broadly safe. Exactly the `nomarchy.hardware.*`-gated targeting the parent item calls for. - **Face-unlock is *not* broken:** `node.disabled` only hides the PipeWire node; the kernel `/dev/video2` stays openable, so Howdy (which reads the IR device directly, bypassing PipeWire) still works. A true MIPI/IPU software-ISP camera with no UVC fallback stays a separate future item. - **Shipped (2026-06-27):** `nomarchy.hardware.camera.hideIrSensor` (+ an overridable `irMatch` regex) in `modules/nixos/hardware.nix` emits rule 1 via `services.pipewire.wireplumber.extraConfig`; the installer's `hardware-db.sh` auto-detects a paired RGB+IR webcam (from `/sys/class/video4linux/*/name`) and bakes the toggle into `system.nix`, with a commented example in the downstream template. **Decision: v4l2 IR-hide only — libcamera is left untouched** so an external camera you plug in is never affected. Surgical internal-only libcamera scoping proved impossible: the distinguishing device props (`api.libcamera.location`, `device.product.name`) bind *after* the monitor rule runs, and the only early-matchable prop (`device.api`) is all-or-nothing — so a broad libcamera-off was the only option and was rejected as the blunt instrument it is (external USB cams are UVC and keep working via v4l2 regardless). Verified: installer detection fires on the T14s; the generated drop-in's serialized content is valid WP-0.5 config; and the exact shipped `irMatch` was re-confirmed live (1 V4L2 source = the colour `/dev/video0`, libcamera untouched, IR node disabled in the WirePlumber log). Remaining: an on-Nomarchy end-to-end check (HARDWARE-QUEUE › T14s); optional `v4l-utils` + `cameractrls` already commented in the template. - ✓ **Portal/Flatpak IR gap documented (#71, docs only):** the shipped v4l2 IR-hide does **not** cover libcamera / xdg-desktop-portal / Flatpak camera pickers (IR can still appear there). User-facing note in [`HARDWARE.md` §7](HARDWARE.md). Engineering options (b) WirePlumber libcamera GREY-only rule and (c) libcamera/udev-layer hide stay deferred — both need a T14s-class dual-sensor machine to verify; no libcamera rules in-tree. - ✓ **Memory-pressure protection (earlyoom, default-on):** `modules/nixos/oom.nix` — running out of memory kills the offending process instead of freezing the desktop. **earlyoom over systemd-oomd, deliberately:** oomd kills whole cgroups, and a Hyprland session is ONE scope (no per-app systemd scopes here, unlike GNOME) — under pressure it would take out the entire desktop; earlyoom kills the single largest process before the thrash point. nixpkgs default-enables oomd *inert* (no slices monitored) — disabled outright so there's one owner. Session plumbing is `--avoid`-listed (Hyprland/hyprlock/greetd/waybar/pipewire/ wireplumber/Xwayland/nix-daemon/systemd — unanchored, since NixOS wrappers rename comm to `.foo-wrapped`); no `--prefer` tuning (largest- RSS selection already finds the hog); kills raise a desktop notification (systembus-notify). All `mkDefault` — opt out with `services.earlyoom.enable = false`. **VM-verified** (`checks.oom-protection`): a chunked allocator peaked at ~686 MB in a 1 GB VM, earlyoom SIGTERM'd it in 0.1 s, a bystander unit survived, and oomd is asserted off. - **Opt-in services & integrations:** the counterpart to the opt-*out* application suite above — heavier or more personal integrations shipped **off by default**, each a `nomarchy.services..enable` toggle a downstream flips on in one line. Keeps the base lean while making common additions trivial. **✓ Seventeen shipped** in `modules/nixos/services.nix` (system-side), each with a commented example in the downstream `system.nix` template: `tailscale`, `syncthing` (runs as the login user, GUI on 127.0.0.1:8384), `podman` (rootless, `docker` aliased, user subuid/subgid), `flatpak` (+ Flathub remote added by a oneshot), `pika` (Pika Backup GUI), `steam` (`programs.steam` — the 32-bit stack, controller udev, Remote-Play ports a bare package can't), `libvirt` (libvirtd + virt-manager, user added to the `libvirtd` group), `obs` (OBS Studio + a v4l2loopback virtual camera usable as a webcam in Zoom/Teams), `docker` (rootful; asserts against the podman docker-compat), `kdeconnect`, `gamemode`, `adb` (android-tools; systemd handles the udev rules), `wireshark` (Qt GUI + the `wireshark` group), `ollama` (local LLM API on 127.0.0.1:11434), `printing` (CUPS + Avahi mDNS), `openrgb` (RGB lighting daemon), and `restic` (scheduled daily backup — a small option surface: `repository`/`passwordFile`/`paths`, 7/4/6 retention). More candidates by area: - ✓ **cloud/sync:** Syncthing (`services.syncthing`); Nextcloud client is an upstream-HM opt-in (`services.nextcloud-client`, commented in the template — installs the app + starts it with the session; #102) - ✓ **local AI:** Ollama (`nomarchy.services.ollama`, optional GPU accel) ships; LM Studio is a (commented) app-suite package (a bare GUI, no service) — pairs with the menu's Ask-Claude philosophy - ✓ **containers/VMs:** Podman (`virtualisation.podman`); libvirt + virt-manager (`nomarchy.services.libvirt`); Docker (`nomarchy.services.docker`) - ✓ **networking:** Tailscale (`services.tailscale`); WireGuard needs no toggle — NetworkManager (on by default) imports `.conf` tunnels natively; the `wireguard-tools` CLI is in the app-suite menu - ✓ **gaming/media:** Steam (`nomarchy.services.steam`); OBS Studio (`nomarchy.services.obs`, with the v4l2loopback virtual camera); GameMode (`nomarchy.services.gamemode`) - ✓ **devices:** printing (CUPS + Avahi, `nomarchy.services.printing`); KDE Connect (`nomarchy.services.kdeconnect`); OpenRGB (`nomarchy.services.openrgb`). Plus dev tooling — Android `adb` and `wireshark` toggles - ✓ **backup:** Pika Backup (`pika-backup`, GUI over Borg); scheduled headless restic (`nomarchy.services.restic`) - ✓ **escape hatch:** Flatpak (`services.flatpak`) + Flathub, for apps outside nixpkgs Surface decided: `nomarchy.services.*`, system-side for system services (home-side ones can extend the same prefix later). Remaining: curate the rest (Ollama, containers, Steam/OBS, printing, backups, Flatpak …). The opt-out application *suite* is deliberately **not** a parallel `nomarchy.apps.*` surface — it's a curated `home.packages` list in the downstream template (see "Default application suite" above), since a bare package install has no config to put behind a toggle. Unfree entries are already covered (`allowUnfree = true`). - ✓ **Display / monitor management:** declarative `nomarchy.monitors` (a per-output schema — name/resolution/position/scale/transform/mirror/ bitdepth/vrr/disable) generates Hyprland `monitor` rules, keeping the `,preferred,auto,1` wildcard as the fallback; Hyprland applies them on hotplug, so a declared external/dock output arranges itself on connect (no kanshi — it fights Hyprland's own output management). `nwg-displays` ships behind `nomarchy.displays.enable` as an interactive arranger (a helper to find values; the declarative config stays the source of truth — its output file isn't sourced). The System ▸ **Display** menu picks an output's resolution from its advertised modes: applied live via `hyprctl keyword monitor` (current position + scale kept) and persisted to the in-flake state (`settings.monitors.`, no rebuild), overlaid onto `nomarchy.monitors` by name so the next rebuild bakes it in — the monitor twin of the keyboard-layout graduation. Remaining: true docked/undocked **profile switching** of the *same* outputs (Hyprland's per-output rules cover the common "external connects → arrange" case, not multi-layout toggles), and optionally workspace-to-monitor binding. - ✓ **Night light / blue-light filter:** `nomarchy.nightlight` (opt-in) — a scheduled colour-temperature shift via `hyprsunset` (Hyprland-native), warm at night, identity (no shift) by day. `modules/home/nightlight.nix` drives the HM `services.hyprsunset` with two time-based profiles (`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the schedule and the on-login state. Active-profile-at-session-start was confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle (opt-in, instant, in-flake state):** two git-tracked keys in the state file, both menu-written (exposed via the new `nomarchy.settings` option) — `settings.nightlight.installed` (**sticky**: gates the hyprsunset unit; `nomarchy.nightlight.enable` `mkDefault`-reads it) and `settings.nightlight.on` (runtime on/off). **Off by default.** The *first* enable from the menu writes `installed` and rebuilds to create the unit (the one accepted rebuild); **every toggle after is instant** — `nomarchy-theme-sync set settings.nightlight.on … --no-switch` (atomic + `git add -N`, no rebuild) plus a `systemctl` start/stop. Splitting the sticky flag from the on/off is what avoids a **decay** bug: an instant-off writes only `on`, so an unrelated later rebuild (e.g. `sys-update`) never drops the unit and "on" stays instant. Persistence across logout/reboot comes from an `ExecCondition` on the unit (`nomarchy-nightlight should-start`) that reads the **live** working-tree on/off at start time — *not* the eval-frozen store copy — so an off survives a reboot with no rebuild; a later rebuild bakes the same value. No more `~/.local/state` marker, no marker `ConditionPathExists`. The `nomarchy-menu` System-submenu entry is always shown (current on/off) so the feature can be enabled from the menu; the self-gating Waybar `custom/nightlight` indicator shows the moon while running and hides otherwise. This is the first cut of a broader principle — **any user-settable config gets a menu writer that lands it in the downstream flake; no state lives outside the checkout** (Phase 1 night-light + Phase 2 the per-device keyboard-layout memory below are done — both now in the git-tracked state file; next target: opt-in auto-commit of the flake on each mutation, owned files only). The on-machine check PASSed 2026-07-04 (enable/disable on hardware). **Geo mode shipped 2026-07-11:** set BOTH `nomarchy.nightlight.latitude`/`.longitude` and wlsunset replaces hyprsunset — sunrise/sunset computed from the location daily, the fixed times ignored; same toggle script, moon indicator, and live-state `ExecCondition` gate (the backing unit name is a single let-binding). Declarative-only on purpose (coords are hardware-independent machine config, like the keyboard layout — no menu writer). Guarded by `checks.nightlight-geo` (pure eval over the rendered units); the visible warm shift is hardware-tier, queued. - ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone` (opt-in, off by default) — geoclue + `services.automatic-timezoned` drive `/etc/localtime` from your location, so travelling to another zone updates the Waybar clock on its own. **Menu-driven, in-flake state** (the night-light / keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json, git-tracked; the System-menu entry (`nomarchy-menu autotimezone` → `nomarchy-autotimezone`) writes it and rebuilds. **Not instant like night-light** — it's a *system* service (not a user unit you can start/stop), so the toggle drives a `sudo nixos-rebuild` (bakes the service + the `time.timeZone` override) plus a `home-manager switch` (the Waybar-refresh watcher), both off the one flag. **time.timeZone handling:** a runtime zone needs `/etc/localtime` writable; automatic-timezoned sets `time.timeZone = null` itself, but the installer's static value would collide (a hard eval error), so the module forces it null (`mkForce`) over the installer's value when enabled, and reverts to it when disabled. **Live clock refresh:** Waybar's clock module captures the zone at construction, so `modules/home/timezone.nix` runs a tiny user service that watches timedate1's change signal and reloads Waybar (SIGUSR2) on a real zone change (also catches a manual `timedatectl set-timezone`). Eval-verified both ways (on: geoclue+daemon on, tz forced null over a static installer value, watcher present; off: static tz intact, no service/watcher). **Pending an on-hardware check** — geoclue detection and the SIGUSR2 clock refresh aren't testable in CI (if SIGUSR2 proves insufficient, fall back to restarting waybar). Remaining (optional): a Waybar tooltip line showing the detected zone. - **Keyboard layouts (per-device + switching):** - ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices` (`{ "" = { layout; variant; }; }`) generates Hyprland `device` blocks that override the session `nomarchy.keyboard.layout` for a named keyboard — e.g. an external board that's physically a different layout than the laptop's. Hyprland applies it whenever that device connects (the "external keyboard remembers its layout" case, done the declarative way). A Waybar `hyprland/language` indicator shows the active layout, gated on more than one layout being in play (comma in the session layout, or any per-device override) so single-layout bars stay clean. - ✓ **Interactive new-keyboard prompt (runtime + persist):** set `nomarchy.keyboard.layouts` (candidate layouts) and a `nomarchy-keyboard- watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a keyboard connects *after* login that isn't in `keyboard.devices` and hasn't been chosen before, it pops a rofi layout picker, applies the choice as a **per-device** `hyprctl keyword device[]:kb_layout` (the runtime twin of the declarative `device{}` blocks — so it isolates that one keyboard and never touches the built-in board), and remembers it. The boot-time set (incl. the built-in keyboard) is never prompted. The runtime-remember complement to the declarative `keyboard.devices`. **Picker verified on hardware (2026-06-18)**; the first cut applied the choice with `switchxkblayout` (a global layout-index flip) and merged the candidate pool into `input.kb_layout`, so a selection leaked onto the laptop keyboard and stuck after unplug — fixed by the per-device keyword + keeping the pool out of the session layout. - ✓ **In-flake state + graduation (2026-06-23):** the remembered picks moved out of `~/.local/state/nomarchy/keyboard-layouts` into the git-tracked state file (`settings.keyboard.devices`, a device-name → layout map) — Phase 2 of the in-flake-state principle, the same path night-light took. The watcher reads the **live** working tree (a pick is honoured at once) and writes instantly with `--no-switch` (no rebuild), merging the whole map back at the dot-free parent path so a device name containing a dot can't corrupt the dotted set-path. Each remembered device **graduates** into `nomarchy.keyboard.devices` on the next rebuild (a generated Hyprland `device{}` block, `mkDefault` so a hand-written entry still wins), after which the watcher sees it as declared and steps back. Eval-verified (graduation + precedence) and the writer round-trips; **still needs the on-hardware hotplug re-verify** (the picker path isn't testable in CI). - ✓ **Multi-layout cycle bind + summer parity (2026-07-04):** `SUPER+SHIFT+K` → `hyprctl switchxkblayout current next`, rendered only when the session layout has a comma (same gate as the Waybar language indicator) — data lives in `keybinds.nix` as a separate `multiLayoutBinds` list, so hyprland.nix and the cheatsheet consume one source and gate identically (verified both ways: absent on a single layout; bind + cheatsheet row render under `us,de` via extendModules). `current` targets the focused keyboard, so a per-device-overridden board (one layout) is a no-op, never a leak. `hyprland/language` (`󰌌 {short}`) added to both summer `waybar.jsonc` whole-swaps + `#language` in their CSS — static JSON can't eval-gate, so on summer themes the module shows even with one layout (a deliberate parity-over-minimalism call; it's small). Noticed en route: summer-night carries an `idle_inhibitor` the generated bar lacks — folded into the idle-inhibit backlog item. - Remaining: keep the system (console/initrd) and session layouts in sync (the LUKS-keymap work). - ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu (`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell indicator (`custom/notification` via `swaync-client -swb`: shows the notification count + DND state, left-click toggles the panel, right-click toggles DND). Turning DND off confirms with a toast; turning it on is silent (notifications are suppressed — the bell-off glyph is the cue). - ✓ **Snapshot browse/restore UX:** `nomarchy-menu snapshot` (in the SUPER+M picker, self-gated on snapper) launches **btrfs-assistant** — the desktop browse/diff/restore/rollback UI, elevating via polkit (the safe choice for a destructive, root-only op vs a fat-fingerable rofi rollback). Shipped system-side gated on `nomarchy.system.snapper`. Also extended snapper to snapshot **/home** by default (hourly 5 / daily 7 / weekly 4) when it's its own BTRFS subvolume; a boot oneshot creates the required `/home/.snapshots` subvolume if missing (covers existing installs, no disko change). **The backend is VM-verified** (a full BTRFS+LUKS install: both `root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots` a real subvolume, the oneshot `Result=success`). - ✓ **Resolved — the "btrfs-assistant 2.2 segfault" was unprivileged-only** (re-diagnosed 2026-07-04): gdb puts the crash in `btrfs_util_subvolume_ iterator_next()` — libbtrfsutil's *unprivileged* subvolume-iteration path in btrfs-progs 6.17.1 (upstream-fixed after 6.17.1, kdave/btrfs-progs `886571653` "re-enable tree search v2 ioctl"; symbol versions were checked and match, so not an ABI/link issue). **As root it runs fine** — VM-proven (root `--version` exits 0, unprivileged exits 139) — and the pkexec launcher runs it as root, so the GUI was never actually broken *when launched right*. What WAS missing distro-wide: **no polkit authentication agent**, so every pkexec prompt in the session failed silently — that's the root cause of "the GUI doesn't open". Fixes: `hyprpolkitagent` (Hyprland's Qt agent, Stylix-themed) now ships via exec-once in `hyprland.nix`; the menu prefers `btrfs-assistant-launcher` again with `nomarchy-snapshots` as fallback; `checks.snapshot-gui` guards the root path on a real btrfs volume (and the GUI event loop offscreen) against lock-bump regressions. No btrfs-progs patch: our flows are all root-side, so the unprivileged fix rides in with a future lock bump. Original diagnosis kept below for the record. ✓ **Fallback shipped:** the menu now launches `nomarchy-snapshots` (system-side, gated on `nomarchy.system.snapper`) instead — a keyboard-driven snapper browser/restore with no btrfs-assistant dependency. snapper is root-only here (no `ALLOW_USERS`) and rofi can't run as root under Wayland, so it runs in a terminal via `sudo` (one password prompt) and uses `fzf` to pick: browse/diff (read-only), restore files (`undochange`), or roll back (root config only) — each behind a typed-`yes` confirmation, which is *safer* than the fat-fingerable one-click rofi rollback this entry originally avoided. btrfs-assistant stays installed for when nixpkgs fixes it. **Pending an on-machine check of the restore/rollback paths.** - Remaining (optional): boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs. - ✓ **Update awareness:** `nomarchy.updates.enable` (opt-in, `modules/home/ updates.nix`) — a `nomarchy-updates` checker on a systemd user timer (`.interval`, default daily) compares each **direct** branch-tracking flake input (nixpkgs, the Nomarchy input, home-manager, stylix … via `git ls-remote` vs the locked rev; offline → skipped, no false alarm) and, when the `flatpak` CLI is present (`.flatpak`), counts Flatpak updates. A self-gating Waybar `custom/updates` indicator (hidden until something's available; accent `󰚰 N`, signal-refreshed) + a notification that fires only when the count *grows* (so a daily timer never nags). Click → the upgrade flow in a terminal (`sys-update` / `flatpak update`, each confirmed). Purely passive — it never changes anything, augmenting the explicit rebuild flow. Indicator is in the generated bar and both summer whole-swaps. **Pending an on-machine check** (the ls-remote/notify/timer path needs a live session). - **Automated upstream lock bumps (maintainer CI):** the upstream twin of the user-side checker above — automate advancing Nomarchy's *own* `flake.lock`, which is the entire delivery channel (a downstream takes a single input, `?ref=v1`, so nixpkgs/home-manager/stylix/nixos-hardware reach it *transitively pinned* through that lock; users can't bump them independently). **Tiered by what CI can't do:** the hardware-QA gate (the Latitude 5410 + the AMD dev box — AMD/NPU/Intel-GPU-compute runtime bits aren't testable in CI or on the Intel Latitudes for the AMD paths) can't be automated, so a bot drives `main` and a human still promotes `main → v1`. A scheduled job (Forgejo Actions, or Renovate's Nix manager opening a reviewable lock-diff PR) runs `nix flake update` → `nix flake check` + the existing `runNixOSTest` suite + builds `system.build.toplevel`, and on green lands on `main` — which nobody tracks, so a bad bump costs a debug session, not a user outage; `v1` stays the manual on-hardware gate (it only ever fast-forwards, never force-pushed, or a user's locked rev can vanish from history). **Cadence weekly** (release-26.05 doesn't move fast; nightly is just churn), and `nix flake update` stays *within* the pinned release branches — no surprise major bump (a 26.11 jump is a deliberate `v2`, hand-edited in flake.nix). Side benefit: shrinks the security-patch latency the single-input model otherwise imposes (users are gated on the maintainer for nixpkgs CVEs), since a fix is usually already eval/build/VM-tested and one hardware promotion away from `v1` — worth fast-laning lock-only/security bumps ahead of feature batches. **Shipped since:** `.gitea/workflows/check.yml` (eval tier on every main push) and `bump.yml` (Monday 05:17 UTC scheduled lock bump, eval + V1 build gate, auto-lands on green — first landing 8fded63, 2026-07-06). 2026-07-12: both gates moved from one big `nix flake check --no-build` (~6.0 GB peak RSS — OOM'd the 4 GB VPS, then could never fit the runner's 2 GB container cap) to `tools/ci-eval.sh`, one output per nix process. **nomarchy-live is excluded by decision** (Bernardo, same day): the ISO is built/distributed manually, so its guard rides that manual flow — and its eval alone is 2.69 GB, the one output that can't fit the cap (Actions run 315: everything else green, it OOM-killed). With it out, the worst CI output is 0.99 GB — no server change needed. Explicitly *not* a binary cache: compile-from-source is a deliberate values call (Gentoo-style), so this automates the *config/lock* channel, not artifact distribution. - ✓ **Opt-in auto-commit of state mutations (in-flake state, Phase 4):** `settings.autoCommit` (menu: **System › Auto-commit**, self-gated on the flake being a git repo) makes every `apply`/`set` also `git commit` theme-state.json in the downstream flake. Design decisions: the flag is **live-read by the tool** on each write — nothing in Nix consumes it, so the toggle is instant, no rebuild, no option-surface addition; the commit is **pathspec-limited** (`git commit -- theme-state.json`) so unrelated dirty files are never swept up; it fires when the flag is on before *or* after the write, so the disable-toggle itself lands in history instead of staying forever-dirty; a same-value `set` no-ops (diff against HEAD); a missing git identity falls back to `Nomarchy `; `bg next` is deliberately excluded (runtime wallpaper churn — the path rides along with the next real commit). Verified: V1 (HM generation builds, generated menu `bash -n` green) + a 7-assertion sandbox-repo round-trip incl. the apply path; on-machine check queued. Rider fix: `get` now prints booleans JSON-style (`true`, not Python's `True`) — which also un-sticks the System menu's "Auto timezone (on/off)" label, whose `= true` comparison could never match before. - ✓ **Auto-commit sweep before switch (2026-07-12):** the pathspec limit above left hand edits (system.nix/home.nix, lock bumps) forever-dirty even with `settings.autoCommit` on. Now `nomarchy-pull`/`-rebuild`/ `-home` run `nomarchy-autocommit` (internal, in nomarchy-lifecycle) first: same live-read flag, commits *everything* dirty with an honest `nomarchy: auto-commit before ` message whose body lists the swept files — so `git log` mirrors the generation list and hand edits still never ride a settings-named commit. Same fallback identity; never fatal (`|| true` at the call sites — a git hiccup can't block a rebuild); before the ff-only pull, it also un-dirties the tree. Guarded by `checks.lifecycle-autocommit` (sandbox-repo round trip of the real binary: sweep, no-op on clean/off/non-repo). - **"nomarchy" control center:** a single TUI/GUI front-end over the common toggles — theme, power profile, opt-in services, display, DND — built on the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already uses. Plus a first-boot welcome that lands new installs in a guided "pick your theme / essentials" flow (ties into the branding work). ## Known issues & follow-ups - **Signal (and some Electron apps) tray icon cannot be themed** (BACKLOG #89, 2026-07-12): Signal Desktop hardcodes a dynamically generated, full-color pixmap (`Signal_status_icon_1`) via its Electron wrapper and pushes it directly over SNI. It ignores GTK icon themes and provides no built-in toggle for a monochrome icon. We will not use brittle `app.asar` repacking overlays to override this due to maintenance overhead and scope creep. It is expected that its tray icon will not match Nomarchy's monochrome bar. - ✓ **README option tables drift from the live surface** (docs review slice (a), 2026-07-04): a fresh reconcile found 9 undocumented options (autoTimezone, camera.hideIrSensor/irMatch, intel.guc, amd.pstate/ vaapi, package, system.stateFile, restic.paths) — rows/mentions added, and the diff is now permanent: `checks.option-docs` walks the option names out of the four option-declaring modules at eval time (their lazy `config` halves never evaluate, so dummy args suffice) and requires each a table row — or a backticked `.leaf` mention for non-enable sub-knobs — plus flags table rows whose option no longer exists (tools/check-option-docs.py). - ✓ **swaync text invisible on summer-day** (Latitude QA, 2026-07-04): the themed CSS paired @subtext on @base, but palette roles are not uniform across themes — summer-day/flexoki-light use subtext as text-*on*-surface (== base) and surface as a dark chip (== text), so body text and widget buttons self-colored. Fix: swaync uses only pairings safe in every palette — @text on @base for text (survey: worst ratio 5.18 across 21 themes) and alpha(@text, 0.1) tints for chips/hovers — guarded permanently by `checks.theme-contrast` (tools/check-theme-contrast.py, WCAG ratios over themes/*.json). The same audit found generated waybar/rofi CSS shared the bug class, exposed on flexoki-light (no whole-swap) — fixed in the follow-on: waybar dim/secondary shades are alpha(@text) tints, rofi @dim/@surface are fg-derived #RRGGBBAA tints, and the contrast check's pairings now cover all three generated surfaces. Status glyph accents (good/warn/bad on base, 2.0–2.7 in some palettes) were left as-is — raising them is palette design, a human call. - ✓ **Identity audit exemptions + import hierarchy** (#69/#70): `tools/audit-theme-design.py` tags expected hue/CVD/ANSI-family noise on white/vantablack/lumon/hackerman/matte-black/miasma as `[identity]` (do not retune into traffic lights). `tools/import-palettes.py` keeps surface≠overlay when ANSI color0==color8, derives light chips from base instead of ANSI black, and documents that roles are first-class — no bulk re-import of shipped themes without a hierarchy pass. - ✓ **Waybar crash on theme switch left the session bar-less** (Latitude hardware QA, 2026-07-04): exec-once has no supervisor, so any crash orphaned the session until relogin — and the switch path itself was crash-prone: `reload_style_on_change` (inotify on style.css) and theme-sync's SIGUSR2 both fired while the HM symlinks flipped, a double-reload race in waybar's in-place reload. Fix: (1) the bar runs under a `nomarchy-waybar` supervisor (waybar.nix) — ANY exit respawns it, with a crash-loop guard (5 fast exits → critical notification, stop); (2) theme-sync now prefers a clean `pkill -x waybar` when it sees the supervisor (a restart with fresh config+style — no reload code path at all), keeping SIGUSR2 only as the fallback for unsupervised/custom bars; `reload_style_on_change` stays for manual `home-update` restyles. VM-verified on the headless software-GL desktop: SIGKILL → new pid; clean kill → respawn; SIGUSR2 → alive. - ✓ **`sys-rebuild`** (Latitude QA request): the no-update twin of `sys-update` — snapshot-first system rebuild against the *current* lock, for config-only changes; `home-update` was already lock-free. README §3/§5 + the motd list all three. - ✓ **Yazi TOML parse error on startup:** yazi 26.x made fetchers' `group` field required, so `[[plugin.prepend_fetchers]]` failed to parse and yazi fell back to presets. Fixed by adding `group = "git"` to both git fetcher entries in `modules/home/yazi.nix` (the git plugin's `setup()` only renders the linemode; the fetcher still needs registering). - ✓ **Starship prompt styling:** dropped the powerline look (the `[]` separator glyph + `bg:` fills) for a flat prompt — bold-accent directory, warn git branch, subtext status/duration, `❯` character, all plain colored text (`modules/home/shell.nix`). Also wired in `cmd_duration`, which was configured but never referenced in `format`. - ✓ **xfce deprecation warnings:** moved `xfce.exo` → `pkgs.xfce4-exo` and the three Thunar plugins to their new top-level names in `modules/nixos/file-manager.nix`; the eval warnings are gone. - ✓ **Rofi function keybindings:** direct `SUPER+CTRL+` binds jump straight to each `nomarchy-menu` module — V clipboard · C calc · W web · F files · N network · B bluetooth · S capture · A ask — added to `modules/home/keybinds.nix`, so they also show in the SUPER+? cheatsheet. - ✓ **Enable nix-ld by default:** `programs.nix-ld.enable` is on distro-wide (`modules/nixos/default.nix`), so prebuilt/foreign dynamically-linked binaries run out of the box. - ✓ **Hyprland border colors off for some themes:** the cause was v1 forcing an `accent→accentAlt` gradient on every theme; legacy used a *solid* border (accent for nord/retro-82/lumon, the text tone for kanagawa/summer-day/summer-night). Added a palette-resolved `border` field (`{active, inactive}`) to the theme schema (`theme.nix`), consumed solid in `hyprland.nix`, and declared it in every preset so a switch always replaces it. All six identity themes now match legacy exactly. - ✓ **Waybar shows non-existent workspaces:** the v1 summer port "fixed" legacy's deprecated `persistent_workspaces` (underscore — silently ignored by current Waybar, so legacy only ever showed existing workspaces) into the modern `persistent-workspaces` (hyphen), which Waybar honours → all 10 rendered as phantoms. Dropped the block from `themes/summer-{day,night}/waybar.jsonc`; the other themes use the generated `waybar.nix`, which never had it, so they already match. - ✓ **GTK/Qt ignore the theme's light/dark mode:** Stylix set `polarity` but not the `org.freedesktop.appearance color-scheme` that GTK4/libadwaita and Qt6 read via the portal, so a light theme could still render apps dark. `stylix.nix` now sets `dconf` `org/gnome/desktop/interface color-scheme` = `prefer-light`/`prefer-dark` from `t.mode` (xdg-desktop-portal-gtk already ships, and `programs.dconf` is on system-side), so libadwaita/Qt follow the palette. Confirmed in a live session (headless QEMU): the portal's `org.freedesktop.appearance` `color-scheme` reads `1` on a dark theme and flips to `2` after switching to a light one (catppuccin-latte) — so xdg-desktop-portal-gtk does relay the dconf value GTK4/Qt apps read. - ✓ **Laptop docking & Multi-monitor UX**: - Rewrote the display watcher (`nomarchy-display-profile-watch`) to listen immediately to Hyprland's IPC socket via `socat`, removing the 3-second polling delay. - Added a fallback to the `base` layout when unplugging an external monitor to guarantee the laptop lid turns back on. - Added explicit `SUPER+ALT+arrow` keybindings for moving active workspaces across physical monitors. - ✓ **Workstation Polish Pack (Security & Stability)**: - Enabled `security.apparmor.enable` system-wide for defense-in-depth confinement. - Added `boot.kernelParams = [ "panic=10" "oops=panic" ]` to auto-reboot upon catastrophic kernel/driver failures. - Integrated `hyprpicker` (`SUPER+SHIFT+C`) for instant Wayland-native color picking. - ✓ **Theme UI Review completed**: - Preserved the full-color Papirus icon set for Rofi menus to provide a colorful layer independent of the Waybar's monochrome style. - Enforced correct typography scales across `swaync` (notifications) and Rofi menus, natively binding to the active theme's `t.fonts.size` and `t.fonts.ui` rather than hardcoding sizes.