fix(plymouth): #137 splash follows the canvas; #145 keyboard hint on the LUKS prompt
All checks were successful
Check / eval (push) Successful in 3m59s

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>
This commit is contained in:
2026-07-16 13:14:05 +01:00
parent 121b91f69c
commit ff0f9d6359
6 changed files with 247 additions and 166 deletions

View File

@@ -177,129 +177,6 @@ migration (state.json created + git-staged, legacy `git rm`'d). Proven by
hand 2026-07-15 (drvPaths identical across the rename) — this just keeps it
proven. Delete together with the shim + the `nomarchy-theme-sync` alias.
### 145. Show the keyboard layout on the LUKS prompt (Fedora does this)
Bernardo 2026-07-16: the splash's password screen should say which layout is
loaded. It is the right ask for this machine specifically — a VT has **one**
keymap and no concept of per-device layouts, so `settings.keyboard.devices`
(his external board remembered as `us`) does **not** apply at the passphrase
box; it follows the session layout via `services.xserver.xkb.layout`
`console.useXkbConfig` (`modules/nixos/default.nix:96-102`). That is the same
structural gap as **#114** (tuigreet), one step earlier in the boot, and with
worse stakes: three wrong tries on a disk you cannot read yet, with no
indication that the keyboard is not the one you think.
**Fedora's mechanism does not port, and the reason matters.** Its label is
`ply_keymap_icon` — a **C widget in the two-step plugin**
(`plugins/splash/two-step/plugin.c:239`, `libply-splash-graphics/ply-keymap-icon.c`);
the **script** plugin has no keyboard API at all (nothing in
`script-lib-plymouth.c`). And it is fed from **`XKBLAYOUT` in
`/etc/vconsole.conf`**, which **NixOS does not write** — this box has exactly
`KEYMAP=/nix/store/…-xkb-console-keymap` and nothing else, so plymouth's own
trace says `XKBLAYOUT: (null)`. Switching to a C-plugin theme *and* teaching
NixOS to emit XKBLAYOUT to get a stock feature is a bad trade for a text label.
**So bake it, the way the palette already is.** The theme derivation seds
`@BG_R@/@BG_G@/@BG_B@` into `nomarchy.script` at build time
(`modules/nixos/plymouth.nix`); add `@LAYOUT@` from
`config.nomarchy.keyboard.layout` and draw it under the entry with
`Image.Text` (already used by `display_message_callback`), in the muted/subtext
colour, only inside `display_password_callback`. It stays honest because the
label and the console keymap are baked from the same option in the same
rebuild.
To settle while building: (a) `keyboard.layout` may be a comma list (`us,gb`) —
the VT loads the **first**, so print that one, not the raw string, or the label
lies in exactly the situation it exists for; (b) wording — Fedora says
"keyboard layout: us"; (c) whether to render it at all when there is only one
layout and no remembered devices (an argument for always: the point is to tell
you what the *console* does, which is not what your session does).
**Do #137 first, and land this with it:** the label is one more sprite that has
to move when the canvas resizes, so it belongs in that item's `layout()`, and
both are proven by the same `tools/plymouth-preview.sh` run — which renders
`ask-for-password`, i.e. exactly this screen.
### 137. Plymouth splash draws on head 0's geometry, so it is off-centre when docked
Bernardo 2026-07-16 (T14s, docked): the boot **and** shutdown logo is not
centred on the external monitor.
**The first diagnosis here was wrong — do not act on it.** It claimed
`Window.GetWidth()` returns head 0 and that the fix was per-head sprites. Both
are false, and per-head sprites would have been a *regression* (duplicate
logos). Read from plymouth's own source (`plymouth.src`, the build nixpkgs
ships) rather than from a summary:
- `Window.GetWidth()` **without an index returns `data->max_width`**
(`script-lib-sprite.c:227`) — the widest head, not head 0.
- The script plugin builds a virtual canvas of `max_width × max_height` and
**centres every display inside it**: `display->x = (max_width - width) / 2`
(`:536`). Sprites are drawn per display at `sprite->x - display->x` (`:452`).
- Therefore a canvas-centred sprite **is** centred on every head, and today's
`Window.GetWidth()/2 - w/2` is *correct at the instant it runs*. Note the DRM
renderer sets every head's `area.x = 0` (`renderers/drm/plugin.c:612`) — the
origins that matter are the plugin's computed ones, not the crtc's.
**The real bug is time, not geometry.** `nomarchy.script` computes every
position as a **top-level statement at parse time** (`:9`, `:1516`, `:129`,
`:210`, `:219`), i.e. once, against whatever canvas existed at that moment.
When a display arrives or leaves *during* the splash — which is exactly
"booting up / shutting down **with** an external" — `max_width/max_height`
change, the plugin re-centres each display (`:532-537`), but the sprite's frozen
canvas coordinate does not move with it. Every element then sits off by
`(new_max - old_max) / 2` **on every head**. A single monitor never changes the
canvas, which is precisely why this has never been seen undocked.
**Fix shape:** hoist the position math into a `reposition()` and call it from
the existing `refresh_callback` (already running at 50 fps) whenever
`Window.GetWidth()/GetHeight()` differ from the last canvas — logo (scale
included), entry, lock, any live bullets, progress box/bar. Keep it a
canvas-change check, not an every-frame recompute.
**The rig works, needs no root, and is in `scratchpad/ply-rig.sh`** (recipe
below — rebuild it, it is ~20 lines). Rendered on the dev box 2026-07-16:
- `unshare -rm` gives a user + mount namespace: root *in there* only, every
mount invisible to the real system and gone when it exits.
- `/run/plymouth` is BOTH the daemon's socket dir **and** its compiled-in
plugin/theme path (`strings plymouthd``/run/plymouth/plugins/`). Bind a
writable dir over it **and recreate its three symlinks** (`plugins`,
`themes`, `plymouthd.defaults``/etc/plymouth/…`) or plymouth cannot find
`x11.so`, silently skips X11 and reaches for a tty.
- **Do not pass `--tty`**: `main.c:2133` only takes the X11 path while
`default_tty` is unset. Bind an empty dir over `/dev/dri` too — with X11
first in the renderer order (`ply-renderer.c:283`) DRM is never used, and the
mask makes that a guarantee rather than a hope.
- `PLY_CREATE_FAKE_MULTI_HEAD_SETUP=1` → 800×600 + 640×480 windows.
`plymouth show-splash`, then `plymouth ask-for-password` renders the LUKS
dialog. Debug: `--kernel-command-line="plymouth.debug=stream:/path splash"`
(**`stream:`**, not `file:``file:` buffers and you get nothing).
- Fast loop: copy the theme into the rig's `themes/` and sed its `.plymouth`
`ScriptFile=`/`ImageDir=` to the copy — then editing the script is a 10s
render instead of a 3min `nixos-rebuild`.
**What the rig already proved (2026-07-16):** with a *stable* two-head canvas
the **current** theme centres correctly on both heads — logo and progress bar
each in their own head's middle. So the geometry is not the bug, which is the
evidence for the timing diagnosis above. The fake path builds both heads up
front, so it cannot reproduce a canvas *change*: for that, plug/unplug while
the splash is up, or compare a boot with and without the external.
**Attempt 1 failed, and the rig caught it — that is the point.** Hoisting the
positions into a `layout()` called from `refresh_callback` rendered the
background on both heads and **nothing else**: no logo, no progress bar. The
script plugin logs `starting boot animation` and **no parse error**, so it
fails *silently* — reverted rather than shipped (that script draws the LUKS box
on an encrypted machine). Next session: bisect with the fast loop. Prime
suspect is scope — plymouth script assigns into `local` unless the name already
exists, so a `logo.image = …` inside a function may not be the `logo` the rest
of the script sees, even though the same pattern works in `update_progress_bar`.
Test that hypothesis *first*, in isolation, before rewriting anything.
Pass = with the rig, a two-head render centred on both **plus the password
dialog visible**; then a real docked boot + shutdown, undocked unchanged.
### 143. Rofi keeps the selected *row number* when the search changes, so the highlight lands on an unrelated entry
Bernardo 2026-07-16, and his repro is exact: open the app menu, type `ca`, move

