Files
Nomarchy/docs/ROADMAP.md
Bernardo Magri 9e37e11915
Some checks failed
Check / eval (push) Failing after 3m11s
feat(lifecycle): auto-commit sweep of the machine flake before pull/rebuild/home
With settings.autoCommit on, only menu/theme mutations were committed
(theme-sync pathspec-limits to theme-state.json by design), so hand
edits to system.nix/home.nix and lock bumps stayed forever-dirty.

New internal nomarchy-autocommit (nomarchy-lifecycle): live-reads the
same flag, commits everything dirty as "nomarchy: auto-commit before
<pull|rebuild|home switch>" with the swept file list in the body,
fallback identity, never fatal (|| true at call sites). Called at the
top of all three lifecycle commands; before the ff-only pull it also
un-dirties the tree. Exposed via passthru.autocommit + package export;
guarded by checks.lifecycle-autocommit (sandbox repos: sweep, no-op on
clean/off/non-repo). Sync sweep: README, RECOVERY, ROADMAP, rofi
toggle, control-center prompt, theme-sync docstring, JOURNAL.

Verified: V1+ — flake check --no-build green; the new check builds;
end-to-end run of the built nomarchy-rebuild (stubbed sudo/rebuild)
against a copy of a real dirty ~/.nomarchy: 4 files → one commit,
clean tree. No VM boot — no session-visible surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 12:52:05 +01:00

76 KiB
Raw Blame History

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
What agents execute next agent/BACKLOG.md
Docs map docs/README.md
Agent loop protocol agent/LOOP.md

Backlog items may reference this file as ROADMAP § <item>. 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/<slug>/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+Spacerofi -show drun (app launcher) · SUPER+Mnomarchy-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+<mnemonic> 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+<mnemonic> 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 200rem(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:

    • calcrofi-calc: live results as you type via libqalculate directly (dodging the qalc-CLI "of" bug), Enter copies, menu persists to chain calculations.
    • emojirofi-emoji (new module, SUPER+CTRL+E): glyph picker, copies via the plugin's Wayland clipboard adapter.
    • files stays the hand-rolled fdrofi -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/<slug>/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/<slug>/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 <distroName>) 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 loadfonts 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. ✓ 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/<slug>.json palette pairs with its optional <slug>/ 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 writeShellScriptBins 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-sensorvideo0 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 nodemonitor.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 backendwireplumber.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. 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.<name>.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.<name>, 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, sunsettemperature), 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 instantnomarchy-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 autotimezonenomarchy-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 ({ "<hyprctl-device-name>" = { 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[<name>]: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+Khyprctl 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 updatenix 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. The repo has no CI today (manual nix flake update only). 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 <nomarchy@localhost>; 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 <pull|rebuild|home switch> 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.02.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.exopkgs.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+<mnemonic> 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.