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>
363 lines
12 KiB
Plaintext
363 lines
12 KiB
Plaintext
# Nomarchy Plymouth Theme Script
|
|
|
|
Window.SetBackgroundTopColor(@BG_R@, @BG_G@, @BG_B@);
|
|
Window.SetBackgroundBottomColor(@BG_R@, @BG_G@, @BG_B@);
|
|
|
|
# Everything is placed by layout(), which runs on every canvas change — never
|
|
# once at parse time (#137).
|
|
#
|
|
# How the canvas works, because it is not obvious and the old code read as
|
|
# correct: the script plugin lays all heads out in ONE virtual canvas of
|
|
# max_width x max_height (the largest head), centres each display inside it
|
|
# (display->x = (max_width - width) / 2) and draws each sprite at
|
|
# (sprite.x - display.x). So Window.GetWidth() is the CANVAS width — the
|
|
# widest head, NOT head 0 — and a canvas-centred sprite lands centred on every
|
|
# head. The arithmetic below is unchanged from the original and it was always
|
|
# right. What it was not is permanent: when a head arrives or leaves the
|
|
# canvas resizes, the plugin re-centres each display, and sprites keep the
|
|
# coordinates they were given — so positions frozen at parse time end up off by
|
|
# (new_max - old_max) / 2 on EVERY head. Booting or shutting down with an
|
|
# external attached is exactly that. One monitor never resizes the canvas,
|
|
# which is why this only ever showed docked.
|
|
#
|
|
# TRAP — read before editing layout(). In plymouth script a bare assignment
|
|
# inside a function writes the GLOBAL if that name already exists globally
|
|
# (`global.foo` and a bare `foo` are the same variable; it only becomes a local
|
|
# when no global of that name exists). So a local named after the global it
|
|
# guards on silently updates that global *before* the comparison, the guard is
|
|
# then always false, and the body never runs — the whole splash renders as a
|
|
# bare background, with no error logged anywhere. Hence `cw`/`ch` below, and
|
|
# never `canvas_width = Window.GetWidth()`.
|
|
logo.original_image = Image("logo.png");
|
|
lock.image = Image("lock.png");
|
|
entry.image = Image("entry.png");
|
|
bullet.image = Image("bullet.png");
|
|
# The keyboard hint for the passphrase prompt (#145): a VT loads ONE keymap and
|
|
# knows nothing of per-device layouts, so what you type here is the console
|
|
# layout — which is worth saying out loud before three wrong tries on a disk
|
|
# nobody can read yet. Both are baked at build time by plymouth.nix.
|
|
kbd.icon_image = Image("keyboard.png");
|
|
kbd.text_image = Image.Text("@LAYOUT@", @FG_R@, @FG_G@, @FG_B@);
|
|
|
|
logo.sprite = Sprite();
|
|
logo.sprite.SetOpacity (1);
|
|
entry.sprite = Sprite(entry.image);
|
|
entry.sprite.SetOpacity (0);
|
|
lock.sprite = Sprite();
|
|
lock.sprite.SetOpacity (0);
|
|
kbd.icon_sprite = Sprite();
|
|
kbd.icon_sprite.SetOpacity (0);
|
|
kbd.text_sprite = Sprite();
|
|
kbd.text_sprite.SetOpacity (0);
|
|
|
|
global.canvas_width = 0;
|
|
global.canvas_height = 0;
|
|
global.bullet_size = 7;
|
|
global.bullet_gap = 5;
|
|
|
|
fun layout ()
|
|
{
|
|
cw = Window.GetWidth();
|
|
ch = Window.GetHeight();
|
|
|
|
if (cw != global.canvas_width || ch != global.canvas_height)
|
|
{
|
|
global.canvas_width = cw;
|
|
global.canvas_height = ch;
|
|
|
|
# Logo: ~15% of canvas height, centred.
|
|
logo_scale = (ch * 0.15) / logo.original_image.GetHeight();
|
|
logo.image = logo.original_image.Scale(
|
|
logo.original_image.GetWidth() * logo_scale,
|
|
logo.original_image.GetHeight() * logo_scale);
|
|
logo.sprite.SetImage(logo.image);
|
|
logo.sprite.SetX(cw / 2 - logo.image.GetWidth() / 2);
|
|
logo.sprite.SetY(ch / 2 - logo.image.GetHeight() / 2);
|
|
|
|
# Password entry, under the logo.
|
|
entry.x = cw / 2 - entry.image.GetWidth() / 2;
|
|
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
|
|
entry.sprite.SetPosition(entry.x, entry.y, 10001);
|
|
|
|
# Lock, slightly shorter than the entry, to its left.
|
|
# (source lock.png is 84x96)
|
|
lock_h = entry.image.GetHeight() * 0.8;
|
|
lock_w = 84 * (lock_h / 96);
|
|
lock.sprite.SetImage(lock.image.Scale(lock_w, lock_h));
|
|
lock.sprite.SetPosition(entry.x - lock_w - 15,
|
|
entry.y + entry.image.GetHeight() / 2 - lock_h / 2,
|
|
10001);
|
|
|
|
# Keyboard hint, centred as one icon+text group under the entry.
|
|
kbd_gap = 8;
|
|
kbd_x = cw / 2 - (kbd.icon_image.GetWidth() + kbd_gap
|
|
+ kbd.text_image.GetWidth()) / 2;
|
|
kbd_y = entry.y + entry.image.GetHeight() + 18;
|
|
kbd.icon_sprite.SetImage(kbd.icon_image);
|
|
kbd.icon_sprite.SetPosition(kbd_x, kbd_y, 10001);
|
|
kbd.text_sprite.SetImage(kbd.text_image);
|
|
kbd.text_sprite.SetPosition(
|
|
kbd_x + kbd.icon_image.GetWidth() + kbd_gap,
|
|
kbd_y + kbd.icon_image.GetHeight() / 2 - kbd.text_image.GetHeight() / 2,
|
|
10001);
|
|
|
|
# Bullets already on screen belong to the old canvas.
|
|
for (index = 0; bullet.sprites[index]; index++)
|
|
{
|
|
bullet.sprites[index].SetPosition(
|
|
entry.x + 20 + index * (global.bullet_size + global.bullet_gap),
|
|
entry.y + entry.image.GetHeight() / 2 - global.bullet_size / 2,
|
|
10002);
|
|
}
|
|
|
|
# Progress box + bar share the entry's line.
|
|
progress_box.sprite.SetPosition(
|
|
cw / 2 - progress_box.image.GetWidth() / 2,
|
|
entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2,
|
|
0);
|
|
progress_bar.sprite.SetPosition(
|
|
cw / 2 - progress_bar.original_image.GetWidth() / 2,
|
|
entry.y + entry.image.GetHeight() / 2
|
|
- progress_bar.original_image.GetHeight() / 2,
|
|
1);
|
|
}
|
|
}
|
|
|
|
# Use these to adjust the progress bar timing
|
|
global.fake_progress_limit = 0.7; # Target percentage for fake progress (0.0 to 1.0)
|
|
global.fake_progress_duration = 15.0; # Duration in seconds to reach limit
|
|
|
|
# Progress bar animation variables
|
|
global.fake_progress = 0.0;
|
|
global.real_progress = 0.0;
|
|
global.fake_progress_active = 0; # 0 / 1 boolean
|
|
global.animation_frame = 0;
|
|
global.fake_progress_start_time = 0; # Track when fake progress started
|
|
global.password_shown = 0; # Track if password dialog has been shown
|
|
global.max_progress = 0.0; # Track the maximum progress reached to prevent backwards movement
|
|
|
|
fun refresh_callback ()
|
|
{
|
|
# Cheap: two Window.Get*() reads; layout() returns at once unless the canvas
|
|
# actually resized (a head arrived or left).
|
|
layout();
|
|
|
|
global.animation_frame++;
|
|
|
|
# Animate fake progress to limit over time with easing
|
|
if (global.fake_progress_active == 1)
|
|
{
|
|
# Calculate elapsed time since start
|
|
elapsed_time = global.animation_frame / 50.0; # Convert frames to seconds (50 FPS)
|
|
|
|
# Calculate linear progress ratio (0 to 1) based on time
|
|
time_ratio = elapsed_time / global.fake_progress_duration;
|
|
if (time_ratio > 1.0)
|
|
time_ratio = 1.0;
|
|
|
|
# Apply easing curve: ease-out quadratic
|
|
# Formula: 1 - (1 - x)^2
|
|
eased_ratio = 1 - ((1 - time_ratio) * (1 - time_ratio));
|
|
|
|
# Calculate fake progress based on eased ratio
|
|
global.fake_progress = eased_ratio * global.fake_progress_limit;
|
|
|
|
# Update progress bar with fake progress
|
|
update_progress_bar(global.fake_progress);
|
|
}
|
|
}
|
|
|
|
|
|
Plymouth.SetRefreshFunction (refresh_callback);
|
|
|
|
#----------------------------------------- Helper Functions --------------------------------
|
|
|
|
fun update_progress_bar(progress)
|
|
{
|
|
# Only update if progress is moving forward
|
|
if (progress > global.max_progress)
|
|
{
|
|
global.max_progress = progress;
|
|
width = Math.Int(progress_bar.original_image.GetWidth() * progress);
|
|
if (width < 1) width = 1; # Ensure minimum width of 1 pixel
|
|
|
|
progress_bar.image = progress_bar.original_image.Scale(width, progress_bar.original_image.GetHeight());
|
|
progress_bar.sprite.SetImage(progress_bar.image);
|
|
}
|
|
}
|
|
|
|
fun show_progress_bar()
|
|
{
|
|
progress_box.sprite.SetOpacity(1);
|
|
progress_bar.sprite.SetOpacity(1);
|
|
}
|
|
|
|
fun hide_progress_bar()
|
|
{
|
|
progress_box.sprite.SetOpacity(0);
|
|
progress_bar.sprite.SetOpacity(0);
|
|
}
|
|
|
|
fun show_password_dialog()
|
|
{
|
|
lock.sprite.SetOpacity(1);
|
|
entry.sprite.SetOpacity(1);
|
|
# The keyboard hint belongs to the prompt: it is only ever the answer to
|
|
# "what am I typing with?", so it appears and leaves with the box (#145).
|
|
kbd.icon_sprite.SetOpacity(1);
|
|
kbd.text_sprite.SetOpacity(1);
|
|
}
|
|
|
|
fun hide_password_dialog()
|
|
{
|
|
lock.sprite.SetOpacity(0);
|
|
entry.sprite.SetOpacity(0);
|
|
kbd.icon_sprite.SetOpacity(0);
|
|
kbd.text_sprite.SetOpacity(0);
|
|
for (index = 0; bullet.sprites[index]; index++)
|
|
bullet.sprites[index].SetOpacity(0);
|
|
}
|
|
|
|
fun start_fake_progress()
|
|
{
|
|
# Don't reset if we already have progress
|
|
if (global.max_progress == 0.0)
|
|
{
|
|
global.fake_progress = 0.0;
|
|
global.real_progress = 0.0;
|
|
update_progress_bar(0.0);
|
|
}
|
|
global.fake_progress_active = 1;
|
|
global.animation_frame = 0;
|
|
}
|
|
|
|
fun stop_fake_progress()
|
|
{
|
|
global.fake_progress_active = 0;
|
|
}
|
|
|
|
#----------------------------------------- Dialogue --------------------------------
|
|
|
|
# Images and sprites are created at the top; every position lives in layout().
|
|
bullet.sprites = [];
|
|
|
|
fun display_normal_callback ()
|
|
{
|
|
hide_password_dialog();
|
|
|
|
# Get current mode
|
|
mode = Plymouth.GetMode();
|
|
|
|
# Only show progress bar for boot and resume modes
|
|
if (mode == "boot" || mode == "resume")
|
|
{
|
|
show_progress_bar();
|
|
start_fake_progress();
|
|
}
|
|
}
|
|
|
|
fun display_password_callback (prompt, bullets)
|
|
{
|
|
global.password_shown = 1; # Mark that password dialog has been shown
|
|
|
|
# Reset progress when password dialog appears
|
|
stop_fake_progress();
|
|
hide_progress_bar();
|
|
global.max_progress = 0.0;
|
|
global.fake_progress = 0.0;
|
|
global.real_progress = 0.0;
|
|
show_password_dialog();
|
|
|
|
# Clear all bullets first
|
|
for (index = 0; bullet.sprites[index]; index++)
|
|
bullet.sprites[index].SetOpacity(0);
|
|
|
|
# Create and show bullets for current password (max 21)
|
|
max_bullets = 21;
|
|
bullets_to_show = bullets;
|
|
if (bullets_to_show > max_bullets)
|
|
bullets_to_show = max_bullets;
|
|
|
|
for (index = 0; index < bullets_to_show; index++)
|
|
{
|
|
if (!bullet.sprites[index])
|
|
{
|
|
# Scale bullet image to 7x7 pixels
|
|
scaled_bullet = bullet.image.Scale(7, 7);
|
|
bullet.sprites[index] = Sprite(scaled_bullet);
|
|
bullet.x = entry.x + 20 + index * (7 + 5);
|
|
bullet.y = entry.y + entry.image.GetHeight() / 2 - 3.5;
|
|
bullet.sprites[index].SetPosition(bullet.x, bullet.y, 10002);
|
|
}
|
|
bullet.sprites[index].SetOpacity(1);
|
|
}
|
|
}
|
|
|
|
Plymouth.SetDisplayNormalFunction(display_normal_callback);
|
|
Plymouth.SetDisplayPasswordFunction(display_password_callback);
|
|
|
|
#----------------------------------------- Progress Bar --------------------------------
|
|
|
|
progress_box.image = Image("progress_box.png");
|
|
progress_box.sprite = Sprite(progress_box.image);
|
|
progress_box.sprite.SetOpacity(0);
|
|
|
|
progress_bar.original_image = Image("progress_bar.png");
|
|
progress_bar.sprite = Sprite();
|
|
progress_bar.image = progress_bar.original_image.Scale(1, progress_bar.original_image.GetHeight());
|
|
progress_bar.sprite.SetOpacity(0);
|
|
|
|
# First placement: everything layout() reads now exists. The refresh callback
|
|
# re-runs it, so a head arriving or leaving mid-splash moves the splash with it
|
|
# instead of stranding it against a canvas that is gone.
|
|
layout();
|
|
|
|
fun progress_callback (duration, progress)
|
|
{
|
|
global.real_progress = progress;
|
|
|
|
# If real progress is above limit, stop fake progress and use real progress
|
|
if (progress > global.fake_progress_limit)
|
|
{
|
|
stop_fake_progress();
|
|
update_progress_bar(progress);
|
|
}
|
|
}
|
|
|
|
Plymouth.SetBootProgressFunction(progress_callback);
|
|
|
|
#----------------------------------------- Quit --------------------------------
|
|
|
|
fun quit_callback ()
|
|
{
|
|
logo.sprite.SetOpacity (1);
|
|
}
|
|
|
|
Plymouth.SetQuitFunction(quit_callback);
|
|
|
|
#----------------------------------------- Message --------------------------------
|
|
|
|
message_sprite = Sprite();
|
|
message_sprite.SetPosition(10, 10, 10000);
|
|
|
|
fun display_message_callback (text)
|
|
{
|
|
my_image = Image.Text(text, 1, 1, 1);
|
|
message_sprite.SetImage(my_image);
|
|
}
|
|
|
|
fun hide_message_callback (text)
|
|
{
|
|
message_sprite.SetOpacity(0);
|
|
}
|
|
|
|
Plymouth.SetDisplayMessageFunction (display_message_callback);
|
|
Plymouth.SetHideMessageFunction (hide_message_callback);
|
|
|
|
# Initialize progress bar immediately for normal boots
|
|
if (Plymouth.GetMode() == "boot" || Plymouth.GetMode() == "resume")
|
|
{
|
|
show_progress_bar();
|
|
start_fake_progress();
|
|
}
|