A git-tracked agent/ directory so AI agents can iterate on the distro unattended (runner-agnostic: /loop, headless claude -p, or a fresh manual session — all state lives in the checkout, per the distro's own philosophy): - LOOP.md — the iteration protocol: orient → pick one BACKLOG task → verify up the V0–V3 ladder → commit+push main → record. Safety rails (v1 untouchable, no force-push, no surprise lock bumps) and stop-and-escalate conditions. - BACKLOG.md — the forward half of docs/ROADMAP.md reworked into a prioritized queue (5 NOW / 6 NEXT / LATER / PROPOSED / Decisions); ROADMAP.md stays the design/decision record + shipped log. - GOALS.md — the four pillars (stable > reproducible/zero-hidden-state > effortless config > beautiful), quality bars, non-goals. - CONVENTIONS.md — coding/design rules (in-flake state, menu placement, Waybar whole-swap parity, toggle-vs-package, no formatter). - MEMORY.md — curated hard-won lessons (VM recipes, btrfs-assistant segfault watch, rofi/WirePlumber/hyprlock gotchas). - HARDWARE-QUEUE.md — every pending V3 on-hardware check collected from the ROADMAP, with exact steps, split by machine. - JOURNAL.md — append-only iteration log, seeded with this bootstrap. Plus a root CLAUDE.md entry point and README/ROADMAP pointers. Verified: V0 — docs-only; nix flake check --no-build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
839 lines
59 KiB
Markdown
839 lines
59 KiB
Markdown
# 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:** a dedicated pass over all the prose,
|
||
not just the code-adjacent cleanup the repo-sanitize item covers. The
|
||
README has grown to ~540 lines with a ~200-line roadmap inline — the
|
||
biggest single move is likely **splitting the roadmap out** (e.g.
|
||
`ROADMAP.md` / `docs/`) so the README stays a focused "what it is / how
|
||
to install / how to override" entry point. Also: reconcile every option
|
||
table against the live `nomarchy.*` surface (snapper and others are
|
||
missing); a pass over `docs/OVERRIDES.md` + `docs/TESTING.md` and the
|
||
`templates/downstream/README.md` for drift; check the install/first-run
|
||
story reads cleanly end to end; and decide whether anything wants a real
|
||
docs site vs. staying Markdown-in-repo. Pairs with the first-boot welcome
|
||
and control-center items (shared "how do I…" surface).
|
||
- **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).
|
||
- **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).
|
||
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
||
`grp:` options) for switching layouts on one keyboard; add the
|
||
`hyprland/language` module to the summer whole-swap themes for parity
|
||
(the gating doesn't translate to their static JSON). 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`).
|
||
- ⚠ **Known bug — btrfs-assistant 2.2 segfaults on launch** in nixpkgs
|
||
26.05: it crashes inside `libbtrfsutil.so.1.4.0` (a library ABI mismatch,
|
||
confirmed in the VM regardless of GL/Qt platform — so it's not a VM/GL
|
||
artifact and will crash on hardware too). The menu wiring is correct and
|
||
the app is installed, but the GUI doesn't open. The snapshot *backend*
|
||
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
|
||
its libbtrfsutil, or wait for an upstream bump. ✓ **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.
|
||
- **"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
|
||
- ✓ **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.
|