#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>
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.
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.
Policy: remove system generations older than 30 days only when they are
also beyond the three most recent past gens, so rare rebuilds never lose
rollback depth. Notes existing 14d nix.gc has no count floor.
System is Connectivity / Devices / Recovery / Preferences under the same
six-entry root. Recovery lists desktop (HM), system boot, and BTRFS files
once each with cost labels. Control Center is gone: blur/gaps live in Look
& Feel; update checks and Bluetooth/Printing package toggles in Preferences
(with rebuilds). docs/RECOVERY paths updated.
Airplane: runtime Wi-Fi+Bluetooth kill-switch with prior-state restore,
System menu + SUPER+CTRL+R, Waybar plane glyph (self-hides, whole-swap
parity). Fastfetch: chafa sextants at 24×12 (was block 20×10) for a
finer monogram; V2 kitty screenshots under boreal and summer-night.
The machine flake's git-tracked settings file is system state, not
"theme" only — rename it to state.json. CLI becomes nomarchy-state-sync
with a nomarchy-theme-sync symlink for scripts and muscle memory.
Eval (mkFlake, doctor, lifecycle) still accepts theme-state.json; the
next write migrates to state.json and removes the legacy file.
Documented in MIGRATION.md; drop the CLI alias after release notes.
Re-boot of the offline install disk proves the residual #94 pass:
chromium on PATH, https/html mime → chromium-browser.desktop, and the
browser window actually opens. Kill ecosystem update-news / donation nag
so first boot is not Hyprland's "updated to …!" dialog.
Bernardo 2026-07-15: docked idle → black screens; external KB/mouse did
not wake; unplugging the external did not restore eDP; Caps Lock still
worked. Captures the "session crash" as a seat brick. HARDWARE-QUEUE
repro + journal steps for the next controlled run.
Every bind in keybinds.nix carries a group; SUPER+? renders section
headers. checks.keybinds-cheatsheet asserts all groups are valid and
every description appears in the sheet (same source as Hyprland binds).
Verified: V0 flake check; built cheatsheet shows ── Window ── / etc.
Bernardo: live/install ISO artifact is nixos-live-….iso (image.baseName);
live VM Display picker appeared to invert higher/lower modes — investigate
sort/apply vs QEMU scaling before fixing.
Ghostty's OpenGL 4.3 floor broke the Acer (HD 4000 / 4.2). Dual-terminal
support (default Ghostty + install-time Kitty fallback) was more complexity
than it was worth.
- Remove ghostty.nix / nomarchy.ghostty.enable
- Kitty always installed and themed from theme-state
- Default nomarchy.terminal = kitty; SUPER+Return, doctor, calendar,
what-changed use kitty --class=com.nomarchy.*
- Drop install-time glxinfo probe and mesa-demos on the installer
- Docs/REQUIREMENTS no longer list OpenGL 4.3 as a terminal floor
Verified: V0 flake check; option-docs; installer-safety; template HM has
kitty, no ghostty; menu/calendar emit classed kitty launches.
Ghostty needs OpenGL 4.3; older GPUs (Intel HD 4000) top out at 4.2.
Installer probes glxinfo and writes settings.terminal=kitty into
theme-state when below the floor. modules/home/kitty.nix enables Kitty
with the same palette, mono font, and opacity as Ghostty so the fallback
still looks like Nomarchy.
Default machines stay Ghostty-only (no kitty in the HM closure). Doctor
and calendar still invoke Ghostty by class — V3 on Acer for SUPER+Return.
Verified: V0 flake check; installer-safety; HM with terminal=kitty builds
themed kitty.conf + TERMINAL=kitty; default template has no kitty bin.
Acer M5-481T fails Ghostty because Ivy Bridge HD 4000 tops out at
OpenGL 4.2 in Mesa; Ghostty needs 4.3 — a generation ceiling, not a
missing driver. Document that plus UEFI/x86_64, RAM, and storage floors
(Nix store generations + BTRFS snapper + hibernate swap≈RAM). Linked
from README, docs map, HARDWARE.md; #95 points here.
Bernardo: XPS starts Ghostty. Aspire only fails — first attempt before
home built (OpenGLOutdated), second after home built (quiet GDK_DISABLE
exit). Kitty was the manual working shell used to run both.
Photo A (Acer): OpenGL 4.2 → OpenGLOutdated / SurfaceError.
Photo B (XPS): silent exit after GDK_DISABLE=gles-api,vulkan with no
OpenGL line — different machine, different root cause. A fix for only
one is not done.
Real installs on Acer Aspire M5-481T and Dell XPS 9350 (2026-07-14):
- #123: HM pre-activate dies at dconf without XDG_RUNTIME_DIR on both
machines — partial first boot (weird GTK + missing nm-applet are
symptoms of the incomplete bake).
- #124: install seeds ?ref=v1 while writing main-era nomarchy.hardware.*,
and v1 lags hard enough that rebuilds die with "option does not exist".
- #95: Ghostty on the Acer is OpenGL 4.2 < required 4.3 (OpenGLOutdated);
XPS starts Ghostty fine.
HARDWARE-QUEUE V3 rows; photos stay local (.gitignore WhatsApp Image*).
A singleton default naming code.desktop left the live ISO with no text
handler — GIO skips missing .desktop files and offers nothing. Prefer
vscode when present, then org.gnome.TextEditor.desktop. Live also gains
mpv (video defaults had the same hole; free via the template pin).
checks.live-baseline-apps now requires every Default Applications key
to resolve to a present .desktop on live (HM + system path) and on the
default template install. Proved to fail on a vscode-only text/plain.
Verified: V2 (live-baseline-apps green + prove-to-fail); V0 flake check.
Bernardo asked whether #122 was quick, and then caught the reason my first
answer was wrong: the mono face is per THEME (ghostty.nix:22 reads
t.fonts.mono from the theme JSON), and Boreal — the default theme and the base
state — is GeistMono Nerd Font, not stylix's JetBrains Mono. I had probed the
wrong font, then drawn a confident conclusion from a single charset.
Both conclusions inverted on measurement, so the item now says the opposite of
what it said an hour ago:
* GeistMono covers no sextants, no braille — and no block elements or box
drawing either. So the CURRENT logo already renders through fontconfig
fallback on the default theme, and always has. Fallback is the normal case
here, not the hazard I painted it as.
* Fallback supplies sextants fine: fc-list :charset=1fb00 finds 37 families
on a real Nomarchy box, including the shipped nerd-fonts.caskaydia-cove and
noto-fonts' Symbols 2; fc-match under GeistMono picks CaskaydiaCove. No
tofu. Braille: 59 families.
So --symbols sextant is available and is roughly a one-flag edit for 6x the
detail — rendered both variants to confirm the shape genuinely sharpens. The
item is re-scoped accordingly: the edit is minutes, the WORK is verification.
Two things can still make it worse and only a real terminal can say — fallback
metrics (CaskaydiaCove's sextants drawn in GeistMono's cell may misalign or
differ in stroke weight, exactly the seam that looks fine in a VM and wrong on
a laptop), and the mark being diagonal-heavy, since diagonals staircase under
any cell symbol: more resolution buys a finer staircase, not a straight line.
If the mark itself wants straight strokes that is a logo.svg design change
hitting all three surfaces, not a chafa flag — settle that first.
V0: nix flake check --no-build passes. Backlog only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bernardo 2026-07-14: the fastfetch logo should be crisper, with straight lines.
Filed with the cause rather than the wish. modules/home/fastfetch.nix converts
the vector mark with `chafa --symbols block --size 20x10`, and that symbol class
is the chunkiness: restricted to full/half blocks, a 20x10 grid carries ~20x20
pixels of detail, so every diagonal staircases. chafa 1.18.2 ships sextant (2x3
subcells), quad and wedge — verified present today, so the cheap lever exists.
Named the trap that decides the approach, because trying it first is what
prevents a wasted round: sextants are Unicode 13 Symbols for Legacy Computing,
and if the shipped terminal font lacks them the logo renders as tofu — strictly
worse than chunky. So font coverage in the real Ghostty gets checked before a
symbol class is chosen, and if it is missing, hand-authored box-drawing art is
the honest alternative (and the only way to get genuinely straight lines; any
image→symbol converter approximates).
Also recorded that the three logo surfaces are different renderers — fastfetch,
the ISO splash, the Waybar glyph font — so this item touches one of them, while
a change to the shared logo.svg would hit all three and should be split.
Visual change, so THEME-DESIGN and the honesty rule apply: pass needs
screenshots viewed under two themes, no tofu, accent tracking intact, and the
fastfetch row grid still aligned.
V0: nix flake check --no-build passes. Backlog only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BACKLOG #118. Bernardo booted the live ISO and the Waybar health icon was red,
reporting smartd. Everything downstream turned out to be working correctly,
which is the part worth recording: smartd's config is DEVICESCAN, and where no
drive answers SMART it exits 17 ("Unable to monitor any SMART enabled
devices"), systemd marks the unit failed, nomarchy-doctor faithfully reports a
failed system unit, and Waybar paints @bad. The doctor was telling the truth.
smartd was the bug.
Scope was never live-only, which is why this sat in NOW rather than as a live
nit: services.smartd.enable mkDefaults true on every machine and QEMU virtio
exposes no SMART, so every VM install has been booting to a health warning
about a daemon with nothing to do — and every V2 run had been showing it as
noise.
Fixed with the distro's own self-gate convention: an ExecCondition running
`smartctl --scan`, which prints nothing exactly when smartd would find nothing.
A failed condition leaves the unit inactive rather than failed. Deliberately
NOT SuccessExitStatus = 17: that would also swallow exit 17 from a machine that
does have drives, which is the entire reason the daemon ships.
V2. checks.smartd-gate boots the REAL distro module rather than a restatement
of it (its nixpkgs.config needs mkForce to yield to the test's pkgs) and
asserts both halves, because they pull in opposite directions: a gate that
never skips leaves the red icon, and a gate that always skips silently disables
drive-health monitoring on real hardware — the failure nobody notices until a
disk dies quietly. So the no-SMART node must go ActiveState=inactive, unfailed,
and absent from `systemctl --failed` (what the doctor actually reads); and the
gate's logic is driven against a scan that DOES find a device, since QEMU
cannot answer SMART honestly and pretending otherwise would test nothing. The
check was proved to fail by unwiring the condition. flake check, doctor,
hardware-toggles, live-baseline-apps, option-docs and state-bridges pass.
V3 pending: that smartd still RUNS where drives have SMART (dev box, real
NVMe). Queued with an explicit fail-condition — if it skips there, revert the
gate rather than tune it.
Also swept: #120's size table said the duplicate chromium was gone; #121 was
reverted, so it is back and the table says so.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>