View File

@@ -509,6 +509,17 @@ Everything else below stays open; order is convenience, not a gate.
active and PPD switching governors; opt-ins: ROCm (`rocminfo`, a GPU
PyTorch/Ollama smoke) and the XDNA NPU driver loading.
- [ ] **Fingerprint**`fprintd-enroll` + (opt-in PAM) login/sudo.
- [ ] **#137/#145 splash on a real docked boot** — the one thing the render rig
cannot do: its fake heads exist from the start, so the canvas never
resizes, which *is* the bug. **Docked**, reboot: pass = the logo is centred
on **both** panels at boot **and** at shutdown (it was off-centre on the
external), and the LUKS prompt shows the padlock, the entry, and beneath it
a keyboard icon + your layout (`us`). Then reboot **undocked** — unchanged.
Rendering is already proven (`tools/plymouth-preview.sh`, both heads +
`ask-for-password`), so what is unproven is only a canvas that changes
under a real DRM boot. If the splash is ever blank, the passphrase prompt
is invisible but still live — type it blind, or pick the previous
generation in the boot menu, which carries the old theme.
- [ ] **#142 dock mode survives a rebuild** — **relogin first** (the watcher is
`exec-once`; `nomarchy-home` alone leaves the old one running and you
would be testing nothing — round 6's lesson). Then, docked: run

