Revert the temporary 17:39 test time now that the manual run has been
kicked off; keep the daily cadence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move bump.yml's schedule from Mondays to every day (temporarily 17:39
UTC to trigger a run shortly). Sync the ROADMAP cadence note + shipped
log to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bare `button, button label` selectors in the #98 safety net load
into every GTK3 process, including Waybar, and a rule matching the label
node outranks the color waybar.css inherits onto `#workspaces button` —
so workspace digits always rendered the gtk.css text color captured at
bar start, never the theme's @muted dots / @base-on-accent active pill
(the pair check-theme-contrast.py gates). With the bar never restarting
on switch (previous commit) the pin also went stale: all three 2026-07-18
screenshots carried kiln-clay's @text #39302A across three dark themes.
Scoped the enabled+disabled pins to the dialog selectors #98 actually
targeted; forcing adw-gtk3-dark remains the primary #98 fix, and the
sheet's own bare `label` rule sets only caret-color, so inheritance
carries the bar colors again.
Verified live on Newton (V3-grade): under the scoped gtk.css, boreal
renders the designed digits — @base #21272F on the @accent pill, @muted
#5E6A78 inactives — pixel-sampled from a bar spawned on a headless
output. V1: HM generation builds; bare selectors absent from the built
gtk.css, dialog-scoped ones present. Journal, hardware-queue (deploy
round-trip) and ROADMAP follow-up ride along.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nixpkgs wraps waybar, so the running comm is `.waybar-wrapped`:
`pkill -x waybar` (state-sync restart/SIGUSR2 branches, tz-watch,
updates) matched nothing — a theme switch neither restarted nor reloaded
the bar, which then kept the gtk.css of whatever theme it started under
(the stale workspace-digit colors reported 2026-07-18; companion gtk fix
in the next commit). The unanchored pokes (recording, airplane,
theme-shot) had the opposite bug: `waybar` substring-matches the
supervisor's `nomarchy-waybar` comm too, and an unhandled RTMIN
terminates its bash — the pokes were killing the crash guard. All seven
sites now use `pkill … -x 'waybar|\.waybar-wrapped'`.
state-sync 0.5.1 → 0.5.2.
Verified live on Newton (V3-grade): new pattern restarts the bar under
the supervisor (fresh pid, supervisor alive); RTMIN+10 poke reaches the
bar, supervisor survives. V0 `nix flake check --no-build` + py_compile;
V1 HM generation + system toplevel build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The timer's OnCalendar now bakes the configured times (falling back
like _hhmm_to_min on a hand-edited state), so the switch lands at the
configured minute instead of up to 15 minutes late. The original
poll's robustness case is covered without it: Persistent catches
transitions missed while powered off, systemd fires elapsed calendar
timers on resume, and OnStartupSec settles the theme after login.
Baked times mean a time edit must rebuild: the menu's Sunrise/Sunset
writes now run `auto --force` when enabled (one rebuild re-bakes the
timer and lands on the right theme, same as the enable flow). Day and
night slugs stay live reads — but with no poll to catch them up,
editing the slot currently on screen applies immediately via a plain
`auto`. New eval check `auto-theme-timer` asserts the bake
state-bridges-style: configured times reach OnCalendar, garbage falls
back to 07:00/20:00, disabled installs no unit. Docs + hardware-queue
entry updated to the new semantics.
V2: checks.auto-theme (KVM VM, passed) covers the auto engine the
timer ticks; checks.auto-theme-timer (eval, passed) covers the unit;
OnCalendar strings systemd-analyze-validated; the rebuilt menu script
passes bash -n with the new branches in place. V3 pending: live timer
firing at the exact configured minute on hardware (existing
"Auto-theme pair flip" queue entry, refreshed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since d8e1a13 removed theme-state.json, auto_commit's fixed pathspec
(state.json + both legacy names) made `git commit` abort — unlike
`git diff`, commit errors on a pathspec that matches nothing — so every
per-mutation settings commit was silently "skipped" and state.json
stayed dirty until a lifecycle sweep caught it. Filter the legacy names
down to those git still knows (index or HEAD — a migration's staged
`git rm` still rides along). checks.auto-theme now runs its state dir
as a git repo with autoCommit on and asserts each set/apply lands as
its own commit leaving nothing dirty — the exact post-#107 repo shape
that used to abort. state-sync 0.5.0 → 0.5.1.
V2: nix build .#checks.x86_64-linux.auto-theme (KVM VM run, passed,
including the new git assertions); the built tool was also exercised
against a clone of a real post-#107 machine flake — the set
auto-committed and the tree stayed clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wallpapers repo's 'updates' commit gave each of the three v1 pair
themes a second background (1-/2-<slug>.png, replacing the single
ember-kiln/clay-kiln era files), but this lock still pinned the
one-wallpaper rev — downstream machines re-lock the transitive
nomarchy-wallpapers input from *this* lock, so SUPER+SHIFT+T
('bg next') cycled a one-item list and visibly did nothing
(hardware report, kiln-clay, 2026-07-18).
Targeted bump of the wallpapers input only — runtime-only content,
never read at eval (see flake.nix). Verified: pkgs.nomarchy-default-themes
builds, which runs the per-slug drift check (every theme json still has a
non-empty backgrounds/), and kiln + kiln-clay now list two files each.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shadow color is the palette mantle: near-black on dark themes (a
normal ambient shadow) but a light warm tone on light ones, where the
20px range reads as a wide glow that bleeds into neighbouring windows
and the bar (hardware report, kiln-clay, 2026-07-18). Light mode now
defaults the range to 5px — the glow stays as an identity feature,
just tight; dark themes keep the soft 20px shadow. Still lib.mkDefault,
so downstream overrides keep working.
Verified V1 (generation renders range=5 under kiln-clay); on-hardware
before/after of the live desktop taken in the reporting session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 dev-box incident: on a warm relogin (user manager kept alive
by the tty1 greetd session — no linger involved) the previous logout's
graphical-session.target stop had been rejected as a destructive
transaction (easyeffects held a queued start job), so the target stayed
active across the logout. Its WantedBy services (cliphist, swaync,
xdg-desktop-portal-*, swayosd, …) crash-looped against the dead Wayland
socket into start-limit-hit, and the next login's plain
'start hyprland-session.target' was a no-op — every session service
stayed failed, doctor red, until a reboot.
Session bring-up (HM's systemd.extraCommands, rendered into the first
exec-once) now stops both hyprland-session.target and a possibly-stale
graphical-session.target, clears failed/start-limit state with
reset-failed, then starts fresh. All three commands are no-ops after a
clean logout or cold boot.
Verified V1: home generation built against this tree renders the new
command chain into hyprland.conf line 1 (nix build of
homeConfigurations.bernardo with the nomarchy input overridden).
V3 pending: warm relogin on hardware — queued in HARDWARE-QUEUE with
exact steps; BACKLOG #149 updated with the incident evidence (the
exec-once posture it watches is defending against a real race).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two hardware-reported contrast bugs, both the same family: surfaces
that fell back to something designed on dark.
1. Inactive workspace numbers unreadable on every light theme.
- Generated bar: the @muted dim role is floor-gated at only 2.0:1 —
fine as "dim" on dark, washed out at number size on light. Light
mode now promotes inactive buttons to @subtext (3.0 floor,
typically 4-7:1); dark themes keep @muted.
- boreal-dawn + kiln-clay whole-swaps: same @muted -> @subtext
(6.4:1 / 6.6:1).
- summer-day whole-swap: had NO button color rule at all — the
container's @fg does not inherit into GTK buttons, so the numbers
took the GTK default over the cream chip. Explicit @fg_dim
(= the palette's subtext, 4.0:1) added.
- executive-ivory already used @subtext; untouched.
2. Notification popup unreadable on hover (bg turns dark, text stays
dark). Root cause found by probing, not guessing: the popup body is
one big GTK button (.notification-default-action) and swaync's
DEFAULT stylesheet — still loaded under our style.css — hovers it to
rgb(56,56,56). Our sheet never overrode that node (only the
control-center row), so the default bled through; on light themes
dark @text sat on it. Now the body + action buttons + close button
are re-pinned to the palette's alpha(@text) tint construction —
mode-proof by construction. The first fix attempt (row-level hover)
was probed, SHOWN insufficient, and replaced.
Verification: V2. nix flake check --no-build, theme-wholeswap,
theme-contrast green. §3 evidence viewed: a purpose-built VM hover
probe under summer-day (notify-send -u critical, cursor parked ON the
popup, screenshot) — before: dark card, drowned text (reproduces the
report exactly, /nix/store/q1qf67fl...notify-shot-summer-day); after:
cream card + faint tint, fully readable (shot-notify2). Bar renders
verified under catppuccin-latte (generated light path incl. the mode
branch) and summer-day (edited whole-swap). An inactive-number visual
needs two live workspaces — VM shows one — so the on-hardware
confirmation is queued (HARDWARE-QUEUE light-theme refits entry);
the ratio change itself is machine-checked.
Bookkeeping riding along: BACKLOG #152 deleted (shipped in d79ad35).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generated bar (every non-whole-swap theme; whole-swaps untouched):
- The active-workspace highlighter filled the bar's full inner height
and visually overhung the rounded border. The pill now gets a 3px
vertical inset margin (the boreal whole-swap's proven pattern) and
the bar grows 34 -> 36 so the inset doesn't cramp the number.
- The end modules sat hard against the bar's rounded corners (the
power button ~8px from the right curve vs ~28px inter-icon gaps).
The Nomarchy mark and the power button get wider outer padding
(14/16px); the item-28c group rhythm is preserved.
Verification: V2. nix flake check --no-build green. §3 protocol:
before/after theme-shot under kanagawa (generated palette) viewed at
2x crops — before: pill overhangs top+bottom, power glyph kisses the
corner; after: pill fully inside the bar, clear end gap
(before /nix/store/iwrvzzdd...theme-shot-kanagawa, after
/nix/store/za...see scratchpad shot-kanagawa-after.path). No-regression
capture under the summer-night whole-swap viewed — its chip identity
unchanged (whole-swaps replace both style and layout, out of scope
per the item). BACKLOG #152 closed: deleted per the finished-items
rule in the next bookkeeping touch of this push.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream gave executive-slate the boreal-style 4x3 icon-grid launcher
while this batch was in flight; the ivory day sibling follows in the
same push so the pair stays shape-invariant. Geometry identical to the
slate grid (pure layout over the ivory rofi.rasi panel; the selected
card keeps the accent-edge highlight).
Verification: V1 here (layout-only file, colors inherited) + the
re-run checks on the rebased tree in this push; launcher grid on real
hardware is already in the batch HARDWARE-QUEUE entry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the three dark/light pairs for v1 (batch: 0ba1762, e51c8a7).
Kiln is a potter's workshop at night, lit only by the kiln: warm
near-black (#1D1815), copper accent (#D68C5C, 6.5:1), brass accentAlt,
CaskaydiaCove, rounding 8. Kiln Clay is the same studio at midday:
cream plaster (#F4EDE3), espresso text (11.1:1 AAA), fired-copper
accent (#A34F1F, 4.89:1), brick-red bad. Both waybars are a NEW
whole-swap identity — 'the workbench': ONE continuous floating plank
(window#waybar carries the surface, modules transparent) — deliberately
distinct from boreal's three pills and slate's edge-to-edge strip;
module set verbatim-parity with the generated config. Matched rofi,
btop themes. The wallpapers share identical ring geometry — banked-coal
heat rings at night, terracotta glaze rings at noon — so the autoTheme
flip visibly rhymes.
Sync sweep in this commit (LOOP §5): README 24->28 presets + the
whole-swap list now names the three pairs; CONVENTIONS waybar parity
list gains the four new slugs; OVERRIDES autoTheme table points at the
pairs; HARDWARE-QUEUE gains the batch V3 entry (real-GL blur, btop
visual, launcher grid, tray menus, pair flip); BACKLOG files #153
(PROPOSED: per-theme desktop widgets — the conky idea, engine spike
needed, not v1).
Verification: V2. Full targeted set green on the final tree:
theme-contrast, theme-wholeswap (previews + backgrounds + CSS
self-containment), waybar-language, waybar-swap-env, rofi-text-width,
state-sync-validate, auto-theme VM; nix flake check --no-build green.
Visual protocol: theme-shot VM desktop + menu viewed for BOTH themes
(plank renders as one island, copper/terracotta semantics, legible
menus; /nix/store/2fizd945...theme-shot-kiln and
/nix/store/0126yd48...theme-shot-kiln-clay). audit-theme-design:
CVD-only findings (≤ executive-slate's own baseline); ANSI slots
in-family after the magenta retune. V3 pending: HARDWARE-QUEUE entry
above. btop visual is VM-blocked (GL terminal never starts under
softGL) — file-level assertions only, listed for hardware.
Design + wallpapers on Fable; assets by a Sonnet subagent to a written
identity spec; reviewed on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second exclusive pair (see 0ba1762 for the batch). Executive Ivory is
the slate boardroom at 9 AM: the night skyline traded for paper, print
and daylight. Warm ivory base (#F2F0E9), ink text (12.1:1 AAA), the
steel blue deepened to letterhead weight (#2F5FBF, 5.25:1 as text),
JetBrainsMono + slate's exact 5/12/2/10 geometry. Assets: the same
solid edge-to-edge strip identity (ink on ivory, accent underline on
the active workspace), rofi, btop, and a line-web wallpaper that
mirrors slate's geometry in silver-on-paper with sparse blue threads.
Verification: V2, same evidence set as 0ba1762 (all targeted checks
green on this tree; theme-shot VM desktop + menu viewed — strip,
underline, paper panel all render legibly; screenshots in
/nix/store/5883sqfz...theme-shot-executive-ivory). Preview is the
480x270 VM render. V3 pending per the batch HARDWARE-QUEUE entry.
Design on Fable; asset port by a Sonnet subagent from the
executive-slate sources; reviewed on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First of three Nomarchy-exclusive dark/light pairs for v1 (with
executive-ivory and kiln/kiln-clay in the next commits), built for
settings.autoTheme: same identity in two lights, so the sunrise flip
feels like the same room brightening.
Boreal Dawn is the boreal forest six hours later — the aurora faded
into a cold clear dawn. Frost-paper base (#EDF2F6), deep-slate text
(10.9:1 AAA), the aurora violet deepened to text-grade (#7458C8,
4.69:1), spruce teal accentAlt (#22766E), GeistMono + the same 6/14/3/12
geometry as boreal so the pair swaps shape-invariant. Assets: whole-swap
waybar (the same three frosted pills, light glass), rofi + 4x3 launcher
grid, btop theme, dawn-treeline wallpaper mirroring aurora-boreal's
composition (same spiky silhouette, dawn gradient, fading stars).
flake.lock: nomarchy-wallpapers 803b48d -> a716cf6 — the single-input
bump carrying backgrounds for all four new slugs (this task is the lock
bump). Preview is a real render: theme-shot VM desktop at 480x270.
Verification: V2. nix flake check --no-build green; checks
theme-contrast (28x7+chips), theme-wholeswap, waybar-language,
waybar-swap-env, rofi-text-width, state-sync-validate, auto-theme (VM
day/night engine) all green. Visual protocol: theme-shot VM renders
viewed for desktop + menu (frosted pills, wash selection, legibility —
screenshots in /nix/store/l26kfdcf...theme-shot-boreal-dawn). audit
findings at/below the boreal baseline (CVD-only + the same
accent-pair Δhue as boreal). V3 pending: real-GL blur, btop visual,
launcher grid on hardware (HARDWARE-QUEUE, this batch's entry).
Design by Fable (palette lab + wallpaper SVGs); asset port by a Sonnet
subagent from the boreal sources; reviewed file-by-file on Fable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New themes/executive-slate/launcher.rasi: @import the sibling rofi.rasi
whole-swap, reshape drun into the 4x3 horizontal-flow grid of 56px icons
(mirrors boreal's launcher), cards at the theme's 4px radius so the flat
control-panel identity holds; the selected card keeps the accent-edge
highlight. Text menus are untouched. Picked up automatically by
hasLauncherOverride in rofi.nix.
V2: headless VM capture (before: vertical list; after: grid renders,
filter/arrow/backspace drive it correctly). Boreal grid re-captured, no
regression. Guest rendering caveat per docs/TESTING.md section 5 applies
to polish judgments; layout and highlight behavior are the claims here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>