# 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`) - 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). - **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 — which also keeps the apps out of the live ISO automatically (it never starts from the template) and 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`. - ✓ **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):** 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) — a `pactl subscribe` watcher that `set-default-sink`s new 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: Display gains "Laptop screen off · everything → " (live-only `keyword monitor ,disable` — a persisted disable could black-screen an undocked boot), "Screen on · ", "Move workspace · → next monitor" (`movecurrentworkspacetomonitor +1`), "Swap workspaces" (`swapactiveworkspaces`, exactly-2 gate); Audio gains a self-gated "Send output → " quick row (the watcher's manual override). Real-dock V3 for both in HARDWARE-QUEUE. ✓ **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); V3 decides if the menu row needs a mirror-based rework. 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. ✓ **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"` restarts 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), and `restart` (not `try-restart`) re-applies even if the boot run was inactive. 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 restarts the oneshot — confirmed by a changed `InvocationID`). 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 a bare tray app (in the app-suite menu, not a service) - ✓ **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 (worst single output: nomarchy-live at 2.69 GB — needs the container's default swap allowance; everything else ≤ 1 GB). 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 - ✓ **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.