View File

@@ -19,6 +19,35 @@ Template:
---
## 2026-07-16 — #137 + #145: the splash follows the canvas, and says your layout
- **Task:** #137 (splash off-centre docked) + #145 (keyboard layout on the LUKS
prompt, with Fedora's icon).
- **Did:** one `layout()`, re-run from the existing refresh callback on canvas
change; `@LAYOUT@` + `@FG_*@` baked by plymouth.nix like the palette already
is; plymouth's own `keyboard.png` copied at build time and recoloured.
- **The lesson, and it is the whole entry:** I was wrong twice, and only the
render rig caught it. Not head-0 geometry (`Window.GetWidth()` = `max_width`);
not per-head sprites (would have duplicated the logo — the plugin already
centres each display in the canvas). The bug was **time**: positions computed
once at parse time vs a canvas that resizes when a head arrives. And attempt 1
of the fix died on a plymouth-script trap: **a bare assignment inside a
function writes the global if that name exists globally**, so
`canvas_width = Window.GetWidth()` updated the very global its guard compared
against — always false, nothing drawn, **no error logged**. Isolated with a
20-line probe (`[after assign: global.canvas_width=800][guard SKIPPED]`).
Both facts are now comments in the script; do not re-derive them.
- **Verified:** V0 + V2 by render — `tools/plymouth-preview.sh` with the real
built theme: both heads centred, and the password dialog showing padlock,
entry, and ⌨ + `us`. Also caught, only because I read the build log: copying a
file from the store keeps it **444**, so `recolor` failed, the phase aborted,
and **every later sed silently did not run** — the theme shipped with `@BG_R@`
literals. `chmod +w`. Check build logs, not just exit codes.
- **Pending:** V3 queued — a real docked boot/shutdown is the only way to resize
a canvas (the rig's fake heads exist from the start). Recovery if the splash
is ever blank: the prompt is invisible but live (type blind), or boot the
previous generation.
- **Next suggestion:** #129/#130 (GTK contrast) or #133 (the #107 shim check).
## 2026-07-16 — "fix the rest": #140, #142, #144 shipped; #137 re-diagnosed
- **Task:** the T14s round's remainder. Bernardo picked configurable-provider +
drop-the-CLI for #140 (and asked for chatgpt/grok in the list).