# Memory — durable lessons, learned the hard way Curated, not append-only: one line per fact, newest at the top of its section; delete entries that stop being true. Details usually live in a docs/ROADMAP.md decision record — pointer given as (§ item). Add a fact here the moment a debugging session teaches you something a future iteration would otherwise rediscover. ## Testing & VM recipes - **theme-shot softGL cannot start Ghostty** — `btop.png` is best-effort (usually identical to desktop). Guest asserts on `~/.config/btop/themes/nomarchy.theme` prove baking; the TUI look is hardware/GL tier (HARDWARE-QUEUE). - **Waybar `custom/doctor` tripwire:** status helper must invoke `nomarchy-doctor` by **absolute store path** (waybar's env can miss system PATH → `command -v … || exit 0` self-hides forever); empty `"text"` also self-hides; strip ANSI before packing the tooltip; use signal 10 + `format = "{}"`. theme-shot asserts class:bad + glyph and pokes RTMIN+10 before the desktop shot. - **tuigreet dies silently under runNixOSTest** (even bare, no theme flag: greetd sits as "(greetd)" with no child, nothing in the journal — its stderr goes to the VT) — nixpkgs' own greetd test uses agreety instead. Greeter *rendering* is interactive-ISO/hardware tier; don't burn another session on a checks.greeter VM test. - In VM tests `pgrep -f PATTERN` can match the test backdoor's own `bash -c` wrapper (the pattern is in its cmdline) — use `pgrep -x` or a `[t]uigreet`-style bracket pattern. - A checks.* fixture CANNOT be a writeText/toFile state file read at eval time ("path … is not valid" — flake check's eval store won't realise it): extract the logic into a pure importable file and unit-test THAT (monitor-rules.nix / checks.display-profiles is the pattern). - CI (`.gitea/workflows/check.yml`) is **eval-tier only**: the act_runner is a docker container (no systemd, no /dev/kvm). Container gotchas are documented in the workflow header (single-user Nix + nixbld users, `sandbox=false` for Stylix IFD, Nix pinned 2.31.5 vs lazy-trees, no JS actions past node20) — learned over the legacy repo's 57 runs; read them before touching the workflow. - The Gitea instance is **1.25.4** — `on: schedule` workflows are supported; bump.yml assumes the Actions token can push to `main` (standard Gitea behaviour, but unconfirmed until the first run lands). - The git server is **Gitea** (gitea/act_runner via docker-compose), NOT Forgejo — workflows are read from `.gitea/workflows/` (or `.github/`), never `.forgejo/workflows/` (a whole push cycle was lost to that). - Reusable headless VM harness: `checks.*` via runNixOSTest — existing examples to crib from: `distro-id` (boots + `switch-to-configuration dry-activate`), `hardware-toggles` (kernel cmdline/PAM assertions), `battery-charge-limit` (fake Mains adapter via `test_power`, real udev uevent, `InvocationID` change proves the restart). - Themed-desktop screenshots work headlessly: software-GL Hyprland (`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP dump — prototyped 2026-06-19, kept as the fallback for theme previews (§ Visual theme picker). - Hyprland/Ghostty need guest GL (`virtio-vga-gl`, `gl=on`) in interactive QEMU or the session won't start; black screen ≈ missing GL (docs/TESTING.md § gotchas). - No KVM = slow, not broken; don't read slowness as failure. ## Known-broken / watchlist - **btrfs-assistant "segfault" was unprivileged-only** (re-diagnosed 2026-07-04): libbtrfsutil's unprivileged subvolume iteration crashes on btrfs-progs 6.17.1 (upstream-fixed after); **as root it works**, and the pkexec launcher runs it as root. The real distro bug was **no polkit agent in the session** (every pkexec failed silently) — hyprpolkitagent now ships (hyprland.nix exec-once). `checks.snapshot-gui` guards the root path. Lesson: before "app X is broken", check WHO it runs as — and whether polkit prompts can render at all (§ Snapshot browse/restore). - **NixOS release bump is a trap:** the discarded attempt (branch deleted 2026-06-22) hit a Hyprland OOM blocker; a redo is a deliberate `v2`, never part of routine lock bumps. - `theme-state.json` is git-tracked inside an 86 MB flake tree, so every state write re-copies the source before eval — the wallpapers-artifact split (BACKLOG LATER) is the decided fix (§ Faster switches). - **Friendly theme-state load** (`modules/theme-state-read.nix`, #66): `builtins.tryEval` does **not** catch `readFile`/`fromJSON` failures — gate with `pathExists` + empty/non-object checks before `fromJSON`. Subtle JSON syntax errors still surface from nlohmann (line/col); field schema stays in `theme.nix`. mkFlake must `builtins.seq` the check onto the whole return set or lazy attr access skips it. ## Design invariants - **Waybar status is never color-only** (item 28 sweep, iteration #69): every status module must distinguish its states by SHAPE (glyph) or presence (self-hide), never color alone — good/warn/bad collapse under color-blindness. When adding a state, give it a distinct glyph or gate the module on it; a new `class` that only recolors an existing glyph is a regression. Suppressed notification states (DND *and* app-inhibited) all use the bell-off glyph + @muted. - **Identity themes are not traffic lights** (#69): white, vantablack, lumon, hackerman, matte-black, miasma — monochrome / mono-hue / earthy status by design. `audit-theme-design.py` tags their hue/CVD/ANSI-family findings `[identity]`; do not "fix" them into R/Y/G. - **Import hierarchy ≠ ANSI** (#70): `import-palettes.py` must not set surface==overlay when color0==color8; light color0 is often ANSI black (not a chip). Roles are first-class — never bulk-reimport shipped JSON without a hierarchy pass. ## Gotchas (cost a debugging session once) - Waybar `layer: top` renders above **even real-fullscreen windows** — the bar draws over a fullscreen video. `layer: bottom` lets the fullscreen surface cover it while the exclusive zone still reserves the bar's space in normal tiling (trade-off: floating windows can now overlap the bar strip). Set in both waybar.nix and every whole-swap jsonc (item 30). - Hyprland binds match the exact modmask: a shifted keysym (`question`) needs SHIFT in `mods` or the bind never fires — the keypress falls through to the focused window (§ item 26; caught on hardware, invisible to eval-tier tests). - Never kill a Wayland session-lock client (hyprlock): its crash failsafe drops to a tty instead of unlocking (§ Hibernate double-unlock). - rofi `element-icon size` is one value = a square cell; `WxH` silently collapses and non-square icons letterbox — pre-crop images square at build (§ Visual theme picker). - WirePlumber 0.5 monitor rules can only early-match `device.api`; `device.product.name` etc. bind *after* the rule runs — surgical libcamera scoping is impossible (§ Webcam). - `hyprctl switchxkblayout` is a *global* layout flip; per-device isolation needs `device[]:kb_layout` keywords (§ Keyboard layouts). - Waybar's clock captures the timezone at construction — a zone change needs SIGUSR2 (watcher in `timezone.nix`) (§ Automatic timezone). - Waybar `persistent_workspaces` (underscore) is dead syntax silently ignored; the hyphen form is honoured and renders phantom workspaces (§ Waybar shows non-existent workspaces). - GTK4/libadwaita/Qt6 read light/dark from the portal's `org.freedesktop.appearance color-scheme` (dconf), not Stylix polarity (§ GTK/Qt ignore the theme's mode). - Update order matters downstream: `sys-update` (lock) before `home-update`, or desktop changes are silently skipped against the old lock (README § 3). - Hyprland 0.53 rewrote window rules: `windowrulev2` is a hard error and the old rule-first `float, class:^…$` no longer parses — both surface a red config-error banner on the default desktop. Hyprlang legacy form is now ` , match: ^…$` (e.g. `float 1, match:class ^…$`); effects carry a value, matchers take `match:` (§ windowrule migration). - grub `loadfont`s every `.pf2` in a theme dir — reuse a bundled DejaVu rather than shipping fonts (§ Distro branding). - `.claude/skills/*/SKILL.md` are now **tracked** (the `.gitignore` `.claude/skills/` line was un-commented→removed, 7d52d4b) — commit skill edits like any repo doc. Still never `git add -A` blindly: check `git status --short` for genuine strangers first (`settings.local.json`, harness-dropped files) and commit with explicit pathspecs (§ loop hygiene).