All checks were successful
Check / eval (push) Successful in 3m17s
Make keyboard typing filter-only with Gum strict mode, validate interactive and unattended choices against the pinned XKB catalog before any disk mutation, and show resolved human-readable choices in review. Verified: V1 flake eval, focused installer builds, shell/Python/diff checks; current-ISO KVM boot proved the packaged new flow while the pre-destructive interaction pass continued.
196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
# 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
|
||
- **Doctor float V2:** `THEME=<slug> nix build --impure -f tools/doctor-float.nix`
|
||
— softGL Hyprland, `nomarchy-menu doctor`, asserts
|
||
`class=com.nomarchy.doctor` + `floating` + centered midpoints +
|
||
screenshots. Ghostty *does* start here under softGL (classed one-shot);
|
||
size often stays Ghostty’s default 800×600 (client geometry wins over
|
||
the percent `size` windowrule).
|
||
- **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). Doctor float is the counter-example
|
||
that a *classed* one-shot Ghostty can still map under softGL.
|
||
- **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.
|
||
- **Don't default a "timer/session" feature to V3 — most of it is VM-testable.**
|
||
A scheduled/session behaviour usually decomposes into a *generic* step
|
||
already covered elsewhere (e.g. `home-manager switch`, exercised by every
|
||
theme apply) and a *specific* decision (which theme, when). Stub the generic
|
||
step (`NOMARCHY_REBUILD=<marker>` for theme-sync) and **simulate time by
|
||
moving the VM clock** (`date -s`, `timedatectl set-ntp false`, `time.timeZone
|
||
= "UTC"`), then assert the decision + state change headlessly. `checks.auto-theme`
|
||
does exactly this for #79's sunset/sunrise. Only the literal
|
||
timer-fires-on-`OnCalendar` is truly on-hardware, and `systemd-analyze
|
||
calendar` validates that schedule. (I first mis-framed #79 as V3 — it's V2.)
|
||
- **`writeShellScriptBin` scripts run `set -euo pipefail`** (nomarchy-doctor,
|
||
the menu, lifecycle CLIs). So a **no-match `grep` inside `$(…)`** (grep exits
|
||
1 → command-sub fails → abort) and a **standalone `cond && action`** (false
|
||
cond → abort) both kill the script mid-run — the tell is output that stops
|
||
before the final/verdict line with no error. Guard: `… | grep … || true`
|
||
inside `$()`, `cmd 2>/dev/null || echo 0` for captures, and `if` instead of
|
||
`&& action`. (#77 doctor hibernate section; caught by the checks.doctor VM
|
||
test on first run.)
|
||
- 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).
|
||
- **Hibernation reference (Latitude / Newton, BACKLOG #76):** LUKS whole
|
||
root BTRFS; `@swap` → `/swap`; file `/swap/swapfile`;
|
||
`boot.resumeDevice` = LUKS root UUID; `resume_offset` from
|
||
`btrfs inspect-internal map-swapfile -r`. Swap is encrypted with root
|
||
(not a cleartext partition). No zram on that box yet — zram is additive
|
||
for live pressure only. Installer already creates this when swapSize>0.
|
||
- CI (`.gitea/workflows/check.yml`) is **eval-tier only** (standing
|
||
decision 2026-07-10): act_runner docker-compose on the Gitea VPS; no
|
||
KVM there. Full VM suite is BACKLOG **FUTURE #20**, not NEXT — needs a
|
||
separate nix+/dev/kvm runner. Container gotchas are 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).
|
||
- 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
|
||
event burst while the oneshot is active; clean inactive result plus an
|
||
`InvocationID` change proves coalesced AC re-apply). AC udev hooks for a
|
||
settling oneshot must use `start`, never `restart`: USB-C docks emit event
|
||
bursts and restarts SIGTERM the in-flight pass into `start-limit-hit`.
|
||
- 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
|
||
- **Dock transitions are ordered safety operations (#100):** dock in one
|
||
`hyprctl --batch` (external on → every internal workspace moved → focus
|
||
external → internal off); undock enables internal *before* moving anything.
|
||
The Hyprland watcher, not a shell-pipeline subshell, owns a low-level
|
||
`handle-lid-switch` inhibitor until the lid is physically open; startup
|
||
cleans a validated stale process group. HDMI availability may appear only
|
||
as `change:sink`, so fresh `monitoradded` is the intent boundary for the
|
||
settled PipeWire/WirePlumber reprobe + sink pick; generic audio changes must
|
||
not override a manual in-dock speaker choice.
|
||
- **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)
|
||
- Gum `filter` returns unmatched typed text by default; catalog-only pickers
|
||
require `--strict` plus an independent exact-membership validation boundary.
|
||
- 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[<name>]: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: `nomarchy-pull` (lock) then
|
||
`nomarchy-rebuild` then `nomarchy-home`, or desktop changes are silently
|
||
skipped against the old
|
||
lock (README § 3).
|
||
- Hyprland 0.55 renames `stayfocused` → `stay_focused` (and similar
|
||
underscore effects); `stayfocused 1` is `invalid field type` at parse
|
||
(§ polkit workspace rules, 2026-07-10 hardware).
|
||
- Never gate a safety listener behind the optional feature it also serves:
|
||
the display menu existed with no profiles while its blackout rescue did
|
||
not. Rofi defaults to mouse-pointer output (`monitor=-5`), which is stale
|
||
in clamshell mode; use focused output (`-1`) for keyboard-launched UI.
|
||
- 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 `<effect> <value>, match:<prop> ^…$` (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).
|
||
- Agent instructions live vendor-neutrally in `agent/` (VERIFICATION,
|
||
DELEGATION, THEME-DESIGN; entry AGENTS.md) — `.claude/` is a thin
|
||
adapter (permissions + subagent defs only; skills were removed
|
||
2026-07-11). 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).
|