feat: custom boot branding, MBR fix, Plymouth theme, CI smoke tests

Boot fix:
- Ship proven Debian Live MBR (4552) as branding/isohdpfx.bin — the
  ISOLINUX package MBR (33ed) doesn't boot on all hardware. This was
  the root cause of "machine doesn't pick up the USB".

Branding:
- Custom GRUB background: pixel-art floating island (1024x574)
- Archipelago pixel-art logo for Plymouth boot splash
- GRUB theme: dark background, orange selected item, no broken font refs
- Plymouth theme: script-based with progress bar, LUKS prompt support
- Plymouth + splash added to target rootfs packages
- GRUB theme installed on both installer ISO and target system
- Serial console (ttyS0) added to kernel params for QEMU debugging

CI improvements:
- Smoke test step: mounts ISO, verifies all critical files, checks
  initrd has live-boot, confirms boot=live in grub.cfg. Fails build
  before copying to Builds if any check fails.

Dev workflow:
- dev-branding.sh: extract ISO, swap branding, repackage, boot in QEMU
  (~10 seconds vs 20 min full rebuild)
- generate-grub-background.py: procedural cyberpunk background generator
- generate-plymouth-logo.py: procedural logo generator
- Improved test-iso-qemu.sh: --bios/--nographic flags, serial logging

