Files
Nomarchy/docs/ROADMAP.md
Bernardo Magri e05e3647e6
All checks were successful
Check / eval (push) Successful in 2m55s
docs(install): stitch the README → template first-run handoff — item 6 complete
Slice (c), the last of the docs review: the install/first-run story
read end to end against the tree. Facts held (mkFlake args match the
table, disko's @snapshots + hibernation swapfile, .#default/.#me
naming); the one seam — README §3 never pointed the manual path at the
template README's first-run steps — got a connective paragraph. Item 6
deleted from BACKLOG; ROADMAP § Full docs review flipped to ✓.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:45:34 +01:00

938 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roadmap & changelog
Forward-looking plans, plus a running log of shipped fixes (the
"Known issues & follow-ups" section). Split out of the README so that
stays a focused entry point — what Nomarchy is, how to install it, and
how to override it. Items marked ✓ are shipped.
> **The live, prioritized queue now lives in [`agent/BACKLOG.md`](../agent/BACKLOG.md)**
> (part of the autonomous-agent loop, `agent/LOOP.md`). This file remains
> the detailed design/decision record and the shipped log — backlog items
> reference it as "ROADMAP § <item>". When a backlog item ships, its
> lasting design notes get a ✓ entry here.
## 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+D is `rofi -show drun`.
- ✓ shipped modules: `network` (nmtui in `$TERMINAL`) · `bluetooth`
(blueman-manager) · `capture` (grim/slurp submenu: region/full →
clipboard/file, saved to `~/Pictures/Screenshots`) · `keybinds` (the
cheatsheet, see below) · `ask` (free-text → claude CLI in a terminal;
auths via OAuth, no API key; pulled fresh via `npx
@anthropic-ai/claude-code@latest` rather than the nixpkgs package, which
lags model releases — `nodejs` is bundled for npx; REPL stays open)
- ✓ keybindings cheatsheet: `modules/home/keybinds.nix` is the **single
source** for both the Hyprland binds and the SUPER+? rofi list, so they
can't drift; `nomarchy-menu keybinds` renders the padded two-column
sheet (generated/mouse binds carried in its `extra` rows)
- ✓ shipped binds: `SUPER+Space``rofi -show drun` (quick launch) ·
`SUPER+M``nomarchy-menu` (main menu) · `SUPER+?` → the cheatsheet;
`SUPER+D` stays `-show drun`
- 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 200``rem(15, 1 B)`, the natural-
language "of" tripping the CLI). Resolved by adopting purpose-built rofi
modi (`programs.rofi.plugins`), all themed through the same `.rasi`:
- **calc** → **rofi-calc**: live results as you type via libqalculate
directly (dodging the qalc-CLI "of" bug), Enter copies, menu persists
to chain calculations.
- **emoji** → **rofi-emoji** (new module, SUPER+CTRL+E): glyph picker,
copies via the plugin's Wayland clipboard adapter.
- **files** stays the hand-rolled `fd``rofi -dmenu` → xdg-open fuzzy
search: rofi-file-browser-extended was tried and dropped — its
navigate-a-tree model felt worse than flat fuzzy-find for a quick
launcher, and yazi (SUPER+E) already covers real browsing.
- **More menu modules from rofi tools:** the script-based counterparts
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
replacement of an existing flow.
-**Network** (`networkmanager_dmenu`): a native rofi wifi/VPN picker
replacing the old `nmtui`-in-terminal `network` flow. Configured (xdg
`networkmanager-dmenu/config.ini`) to drive rofi with `rofi_highlight`
for connected/available rows; editing a connection drops to nmtui in the
terminal. Inherits the generated theme like every other module.
-**Audio** (`rofi-pulse-select`): a PipeWire/pulse sink/source switcher
under System → Audio (Output/Input), self-gated on the pulse socket.
- Deferred: **rofi-rbw / rofi-pass** secrets module — no Bitwarden/`pass`
setup here today (secrets are gpg/ssh + gnome-keyring, see `keys.nix`),
so it'd mean adopting a new secret manager. Revisit if that changes.
- Also: menu search is now **case-insensitive fuzzy** (`matching = fuzzy`,
`sorting-method = fzf`) across every module + the launcher.
-**Printer setup menu module:** a `nomarchy-menu printers` entry in the
**System** submenu opens **system-config-printer** (the CUPS admin GUI —
discovery, drivers/PPDs, options, test page), mirroring the bluetooth/blueman
pattern: the printing service ships the package (`environment.systemPackages`)
and the menu execs it, self-gated on the `system-config-printer` binary so
the entry appears only when `nomarchy.services.printing` is on. No direct
`SUPER+CTRL` bind — printer setup is a rare one-off, not a frequent utility
(left out deliberately; easy to add). Chose the GUI over a rofi-native
`lpadmin` flow (driver/PPD picking is impractical in rofi) and the CUPS web
UI (an unthemed browser page). Validated: the generated menu script's
`bash -n` build check passes, and an eval confirms printing-on puts
system-config-printer in `systemPackages`. Pending an on-machine check.
-**VPN setup & management menu:** a dedicated **System → VPN** flow
(`nomarchy-vpn`, `modules/home/rofi.nix`) that goes past the Network module
(`networkmanager_dmenu` only connects/disconnects *existing* VPNs) to a guided
setup + management surface across the three common kinds:
- **WireGuard:** import a `.conf` into NetworkManager (`nmcli connection import
type wireguard file …` — NM handles wg tunnels natively, no plugin) and
toggle it up/down.
- **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`);
the `networkmanager-openvpn` plugin ships system-side
(`networking.networkmanager.plugins`, mkDefault) so the openvpn type is
available — import type is chosen by file extension.
- **Tailscale:** status (read-only) + `up`/`down` + **exit-node** selection. It
lives outside NetworkManager, so the menu drives the `tailscale` CLI
directly, **self-gated** on the CLI being present (= `nomarchy.services.tailscale`,
which makes the login user the **operator** via `extraSetFlags`). So
up/down/exit-node run **inline without sudo**, falling back to a sudo terminal
only if the operator grant is absent; the first interactive login uses a
terminal (the auth URL is visible).
Shape: a `nomarchy-menu vpn` rofi submenu under **System** — NM
VPN/WireGuard connections shown ● active / ○ inactive and toggled on select
(networkmanager-group users need no sudo), Import via the Files/`fd` picker
(`*.conf`/`*.ovpn`), the Tailscale block when present — ending in `↩ Back`. A
self-gating Waybar **`custom/vpn`** shield (`nomarchy-vpn-status`: shown only
while a NM tunnel or Tailscale is up; `@good` tone; click opens the submenu),
wired into the generated bar **and the summer whole-swaps**. Secrets stay in
the connection manager's own store (NetworkManager / Tailscale) — no new secret
manager (cf. the deferred rofi-rbw/pass note above). **Decided: import-first**
— a from-scratch WireGuard keypair/peer editor is too much rofi surface, so
creation is deferred to `nm-connection-editor`. Eval + build green; **pending
an on-machine check** (the nmcli import/up-down + Tailscale paths need a live
session with real configs). Remaining (optional): richer exit-node display
(country/city).
- **Theme parity with legacy:** summer-day/night now carry their legacy
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
logo button opens nomarchy-menu); the other four identity themes are
palette recolors and already match. Remaining: a visual pass over all
six on the live ISO
- ✓ **Per-theme rofi identity:** the `themes/<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 `loadfont`s every `.pf2` in the dir); the
stock NixOS `logo.png` is dropped since ours is in the background. Built +
structure-verified (theme.txt palette colours, 1920×1080 background, font
present). Remaining: a UEFI ISO-boot render check on hardware (the file
wiring is confirmed; the visual is not CI-testable, same as the splash).
✓ **`distroId = "nomarchy"`:** os-release is now honest — `ID=nomarchy`,
`ID_LIKE=nixos` (the standard derivative-distro lineage marker, cf.
Ubuntu→debian), `DEFAULT_HOSTNAME=nomarchy`, lsb `DISTRIB_ID`/`CPE_NAME`
follow. **Verified safe:** `switch-to-configuration` builds its "is this
NixOS?" guard from the *configured* distroId (and `/etc/NIXOS` remains as
the fallback), so rebuilds keep working — a new `checks.distro-id`
VM-test boots such a system and runs `switch-to-configuration dry-activate`
green; nixos-* CLI tools are package names, untouched. The one side effect
(isNixos→false blanks the upstream nixos.org URLs) is handled by
`extraOSReleaseArgs` restoring `HOME_URL`/`DOCUMENTATION_URL`/`SUPPORT_URL`/
`BUG_REPORT_URL` to the project. os-release output eval-verified from the
real downstream config.
- ✓ **fastfetch branding:** `modules/home/fastfetch.nix`
(`nomarchy.fastfetch.enable`) — the vendored vector logo, recolored to
the palette accent and rendered to compact block-art via chafa at build
time (tracks the theme), fronting a curated module list. Replaces the
oversized legacy ASCII with a themed, sized logo.
- ✓ **Nomarchy logo font in Waybar:** vendored `Nomarchy.ttf`
(`modules/nixos/branding/`), installed via `fonts.packages`, and the
summer-day/night menu buttons now use its `U+F000` glyph with
`font-family: Nomarchy` pinned in their CSS (Nerd Fonts also occupy
U+F000, so the pin is required). The other themes have no logo button.
- ✓ **Quality-of-life command aliases:** a curated set ships on by default in
`modules/home/shell.nix` — navigation (`..`/`...`/`....`), a git block
(`g`, `gst`, `ga`/`gaa`, `gc`/`gcm`, `gco`/`gsw`, `gb`, `gd`/`gds`,
`gl`/`glg`, `gp`/`gpl`, `gf`), and a nix block (`ns`, `nr`, `nfu`, `nfc`,
`nsearch`, `ngc`), plus `path` and `reload`. Scope decision: short where it
helps but **never shadow a real binary** (same rule as rg/fd — the git block
deliberately avoids `gs`/ghostscript); system/home rebuilds keep their full
`sys-update`/`home-update` names rather than getting cryptic aliases. The set
is documented in README §5 (Day-to-day), and `alias` lists them in-shell.
- **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`. Remaining (optional): per-theme
`icons` overrides for the presets, or shipping more icon packs.
- **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. **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).
- ✓ **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 `writeShellScriptBin`s on PATH (`waybar.nix` home.packages), so the
generated config and the static `waybar.jsonc` both exec them by bare
name (the same parity the DND bell has). General pattern: any new
generated-config module needs adding to the whole-swap themes too.
- **Full hardware enablement (beyond nixos-hardware):** nixos-hardware
profiles cover *basic* per-model functionality (quirks, drivers), but the
*advanced* features of a machine — keyed more on the CPU/GPU generation
than the model — are left for the user to wire up by hand. Close that gap
without making it finnicky: extend the installer's existing autodetection
(DMI → nixos-hardware profile today; add CPU vendor/arch, GPU, NPU,
fingerprint reader, SSD) to switch on the *safe, broadly-beneficial*
features automatically, and expose a small `nomarchy.hardware.*` surface
(per-vendor where it helps, e.g. `nomarchy.hardware.amd.*`) for the heavy
or experimental bits. Goal: full utilisation out of the box — opt-*out* for
the safe defaults, opt-*in* for the multi-GB / experimental extras.
Complements nixos-hardware (model quirks) rather than replacing it; keep
the detection in the installer's `hardware-db` so an installed machine
bakes the right defaults, all overridable through `nomarchy.hardware.*`.
- ✓ **Mechanism + broad seed shipped** (`modules/nixos/hardware.nix`): a
vendor-keyed `nomarchy.hardware.*` surface (`intel`, `amd`, `fingerprint`,
`npu`), `hardware-db.sh` extended to detect Intel/AMD, a fingerprint reader
(libfprint USB vendor IDs) and an NPU (Intel VPU / AMD XDNA PCI IDs), and
the installer bakes the matching toggles into `system.nix` — safe defaults
active (GuC/HuC, amd-pstate, AMD VA-API env, fprintd), heavy/experimental
opt-ins commented (Intel compute-runtime, ROCm, fingerprint PAM, NPU
driver). Audited against the nixos-hardware commons so it only fills the
gap above them — microcode / the media-driver VA-API stack / weekly fstrim
already come from `common-cpu-*` / `common-gpu-*` / `common-pc-ssd`.
Exercised live on the dev machine's detection (an AMD Ryzen-AI laptop: AMD
+ fingerprint + NPU all detected) and a toggles-on build (`amd_pstate=active`
+ `i915.enable_guc=3` in kernel-params, `fprintd.service` present).
**Remaining: on-hardware verification of the runtime bits (AMD/NPU/Intel
GPU-compute) — none are testable in CI or on the Intel Latitudes for the
AMD paths.** SSD TRIM is intentionally left to `common-pc-ssd`.
- ✓ **Hardening (driver-gen + kernel awareness):** `intel.guc` emits the
*i915* param, so the installer turns it off when the GPU is on the newer
`xe` driver (Lunar Lake / Battlemage / Panther Lake — GuC is default-on
there); the NPU detector matches the PCI *accelerator class* (not just a
device-ID list) so new gens are caught without a code change; and a
`nomarchy.hardware.latestKernel` escape hatch (+ a build-time warning when
`npu.enable` predates the shipped kernel) covers very-new hardware whose
drivers only just landed. A config-assertion VM test guards the wiring
(`checks.hardware-toggles`: kernel cmdline + fprintd + PAM, booted in a VM).
Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M):
- **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't
officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB
closure → opt-in (`nomarchy.hardware.amd.rocm.enable`); unlocks GPU
PyTorch / Ollama.
- **Ryzen AI NPU (XDNA):** the `amdxdna` kernel driver (mainline 6.14+) +
the XRT/xdna userspace runtime + firmware. Niche/experimental → opt-in.
- **AMD P-State EPP (Zen 4 power):** `amd_pstate=active` for the EPP
governor, which power-profiles-daemon (already shipped, `power.nix`)
drives per profile. Broadly beneficial on Zen 2+ → default on when the
installer sees an AMD CPU.
- **Hardware video acceleration (VA-API):** mesa's radeonsi exposes VA-API
on AMD (`LIBVA_DRIVER_NAME=radeonsi`, libva + drivers in
`hardware.graphics.extraPackages`); Intel uses `intel-media-driver`.
Broadly beneficial → default on, vendor-detected.
- **LVFS firmware & biometrics:** fwupd already ships (✓, `services.fwupd`).
Add fingerprint: `services.fprintd` when a reader is detected, with
opt-in PAM integration for login/sudo (password-only stays the default
for the cautious).
- **SSD periodic TRIM:** `services.fstrim.enable` (weekly) — safe and
beneficial, so default on (trivial enough it may warrant being
distro-wide regardless).
Sub-items here can graduate into their own roadmap entries as they're
scoped; the unifying work is the detection + `nomarchy.hardware.*` surface.
- **Webcam support & tuning:** improve out-of-the-box webcam behaviour.
Motivating case — on the ThinkPad T14s AMD Gen 4 the camera shows a dark,
low-quality image. **Diagnosed on hardware (2026-06-26)**, and it's *not* what
the first cut of this item guessed (UVC default-controls tuning vs a MIPI/
libcamera gap):
- The camera is a **USB UVC** module (Chicony `04f2:b7c0`, `uvcvideo`) and is
**dual-sensor** — `video0` Color (MJPG, up to 2592×1944 / 1080p) + `video2`
**IR** (8-bit `GREY` only, the face-unlock sensor). The AMD IPU `[1022:1502]`
is present on PCI but **unused** (the camera enumerates over USB, not the
MIPI/ISP path), so the libcamera-software-ISP branch is moot on this machine.
- The **raw color capture is fine**: at factory defaults with auto-exposure,
`/dev/video0` measures luma ≈130/255 *from the first frame* (no AE ramp, not
dark); forcing manual exposure made it *worse*. So there is **no v4l2 control
default to bake** — the speculated `v4l2-ctl`-tuning / udev-oneshot fix is the
wrong tree.
- **Real cause is the consumption path.** PipeWire/WirePlumber exposes the
device through **both** backends at once — two `[v4l2]` sources (node 144 =
`/dev/video0` Color, node 146 = `/dev/video2` **IR**) **plus** two
`[libcamera]` nodes (Color + IR) — and the IR sensor is presented as an
**indistinguishable** "Integrated Camera". So an app's camera picker shows up
to *four* identical entries, and choosing the IR one yields a black/dark
monochrome frame; the libcamera path can also negotiate the low-res `YUYV`
640×480 mode ("bad quality"). The default source is the color node, so apps
that don't let you choose are fine — the breakage is selecting (or an app
auto-selecting) the wrong node.
- **Fix — validated live on hardware (2026-06-26).** Two WirePlumber 0.5
drop-ins collapse the four entries to one clean color camera, confirmed via
`wpctl status` (before: 2 v4l2 + 2 libcamera incl. both IR nodes → after:
**1** v4l2 source = `/dev/video0` color, **0** libcamera):
1. **Hide the IR node** — `monitor.v4l2.rules` matching the IR sensor →
`node.disabled = true`. (Tested by card name `~.*Integrated I`; the
**shipping** match should key on the more robust, vendor-neutral heuristic
of a **`GREY`-only / no-color-format** node, since other vendors' IR cards
are named differently.)
2. **Drop the duplicate backend** — `wireplumber.profiles.main.monitor.libcamera
= disabled`, since a plain UVC cam is fully covered by v4l2 (also kills the
libcamera low-res-`YUYV` path).
**Drawbacks / design constraints for the module:**
- Rule 2 (libcamera off) is only safe **when a UVC camera exists** — on a
MIPI/IPU-only machine (no UVC fallback) it would kill the camera entirely,
so it **must be conditional on detection**, not blanket. Rule 1 (IR-hide)
is broadly safe. Exactly the `nomarchy.hardware.*`-gated targeting the
parent item calls for.
- **Face-unlock is *not* broken:** `node.disabled` only hides the PipeWire
node; the kernel `/dev/video2` stays openable, so Howdy (which reads the IR
device directly, bypassing PipeWire) still works.
A true MIPI/IPU software-ISP camera with no UVC fallback stays a separate
future item.
- **Shipped (2026-06-27):** `nomarchy.hardware.camera.hideIrSensor` (+ an
overridable `irMatch` regex) in `modules/nixos/hardware.nix` emits rule 1 via
`services.pipewire.wireplumber.extraConfig`; the installer's `hardware-db.sh`
auto-detects a paired RGB+IR webcam (from `/sys/class/video4linux/*/name`)
and bakes the toggle into `system.nix`, with a commented example in the
downstream template. **Decision: v4l2 IR-hide only — libcamera is left
untouched** so an external camera you plug in is never affected. Surgical
internal-only libcamera scoping proved impossible: the distinguishing device
props (`api.libcamera.location`, `device.product.name`) bind *after* the
monitor rule runs, and the only early-matchable prop (`device.api`) is
all-or-nothing — so a broad libcamera-off was the only option and was rejected
as the blunt instrument it is (external USB cams are UVC and keep working via
v4l2 regardless). Verified: installer detection fires on the T14s; the
generated drop-in's serialized content is valid WP-0.5 config; and the exact
shipped `irMatch` was re-confirmed live (1 V4L2 source = the colour
`/dev/video0`, libcamera untouched, IR node disabled in the WirePlumber log).
Remaining: an on-Nomarchy end-to-end check; optional `v4l-utils` +
`cameractrls` for the rare genuine-tuning case; and a follow-up for
portal/Flatpak apps that consume the libcamera path (where the internal IR is
still listed, since libcamera stays on).
- ✓ **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, `sunset` → `temperature`), so hyprsunset handles the
schedule and the on-login state. Active-profile-at-session-start was
confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle (opt-in,
instant, in-flake state):** two git-tracked keys in the state file, both
menu-written (exposed via the new `nomarchy.settings` option) —
`settings.nightlight.installed` (**sticky**: gates the hyprsunset unit;
`nomarchy.nightlight.enable` `mkDefault`-reads it) and
`settings.nightlight.on` (runtime on/off). **Off by default.** The *first*
enable from the menu writes `installed` and rebuilds to create the unit (the
one accepted rebuild); **every toggle after is instant** —
`nomarchy-theme-sync set settings.nightlight.on … --no-switch` (atomic +
`git add -N`, no rebuild) plus a `systemctl` start/stop. Splitting the sticky
flag from the on/off is what avoids a **decay** bug: an instant-off writes
only `on`, so an unrelated later rebuild (e.g. `sys-update`) never drops the
unit and "on" stays instant. Persistence across logout/reboot comes from an
`ExecCondition` on the unit (`nomarchy-nightlight should-start`) that reads the
**live** working-tree on/off at start time — *not* the eval-frozen store copy
— so an off survives a reboot with no rebuild; a later rebuild bakes the same
value. No more `~/.local/state` marker, no marker `ConditionPathExists`. The
`nomarchy-menu` System-submenu entry is always shown (current on/off) so the
feature can be enabled from the menu; the self-gating Waybar
`custom/nightlight` indicator shows the moon while running and hides otherwise.
This is the first cut of a broader principle — **any user-settable config
gets a menu writer that lands it in the downstream flake; no state lives
outside the checkout** (Phase 1 night-light + Phase 2 the per-device
keyboard-layout memory below are done — both now in the git-tracked state
file; next target: opt-in auto-commit of the flake on each mutation, owned
files only). Remaining (optional): geo (lat/long) auto
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
on-machine check (first enable rebuilds + comes on; later toggles instant; off
persists across reboot via ExecCondition; stopping hyprsunset restores gamma).
- ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone`
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
`/etc/localtime` from your location, so travelling to another zone updates the
Waybar clock on its own. **Menu-driven, in-flake state** (the night-light /
keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json,
git-tracked; the System-menu entry (`nomarchy-menu autotimezone` →
`nomarchy-autotimezone`) writes it and rebuilds. **Not instant like
night-light** — it's a *system* service (not a user unit you can start/stop),
so the toggle drives a `sudo nixos-rebuild` (bakes the service + the
`time.timeZone` override) plus a `home-manager switch` (the Waybar-refresh
watcher), both off the one flag. **time.timeZone handling:** a runtime zone
needs `/etc/localtime` writable; automatic-timezoned sets `time.timeZone = null`
itself, but the installer's static value would collide (a hard eval error), so
the module forces it null (`mkForce`) over the installer's value when enabled,
and reverts to it when disabled. **Live clock refresh:** Waybar's clock module
captures the zone at construction, so `modules/home/timezone.nix` runs a tiny
user service that watches timedate1's change signal and reloads Waybar
(SIGUSR2) on a real zone change (also catches a manual `timedatectl
set-timezone`). Eval-verified both ways (on: geoclue+daemon on, tz forced null
over a static installer value, watcher present; off: static tz intact, no
service/watcher). **Pending an on-hardware check** — geoclue detection and the
SIGUSR2 clock refresh aren't testable in CI (if SIGUSR2 proves insufficient,
fall back to restarting waybar). Remaining (optional): a Waybar tooltip line
showing the detected zone.
- **Keyboard layouts (per-device + switching):**
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
(`{ "<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+K` → `hyprctl switchxkblayout current next`, rendered
only when the session layout has a comma (same gate as the Waybar
language indicator) — data lives in `keybinds.nix` as a separate
`multiLayoutBinds` list, so hyprland.nix and the cheatsheet consume
one source and gate identically (verified both ways: absent on a
single layout; bind + cheatsheet row render under `us,de` via
extendModules). `current` targets the focused keyboard, so a
per-device-overridden board (one layout) is a no-op, never a leak.
`hyprland/language` (`󰌌 {short}`) added to both summer
`waybar.jsonc` whole-swaps + `#language` in their CSS — static JSON
can't eval-gate, so on summer themes the module shows even with one
layout (a deliberate parity-over-minimalism call; it's small).
Noticed en route: summer-night carries an `idle_inhibitor` the
generated bar lacks — folded into the idle-inhibit backlog item.
- Remaining: keep the system (console/initrd) and session layouts in
sync (the LUKS-keymap work).
- ✓ **Do-Not-Disturb:** swaync DND toggle wired into the menu
(`nomarchy-menu dnd`, in the picker, SUPER+CTRL+D) and a Waybar bell
indicator (`custom/notification` via `swaync-client -swb`: shows the
notification count + DND state, left-click toggles the panel,
right-click toggles DND). Turning DND off confirms with a toast; turning
it on is silent (notifications are suppressed — the bell-off glyph is the
cue).
- ✓ **Snapshot browse/restore UX:** `nomarchy-menu snapshot` (in the SUPER+M
picker, self-gated on snapper) launches **btrfs-assistant** — the
desktop browse/diff/restore/rollback UI, elevating via polkit (the safe
choice for a destructive, root-only op vs a fat-fingerable rofi rollback).
Shipped system-side gated on `nomarchy.system.snapper`. Also extended
snapper to snapshot **/home** by default (hourly 5 / daily 7 / weekly 4)
when it's its own BTRFS subvolume; a boot oneshot creates the required
`/home/.snapshots` subvolume if missing (covers existing installs, no disko
change). **The backend is VM-verified** (a full BTRFS+LUKS install: both
`root`+`home` configs, a `/home` timeline snapshot taken, `/home/.snapshots`
a real subvolume, the oneshot `Result=success`).
- ✓ **Resolved — the "btrfs-assistant 2.2 segfault" was unprivileged-only**
(re-diagnosed 2026-07-04): gdb puts the crash in `btrfs_util_subvolume_
iterator_next()` — libbtrfsutil's *unprivileged* subvolume-iteration path
in btrfs-progs 6.17.1 (upstream-fixed after 6.17.1, kdave/btrfs-progs
`886571653` "re-enable tree search v2 ioctl"; symbol versions were checked
and match, so not an ABI/link issue). **As root it runs fine** — VM-proven
(root `--version` exits 0, unprivileged exits 139) — and the pkexec
launcher runs it as root, so the GUI was never actually broken *when
launched right*. What WAS missing distro-wide: **no polkit authentication
agent**, so every pkexec prompt in the session failed silently — that's
the root cause of "the GUI doesn't open". Fixes: `hyprpolkitagent`
(Hyprland's Qt agent, Stylix-themed) now ships via exec-once in
`hyprland.nix`; the menu prefers `btrfs-assistant-launcher` again with
`nomarchy-snapshots` as fallback; `checks.snapshot-gui` guards the root
path on a real btrfs volume (and the GUI event loop offscreen) against
lock-bump regressions. No btrfs-progs patch: our flows are all root-side,
so the unprivileged fix rides in with a future lock bump. Original
diagnosis kept below for the record. ✓ **Fallback shipped:**
the menu now launches `nomarchy-snapshots` (system-side, gated on
`nomarchy.system.snapper`) instead — a keyboard-driven snapper
browser/restore with no btrfs-assistant dependency. snapper is root-only
here (no `ALLOW_USERS`) and rofi can't run as root under Wayland, so it
runs in a terminal via `sudo` (one password prompt) and uses `fzf` to pick:
browse/diff (read-only), restore files (`undochange`), or roll back (root
config only) — each behind a typed-`yes` confirmation, which is *safer*
than the fat-fingerable one-click rofi rollback this entry originally
avoided. btrfs-assistant stays installed for when nixpkgs fixes it.
**Pending an on-machine check of the restore/rollback paths.**
- Remaining (optional): boot-from-snapshot needs a systemd-boot equivalent
of grub-btrfs.
- ✓ **Update awareness:** `nomarchy.updates.enable` (opt-in, `modules/home/
updates.nix`) — a `nomarchy-updates` checker on a systemd user timer
(`.interval`, default daily) compares each **direct** branch-tracking flake
input (nixpkgs, the Nomarchy input, home-manager, stylix … via `git
ls-remote` vs the locked rev; offline → skipped, no false alarm) and, when
the `flatpak` CLI is present (`.flatpak`), counts Flatpak updates. A
self-gating Waybar `custom/updates` indicator (hidden until something's
available; accent `󰚰 N`, signal-refreshed) + a notification that fires only
when the count *grows* (so a daily timer never nags). Click → the upgrade
flow in a terminal (`sys-update` / `flatpak update`, each confirmed). Purely
passive — it never changes anything, augmenting the explicit rebuild flow.
Indicator is in the generated bar and both summer whole-swaps. **Pending an
on-machine check** (the ls-remote/notify/timer path needs a live session).
- **Automated upstream lock bumps (maintainer CI):** the upstream twin of the
user-side checker above — automate advancing Nomarchy's *own* `flake.lock`,
which is the entire delivery channel (a downstream takes a single input,
`?ref=v1`, so nixpkgs/home-manager/stylix/nixos-hardware reach it
*transitively pinned* through that lock; users can't bump them independently).
**Tiered by what CI can't do:** the hardware-QA gate (the Latitude 5410 + the
AMD dev box — AMD/NPU/Intel-GPU-compute runtime bits aren't testable in CI or
on the Intel Latitudes for the AMD paths) can't be automated, so a bot drives
`main` and a human still promotes `main → v1`. A scheduled job (Forgejo
Actions, or Renovate's Nix manager opening a reviewable lock-diff PR) runs
`nix flake update` → `nix flake check` + the existing `runNixOSTest` suite +
builds `system.build.toplevel`, and on green lands on `main` — which nobody
tracks, so a bad bump costs a debug session, not a user outage; `v1` stays
the manual on-hardware gate (it only ever fast-forwards, never force-pushed,
or a user's locked rev can vanish from history). **Cadence weekly**
(release-26.05 doesn't move fast; nightly is just churn), and `nix flake
update` stays *within* the pinned release branches — no surprise major bump
(a 26.11 jump is a deliberate `v2`, hand-edited in flake.nix). Side benefit:
shrinks the security-patch latency the single-input model otherwise imposes
(users are gated on the maintainer for nixpkgs CVEs), since a fix is usually
already eval/build/VM-tested and one hardware promotion away from `v1` —
worth fast-laning lock-only/security bumps ahead of feature batches. 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.
- **"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.
- ✓ **Waybar crash on theme switch left the session bar-less** (Latitude
hardware QA, 2026-07-04): exec-once has no supervisor, so any crash
orphaned the session until relogin — and the switch path itself was
crash-prone: `reload_style_on_change` (inotify on style.css) and
theme-sync's SIGUSR2 both fired while the HM symlinks flipped, a
double-reload race in waybar's in-place reload. Fix: (1) the bar runs
under a `nomarchy-waybar` supervisor (waybar.nix) — ANY exit respawns
it, with a crash-loop guard (5 fast exits → critical notification,
stop); (2) theme-sync now prefers a clean `pkill -x waybar` when it
sees the supervisor (a restart with fresh config+style — no reload
code path at all), keeping SIGUSR2 only as the fallback for
unsupervised/custom bars; `reload_style_on_change` stays for manual
`home-update` restyles. VM-verified on the headless software-GL
desktop: SIGKILL → new pid; clean kill → respawn; SIGUSR2 → alive.
- ✓ **`sys-rebuild`** (Latitude QA request): the no-update twin of
`sys-update` — snapshot-first system rebuild against the *current*
lock, for config-only changes; `home-update` was already lock-free.
README §3/§5 + the motd list all three.
- ✓ **Yazi TOML parse error on startup:** yazi 26.x made fetchers'
`group` field required, so `[[plugin.prepend_fetchers]]` failed to parse
and yazi fell back to presets. Fixed by adding `group = "git"` to both
git fetcher entries in `modules/home/yazi.nix` (the git plugin's
`setup()` only renders the linemode; the fetcher still needs registering).
- ✓ **Starship prompt styling:** dropped the powerline look (the `[]`
separator glyph + `bg:` fills) for a flat prompt — bold-accent directory,
warn git branch, subtext status/duration, `` character, all plain
colored text (`modules/home/shell.nix`). Also wired in `cmd_duration`,
which was configured but never referenced in `format`.
- ✓ **xfce deprecation warnings:** moved `xfce.exo` → `pkgs.xfce4-exo` and
the three Thunar plugins to their new top-level names in
`modules/nixos/file-manager.nix`; the eval warnings are gone.
- ✓ **Rofi function keybindings:** direct `SUPER+CTRL+<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.