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