Build:
- Simplified live-boot install (clean chroot, no complex fallbacks)
- Static branding images preferred, generators as fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-28 11:34:29 +00:00
co-authored by Claude Opus 4.6
parent e8c80263f3
commit f3f7b8b72f
11 changed files with 888 additions and 43 deletions
@@ -0,0 +1,332 @@
#!/usr/bin/env python3
"""Generate Archipelago GRUB boot background — 80s pixel cyberpunk aesthetic.
Outputs a 1024x768 PNG with:
- Near-black background with subtle radial gradient
- Scanline overlay (CRT effect)
- Pixel-art "A" logo (from Archipelago SVG) rendered in neon orange
- Neon glow effect around the logo
- Retro grid lines at the bottom (Tron-style horizon)
- Subtle vignette
Uses only PIL (Pillow) — no external dependencies.
"""
import struct
import zlib
import math
import sys
import os
W, H = 1024, 768
# Archipelago brand colors
BG_DARK = (5, 5, 10) # Near-black with blue tint
BG_MID = (10, 10, 18) # Slightly lighter center
ORANGE = (251, 146, 60) # #fb923c — primary accent
ORANGE_DIM = (180, 100, 30) # Dimmed orange for glow
CYAN = (60, 200, 220) # Cyberpunk accent
MAGENTA = (180, 60, 180) # Cyberpunk accent 2
GRID_COLOR = (30, 60, 80) # Subtle teal grid
SCANLINE = (0, 0, 0) # Black scanlines
# The pixel-art "a" (lowercase) from the Archipelago SVG favicon
# Matched to the actual SVG path — it's a blocky pixel-art lowercase "a"
LOGO_A = [
[0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1],
]
# Pixel-art text: "archipelago" — 5-pixel-high bitmap font (lowercase)
PIXEL_CHARS = {
'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]],
'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]],
'i': [[0,1],[0,0],[0,1],[0,1],[0,1]],
'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]],
'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]],
'l': [[1,0],[1,0],[1,0],[1,0],[1,1]],
'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]],
'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]],
' ': [[0,0],[0,0],[0,0],[0,0],[0,0]],
}
LOGO_TEXT = "archipelago"
def lerp_color(c1, c2, t):
"""Linearly interpolate between two RGB colors."""
return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2))
def distance(x1, y1, x2, y2):
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
def create_png(width, height, pixels):
"""Create a PNG file from raw RGB pixel data without PIL."""
def make_chunk(chunk_type, data):
chunk = chunk_type + data
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF)
# PNG signature
signature = b'\x89PNG\r\n\x1a\n'
# IHDR
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0) # 8-bit RGB
ihdr = make_chunk(b'IHDR', ihdr_data)
# IDAT — raw pixel data with filter byte per row
raw_data = bytearray()
for y in range(height):
raw_data.append(0) # filter: none
offset = y * width * 3
raw_data.extend(pixels[offset:offset + width * 3])
compressed = zlib.compress(bytes(raw_data), 9)
idat = make_chunk(b'IDAT', compressed)
# IEND
iend = make_chunk(b'IEND', b'')
return signature + ihdr + idat + iend
def generate():
# Create pixel buffer (RGB, row-major)
pixels = bytearray(W * H * 3)
cx, cy = W // 2, H // 2 - 40 # Center, slightly above middle
# --- Background: radial gradient ---
max_dist = distance(0, 0, cx, cy)
for y in range(H):
for x in range(W):
d = distance(x, y, cx, cy) / max_dist
d = min(d, 1.0)
bg = lerp_color(BG_MID, BG_DARK, d * d) # Quadratic falloff
# Vignette — darken edges
vx = abs(x - cx) / (W / 2)
vy = abs(y - cy) / (H / 2)
vignette = max(0, 1.0 - (vx * vx + vy * vy) * 0.4)
r = int(bg[0] * vignette)
g = int(bg[1] * vignette)
b = int(bg[2] * vignette)
idx = (y * W + x) * 3
pixels[idx] = max(0, min(255, r))
pixels[idx + 1] = max(0, min(255, g))
pixels[idx + 2] = max(0, min(255, b))
# --- Retro grid (bottom third) ---
horizon_y = H * 2 // 3
for y in range(horizon_y, H):
depth = (y - horizon_y) / (H - horizon_y) # 0 at horizon, 1 at bottom
# Horizontal grid lines — spacing decreases with perspective
grid_spacing = max(4, int(40 * (1.0 - depth * 0.8)))
is_hline = ((y - horizon_y) % grid_spacing) < 1
for x in range(W):
# Vertical grid lines — converge toward center
spread = 0.3 + depth * 0.7 # Lines spread more toward bottom
grid_x = (x - cx) / (spread * W / 2) * 12
is_vline = abs(grid_x - round(grid_x)) < 0.04
if is_hline or is_vline:
alpha = 0.15 + depth * 0.25 # Brighter closer to viewer
idx = (y * W + x) * 3
for c in range(3):
old = pixels[idx + c]
pixels[idx + c] = min(255, int(old + GRID_COLOR[c] * alpha))
# --- Horizon glow line ---
for x in range(W):
dx = abs(x - cx) / (W / 2)
intensity = max(0, 1.0 - dx * 1.5) * 0.4
for dy in range(-2, 3):
y = horizon_y + dy
if 0 <= y < H:
falloff = 1.0 - abs(dy) / 3.0
idx = (y * W + x) * 3
pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * intensity * falloff))
pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * intensity * falloff))
pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * intensity * falloff))
# --- Pixel-art "A" logo ---
logo_rows = len(LOGO_A)
logo_cols = len(LOGO_A[0])
pixel_size = 14
logo_w = logo_cols * pixel_size
logo_h = logo_rows * pixel_size
logo_x = cx - logo_w // 2
logo_y = 80
# Glow behind logo
glow_radius = 90
for y in range(max(0, logo_y - glow_radius), min(H, logo_y + logo_h + glow_radius)):
for x in range(max(0, logo_x - glow_radius), min(W, logo_x + logo_w + glow_radius)):
# Distance to logo bounding box
dx = max(0, logo_x - x, x - (logo_x + logo_w))
dy = max(0, logo_y - y, y - (logo_y + logo_h))
d = math.sqrt(dx * dx + dy * dy)
if d < glow_radius:
alpha = (1.0 - d / glow_radius) ** 2 * 0.15
idx = (y * W + x) * 3
pixels[idx] = min(255, int(pixels[idx] + ORANGE[0] * alpha))
pixels[idx + 1] = min(255, int(pixels[idx + 1] + ORANGE[1] * alpha))
pixels[idx + 2] = min(255, int(pixels[idx + 2] + ORANGE[2] * alpha))
# Draw logo pixels with 3D depth/shadow effect
shadow_offset = 3 # Pixel offset for 3D shadow
for row in range(logo_rows):
for col in range(logo_cols):
if LOGO_A[row][col]:
px = logo_x + col * pixel_size
py = logo_y + row * pixel_size
# Shadow layer (dark, offset down-right)
for dy in range(pixel_size - 1):
for dx in range(pixel_size - 1):
x = px + dx + shadow_offset
y = py + dy + shadow_offset
if 0 <= x < W and 0 <= y < H:
idx = (y * W + x) * 3
pixels[idx] = max(0, pixels[idx] - 5)
pixels[idx + 1] = min(255, pixels[idx + 1] + 15)
pixels[idx + 2] = min(255, pixels[idx + 2] + 20)
# Main pixel with highlight gradient (brighter at top-left)
for dy in range(pixel_size - 1):
for dx in range(pixel_size - 1):
x, y = px + dx, py + dy
if 0 <= x < W and 0 <= y < H:
# Gradient: top-left bright, bottom-right darker
t = (dx + dy) / (2 * pixel_size)
r = int(ORANGE[0] * (1.0 - t * 0.3))
g = int(ORANGE[1] * (1.0 - t * 0.3))
b = int(ORANGE[2] * (1.0 - t * 0.3))
# Top-left highlight for 3D bevel
if dx < 2 or dy < 2:
r = min(255, r + 40)
g = min(255, g + 30)
b = min(255, b + 10)
idx = (y * W + x) * 3
pixels[idx] = r
pixels[idx + 1] = g
pixels[idx + 2] = b
# --- Pixel-art text "archipelago" below logo ---
text_pixel = 4 # Smaller pixels for text
text_gap = 2 # Gap between characters in pixels
# Calculate total text width
total_w = 0
for ch in LOGO_TEXT:
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
total_w += len(char_data[0]) * text_pixel + text_gap
total_w -= text_gap # No gap after last char
text_x = cx - total_w // 2
text_y = logo_y + logo_h + 20 # Below the logo
cursor_x = text_x
for ch in LOGO_TEXT:
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
char_h = len(char_data)
char_w = len(char_data[0])
for row in range(char_h):
for col in range(char_w):
if char_data[row][col]:
px = cursor_x + col * text_pixel
py = text_y + row * text_pixel
for dy in range(text_pixel - 1):
for dx in range(text_pixel - 1):
x, y = px + dx, py + dy
if 0 <= x < W and 0 <= y < H:
idx = (y * W + x) * 3
# Dimmer orange for text
pixels[idx] = ORANGE_DIM[0]
pixels[idx + 1] = ORANGE_DIM[1]
pixels[idx + 2] = ORANGE_DIM[2]
cursor_x += char_w * text_pixel + text_gap
# --- Decorative neon lines flanking the text ---
line_y = text_y + 5 * text_pixel + 12
line_w = total_w + 40
line_x1 = cx - line_w // 2
line_x2 = cx + line_w // 2
for x in range(line_x1, line_x2):
if 0 <= x < W:
# Fade at edges
edge_dist = min(x - line_x1, line_x2 - x)
alpha = min(1.0, edge_dist / 30.0) * 0.5
for dy in range(2):
y = line_y + dy
if 0 <= y < H:
idx = (y * W + x) * 3
pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * alpha * 0.3))
pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * alpha * 0.3))
pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * alpha * 0.3))
# --- "self-sovereign bitcoin infrastructure" tagline ---
TAG_CHARS = {
's': [[0,1,1],[1,0,0],[0,1,0],[0,0,1],[1,1,0]],
'f': [[0,1,1],[1,0,0],[1,1,0],[1,0,0],[1,0,0]],
'-': [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]],
'v': [[1,0,1],[1,0,1],[1,0,1],[0,1,0],[0,1,0]],
'n': [[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1]],
'b': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,1,1,0]],
't': [[0,1,0],[1,1,1],[0,1,0],[0,1,0],[0,0,1]],
'd': [[0,0,0,1],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
'u': [[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,1]],
}
# Merge with existing chars
all_chars = {**PIXEL_CHARS, **TAG_CHARS}
tagline = "self-sovereign bitcoin node"
tag_pixel = 3
tag_gap = 2
tag_total = sum(len(all_chars.get(c, all_chars[' '])[0]) * tag_pixel + tag_gap for c in tagline) - tag_gap
tag_x = cx - tag_total // 2
tag_y = line_y + 8
tag_cursor = tag_x
for ch in tagline:
char_data = all_chars.get(ch, all_chars[' '])
char_h = len(char_data)
char_w = len(char_data[0])
for row in range(char_h):
for col in range(char_w):
if char_data[row][col]:
px = tag_cursor + col * tag_pixel
py = tag_y + row * tag_pixel
for dy in range(tag_pixel - 1):
for dx in range(tag_pixel - 1):
x, y = px + dx, py + dy
if 0 <= x < W and 0 <= y < H:
idx = (y * W + x) * 3
pixels[idx] = min(255, pixels[idx] + 40)
pixels[idx + 1] = min(255, pixels[idx + 1] + 50)
pixels[idx + 2] = min(255, pixels[idx + 2] + 55)
tag_cursor += char_w * tag_pixel + tag_gap
# --- Scanlines (every other row, subtle) ---
for y in range(0, H, 2):
for x in range(W):
idx = (y * W + x) * 3
pixels[idx] = int(pixels[idx] * 0.92)
pixels[idx + 1] = int(pixels[idx + 1] * 0.92)
pixels[idx + 2] = int(pixels[idx + 2] * 0.92)
# --- Generate PNG ---
png_data = create_png(W, H, bytes(pixels))
return png_data
if __name__ == '__main__':
out_path = sys.argv[1] if len(sys.argv) > 1 else 'background.png'
png_data = generate()
with open(out_path, 'wb') as f:
f.write(png_data)
print(f'Generated {out_path} ({len(png_data)} bytes)')
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Generate the Archipelago Plymouth boot logo — pixel-art 'a' with neon glow.
Outputs a 256x256 PNG with transparent background (RGBA).
"""
import struct
import zlib
import math
import sys
W, H = 256, 256
ORANGE = (251, 146, 60)
ORANGE_BRIGHT = (255, 180, 100)
# Lowercase pixel-art "a" — 6x6 grid
LOGO_A = [
[0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1],
[0, 1, 1, 1, 1, 1],
]
# "archipelago" text — same pixel font as GRUB generator
PIXEL_CHARS = {
'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]],
'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]],
'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]],
'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]],
'i': [[0,1],[0,0],[0,1],[0,1],[0,1]],
'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]],
'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]],
'l': [[1,0],[1,0],[1,0],[1,0],[1,1]],
'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]],
'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]],
' ': [[0,0],[0,0],[0,0],[0,0],[0,0]],
}
def create_png_rgba(width, height, pixels):
"""Create a PNG with RGBA pixel data."""
def make_chunk(chunk_type, data):
chunk = chunk_type + data
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF)
signature = b'\x89PNG\r\n\x1a\n'
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0) # 8-bit RGBA
ihdr = make_chunk(b'IHDR', ihdr_data)
raw_data = bytearray()
for y in range(height):
raw_data.append(0)
offset = y * width * 4
raw_data.extend(pixels[offset:offset + width * 4])
compressed = zlib.compress(bytes(raw_data), 9)
idat = make_chunk(b'IDAT', compressed)
iend = make_chunk(b'IEND', b'')
return signature + ihdr + idat + iend
def generate():
pixels = bytearray(W * H * 4) # RGBA
cx, cy = W // 2, W // 2 - 30
logo_rows = len(LOGO_A)
logo_cols = len(LOGO_A[0])
pixel_size = 18
logo_w = logo_cols * pixel_size
logo_h = logo_rows * pixel_size
logo_x = cx - logo_w // 2
logo_y = 30
# Glow
glow_radius = 60
for y in range(H):
for x in range(W):
dx = max(0, logo_x - x, x - (logo_x + logo_w))
dy = max(0, logo_y - y, y - (logo_y + logo_h))
d = math.sqrt(dx * dx + dy * dy)
if d < glow_radius:
alpha = (1.0 - d / glow_radius) ** 2 * 0.25
idx = (y * W + x) * 4
pixels[idx] = ORANGE[0]
pixels[idx + 1] = ORANGE[1]
pixels[idx + 2] = ORANGE[2]
pixels[idx + 3] = int(alpha * 255)
# Logo pixels with 3D bevel
for row in range(logo_rows):
for col in range(logo_cols):
if LOGO_A[row][col]:
px = logo_x + col * pixel_size
py = logo_y + row * pixel_size
for dy in range(pixel_size - 1):
for dx in range(pixel_size - 1):
x, y = px + dx, py + dy
if 0 <= x < W and 0 <= y < H:
t = (dx + dy) / (2 * pixel_size)
r = int(ORANGE[0] * (1.0 - t * 0.3))
g = int(ORANGE[1] * (1.0 - t * 0.3))
b = int(ORANGE[2] * (1.0 - t * 0.3))
if dx < 2 or dy < 2:
r = min(255, r + 40)
g = min(255, g + 30)
b = min(255, b + 10)
idx = (y * W + x) * 4
pixels[idx] = r
pixels[idx + 1] = g
pixels[idx + 2] = b
pixels[idx + 3] = 255
# Text "archipelago" below logo
text = "archipelago"
text_pixel = 3
text_gap = 2
total_w = sum(len(PIXEL_CHARS.get(c, PIXEL_CHARS[' '])[0]) * text_pixel + text_gap for c in text) - text_gap
text_x = cx - total_w // 2
text_y = logo_y + logo_h + 16
cursor = text_x
for ch in text:
char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' '])
for row in range(len(char_data)):
for col in range(len(char_data[0])):
if char_data[row][col]:
for dy in range(text_pixel - 1):
for dx in range(text_pixel - 1):
x = cursor + col * text_pixel + dx
y = text_y + row * text_pixel + dy
if 0 <= x < W and 0 <= y < H:
idx = (y * W + x) * 4
pixels[idx] = 180
pixels[idx + 1] = 100
pixels[idx + 2] = 30
pixels[idx + 3] = 200
cursor += len(char_data[0]) * text_pixel + text_gap
return create_png_rgba(W, H, bytes(pixels))
if __name__ == '__main__':
out_path = sys.argv[1] if len(sys.argv) > 1 else 'logo.png'
data = generate()
with open(out_path, 'wb') as f:
f.write(data)
print(f'Generated {out_path} ({len(data)} bytes)')
Binary file not shown.

After

Width:  |  Height:  |  Size: 675 KiB

@@ -4,6 +4,7 @@
title-text: ""
desktop-color: "#0a0a0a"
desktop-image: "background.png"
+ boot_menu {
left = 25%
Binary file not shown.
@@ -0,0 +1,8 @@
[Plymouth Theme]
Name=Archipelago
Description=Archipelago Bitcoin Node OS — cyberpunk boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/archipelago
ScriptFile=/usr/share/plymouth/themes/archipelago/archipelago.script
@@ -0,0 +1,109 @@
// Archipelago Plymouth Theme — cyberpunk boot splash
// Dark background, neon orange pixel-art logo, animated progress bar
// Screen dimensions
screen_w = Window.GetWidth();
screen_h = Window.GetHeight();
// Background — solid near-black (the GRUB background handles the fancy stuff)
Window.SetBackgroundTopColor(0.02, 0.02, 0.04);
Window.SetBackgroundBottomColor(0.01, 0.01, 0.02);
// Load logo image (generated during build)
logo_image = Image("logo.png");
logo_sprite = Sprite(logo_image);
logo_w = logo_image.GetWidth();
logo_h = logo_image.GetHeight();
logo_sprite.SetX(screen_w / 2 - logo_w / 2);
logo_sprite.SetY(screen_h / 2 - logo_h / 2 - 60);
logo_sprite.SetOpacity(1.0);
// --- Progress bar ---
// Neon orange bar with glow, centered below logo
bar_w = 300;
bar_h = 4;
bar_x = screen_w / 2 - bar_w / 2;
bar_y = screen_h / 2 + logo_h / 2;
// Progress bar background (dark glass)
bar_bg = Image(bar_w, bar_h);
for (x = 0; x < bar_w; x++) {
for (y = 0; y < bar_h; y++) {
bar_bg.SetPixel(x, y, 0.1, 0.1, 0.12, 0.8);
}
}
bar_bg_sprite = Sprite(bar_bg);
bar_bg_sprite.SetX(bar_x);
bar_bg_sprite.SetY(bar_y);
// Progress bar fill (neon orange)
progress_val = 0;
fun refresh_callback() {
// Animate progress smoothly
if (Plymouth.GetMode() == "boot") {
progress_val = progress_val + 0.002;
if (progress_val > 1.0) progress_val = 1.0;
}
fill_w = Math.Int(bar_w * progress_val);
if (fill_w > 0) {
bar_fill = Image(fill_w, bar_h);
for (x = 0; x < fill_w; x++) {
for (y = 0; y < bar_h; y++) {
// Orange: rgb(251, 146, 60) = 0.984, 0.573, 0.235
bar_fill.SetPixel(x, y, 0.984, 0.573, 0.235, 1.0);
}
}
bar_fill_sprite = Sprite(bar_fill);
bar_fill_sprite.SetX(bar_x);
bar_fill_sprite.SetY(bar_y);
bar_fill_sprite.SetZ(1);
}
}
Plymouth.SetRefreshFunction(refresh_callback);
// --- Boot progress callback ---
fun boot_progress_callback(duration, progress) {
progress_val = progress;
}
Plymouth.SetBootProgressFunction(boot_progress_callback);
// --- Status message (below progress bar) ---
msg_sprite = Sprite();
msg_sprite.SetPosition(screen_w / 2, bar_y + 30, 2);
fun message_callback(text) {
// Plymouth passes boot messages here
// We could render them but keeping it clean — just the logo and bar
}
Plymouth.SetMessageFunction(message_callback);
// --- Password prompt (for LUKS) ---
fun display_password_callback(prompt, bullets) {
// LUKS unlock prompt
pass_image = Image.Text(prompt, 0.984, 0.573, 0.235);
pass_sprite = Sprite(pass_image);
pass_sprite.SetX(screen_w / 2 - pass_image.GetWidth() / 2);
pass_sprite.SetY(screen_h / 2 + 80);
// Bullet dots for password
if (bullets > 0) {
bullet_text = "";
for (i = 0; i < bullets; i++) {
bullet_text = bullet_text + "* ";
}
bullet_image = Image.Text(bullet_text, 0.984, 0.573, 0.235);
bullet_sprite = Sprite(bullet_image);
bullet_sprite.SetX(screen_w / 2 - bullet_image.GetWidth() / 2);
bullet_sprite.SetY(screen_h / 2 + 110);
}
}
Plymouth.SetDisplayPasswordFunction(display_password_callback);
// --- Quit callback ---
fun quit_callback() {
logo_sprite.SetOpacity(0);
}
Plymouth.SetQuitFunction(quit_callback);
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB