Upstream keeps the selected row *index* across a refilter (rofi#2317),
so backspacing grew the list back under a stationary highlight and Enter
launched the wrong app. The maintainer's reply pointed at the
inputchange {} config block — adopted as pure config in the generated
config.rasi (kb-row-first on every query edit), no patch, no source
build. Covers every rofi surface: drun, text menus, dmenu pickers.
V2: headless VM (theme-shot variant driving QMP keystrokes). On HEAD the
repro left the highlight on row 3 (kitty -> Bulk Rename); patched, the
highlight snaps to the first entry after backspace — verified in the
executive-slate drun grid, boreal's whole-swap grid, and the
keyboard-layout dmenu picker. nix flake check --no-build passes; the
rendered config.rasi carries the nested block.
Backlog #143 deleted (watch closed), decision recorded as Resolved,
ROADMAP checkmark added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bernardo, filed live: the generated bar (non-whole-swap themes) should be
a bit taller so the selected-workspace highlighter stops overflowing it,
and the right module cluster needs consistent icon spacing + end padding
(power currently sits too close to the screen edge). Full §3 visual
protocol when taken.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bernardo pulled, rebuilt and switched themes on the dev box; awww query
shows both outputs rendering summer-night from the nomarchy-wallpapers
store path. Queue entry closed; custom-override precedence is code-order
(THEMES_DIRS), not re-queued. Side catch during the same rebuild: the
reload re-lit eDP-1 inside the shut lid — expected until the relogin
that arms the #148 watcher fix; re-docked manually via the transition
tool.
Verification: V3 (human-in-the-loop on the incident hardware + CLI
evidence).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The split line is consumption time: palette JSONs, previews and
per-theme overrides are read at Nix eval and stay in-repo (~3 MB);
wallpapers are runtime-only (swww via nomarchy-state-sync, never read
at eval) and move to the pinned nomarchy-wallpapers non-flake input
(git.bemagri.xyz/bernardo/Nomarchy-Wallpapers @ 803b48d, all 24 slugs).
New overlay drv nomarchy-default-themes merges the two: repo themes
copied, <slug>/backgrounds symlinked from the input, build FAILS if any
theme JSON lacks non-empty backgrounds — the drift guard for the
two-repo dance. nomarchy-state-sync's wrapper now points straight at
the merged store path (no in-package copy): the package drops from
~94 MB to 32.6 KiB and the wallpapers source appears exactly once in
any system closure, ISO included — offline promise intact. Downstream
custom wallpapers still win ($NOMARCHY_PATH/themes is checked first),
and an explicit state.json wallpaper path still works.
checks re-pointed at the merged tree (theme-wholeswap,
state-sync-validate, auto-theme); theme-contrast and airplane stay on
./themes (JSON/text only). import-palettes.py + README updated;
ROADMAP § Faster switches marked shipped; LATER now carries only the
pre-built-variants follow-on. Also files #151 (v1 launch plan, [human])
with measured history numbers: the 107 MiB pack is 94 MB wallpaper
blobs; all 848 text commits pack to ~15-20 MiB — recommendation
recorded to filter-repo at the GitHub move, not fresh-start.
Verification: V2 — nix flake check --no-build green; theme-wholeswap,
state-sync-validate, auto-theme VM check, downstream-template-home all
green; wallpapers store path exactly once in the state-sync closure;
`list` works from a scratch state dir. V3 queued: live wallpaper render
+ bg-next + custom-override precedence after next pull (HARDWARE-QUEUE).
Lock gained exactly the one input node. Implementation by a Sonnet
subagent; design and review on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-way audit ahead of v1 (structure/necessity, docs-vs-code drift,
mechanical checkers) came back clean: zero dead files, zero doc hard
errors, all 92 options documented, all consistency checkers green, no
untracked files. What it did surface, fixed here:
- LICENSE was missing entirely — the one hard launch blocker. MIT
(Bernardo's call), pointer at the README foot.
- checks/ was the only top-level dir outside the documented layout rule;
its two state-legacy-name fixtures moved to tools/fixtures/ (Bernardo
chose move over documenting an exception), flake.nix refs + drift
messages updated.
- Doc staleness: two leftover "theme-state" leads (HARDWARE.md,
REQUIREMENTS.md) now say state.json; VISION Theme C charge-limit row
gets its ✓; VISION agent-slices datestamp bumped.
- The idle.nix miniTransition duplication (same binary name, lossy
subset of the real display-transition tool) is deliberately NOT
refactored here — filed as PROPOSED #150 with the audit evidence.
Wallpapers-in-repo (97 MB) is the known LATER artifact-split item, not
re-filed.
Verification: V0 — nix flake check --no-build green after the fixture
move (state-legacy-name's eval asserts exercise the new paths); doc
edits prose-reviewed. Audits by three Sonnet passes; triage on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nomarchy now carries a nixpkgs-unstable input and exposes it through
overlays.default as an `unstable` attrset: in the template's
`home.packages = with pkgs; [ … ]` a user writes `unstable.lmstudio`
and is done. Downstream stays one locked input; nomarchy-pull moves it.
lib.nix needed zero changes — mkFlake's pkgs already applies the
overlay. The import is plain and non-recursive, so configs that never
reference unstable. pay nothing beyond the input fetch; allowUnfree
mirrors the base set so the motivating lmstudio resolves.
Home packages are the supported surface (documented convention). Costs
stated in the README, not hidden: you own the pinned+unstable
combination; a second toolchain lands in your closure (~690 of ~4k
paths shared, measured on lmstudio); it moves when the lock moves.
Lock bumps now move two channels — VERIFICATION §5 and CONVENTIONS
updated accordingly. flake.lock gained exactly one node
(nixos-unstable @ 753cc8a); nothing else moved.
Verification: V1 — nix flake check --no-build green; new eval-only
checks.unstable-seam green (26.05pre vs 26.11pre lib.version, no
builds); template HM activation built via mkFlake with unstable.hello
enabled and its closure carries the unstable store path (pg2zfr… vs
pinned nm7p8w…). Implementation by a Sonnet subagent; design and
review on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The incident was misdated: it did not happen before 12:51:34 Jul 15
(that boot was healthy all day) — it ended the previous boot at 10:54:22
with the power button. In that boot, running the pre-fix generation:
idle cascade on schedule (lock 10:21, dpms off 10:26, suspend correctly
skipped on AC), then hypridle logs "BUG THIS: inhibit locks < 0: -1" at
10:36:29 and its rule state is corrupted from there — the fired DPMS
listener's on-resume, then the session's ONLY dpms-on caller, never
dispatches again despite provable real input (two blind hyprlock PAM
attempts) and three output remove/re-add cycles. The wedged-compositor/
DRM-master lead is contradicted: Hyprland tracked hotplug live through
the whole window, PAM worked, kernel spotless. The shipped fix (a2151f4,
input wakes DPMS compositor-side) closes exactly this mechanism.
Filed upstream with the full trace: hyprwm/hypridle#208 (0.1.7; their
closed #104/#128 are the same class). Watch folded into #146.
Sweep: BACKLOG #127 deleted (NOW keeps only the install-findings context
stub, flagged to Bernardo); ROADMAP gains "The idle brick's CAUSE found"
and both stale "cause still open" tails patched; checked-off #127
HARDWARE-QUEUE blocks pruned, their method lessons moved to MEMORY
§ Gotchas.
Verification: V0 (docs/backlog only). The evidence is journal forensics
on the incident hardware (two Sonnet passes, verdict on Fable), quoted
in the ROADMAP entry; the fix itself was V3-proven 2026-07-16.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After the #81 "you're set" card, at most one additional "Hardware tips"
toast: a Firmware line when fwupdmgr is on PATH, a Fingerprint line when
fprintd-list is (VISION § B). One-shot marker settings.hardwareHintsShown,
in-checkout like firstBootShown. When no tooling matches, the marker is
deliberately left unset — the re-check costs a command -v per session,
and a later-enabled fwupd or newly-added reader still gets its hint once.
Never a permanent nag; live-ISO gate covers the new stage too.
Lives inside nomarchy-first-boot: the card's semantics are unchanged
(retry loop, exit 1 so a failed notify retries next login, marker only
after success); hints run only once the card has landed. Docs: README
option row, HARDWARE.md hint paragraphs, VISION § B checked off.
Verification: V2 — shellcheck clean, nix flake check --no-build green,
nix build .#checks.x86_64-linux.first-boot green (bare node silent →
fwupdmgr appears → fires once → silent again → live-ISO still skips).
Real toast rendering is the same session surface as the proven #81 card.
Implementation by a Sonnet subagent; design and review on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every nomarchy-home re-lit eDP-1 inside the shut lid: HM activation fires
hyprctl reload, the catch-all monitor rule re-enables the panel, and the
tick invariant built for exactly this (#142, enforce_dock_intent) never
fired — it was gated on settings.displayProfileAuto, the *named-profile*
auto-switch toggle, which is unrelated to dock intent and off unless
opted into. Confirmed live on the incident machine: dockMode=true,
displayProfileAuto unset, watcher journal empty — every tick returned at
that guard.
Fix: drop the auto_on gate. settings.display.dockMode alone is the
user's recorded intent (written by the menu and the dock transition);
the profile-active check remains as the real guard (a named profile
outranks the dock heuristic). No fight with the #142 undock hammer:
undock clears the intent before its rescue reload, so the enforcer
stands down by ordering.
tools/monitor-fallback.nix grows the reload-mid-dock scenario: fake lid
closed, hyprctl reload, panel re-disabled within ~4s, action=re-dock
logged — under displayProfileAuto's default false, i.e. the exact
condition the bug shipped under.
Verification: V2 — nix flake check --no-build green, checks.docking-ux
green, monitor-fallback harness green three times (implementer twice,
reviewer once, ~71s VM). V3 pending: relogin + nomarchy-home while
docked-clamshell (HARDWARE-QUEUE; the watcher is exec-once, the fix
arms at next login). Implementation by a Sonnet subagent; diagnosis,
design and review on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/var/lib survives a migration untouched, and a stray pre-Nomarchy
Linger=yes marker shaped the QA box's session behavior for six months
with nothing in the flake to show for it. Ship the class as guards:
- nomarchy-doctor: new machine-state-drift check after the flake-checkout
check — loginctl show-user -p Linger; "yes" warns (nothing is broken —
it is drift) and prints the disable-linger fix; unknown-to-logind skips.
- docs/MIGRATION.md § Post-migration cleanup: "/var/lib machine state
survives migration" — the worked example and the general suspicion rule.
- checks.doctor: linger sub-case — warn fires under enable-linger, exit
stays 0, warning clears after disable-linger.
Deliberately NOT hardening the session wiring against the portal-gtk
restart race linger enabled: a clean install cannot reach that state.
The Waybar exec-once loose thread survives as BACKLOG #149.
Verification: V2 — shellcheck clean, nix flake check --no-build green,
nix build .#checks.x86_64-linux.doctor green (both branches exercised).
Implementation by a Sonnet subagent; design and review on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live triage with Bernardo. #114 (greeter ignores per-device layouts):
ruled document-only — a kernel VT has exactly one keymap, so tuigreet
structurally cannot honour settings.keyboard.devices. README gets the
"Greeter keyboard layout" note after the home.nix options table,
RECOVERY.md §2 a one-line pointer for the password-rejected case,
ROADMAP's #145 cross-ref updated, BACKLOG entry deleted.
Also recorded: #120 stays deferred (cachix likely first step when
promoted); #134 decided — Nomarchy carries nixpkgs-unstable, unstable.*
via overlay, home-scope only — rewritten into NEXT; #143 upstream check
(rofi#2317 still open, no response) noted in the item.
Verification: V0 — docs and backlog only, prose reviewed in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "blank + hypridle deadlock = the brick" cause published this morning is
retracted everywhere it was asserted: ROADMAP, hyprland.nix's comment, #146,
HARDWARE-QUEUE and two commit messages (a2151f4, 321ccf7). It rested on two
pieces of evidence and both were artifacts of how I measured:
- "19 disconnects in 11 days": 18 of 19 are hypridle's noisy goodbye while
systemd DELIBERATELY stops it — each bracketed by `Stopped hypridle` /
`Started hypridle`, healthy replacement seconds later.
- "Jul 14: CRITICAL at 13:08:17, then eleven hours of silence": an artifact of
grepping ONE PID. systemd logged `Stopped hypridle` 13:08:17 and `Started
hypridle` 13:09:00; PID 1678 ran fine all evening, invisible to the filter.
Decisive: at the incident (before the 12:51:34 mitigation commit) hypridle had
NO disconnect at all and had been healthy since Jul 14 13:09. It should have
fired on-resume normally. The deadlock theory never had a case.
The FIX stands on its own evidence and is untouched: the options really were
off, hypridle's on-resume really was the only caller of `dpms on`, and input now
wakes the screen with hypridle deliberately stopped (4s docked / 6s clamshell).
Removing a proven single point of failure never needed a story about why the
daemon might fail — which is exactly why it survived the story being wrong.
#127 is RESTORED to NOW (deleted this morning as "shipped"; only the fix
shipped). Its cause is open and the lead is the dead Ctrl+Alt+F3 — neither
DPMS-off nor a dead hypridle can break VT switching, so something was wedged
below the compositor holding DRM master. Every theory so far has been about the
wake path, i.e. a symptom of the screen already being black.
#146 → [watch], near-dead: one genuine zombie ever (verified by ps/ss, not
grep); upstream #171 is real but its impact path closed when #147's stray linger
went. Re-measure properly before building anything; close unbuilt if zero.
Also closes the #127 hardware-queue entry: Bernardo ran the real 10-min idle —
19:27:58 dpms off (genuine timeout, shipped config, and it BLANKED = the revert
working) -> mouse -> 19:32:14 `done enabled=1 first=DP-2`, clamshell, the
incident geometry, one healthy hypridle throughout. All 4 steps pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Investigated #147 and the filing was wrong in the same direction as the 321ccf7
claim: it asserted something missing that is present. The repo DOES have the
session wiring — HM's wayland.windowManager.hyprland.systemd.enable defaults on
and is line 1 of the generated hyprland.conf — and it fired correctly. Rewritten
so nobody "fixes" it by adding what is already there.
What actually happened, per systemd at 17:25:54: hyprland-session.target
stopped, StopWhenUnneeded correctly tried to take graphical-session.target with
it, and systemd REFUSED the stop — xdg-desktop-portal-gtk had a 'start' job
queued from its own post-crash restart loop, making the stop a destructive
transaction. graphical-session stayed up from the morning; hyprland-session
returned 8ms later to find it active; nothing WantedBy it was re-pulled.
None of which a Nomarchy install can reach: it needs the user manager to survive
logout, i.e. Linger=yes — and that came from an empty marker dated 2026-01-24,
PRE-DATING Bernardo's migration to Nomarchy (custom flake at the time). Nothing
here sets linger; git log -S says it was never shipped, ever. Removed via
loginctl disable-linger (no restart needed, 0 failed units after); nothing wanted
it, as nomarchy-updates.timer is Persistent=yes and catches up on login.
The item is rewritten to the class that IS real: /var/lib is machine state, a
migration reconciles the flake and leaves the rest, so Nomarchy ran six months on
its own primary V3 box with a setting it never chose and could not see. That hits
the in-flake-state promise and V3's trustworthiness at once. Pitch is now a
doctor check for drift of this class + a MIGRATION.md note — explicitly NOT
hardening the session wiring against a race a clean install cannot reach.
Also corrects #146, which claimed an ordering dependency on #147 ("fail-loops
like cliphist"). Void — cliphist's fail-loop was the same stray state. #146 has
no dependency and can be taken first; its 19-in-11-days rate was measured with
linger on and should be re-measured before a guard is built.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
321ccf7 claimed the 4s step-4 run was "docked, lid OPEN" and that it retired
the old "docked lid-open is untested" caveat. Both wrong, per Bernardo: the
lid was SHUT throughout. eDP-1 was lit inside a closed clamshell because a
reload had re-enabled it, and "lid open" was inferred from an output being
enabled — which is not evidence of a lid position. /proc/acpi/button/lid/*/
state is, and it was never checked; it reads `closed` with eDP-1 enabled and
dpms_on=true right now. Docked-lid-open stays untested (moot — the gate is
gone — but untested). The step-4 result itself stands: input woke it in 4s
with hypridle stopped, with two outputs enabled rather than one.
That correction exposes a real bug, filed as #148: `hyprctl reload` re-enables
every output (the #142 hammer the undock rescue relies on), but
nomarchy-display-watch only reacts to monitoradded/monitorremoved — a reload
emits neither, so nothing re-asserts the dock profile. Every HM activation
reloads Hyprland, so every rebuild while docked-clamshell lights the panel and
leaves it lit. Bernardo hit it twice in ten minutes today: once from
nomarchy-home, once from an agent's reload that silently undid the fix he had
just made by hand in the Display menu.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a2151f4 applied on the dev box (nomarchy-pull + nomarchy-home). Step 1: both
misc options read int: 1 after `hyprctl reload` — which discards runtime
keywords — so our Nix supplies them, not the keyword used to prove the fix
originally; nomarchy-dpms-off is gone from the closure and the live
hypridle.conf reads `on-timeout=hyprctl dispatch dpms off`.
Step 4, the one that matters: hypridle stopped on purpose (the brick
condition), `dpms off` -> keypress woke it in 4s from config alone. That run
happened to be docked with the lid OPEN (DP-2,eDP-1), which also retires the
old "docked lid-open is untested, not exempted" caveat. With the clamshell
6s run the same day, input-wake is now proven in all three geometries:
undocked lid-open, clamshell-docked, docked lid-open.
Steps 2+3 stay open deliberately: nothing yet has watched a genuine 10-minute
hypridle timeout fire — only forced blanks — so the real idle path is still
unobserved. Recorded that step 1 used reload, not a literal relogin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The idle brick was never DPMS. Hyprland ships misc:key_press_enables_dpms
and misc:mouse_move_enables_dpms OFF, so hypridle's on-resume was the ONLY
caller of `dpms on` in the session — an idle daemon with the display held
hostage behind it. hypridle then deadlocks on an unfixed upstream bug
(hyprwm/hypridle#171, "Disconnected from pollfd id 1", open since 2025-09,
absent from 0.1.7 and main) and hangs WITHOUT exiting, so Restart=always
never fires and systemd still reports it active (running). Blank + deadlock
= a black seat no keypress can escape. That was the brick: two mundane bugs,
not one exotic DRM state. 19 disconnects in 11 days on the dev box, incl.
the incident day; the compositor is healthy through all of them.
Both options on, so the wake lives in the compositor where no daemon can
lose it — a dead hypridle now costs auto-lock, not the machine. The #127
clamshell DPMS-off skip (060bf52) is therefore reverted: idle blanks again
in every dock/lid state, and the 27" panels stop being held lit all night
under a static lock screen. #135 (an option to opt back in) is retired
unbuilt — there is nothing left to opt into.
TEST B's first run was invalid and nearly bought the wrong answer: it ran
`hyprctl dispatch dpms off` by hand, which no wake path watches, so "input
did not wake it" was guaranteed on any hardware. The undocked control
returning the SAME result — where the theory demanded a difference — is what
exposed it. Driven through hypridle instead, input wakes it in all three
configurations, including clamshell-docked on the sole live output.
Filed from the diagnosis: #146 (hypridle dies silently, systemd calls it
healthy — the surviving bug) and #147 (a re-login leaves session units dead;
graphical-session.target never re-enters, which is why Waybar already runs
from exec-once).
V3 on the incident hardware (AMD dev box, clamshell): hypridle deliberately
stopped, `dpms off` -> keypress woke it in 6s; the identical test before the
options stayed black the full 45s. V1: nix flake check, checks.option-docs,
checks.clamshell-logind, checks.docking-ux, checks.display-profiles; read the
emitted hyprland.conf/hypridle.conf rather than the source.
V3 pending: the fix is proven via runtime `hyprctl keyword`, not yet from our
own Nix — HARDWARE-QUEUE re-checks it after nomarchy-home + relogin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Light-theme accent buttons sat at 2.72:1 — under AA and under even the 3:1
large-text floor. Bernardo: fix it. The plan on file ("darken the label")
turned out to be IMPOSSIBLE, and measuring all 24 palettes is what showed
it: on a saturated mid-tone accent nothing clears 4.5 — cream base 2.72,
dark text 1.65. That is why adw-gtk3 ships white-on-accent at ~2.7. The
chip has to move.
The chip is now the accent pulled AWAY from the label: toward white when
the label is dark, toward black when it is cream. One factor (0.70), only
the anchor flips, so "solid accent button + base label" stays the design
and just shifts tone. Worst dark 4.86 (nord/bad), worst light 5.59
(summer-day). The old 0.90 chip failed SEVEN themes (miasma/bad 3.43,
rose-pine 2.70) — never only a light-theme bug, contrary to the item.
#129 rides along, and the two are one idea. #98 shipped an invisible button
with every check green because the background was not a palette role —
adw-gtk3 computed it from currentColor. So the guard is not a CSS lint
(which would flag the harmless generic-button rule and get muted) but
arithmetic: pin both ends and the pair becomes checkable.
check-theme-contrast.py now models GTK's own mix(a,b,f) = a + (b-a)*f —
READ from gtkcsscolorvalue.c:234, not assumed — and asserts base-on-chip
>= 4.5 for all 24 themes. Its negative test reproduces the item's reported
2.72/2.74 exactly from an independent model, which is the best evidence
available that the arithmetic matches the pixels.
Invariant to keep: pin the pair, or pin neither. Both chips are pinned now
(suggested + .default + destructive), where #98 pinned only destructive.
tools/dialog-shot.nix deliberately not built: the checker covers the
numbers, and a VM that only ever confirms arithmetic is maintenance for
nothing. Render if a new widget class appears.
Also sweeps #127 with the latch finding (ff5017e): symptom 2 solved, not
DPMS — which raises the odds the mitigation treats the wrong cause.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo, live, right after #127 TEST A: undocked → the panel stayed dark;
re-docked and undocked again → it came back. Re-docking is not what fixed
it. OPENING THE LID was.
awaiting_lid_open is set by every successful undock and cleared only when
the external is gone AND the lid is open — so a clamshell undock latches
it. Re-docking clears its sibling undock_pending ("a new external
supersedes any undock still queued for an older departure") but not this
one, and the clear cannot fire while an external is present. The next
departure then hits the `[ -z "$awaiting_lid_open" ]` guard, queues
nothing, and the panel never returns.
His journal is unambiguous — the first undock logged NOTHING (no
outputs-changed removed, no transition), then:
14:42:31 lid-inhibitor=released <- he opened the lid: latch cleared
14:42:40 outputs-changed added= DP-2 <- re-dock
14:42:47 outputs-changed removed= DP-2 transition=undock-queued
14:42:49 transition=undock result=ok
Fix: clear the latch where undock_pending is already cleared — a new
external ends the departure the latch describes. Replaying his sequence
with the lid never opened: old = 1 of 3 undocks run, new = 3 of 3.
This is very likely #127's SECOND symptom ("unplugging did not turn the
laptop panel back on") and it has nothing to do with DPMS. Which raises the
odds that the shipped mitigation is treating the wrong cause — TEST B now
decides that, and with this fix an undock is a real escape hatch during it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#136 CLOSED: he confirms the incident was clamshell (laptop closed), so the
gate already matched the evidence and only the words overclaimed. Swept:
idle.nix's comment + its journal line (`dpms-off skipped: docked` →
`clamshell (internal $m present but off)`), the ROADMAP entry, and #127's
own text. Docked-with-lid-open still blanks and is now explicitly untested
rather than implicitly covered.
#127 gains the exact protocol he asked for — two tests, separated by risk:
A) the mitigation holds (safe, unattended, journal line to grep); B) the
decisive one, whether DPMS-off was ever the trigger, run by hand because the
shipped mitigation would otherwise skip it. B self-recovers after 45s
(dpms on + hyprctl reload — #142's hammer re-lights eDP) because this box
has no sshd. All three outcomes are written down with what each implies,
including "input wakes it" → revert the mitigation and #135 never exists.
#130 decided: fix the contrast, we hold ourselves higher than GNOME —
matching upstream is no defence at 2.7:1, under even the large-text floor.
Recorded with the two candidate shapes and a preference for darkening our
label over the user's accent, plus the note that check-theme-contrast.py
covers palette pairings only, so it needs its own assertion or it rots
exactly like #98.
#143 decided: wait for rofi#2317, do not carry a patch — a patched rofi
means every machine compiles rofi from source, on every lock bump, to fix a
highlight. Now a watch, not work.
#120 deferred to PROPOSED intact: the measurements stand, and the no-binary-
cache gotcha probably makes cachix the real first step anyway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Yarara is nixpkgs' release name. Carrying it in PRETTY_NAME told users
something true about our upstream and nothing about our product, in the one
string every fetch tool and greeter reads.
Upstream hardcodes the codename into VERSION, PRETTY_NAME and
VERSION_CODENAME (misc/version.nix) with no switch, and
`system.nixos.codeName = ""` is the wrong lever — it renders "26.05 ()".
So the assembled strings are overridden through the merge hooks upstream
does provide (extraOSReleaseArgs / extraLSBReleaseArgs), which is already
where our project URLs live. lsb-release gets the same treatment; it
carried the codename in three fields of its own.
Empty VERSION_CODENAME/DISTRIB_CODENAME is deliberate: `//` cannot remove a
key, os-release(5) makes every field optional, and the file already ships
ANSI_COLOR="" and IMAGE_ID="".
nixos-version still prints "(Yarara)" and is left alone on purpose: that
command reports which nixpkgs the system was built from — the one question
the codename actually answers. Reaching it would mean patching a nixpkgs
script for no gain.
Verified on the built system: PRETTY_NAME="Nomarchy 26.05", VERSION="26.05",
VERSION_ID="26.05"; lsb DISTRIB_DESCRIPTION="Nomarchy 26.05",
LSB_VERSION="26.05". fastfetch's os line reads PRETTY_NAME, so it follows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mkFlake prefers state.json and still accepts the legacy theme-state.json,
because a machine only migrates on its next menu write. Nothing evaluated a
checkout carrying ONLY the legacy name, so the shim could rot with every
check green — breaking rebuilds for precisely the users who have not opened
the menu since #107, i.e. the ones least likely to see it coming.
checks.state-legacy-name evaluates two fixtures: legacy-only must resolve,
and no-state-at-all must still throw. The gate is half the shim's contract
(it turns "no state file" into one readable error instead of a readFile
stack from inside the module system) and was equally unpinned.
Cheap by construction: mkFlake wraps its return in `builtins.seq
_themeState`, so forcing the attrset forces the state read and nothing else
— no module system, no home.nix. Hence a one-file fixture and no measurable
cost (flake check still ~35s). That file is a SYMLINK to the shipped
template's state.json: a copy would drift and the check would quietly start
testing a fossil. The fixture also asserts it has no state.json, or it would
pass while testing nothing.
Both assertions proven by breaking lib.nix — deleting the fallback, then
defeating the gate — and watching each fail by name. Delete this check
together with the shim and the nomarchy-theme-sync alias.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The guest's unfaithful rofi geometry was already documented in TESTING.md
§5. What was not, and what this closes, is that the bullet's fallback
advice — "measure text against the 40% window width instead" — is exactly
how #131 stayed wrong for weeks, and my own #131 fix made it stale on top
of that (the width is calc( 84ch min 65% ) now).
Measuring needs a width and a font, and both are per-theme: text menus
render through themes/<slug>/rofi.rasi, where boreal + neon-glass pinned a
fixed 620px while the rest used 40%, in fonts from Inter 11 to
JetBrainsMono 14. The item measured one combination no theme ships,
concluded "only truncates below 1920", and the truncation was live on a
2560 panel.
So §5 now says: judge by rendering — hardware, or `rofi -dmenu -theme <the
built rasi>` + `hyprctl layers` for the real width — and notes
checks.rofi-text-width guards the invariant that makes it fit, never how it
looks. Declined the other half (fixing the guest's fontconfig/icon cache):
the screenshots are useful as content checks, and a guest that lied
convincingly about spacing would be worse than one that obviously cannot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The logo was off-centre on the external when booting/shutting down docked.
Both earlier diagnoses in this repo were WRONG, and that is the part worth
keeping: Window.GetWidth() returns max_width (not head 0), and per-head
sprites would have DUPLICATED the logo — the script plugin already builds
one max_width x max_height canvas, centres every display inside it
(script-lib-sprite.c:536) and draws sprites at (sprite.x - display.x), so a
canvas-centred sprite is centred on every head. The arithmetic was right.
The bug was TIME: every position was a top-level statement evaluated once
at parse time. A head arriving/leaving mid-splash resizes the canvas, the
plugin re-centres the displays, and frozen sprites end up off by
(new_max - old_max)/2 on every head. One monitor never resizes the canvas —
hence "only when docked". Fix: one layout(), re-run from the existing
refresh callback on canvas change.
The trap that cost two attempts (now a comment): in plymouth script a bare
assignment inside a function writes the GLOBAL if that name already exists
globally. So `canvas_width = Window.GetWidth()` updated global.canvas_width
BEFORE the guard compared against it — always false, body never ran, splash
rendered as a bare background, NO error logged. Isolated with a 20-line
probe. Hence cw/ch.
#145 rides along: a VT loads one keymap and knows nothing of per-device
layouts, so the passphrase box types with the console layout — worth saying
before three wrong tries on a disk you cannot read (same gap as #114, one
step earlier). Fedora's mechanism does not port: ply_keymap_icon is a C
widget in the two-step plugin (the script plugin has no keyboard API), fed
from XKBLAYOUT in /etc/vconsole.conf, which NixOS never writes (plymouth's
trace says `XKBLAYOUT: (null)`). So @LAYOUT@ is baked from
services.xserver.xkb.layout like the palette — the FIRST of a comma list,
since that is what the VT loads. Icon is plymouth's own keyboard.png (the
same glyph Fedora shows), copied at build time so no GPL bytes enter the
repo, recoloured to subtext.
chmod +w after that copy: store files are 444 and recolor rewrites in
place. Without it magick fails, the phase aborts, and EVERY LATER SED
SILENTLY DOES NOT RUN — the theme shipped with @BG_R@ literals and I only
caught it by reading the build log.
Verified by render (tools/plymouth-preview.sh, real built theme): both
heads centred, password dialog showing padlock + entry + keyboard icon and
"us". V3 queued — only a real docked boot can resize a canvas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo, and it is the right ask for this machine: a VT has one keymap and
no per-device layouts, so settings.keyboard.devices (his external board
remembered as `us`) does NOT apply at the passphrase box — it follows
xkb.layout via console.useXkbConfig. Same structural gap as #114
(tuigreet), one step earlier in the boot, with worse stakes: three wrong
tries on a disk you cannot read yet.
Fedora's mechanism does not port, twice over: its label is ply_keymap_icon,
a C widget in the two-step plugin (the script plugin has no keyboard API at
all), and it is fed from XKBLAYOUT in /etc/vconsole.conf — which NixOS does
not write. This box has only KEYMAP=<store path>, and plymouth's own trace
says `XKBLAYOUT: (null)`.
So bake it like the palette already is: @LAYOUT@ sed into nomarchy.script
from config.nomarchy.keyboard.layout, drawn with Image.Text (already used by
display_message_callback) inside display_password_callback. Label and
console keymap then come from one option in one rebuild.
Flagged for whoever takes it: keyboard.layout can be a comma list and the VT
loads the FIRST — print that, or the label lies in exactly the case it
exists for. Bundle with #137: it is one more sprite to reposition on canvas
change, and tools/plymouth-preview.sh already renders ask-for-password —
this very screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Answering "how do I give you root": you don't. tools/plymouth-preview.sh
renders the splash — LUKS dialog included — into X11 windows in ~10s with
no reboot, no root and no DRM:
- unshare -rm = user + mount namespace; every mount dies with it.
- /run/plymouth is BOTH the socket dir and the compiled-in plugin path
(`strings plymouthd` → /run/plymouth/plugins/), so we bind a writable dir
over it AND recreate its symlinks — miss those and plymouth can't find
x11.so, silently skips X11 and reaches for a tty.
- never pass --tty (main.c:2133 takes the X11 path only while default_tty
is unset); /dev/dri is masked so a preview physically cannot mode-set a
real output — which matters on a docked session (#127).
- PLY_CREATE_FAKE_MULTI_HEAD_SETUP fakes 800x600 + 640x480 heads.
- debug wants plymouth.debug=stream: — `file:` buffers and yields nothing.
What it proved immediately: with a STABLE two-head canvas the current theme
centres correctly on both heads. So the geometry is fine and #137 really is
the timing bug (positions frozen at parse time vs a canvas that resizes
when a head arrives) — recorded in the item.
And it caught my own fix: hoisting positions into layout() rendered the
background on both heads and nothing else — no logo, no bar — with NO parse
error logged. Reverted rather than shipped, because that script draws the
passphrase box on an encrypted machine. #137 carries the failure, the prime
suspect (plymouth script scope: assignment lands in `local` unless the name
already exists) and the fast edit/render loop for the next attempt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The entry claimed Window.GetWidth() returns head 0 and that the fix was
per-head sprites. Both false, and per-head sprites would have REGRESSED it
into duplicate logos. From plymouth.src (the build nixpkgs ships):
- Window.GetWidth() with no index returns data->max_width — the widest
head (script-lib-sprite.c:227), not head 0;
- the plugin builds a max_width x max_height canvas and CENTRES every
display inside it (display->x = (max_width - width)/2, :536), drawing
sprites per display at sprite->x - display->x (:452);
- so a canvas-centred sprite IS centred on every head, and today's math is
correct at the instant it runs. (The DRM renderer's area.x = 0 for every
head is a red herring — the offsets that matter are the plugin's.)
The real bug is TIME: nomarchy.script computes every position as a
top-level statement at parse time, against the canvas that existed then. A
display arriving or leaving mid-splash — exactly "boot/shutdown with an
external" — changes max_*, the plugin re-centres each display, and the
frozen sprite coords are then off by (new_max - old_max)/2 on every head.
A single monitor never changes the canvas, which is why it was never seen
undocked.
Fix shape recorded (reposition from the existing refresh callback on
canvas change). Not shipped: the same script draws the LUKS passphrase box
on an encrypted machine, and the render rig — plymouth ships x11.so plus a
built-in PLY_CREATE_FAKE_MULTI_HEAD_SETUP — needs root.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every rebuild while docked re-lit the panel and threw a workspace onto
it. Not forgetfulness: a rebuild PERFORMS an undock. Dock mode lived only
in the compositor (`keyword monitor <internal>, disable`, nothing written
down), the config's only monitor line is the catch-all
`monitor=,preferred,auto,1`, and rewriting the config makes Hyprland
reload it — re-enabling every output. Our own undock path already uses
`hyprctl reload` as exactly that hammer (hardware-proven 2026-07-14).
The watcher could not rescue it: reconcile diffs `monitors all`, which
lists disabled outputs, so the connected set is identical across the flip
— structurally blind, the blind spot of rounds 4-5's "the output set is
the truth".
So the intent is recorded where every other setting lives —
settings.display.dockMode, in-flake, menu-writable — written at the one
choke point both menu and watcher pass through (transition: dock → true,
undock → false, `enable` of an internal → false, because "Screen on" IS
leaving dock mode and must not be undone a second later). The watcher
re-asserts it as a tick invariant rather than an event reaction, which is
what makes the reload case work; auto_dock was already idempotent, so it
costs one `hyprctl monitors` per tick.
The trap avoided: baking the disable into the config would survive a
reload and hand a user with no external a zero-output session at login —
the #100/#127 brick. The config cannot know what is plugged in, so it
keeps defaulting to "everything on" and the re-assert is gated on an
external being present.
Verified: 9/9 gates unit-tested by driving the REAL generated function
against stubs (incl. no-external → never re-dock, profile active → yield,
already-docked → no churn), plus the state write/read round-trip on a
throwaway checkout so the live flake was untouched. V3 queued — needs a
relogin, the watcher is exec-once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A real migration (2026-07-16) lost ~/.thunderbird/profiles.ini, so
Thunderbird made an empty profile and opened it while 30 GB of mail —
19 GB ImapMail, 64 account prefs — sat in the directory it had stopped
looking at. The fix was eight lines.
MIGRATION.md already warned about this class for Chromium extensions.
The Mozilla case gets its own bullet because it is the inverse failure
and the inverse danger: Chromium silently deletes extension data and
looks minor; Thunderbird loses nothing and looks catastrophic — so the
reflex it provokes, restoring a snapshot over a good profile, is the one
thing that can actually cost you mail. The bullet therefore leads with
`ls`/`du` ("a multi-GB dir = your data is fine"), then gives the working
profiles.ini shape and the Path=/Default= semantics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo: "opening claude-code is too disruptive". SUPER+CTRL+A spent a
terminal, an npm fetch and a REPL on "what's the syntax for…" — the prompt
was the right surface, the destination was not. The query is now
URL-encoded into the provider's web chat and handed to xdg-open.
settings.ask.provider (claude|chatgpt|gemini|grok, default claude) with a
Preferences row, read from state at RUN TIME so switching needs no
rebuild, and any unknown/absent value falls back rather than opening
nothing. He asked for chatgpt + grok alongside claude/gemini, and for the
CLI to go: claude-code is deleted and pkgs.nodejs leaves with it (carried
only for that npx). Keybind is "Ask AI" now the destination is the user's.
The failure mode is the design point: prefill is undocumented on all four
vendors, each can drop `?q=` unilaterally, and it fails SILENTLY — you
land in an empty chat having already typed the question. So the query is
also copied to the clipboard (a vendor breaking change costs one paste,
not the question), and the URLs sit in one attrset so the repair is one
line.
Verified against the generated script: bash -n clean; every bad state
value (unset/null/garbage) falls back to claude; a hostile query encodes
correctly (& → %26, ? → %3F, # → %23, $(whoami) neutralised, café →
caf%C3%A9). NOT verified on purpose: whether each vendor fills the box or
auto-submits — that means posting into Bernardo's own accounts.
Sweep: ROADMAP's shipped-modules line described the npx REPL; nix-ld's
comment cited claude-code as its example; both fixed. Menu row + keybind
renamed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Filed as bemagri against davatorium/rofi with the 2.0.0 line numbers
(view.c:861 → listview.c:622-631 → :646-651) read from the exact source
nixpkgs builds, not the next branch. No duplicate: the nearest, #1064,
is the opposite request (keep the selection after EXECUTING an entry —
closed, milestone 1.7.4), which is also why the report scopes itself to
the filter-changed path and sends no patch: that behaviour is deliberate
for reloads and the scoping call is the maintainer's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#143 — Bernardo: type `ca`, move onto ChatGPT, backspace once, and the
highlight sits on GHex while Slack is row 1. Upstream rofi 2.0.0, not our
config: listview_set_num_elements() re-asserts the OLD index —
`listview_set_selected(lv, lv->selected)` — and set_selected only clamps
(`MIN(selected, req_elements - 1)`). So the ROW NUMBER survives a
refilter, never the entry, and nothing checks it still matches the query.
Typing more hides it; backspacing exposes it. No option controls it
(auto-select is a different feature). Correction to the report: it is the
listview widget, so every rofi surface does it — drun is just where the
list is long and (sort = true) reorders each keystroke.
#144 — "I lost all my thunderbird accounts and emails" was a missing
profiles.ini. The profile was untouched: 30 GB, 64 account prefs, 19 GB
ImapMail, last used 2026-06-26; TB found no profile list and made an
empty one. Fixed live in ~8 lines (backup kept). MIGRATION.md already
warns about this exact class for Chromium extensions but never names
Mozilla's profiles.ini — which deserves its own bullet because it is the
inverse: no data lost, looks catastrophic, and the tempting fix (restore
a snapshot over a good 30 GB profile) is the actual risk. Firefox here
has the same fingerprint (hm-bak from Jun 2025 names a different profile
than today's ini) — left alone; repointing it is Bernardo's call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo saw Recovery ellipsize on a 2560x1440 external, which falsified
the item's model (40% + Inter 11 → "only below 1920"). Real cause: text
menus render through themes/<slug>/rofi.rasi, where boreal + neon-glass
pinned width: 620px (fixed — a wide panel buys nothing) while seven used
40%, and the font is whatever that file says — mostly monospace, far
wider than the Inter that was measured. The modelled combination ships in
no theme.
The insight: a menu must fit its longest label, which is a count of
characters in the THEME's font — so the window must scale with the font,
not the screen. 40% gives a 1366 panel 546px and a 2560 one 1024px for
the same row, and a 14pt mono theme needs ~25% more room than an 11pt one
on both; a percentage cannot see either fact.
Rofi has the right unit (`ch` = width of one digit in the current font)
and `calc( a min b )` to cap it, so every text menu — generated and all
nine whole-swaps — is now `width: calc( 84ch min 65% )`. 84ch fits the
longest row we ship (69 chars) plus icon and padding; the cap is
Bernardo's condition (never sprawl on a low-res panel) and it is
measured, not assumed.
Verified on hardware, not by arithmetic: 84ch = 756px in GeistMono 11 and
924px in JetBrainsMono 14; `calc( 84ch min 300px )` → 300px, so the clamp
really clamps. Screenshots at both ends — the real Recovery menu at 756px
(29% of 2560) with every label complete, and the Acer worst case
reproduced pixel-exactly (888px = 65% of 1366, JetBrainsMono 14) also
complete. Grid views override width per-invocation and are untouched.
checks.rofi-text-width guards the class, proven by pinning boreal back to
620px and watching it fail by name. Acer V3 queued for its own fontconfig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo saw the Recovery labels ellipsize on a 2560x1440 external, which
falsifies both halves of this item's model (width: 40% + Inter 11 →
"only below ~1920, confirm on the Acer"):
- text menus render through themes/<slug>/rofi.rasi, and boreal (his) +
neon-glass pin `width: 620px` — fixed, so a wide panel buys nothing;
the other seven use 40%. launcher.rasi's 780px is a red herring (it
themes -show drun only).
- the font is whatever that file sets — boreal GeistMono 11, summer/
kanagawa JetBrainsMono 14, nord/lumon/retro-82 13 — mostly MONOSPACE,
much wider than the Inter the item measured. Only the generated theme
follows fonts.ui.
- so the modelled 40% + Inter 11 is shipped by no theme, and truncation
is most theme x monitor pairs, not an edge case.
Measured for boreal (9.33px/char off kitty at the same font/size, 518px
room): 3 of 4 rows cut, "What changed" fits — "some of the submenus",
exactly as reported.
Names the decision instead of taking it: fitting the narrowest combo we
ship (40% @ 1366 + JBMono 14) is ~39 chars, which cannot hold scope AND
cost — so "just shorten the labels" would gut #111 for everyone on a big
screen. Options: (a) width in `ch` so the window scales with the font
(fixes the class, 9 theme files), (b) cost to a rofi -mesg line, (c)
shorten and lose the hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#139 — calcurse/doctor floated full-screen. Two faults stacked, which is
why it looked like the float rules were dead:
1. Hyprland 0.55.4 silently ignores percentage `size` rules. Matrix on
hardware (both orders, kitty memory off): `size 60% 65%` never
applies, `size 1536 936` applies either way, configerrors empty both
times. Rule order was a red herring.
2. Kitty defaults to remember_window_size=yes and replays the last OS
window's size into every float — so after a tiled terminal, a sheet
opens maximized. This is the Ghostty regression: Ghostty had no such
memory, so fault 1 stayed invisible until #95.
Fix: remember_window_size=no (floats must be deterministic) and the sheets
ask for their own size — term-sheet.nix reads the focused monitor and
hands kitty the px, because px in a rule cannot mean "a fraction of *this*
screen" and one pair cannot serve 2560x1440 and the 1366x768 Acer (#131).
Those windows keep float/center and deliberately carry no size rule.
#141 — the updates click did nothing: whole-swaps hand-wrote
`sh -c '$TERMINAL …'` and Waybar has no TERMINAL (home.sessionVariables →
login shells only; the bar is spawned by Hyprland). Fixed at the root:
`nomarchy-updates upgrade-window` opens its own window, so all five call
sites name one env-free command and a theme file stops having an opinion
about terminals. checks.waybar-swap-env guards the class.
Bernardo asked mid-task for the update window to float — it is now the
third caller of the sheet helper (com.nomarchy.updates, 45%x50%).
V3 on the dev box: calendar 1536x936, doctor 1408x1008, updates 1152x720
— exactly the intended fractions, floating and centred; #141 driven under
`env -i` with no TERMINAL, matching Waybar's real environment; the guard
proven by reintroducing the bug. Acer V3 queued.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo: every rebuild while docked re-lights the panel and throws a
workspace onto it; he re-picks Dock mode by hand. Static diagnosis, three
steps, each already proven by our own code:
- dock mode is runtime-only (`keyword monitor <internal>, disable`,
hyprland.nix:205) and absent from state.json — the one display setting
outside the in-flake state;
- the config's only monitor line is the catch-all `,preferred,auto,1`, so
a reload re-enables every output. The undock path already uses `hyprctl
reload` AS the hammer that re-enables a disabled panel (hardware-proven
2026-07-14) — so a rebuild performs an undock by construction;
- reconcile diffs `monitors all`, which lists disabled monitors, so the
set is unchanged across the flip and the watcher never sees it. Blind
spot of round 4/5's "the output set is the truth".
Notes the trap: baking the disable into the config is a zero-output
session at login (the #100/#127 brick). Persist intent, re-assert from
the watcher, gate on an external being present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo's Meet calls lost their microphone because our own dock-audio
reprobe restarted PipeWire/Pulse/WirePlumber on every monitoradded (#100).
Chromium's audio service does not reconnect when the server disappears
under it, so it kept running and enumerated nothing; Zoom, started fresh
into a settled graph, looked fine and made it read like a browser bug.
reprobe now escalates cheapest-first — select → repair a parked card's
profile → restart the graph — so an ordinary plug never touches the graph
and no client loses its connection. wait_for_dock_sink polls for the sink
the plug is about to publish, so "not yet" is not mistaken for "not ever"
and cannot fall through to the restart. The restart stays as rung 3 for
the codec that only publishes its route after a re-probe: the one claim
the original comment made, and the only one it never demonstrated.
Verified on the dev box against the live graph: rung 1 selects the same
sink the restart-first path picked (HiFi__HDMI1__sink) in 2.0s instead of
~3.5s with pipewire/pulse/wireplumber MainPIDs unchanged; rung 3 reachable
and restarts+retries. Diagnosis proof was ordering across four boots —
restart-after-Chromium in both broken sessions, 24s before it in the
healthy one — and Bernardo reproduced on demand with a dock/undock.
Sweep: BACKLOG #138 deleted, ROADMAP gets the design record (incl. the
stale restart-first sentence in the #100 entry), V3 queued on the dev box
(relogin first — the watcher is exec-once), journal entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo's hypothesis (pipewire restarts, Chromium never reconnects) is
right, and the journal names the restarter: dock-audio.nix:153 runs
`systemctl --user restart pipewire pipewire-pulse wireplumber` on every
monitoradded (#100). Chromium's audio service does not reconnect, so it
enumerates nothing — Meet's "no mic or speakers".
Ordering across four boots is decisive, and Bernardo asked for exactly
this check: restart-after-Chromium in both broken sessions (boot -3
14:06:49 → 14:07:52; boot -2 11:10:34 → 11:31:22), restart-BEFORE in
today's working one (08:04:19 vs 08:03:55, a 24s miss). Hibernate is not
the cause, only how a Chromium lives long enough to meet a plug event.
Retiers off [human] — no repro needed, and it predicts a live one:
dock/undock now should break Meet in this session. Fix is ours, not
Chromium's: the graph restart is inherited from "the old working flake",
never shown necessary, and repair_dock_cards already handles the parked-
card case without it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo's 2026-07-16 observations, filed with the diagnosis done on the
machine rather than as symptoms:
- #137 plymouth: unindexed Window.Get* → whole splash placed on head 0's
geometry; off-centre on any second monitor (LUKS prompt included).
- #138 [human] Meet claims no mic/speakers: audio graph + Flatpak sandbox
ruled out live; needs the browser named and the failure in front of us.
- #139 terminal floats full-screen: % size rules silently no-op on
Hyprland 0.55.4 (absolute px applies), and Kitty replays its remembered
window size — the second half is the Ghostty regression from #95.
- #141 waybar updates click dead: whole-swaps use $TERMINAL, which is
absent from Waybar's env; 4 jsonc lines, a parity break not a bug.
- #140 PROPOSED: Ask Claude → web chat instead of the claude-code REPL.
Also gives #127 its "if diagnosis fails" branch: a screensaver while
docked, which is the panel-protection consolation prize, not a new item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewing 060bf52 at Bernardo's request. The call is right — the failure
mode is a brick (no wake, no VT, hard power-off with the btrfs risk that
carries), so prevent-over-recover beats a lit panel. Three things it leaves
open, now tracked instead of remembered:
#135 — the mitigation is hardcoded policy with no way back. Docked externals
never blank and hyprlock has no timeout, so they stay lit indefinitely:
~60-100W nightly and OLED burn-in from a near-static lock screen. Add
settings.idle.dpmsWhenDocked (default false = today's behaviour), mostly so
the mitigation has a named surface to DELETE once wake is proven, rather
than a LATER note that quietly becomes the product. Ordered after the
hardware test: if the mitigation is withdrawn, the key never exists.
#136 — the gate is clamshell, not "docked". It skips only when an internal
is present-but-not-enabled; lid-open docked still falls through to dpms off.
Commit body, BACKLOG and journal all claim the broader "docked". Either the
scope is deliberate (fix the wording) or the mitigation misses half the
docked cases. Bernardo's call; the hardware test settles it.
Also recorded in #127 the caveat that outranks both: the fix is a cure for
an UNCONFIRMED cause. If DPMS-off is not the trigger, we hold the cost and
still have the brick.
No code change — #127 stays [blocked:hw] pending the hardware run.
Bernardo wants a newer LM Studio and rejected the only mechanism that works
today (a hand-pinned fetchTarball rev+sha256 in home.nix — verified working,
but a second unlocked pin and exactly the Nix expertise the distro exists to
spare its users). He is taking stable 0.4.15-2 for now.
Records why there is no seam (mkFlake takes src/username/hardwareProfile/
system; homeConfigurations uses mkFlake's own pkgs, so a system.nix overlay
cannot reach a home package), the sketch he asked for (unstable.lmstudio,
maybe stable. as an explicit synonym), and the shape that keeps the one-input
promise: carry nixpkgs-unstable upstream and expose it via overlays.default,
so downstream stays single-input and locked.
Costs left open for triage, not decided: two-channel lock churn against the
full-checklist rule, closure duplication (measured: the two lmstudio build
closures share 690 of ~3.7-4k paths), the weakening of "tested together
upstream", whether an unreferenced input should lock at all, and whether the
seam covers system.nix too. Cheaper fallback noted (mkFlake `overlays ? []`).
PROPOSED per BACKLOG rules — awaiting triage, not implemented.
Verifying Bernardo's #107 upgrade path (legacy theme-state.json checkout →
new code) turned up a footgun in state-read.nix's own help text. It said:
• Still on theme-state.json? Rename it to state.json (…)
On a git flake only tracked files exist, so a hand-rename leaves state.json
untracked and therefore invisible — producing exactly the "state file is
missing" error the tip hangs off. Point at the menu write (which stages the
rename via git add --intent-to-add + git rm), name `git add state.json` for
the manual path, and add a bullet for "file is on disk but eval says
missing", which is otherwise a genuinely confusing dead end.
Message verified by rendering the real failure, not by reading the source.
The rename itself is safe and needs no code change: with identical content,
theme-state.json and state.json produce byte-identical derivations (HM
2yqqy8h…, system 9qp9l3g…), so it cannot change what boots. Legacy eval,
the migration write, and re-eval on a dirty/intent-to-add tree all pass.
#133 filed: no check pins the compat shim, so it can rot before we drop it.
856a9d1 restructured the picker and shipped at "V0 menu-back; HM gen builds",
which §2.1 does not allow for a user-visible change. Rendered all eight
screens under the real HM session and viewed them.
IA verified: six-entry root; System → Connectivity/Devices/Recovery/
Preferences; Recovery lists each scope once; Preferences carries the migrated
update-checks/Bluetooth/Printing toggles; Control Center gone from the
dispatch (asserted, not eyeballed); no stderr on any screen. No code change.
Two findings, both filed rather than fixed:
#131 — the VM showed the long Recovery labels ellipsized. That specific
symptom is a guest artifact (they measure 462px/509px in Inter 11 against
~668px of room at 1920), but the same measurement says they DO truncate at
1366, where the picker is only 546px wide — cutting exactly the cost hint
#111 added them to carry. The Acer Aspire M5-481T is 1366x768 and in the QA
fleet. Shortening vs -mesg vs widening is a design call.
#132 — the guest draws icons at ~1/4 of ui.iconSize and clips a 6-row root
behind a scrollbar despite lines=8. Root predates #105, so this is the test
environment, not a regression — but it means menu screenshots are evidence
for content and never for geometry. Documented in docs/TESTING.md §5 so the
next agent doesn't chase it.
e9dd3d1 pinned dialog button labels to base00 for dark palettes. That is
correct for suggested-action, whose background adw-gtk3 builds opaquely as
mix(white,@accent_bg_color,0.9) — but destructive-action is the one class
whose background is derived from currentColor:
background-color: mix(@destructive_color, alpha(currentColor, 0.15), 0.9)
so pinning the label dragged the background down with it. On Boreal the
"Delete" button of a GtkMessageDialog rendered #21272F on #252931 —
1.03:1, invisible — and light palettes hit the same 1.03:1. The most
safety-critical button in a confirmation dialog was the one that vanished.
Pin the destructive background to the same opaque pastel ladder
suggested-action uses. Enabled-only (:not(.flat):not(:disabled)): the
sheet's disabled chrome already renders legibly and must keep it.
Boreal 1.03 -> 6.19:1, summer-day 1.03 -> 2.74:1 (= upstream's
white-on-accent convention; the residual light-mode weakness is #130).
V2: GtkMessageDialog rendered under the real HM session and viewed —
boreal before/after, summer-night (whole-swap), summer-day (light).
Scripted checks stayed green throughout and never saw this; only the
render did. Follow-ups #129 (permanent guard) and #130 filed.
Blanking the only live output in dock mode led to a brick where wake and
even VT switch failed. Skip hypridle's 10m dpms off when any laptop
internal (eDP/LVDS/DSI) is present but not enabled; lock at 5m remains.
Undocked still blanks. Document LATER revisit for intentional
DPMS-when-docked once wake is proven.
V1: nix flake check --no-build. V3: docked idle stays lit under lock.
Recover from DPMS/lock blackout without a second machine: undock/enable
always issue dpms on; hypridle on-resume and after_sleep run
nomarchy-display-wake (zero-output undock rescue + auto-dump).
nomarchy-display-dump writes ~/nomarchy-display-dump-*.txt (SUPER+SHIFT+D
or Ctrl+Alt+F3 TTY). HARDWARE-QUEUE #127 no longer requires SSH.
V1: nix flake check --no-build. V3: docked idle on AMD box still open.
Human decision: 1h delay, battery only, Preferences enable/disable.
Wire systemd suspend-then-hibernate + HibernateDelaySec=1h +
HibernateOnACPower=false when settings.power.suspendThenHibernate is on
and boot.resumeDevice is set. Lid: s2h undocked, plain suspend on AC.
nomarchy-suspend for hypridle + Power menu (live state). LUKS unlock
before encrypted hibernate so s2h→disk is not double-password.
state-bridges assert the policy; HARDWARE-QUEUE #115 + suggested order.
V1: nix flake check --no-build. V3: bag-carry on hardware.
Stylix always used the light adw-gtk3 sheet with recolored tokens; that
sheet hardcodes near-black button labels, which became black-on-dark in
Boreal GTK dialogs. Force adw-gtk3-dark (and prefer-dark) for dark mode,
and pin dialog button label colours in Stylix extraCss.
RECOVERY §4 documents the 14-day / keep-≥3-past policy for system and
Home Manager gens, with dry-run and timer commands. README and
REQUIREMENTS point here so install and day-2 hygiene match the product.
Weekly nomarchy-gen-prune deletes only generations that are both older
than 14 days and beyond the three most recent past gens (current always
kept), for system and Home Manager profiles. Stock nix.gc no longer uses
--delete-older-than. checks.gen-prune covers selection self-test and
unit wiring; doctor points at the new tool when disk is tight.