Raise priority.session for HDMI/DisplayPort (1100) and USB (1050) ALSA outputs so the default sink follows dock plug/unplug; built-in analog wins again when externals disappear. Rules live in modules/nixos/dock-audio-rules.nix; checks.dock-audio covers the contract. Verified: V2 — flake check --no-build; checks.dock-audio green. V3 pending: HARDWARE-QUEUE #87 real HDMI/USB-dock hotplug.
74 KiB
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.mdWhat agents execute next agent/BACKLOG.mdDocs map docs/README.mdAgent loop protocol agent/LOOP.mdBacklog 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
.rasitheme 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.rasiwhole-swap) and thenomarchy-menudispatcher: 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 isrofi -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 vianpx @anthropic-ai/claude-code@latestrather than the nixpkgs package, which lags model releases —nodejsis bundled for npx; REPL stays open) - ✓ keybindings cheatsheet:
modules/home/keybinds.nixis the single source for both the Hyprland binds and the SUPER+? rofi list, so they can't drift;nomarchy-menu keybindsrenders the padded two-column sheet (generated/mouse binds carried in itsextrarows) - ✓ 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-iconson, 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) andSystem(Network, Bluetooth, DND, and the self-gated Snapshots / Power-profile) are submenus the dispatcher routes vianomarchy-menu tools|system, each ending in a← Backentry that re-opens the root. The directSUPER+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 directSUPER+CTRL+<mnemonic>bind and self-gating as applicable. - ✓ Back everywhere: every list menu now ends with a
↩ Backentry 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. Abackhelper + a sharedBACKlabel keep it uniform; matched exactly so it can't collide with clipboard/filename content. Esc still quits instantly.
- ✓ shipped:
-
✓ Menu modules from rofi plugins: the old
calcflow committed the expression blind (result only in the next menu's-mesg) andqalc -tmisparsed 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 oldnmtui-in-terminalnetworkflow. Configured (xdgnetworkmanager-dmenu/config.ini) to drive rofi withrofi_highlightfor 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/
passsetup here today (secrets are gpg/ssh + gnome-keyring, seekeys.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.
- ✓ Network (
-
✓ Printer setup menu module: a
nomarchy-menu printersentry 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 thesystem-config-printerbinary so the entry appears only whennomarchy.services.printingis on. No directSUPER+CTRLbind — printer setup is a rare one-off, not a frequent utility (left out deliberately; easy to add). Chose the GUI over a rofi-nativelpadminflow (driver/PPD picking is impractical in rofi) and the CUPS web UI (an unthemed browser page). Validated: the generated menu script'sbash -nbuild check passes, and an eval confirms printing-on puts system-config-printer insystemPackages. 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_dmenuonly connects/disconnects existing VPNs) to a guided setup + management surface across the three common kinds:- WireGuard: import a
.confinto 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 …); thenetworkmanager-openvpnplugin 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 thetailscaleCLI directly, self-gated on the CLI being present (=nomarchy.services.tailscale, which makes the login user the operator viaextraSetFlags). 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: anomarchy-menu vpnrofi submenu under System — NM VPN/WireGuard connections shown ● active / ○ inactive and toggled on select (networkmanager-group users need no sudo), Import via the Files/fdpicker (*.conf/*.ovpn), the Tailscale block when present — ending in↩ Back. A self-gating Waybarcustom/vpnshield (nomarchy-vpn-status: shown only while a NM tunnel or Tailscale is up;@goodtone; 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 tonm-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).
- WireGuard: import a
-
Theme parity with legacy: summer-day/night now carry their legacy bar layouts as
waybar.jsoncwhole-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.rasiwhole-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-strlayers onto; all four parse clean underrofi -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 themeis 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 inrofi.nix(builtins.readDir+fromJSON); "active" is justt.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, + verticalelementcards, name centred below). Sizing gotcha (learned the hard way): rofi'selement-iconsizeis a single value → a square cell (a two-valueWxHis 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 asthemes/<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 nopreview.png: a plain-name row, so it degrades cleanly. (A headless VM-render route —runNixOSTest+ software-GL Hyprland (LIBGL_ALWAYS_SOFTWAREon 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 andbackgrounds/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 Pythonswwwpath uses them). Buttheme-state.jsonis git-tracked, so everyapplyrewrites it → the flake tree changes → Nix re-copies the whole 86 MB source beforehome-manager switchcan 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 — aNomarchy-wallpapersrepo or release tarball, pulled once via a flake input /fetchurl(pinned by hash → content-addressed, never re-copied on a state write), withnomarchy-theme-syncreading wallpapers from that stable store path (theNOMARCHY_DEFAULT_THEMESenv 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: movingtheme-state.jsonout to~/.config+--impureeval (one repo, no second artifact, but trades away the in-tree-pinned-state reproducibility). Follow-on ifhome-manager switchitself 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-installis single-disk UEFI — seepkgs/nomarchy-install) -
launch-or-focus UX scripts (swayosd volume/brightness OSD ships since v1:
nomarchy.osd.*, media keys driveswayosd-client, themed from the JSON) -
Distro branding, round 2:
distroName = "Nomarchy"ships (os-releasePRETTY_NAME, systemd-boot entries, ISO menu label). ✓ tuigreet greeting (Welcome to <distroName>) and a brandedusers.motd(doubling as a helper cheat sheet), both keyed offdistroName. ✓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.nixnow builds anomarchyGrubThemedir 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 fromnixos-grub2-themeonly to reuse its bundled DejaVu.pf2(grubloadfonts every.pf2in the dir); the stock NixOSlogo.pngis 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, lsbDISTRIB_ID/CPE_NAMEfollow. Verified safe:switch-to-configurationbuilds its "is this NixOS?" guard from the configured distroId (and/etc/NIXOSremains as the fallback), so rebuilds keep working — a newchecks.distro-idVM-test boots such a system and runsswitch-to-configuration dry-activategreen; nixos-* CLI tools are package names, untouched. The one side effect (isNixos→false blanks the upstream nixos.org URLs) is handled byextraOSReleaseArgsrestoringHOME_URL/DOCUMENTATION_URL/SUPPORT_URL/BUG_REPORT_URLto 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 viafonts.packages, and the summer-day/night menu buttons now use itsU+F000glyph withfont-family: Nomarchypinned 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), pluspathandreload. Scope decision: short where it helps but never shadow a real binary (same rule as rg/fd — the git block deliberately avoidsgs/ghostscript); lifecycle isnomarchy-pull/nomarchy-rebuild/nomarchy-home(full names on PATH, not aliases). The set is documented in README §5 (Day-to-day), andaliaslists 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 onnomarchy.theme.iconTheme(the JSON's optionaliconsfield, else Papirus-Dark/Light bymode) and feeds both Stylix'sgtk.iconTheme(Thunar/GTK apps) and rofi'sshow-icons. ✓ Opt-in cross-pack overrides (no default bloat):theme.nixnow resolvesnomarchy.theme.iconThemePackagefrom a small pack registry (iconPacks: Papirus·Tela·Qogir·Reversal·Numix). Default themes namePapirus-*, so the resolved package is the plainpapirus-icon-themederivation — byte-identical closure, zero added MB. Only when a theme'siconsnames a set from another pack is that pack (and only it) symlinkJoined alongside papirus. Adding a pack = oneiconPacksrow. 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}drivesnomarchy-theme-sync auto, which picks day vs night by the local clock and applies the preset only if it differs (idempotent).modules/home/autotheme.nixinstalls a user timer (OnStartupSec+OnCalendar=*:0/15+Persistent, gated on the state flag) that ticksauto; Look & Feel › Auto theme toggles it and picks the pair/times.cmd_apply's core was factored intoapply_namedsoautoreuses it verbatim. Periodic (not exact sunrise/sunset timers) for robustness to suspend/DST; the idempotentautokeeps ticks cheap. Enable rebuilds once (installs the timer +auto --force); disable is instant (the flag flips andautoself-gates). V2:checks.auto-thememoves the VM clock across a configured sunset/sunrise and asserts the theme flips day→night→day with the right rebuild count (idempotent between) — thehome-manager switchitself is stubbed viaNOMARCHY_REBUILD(that generic apply path is exercised by every manual theme change). The only on-hardware residual is the live user timer firing on itsOnCalendarschedule (the schedule issystemd-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, pluslt/tree.nomarchy.shell.enable. (Deliberately did NOT alias grep→rg / find→fd: their flags differ enough to surprise;rg/fdship as themselves.) -
✓ Default application suite: a starter complete-workstation set —
libreoffice-fresh,vscode,gimp,inkscape— ships active in the downstreamtemplates/downstream/home.nixhome.packages, with the heavier opt-ins (texliveFull— multi-GB;texliveMediumis lighter — plus browser/email) commented just below — Chromium is active by default (2026-07-10); other browsers stay commented. Decision: nonomarchy.apps.*option surface. Unlikenomarchy.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# firefoxconvention), 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. Unfreevscodeevaluates fine —mkFlakeand the system both setallowUnfree = true. -
✓ Plymouth logo contrast: the shipped art was a fixed navy that vanished on dark bases.
modules/nixos/plymouth.nixnow 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.nixenableszramSwap(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@swapbtrfs subvolume inside the encrypted volume, withpatch-template.pywiringswapDevices+boot.resumeDevice+resume_offset(frombtrfs 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=0stays 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 indocs/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), andchecks.hibernate-swapfile(the installer's btrfs@swapswapfile build — NOCOWmkswapfile, file-type swapon, a validmap-swapfile -roffset the kernel accepts, zram above it). A full hibernate from a runtime swapfile isn't headless-reachable (systemdCanHibernate=nofor undeclared swap; and a runtime-computedresume_offsetcan't be fed into the boot cmdline), so the literal encrypted-swapfile power-cycle + LUKS-initrd unlock stays a V3 laptop check. ✓ Doctor hibernate/sleep section (#77,VISION § C):nomarchy-doctorgained a read-only hibernate row group — resume device on the cmdline (+resume_offsetfor a swapfile), disk swap ≥ RAM, zram active, and any suspend/hibernate error in the previous boot's kernel log. Self-gates (skiponswap=0); onlyok/warn/skip, never fails the sheet. Thechecks.doctorVM test asserts thehibernaterow renders. (Gotcha recorded: the script runsset -euo pipefail, so a no-matchgrepinside$(…)or acond && actionaborts it — use|| trueandif.) ✓ Doctor battery health (#80,VISION § C): report-onlycycle_count+ retained capacity (charge_full/charge_full_designorenergy_*) as% of design, system batteries only (not Device-scope peripherals). Self-gates when attrs missing; soft warn below 70% of design.NOMARCHY_POWER_SUPPLY_ROOTfor thechecks.doctorfixture (test_powerhas no cycle/design attrs). ✓ First-boot welcome toast (#81,VISION § B): oneshot user unitnomarchy-first-bootfires a dismissible “You're set” notify (SUPER+M / SUPER+T / SUPER+? + Network + Doctor pointer) once, then writessettings.firstBootShowninto the flake checkout (--no-switch). Skipshostname == nomarchy-live(live ISO keeps its own welcome). Toggle:nomarchy.firstBootWelcome.enable.checks.first-bootgates toast / marker / re-run silence / live skip. ✓ Generation readability (#82,VISION § B):nomarchy-what-changedwraps nvd into plain-language counts (added/removed/updated); post-switch toast fromnomarchy-rebuild/nomarchy-home; System › What changed? (toast + floating full report).checks.what-changedfixtures nvd viaNOMARCHY_NVD. ✓ Doctor first-boot pre-activate (#83,VISION § A): if/var/log/nomarchy-hm-preactivate.logexists and no Home Manager generation is present, doctor ✖ withhome-manager switch --flake ~/.nomarchy -b bak. Skips when log absent; ok once a generation exists.checks.doctorcovers 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): explicitservices.logind.settings.Login.HandleLidSwitchDocked = "ignore"when power management is on (systemd docked = external display present). Undocked lid suspend left to upstream.checks.clamshell-logindasserts logind.conf + active logind. Real lid+external V3 in HARDWARE-QUEUE. ✓ Dock audio sink priority (#87,VISION § C): WirePlumber90-nomarchy-dock-audioraisespriority.sessionfor HDMI/DP and USB ALSA sinks so they become default when present; unplug falls back to built-in. Rules inmodules/nixos/dock-audio-rules.nix;checks.dock-audiopure + conf-file VM. Real hotplug V3 in HARDWARE-QUEUE. -
✓ 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-sleepsystemd unit (modules/nixos/default.nix) takes over from hypridle'sbefore_sleep_cmdand locks on the RAM-resume sleeps (suspend / hybrid-sleep / suspend-then- hibernate) always, but skipshibernate.targetwhen 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-agentwithenableSshSupportfronts SSH, so a singlepinentry-qt(native-Wayland, Stylix-themed Qt) handles GPG and SSH passphrases alike.SSH_AUTH_SOCKcomes 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-levelSSH_AUTH_SOCK: besides the zsh integration, ahome.sessionVariablesexport now covers GUI clients launched outside a shell (rofi launcher, autostarted apps) that never inherit the interactive shell's copy — resolved withgpgconf --list-dirs agent-ssh-socketat 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 wayNIXOS_OZONE_WLdoes. Verified by building the home generation and inspecting the renderedhm-session-vars.sh— it carriesexport 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.enableintosystem.nix(distro-wide defaults since the LUKS-keymap fix), nor the offline-pin test fixture inflake.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:
mainis the development default;v1is the release pointer downstreams pin (?ref=v1), advanced to main once a batch is tested — rolling, no tags. Broughtmaincurrent (it was a stale legacy-merge), and deleted the deadrefactor/wave/nixos-26.05branches (keptlegacyfor history). See the comment inflake.nix. - ✓ re-checked that every file earns its place under the
modules/hosts/themes/pkgs/toolsrule of thumb: no orphaned modules (keybinds.nixis a data moduleimport-ed by hyprland/rofi, so correctly absent fromdefault.nix), no dead-code markers, no editor/backup cruft, eachthemes/<slug>.jsonpalette pairs with its optional<slug>/overrides dir, andhosts/{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);.nixfiles use deliberate aligned hand-formatting, sonixfmt-rfc-style --checkflags ~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.
- ✓ pruned now-redundant config: the installer no longer writes
-
✓ 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 "onlyservices.upowerfor Waybar reporting" baseline:- ✓ profiles:
power-profiles-daemonships by default (the upstream- aligned choice — power-saver/balanced/performance viapowerprofilesctl, switched through polkit so no root prompt). Apower-profilemenu module (in the SUPER+M picker) and the first Waybarcustom/*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:
thermaldbehindpower.thermal.enable; the installer turns it on for aGenuineIntelCPU. - ✓ longevity:
power.batteryChargeLimit(e.g. 80) — a backend- independent sysfs oneshot writescharge_control_end_threshold. Off by default; the installer scaffolds it commented-out on laptops. ✓ the threshold is now re-applied on AC state changes (aservices.udevrule onSUBSYSTEM=="power_supply", ATTR{type}=="Mains"restarts the oneshot viasystemctl --no-block), closing the firmware-resets-on- unplug gap — the match is by adapter type, not kernel name (AC/AC0/ADP1/ACAD vary), andrestart(nottry-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: thetest_powermodule fakes a Mains adapter, togglingac_onlineemits a realpower_supplyuevent, and the udev rule restarts the oneshot — confirmed by a changedInvocationID). The sysfs write itself needs a realcharge_control_end_threshold, so a final on-hardware check (asudo nixos-rebuild+ a physical unplug) remains — the dev box has both anACMains adapter and aBAT0charge_control_end_threshold, so it can exercise it. - ✓ installer: writes
power.laptop = true(battery probe) andpower.thermal.enable(Intel) into the generatedsystem.nix. - ✓ idle cohesion:
modules/home/idle.nixnow suspends only on battery (and sooner — 15 min vs the old fixed 30), gating the hypridle suspend listener on anomarchy-on-acprobe 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/powerprofileindicator now shows in the summer-day/night whole-swap themes too.powerProfileStatus/Cycleare namedwriteShellScriptBins on PATH (waybar.nixhome.packages), so the generated config and the staticwaybar.jsoncboth 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.
- ✓ profiles:
-
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'shardware-dbso an installed machine bakes the right defaults, all overridable throughnomarchy.hardware.*.- ✓ Mechanism + broad seed shipped (
modules/nixos/hardware.nix): a vendor-keyednomarchy.hardware.*surface (intel,amd,fingerprint,npu),hardware-db.shextended 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 intosystem.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 fromcommon-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=3in kernel-params,fprintd.servicepresent). 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 tocommon-pc-ssd.
- fingerprint + NPU all detected) and a toggles-on build (
- ✓ Hardening (driver-gen + kernel awareness):
intel.gucemits the i915 param, so the installer turns it off when the GPU is on the newerxedriver (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 anomarchy.hardware.latestKernelescape hatch (+ a build-time warning whennpu.enablepredates 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
amdxdnakernel driver (mainline 6.14+) + the XRT/xdna userspace runtime + firmware. Niche/experimental → opt-in. - AMD P-State EPP (Zen 4 power):
amd_pstate=activefor 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 inhardware.graphics.extraPackages); Intel usesintel-media-driver. Broadly beneficial → default on, vendor-detected. - LVFS firmware & biometrics: fwupd already ships (✓,
services.fwupd). Add fingerprint:services.fprintdwhen 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.
- ✓ Mechanism + broad seed shipped (
-
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 —video0Color (MJPG, up to 2592×1944 / 1080p) +video2IR (8-bitGREYonly, 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/video0measures 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 speculatedv4l2-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/video0Color, node 146 =/dev/video2IR) 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-resYUYV640×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/video0color, 0 libcamera):- Hide the IR node —
monitor.v4l2.rulesmatching 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 aGREY-only / no-color-format node, since other vendors' IR cards are named differently.) - 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-YUYVpath). 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.disabledonly hides the PipeWire node; the kernel/dev/video2stays 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.
- Hide the IR node —
- Shipped (2026-06-27):
nomarchy.hardware.camera.hideIrSensor(+ an overridableirMatchregex) inmodules/nixos/hardware.nixemits rule 1 viaservices.pipewire.wireplumber.extraConfig; the installer'shardware-db.shauto-detects a paired RGB+IR webcam (from/sys/class/video4linux/*/name) and bakes the toggle intosystem.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 shippedirMatchwas 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); optionalv4l-utils+cameractrlsalready 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.
- The camera is a USB UVC module (Chicony
-
✓ 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--prefertuning (largest- RSS selection already finds the hog); kills raise a desktop notification (systembus-notify). AllmkDefault— opt out withservices.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>.enabletoggle a downstream flips on in one line. Keeps the base lean while making common additions trivial. ✓ Seventeen shipped inmodules/nixos/services.nix(system-side), each with a commented example in the downstreamsystem.nixtemplate:tailscale,syncthing(runs as the login user, GUI on 127.0.0.1:8384),podman(rootless,dockeraliased, 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 thelibvirtdgroup),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 + thewiresharkgroup),ollama(local LLM API on 127.0.0.1:11434),printing(CUPS + Avahi mDNS),openrgb(RGB lighting daemon), andrestic(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.conftunnels natively; thewireguard-toolsCLI 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 — Androidadbandwiresharktoggles - ✓ 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 parallelnomarchy.apps.*surface — it's a curatedhome.packageslist 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). - ✓ cloud/sync: Syncthing (
-
✓ Display / monitor management: declarative
nomarchy.monitors(a per-output schema — name/resolution/position/scale/transform/mirror/ bitdepth/vrr/disable) generates Hyprlandmonitorrules, keeping the,preferred,auto,1wildcard 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-displaysships behindnomarchy.displays.enableas 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 viahyprctl keyword monitor(current position + scale kept) and persisted to the in-flake state (settings.monitors.<name>, no rebuild), overlaid ontonomarchy.monitorsby 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 viahyprsunset(Hyprland-native), warm at night, identity (no shift) by day.modules/home/nightlight.nixdrives the HMservices.hyprsunsetwith 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 newnomarchy.settingsoption) —settings.nightlight.installed(sticky: gates the hyprsunset unit;nomarchy.nightlight.enablemkDefault-reads it) andsettings.nightlight.on(runtime on/off). Off by default. The first enable from the menu writesinstalledand 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 asystemctlstart/stop. Splitting the sticky flag from the on/off is what avoids a decay bug: an instant-off writes onlyon, so an unrelated later rebuild (e.g.sys-update) never drops the unit and "on" stays instant. Persistence across logout/reboot comes from anExecConditionon 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/statemarker, no markerConditionPathExists. Thenomarchy-menuSystem-submenu entry is always shown (current on/off) so the feature can be enabled from the menu; the self-gating Waybarcustom/nightlightindicator 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 BOTHnomarchy.nightlight.latitude/.longitudeand wlsunset replaces hyprsunset — sunrise/sunset computed from the location daily, the fixed times ignored; same toggle script, moon indicator, and live-stateExecConditiongate (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 bychecks.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-timezoneddrive/etc/localtimefrom 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 issettings.autoTimezonein 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 asudo nixos-rebuild(bakes the service + thetime.timeZoneoverride) plus ahome-manager switch(the Waybar-refresh watcher), both off the one flag. time.timeZone handling: a runtime zone needs/etc/localtimewritable; automatic-timezoned setstime.timeZone = nullitself, 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, somodules/home/timezone.nixruns a tiny user service that watches timedate1's change signal and reloads Waybar (SIGUSR2) on a real zone change (also catches a manualtimedatectl 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 Hyprlanddeviceblocks that override the sessionnomarchy.keyboard.layoutfor 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 Waybarhyprland/languageindicator 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 anomarchy-keyboard- watchdaemon runs (exec-once): it pollshyprctl devices, and when a keyboard connects after login that isn't inkeyboard.devicesand hasn't been chosen before, it pops a rofi layout picker, applies the choice as a per-devicehyprctl keyword device[<name>]:kb_layout(the runtime twin of the declarativedevice{}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 declarativekeyboard.devices. Picker verified on hardware (2026-06-18); the first cut applied the choice withswitchxkblayout(a global layout-index flip) and merged the candidate pool intoinput.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-layoutsinto 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 intonomarchy.keyboard.deviceson the next rebuild (a generated Hyprlanddevice{}block,mkDefaultso 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 inkeybinds.nixas a separatemultiLayoutBindslist, so hyprland.nix and the cheatsheet consume one source and gate identically (verified both ways: absent on a single layout; bind + cheatsheet row render underus,devia extendModules).currenttargets the focused keyboard, so a per-device-overridden board (one layout) is a no-op, never a leak.hyprland/language( {short}) added to both summerwaybar.jsoncwhole-swaps +#languagein 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 anidle_inhibitorthe 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).
- ✓ Per-device declarative layout:
-
✓ 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/notificationviaswaync-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 onnomarchy.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/.snapshotssubvolume if missing (covers existing installs, no disko change). The backend is VM-verified (a full BTRFS+LUKS install: bothroot+homeconfigs, a/hometimeline snapshot taken,/home/.snapshotsa real subvolume, the oneshotResult=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-progs886571653"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--versionexits 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 inhyprland.nix; the menu prefersbtrfs-assistant-launcheragain withnomarchy-snapshotsas fallback;checks.snapshot-guiguards 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 launchesnomarchy-snapshots(system-side, gated onnomarchy.system.snapper) instead — a keyboard-driven snapper browser/restore with no btrfs-assistant dependency. snapper is root-only here (noALLOW_USERS) and rofi can't run as root under Wayland, so it runs in a terminal viasudo(one password prompt) and usesfzfto pick: browse/diff (read-only), restore files (undochange), or roll back (root config only) — each behind a typed-yesconfirmation, 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.
- ✓ Resolved — the "btrfs-assistant 2.2 segfault" was unprivileged-only
(re-diagnosed 2026-07-04): gdb puts the crash in
-
✓ Update awareness:
nomarchy.updates.enable(opt-in,modules/home/ updates.nix) — anomarchy-updateschecker on a systemd user timer (.interval, default daily) compares each direct branch-tracking flake input (nixpkgs, the Nomarchy input, home-manager, stylix … viagit ls-remotevs the locked rev; offline → skipped, no false alarm) and, when theflatpakCLI is present (.flatpak), counts Flatpak updates. A self-gating Waybarcustom/updatesindicator (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 drivesmainand a human still promotesmain → v1. A scheduled job (Forgejo Actions, or Renovate's Nix manager opening a reviewable lock-diff PR) runsnix flake update→nix flake check+ the existingrunNixOSTestsuite + buildssystem.build.toplevel, and on green lands onmain— which nobody tracks, so a bad bump costs a debug session, not a user outage;v1stays 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), andnix flake updatestays within the pinned release branches — no surprise major bump (a 26.11 jump is a deliberatev2, 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 fromv1— worth fast-laning lock-only/security bumps ahead of feature batches. The repo has no CI today (manualnix flake updateonly). 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 everyapply/setalsogit committheme-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-valuesetno-ops (diff against HEAD); a missing git identity falls back toNomarchy <nomarchy@localhost>;bg nextis deliberately excluded (runtime wallpaper churn — the path rides along with the next real commit). Verified: V1 (HM generation builds, generated menubash -ngreen) + a 7-assertion sandbox-repo round-trip incl. the apply path; on-machine check queued. Rider fix:getnow prints booleans JSON-style (true, not Python'sTrue) — which also un-sticks the System menu's "Auto timezone (on/off)" label, whose= truecomparison could never match before. -
"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-docswalks the option names out of the four option-declaring modules at eval time (their lazyconfighalves never evaluate, so dummy args suffice) and requires each a table row — or a backticked.leafmention 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.pytags 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.pykeeps 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 anomarchy-waybarsupervisor (waybar.nix) — ANY exit respawns it, with a crash-loop guard (5 fast exits → critical notification, stop); (2) theme-sync now prefers a cleanpkill -x waybarwhen 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_changestays for manualhome-updaterestyles. 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 ofsys-update— snapshot-first system rebuild against the current lock, for config-only changes;home-updatewas already lock-free. README §3/§5 + the motd list all three. - ✓ Yazi TOML parse error on startup: yazi 26.x made fetchers'
groupfield required, so[[plugin.prepend_fetchers]]failed to parse and yazi fell back to presets. Fixed by addinggroup = "git"to both git fetcher entries inmodules/home/yazi.nix(the git plugin'ssetup()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 incmd_duration, which was configured but never referenced informat. - ✓ xfce deprecation warnings: moved
xfce.exo→pkgs.xfce4-exoand the three Thunar plugins to their new top-level names inmodules/nixos/file-manager.nix; the eval warnings are gone. - ✓ Rofi function keybindings: direct
SUPER+CTRL+<mnemonic>binds jump straight to eachnomarchy-menumodule — V clipboard · C calc · W web · F files · N network · B bluetooth · S capture · A ask — added tomodules/home/keybinds.nix, so they also show in the SUPER+? cheatsheet. - ✓ Enable nix-ld by default:
programs.nix-ld.enableis 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→accentAltgradient 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-resolvedborderfield ({active, inactive}) to the theme schema (theme.nix), consumed solid inhyprland.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 modernpersistent-workspaces(hyphen), which Waybar honours → all 10 rendered as phantoms. Dropped the block fromthemes/summer-{day,night}/waybar.jsonc; the other themes use the generatedwaybar.nix, which never had it, so they already match. - ✓ GTK/Qt ignore the theme's light/dark mode: Stylix set
polaritybut not theorg.freedesktop.appearance color-schemethat GTK4/libadwaita and Qt6 read via the portal, so a light theme could still render apps dark.stylix.nixnow setsdconforg/gnome/desktop/interface color-scheme=prefer-light/prefer-darkfromt.mode(xdg-desktop-portal-gtk already ships, andprograms.dconfis on system-side), so libadwaita/Qt follow the palette. Confirmed in a live session (headless QEMU): the portal'sorg.freedesktop.appearancecolor-schemereads1on a dark theme and flips to2after 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 viasocat, removing the 3-second polling delay. - Added a fallback to the
baselayout when unplugging an external monitor to guarantee the laptop lid turns back on. - Added explicit
SUPER+ALT+arrowkeybindings for moving active workspaces across physical monitors.
- Rewrote the display watcher (
- ✓ Workstation Polish Pack (Security & Stability):
- Enabled
security.apparmor.enablesystem-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.
- Enabled
- ✓ 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'st.fonts.sizeandt.fonts.uirather than hardcoding sizes.