aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 10:22:53 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 10:22:53 +0200
commit27c22afb02a819c8d9e28c9232e515716e92f263 (patch)
treebcf726a42a271563131044d0283b6b735e457403
parent37d5d83c5b833394d909728ab19f9a1a37c94803 (diff)
downloadgrub-theme-udt-27c22afb02a819c8d9e28c9232e515716e92f263.tar.gz
grub-theme-udt-27c22afb02a819c8d9e28c9232e515716e92f263.zip
Fix panel covering the menu; add QEMU preview
GRUB paints theme components in reverse declaration order, so the panel image declared first was drawn on top of the menu and labels, dimming them. Declare it last. preview boots dist/udt in real GRUB x86_64-efi under QEMU and OVMF and screenshots it, which is how the ordering bug was found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--README.md8
-rwxr-xr-xpreview94
-rw-r--r--theme.txt.in20
3 files changed, 111 insertions, 11 deletions
diff --git a/README.md b/README.md
index bcb0528..104863d 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,13 @@ and Inconsolata Nerd Font Mono.
## Test
./build --selftest
+ ./preview # headless, screenshot to dist/preview.png
+ ./preview --live # QEMU window: move through the menu, 'c' for the console
+ ./preview 1920x1080 # another mode
-There is no GRUB emulator here, so the visual check is a reboot. Keep the
-previous `GRUB_THEME` line to go back to.
+`preview` boots `dist/udt` in real GRUB (x86_64-efi) under QEMU and OVMF, with
+stand-in menu entries, so the theme can be checked before root installs it.
+It needs `qemu-system-x86_64`, OVMF in `/usr/share/OVMF` and `grub-mkimage`.
## License
diff --git a/preview b/preview
new file mode 100755
index 0000000..944d082
--- /dev/null
+++ b/preview
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+# preview: boot dist/udt in real GRUB under QEMU, before root installs it.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+"""Boot the built theme in GRUB x86_64-efi under QEMU and OVMF.
+
+Usage: preview [--live] [WIDTHxHEIGHT]
+
+Default: screenshot to dist/preview.png, headless. --live opens a QEMU
+window instead, to move through the menu and try the console ('c').
+The mode defaults to 2560x1080. The menu entries are stand-ins.
+"""
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parent
+THEME = REPO / "dist" / "udt"
+SHOT = REPO / "dist" / "preview.png"
+OVMF = Path("/usr/share/OVMF")
+MODULES = ("part_gpt part_msdos fat normal configfile all_video efi_gop gfxterm "
+ "gfxmenu png jpeg font true").split()
+BOOT_WAIT = 12 # ponytail: fixed wait for OVMF + GRUB; raise it if the shot is blank
+CFG = """\
+insmod all_video
+insmod gfxterm
+set gfxmode={mode},auto
+terminal_output gfxterm
+{fonts}
+set theme=($root)/boot/grub/themes/udt/theme.txt
+set timeout=30
+menuentry 'Slackware (preview)' --class slackware --class os {{ true }}
+submenu 'Advanced options for Slackware' {{ menuentry 'Recovery' {{ true }} }}
+menuentry 'UEFI Firmware Settings' --class efi {{ true }}
+"""
+
+
+def main():
+ args = sys.argv[1:]
+ live = "--live" in args
+ args = [a for a in args if a != "--live"]
+ mode = args[0] if args else "2560x1080"
+ xres, yres = mode.split("x")
+ if not (THEME / "theme.txt").exists():
+ sys.exit("preview: dist/udt missing; run ./build first")
+ with tempfile.TemporaryDirectory() as tmp:
+ tmp = Path(tmp)
+ root = tmp / "esp"
+ grub = root / "boot/grub"
+ (root / "EFI/BOOT").mkdir(parents=True)
+ shutil.copytree(THEME, grub / "themes/udt")
+ # grub-mkconfig writes one loadfont per .pf2 in the theme dir; do the same.
+ fonts = "\n".join(f"loadfont ($root)/boot/grub/themes/udt/{f.name}"
+ for f in sorted(THEME.glob("*.pf2")))
+ (grub / "grub.cfg").write_text(CFG.format(mode=mode, fonts=fonts))
+ subprocess.run(["grub-mkimage", "-O", "x86_64-efi", "-p", "/boot/grub",
+ "-o", str(root / "EFI/BOOT/BOOTX64.EFI"), *MODULES], check=True)
+ vars_fd = tmp / "vars.fd"
+ vars_fd.write_bytes((OVMF / "OVMF_VARS.fd").read_bytes())
+ mon = tmp / "mon.sock"
+ cmd = ["qemu-system-x86_64", "-machine", "q35", "-m", "512",
+ "-drive", f"if=pflash,format=raw,readonly=on,file={OVMF / 'OVMF_CODE.fd'}",
+ "-drive", f"if=pflash,format=raw,file={vars_fd}",
+ "-drive", f"file=fat:rw:{root},format=raw",
+ "-vga", "none", "-device", f"VGA,edid=on,xres={xres},yres={yres}"]
+ if live:
+ return subprocess.run(cmd).returncode
+ q = subprocess.Popen(cmd + ["-display", "none", "-monitor", f"unix:{mon},server,nowait"])
+ try:
+ time.sleep(BOOT_WAIT)
+ s = socket.socket(socket.AF_UNIX)
+ s.connect(str(mon))
+ ppm = tmp / "shot.ppm"
+ s.sendall(f"screendump {ppm}\n".encode())
+ for _ in range(50): # the dump is written asynchronously
+ if ppm.exists() and ppm.stat().st_size:
+ break
+ time.sleep(0.2)
+ time.sleep(0.5)
+ s.sendall(b"quit\n")
+ q.wait(timeout=10)
+ finally:
+ if q.poll() is None:
+ q.kill()
+ subprocess.run(["magick", str(ppm), str(SHOT)], check=True)
+ print(f"preview: {SHOT}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/theme.txt.in b/theme.txt.in
index 91cda7d..b864a86 100644
--- a/theme.txt.in
+++ b/theme.txt.in
@@ -18,15 +18,6 @@ terminal-width: "100%"
terminal-height: "100%"
terminal-border: "0"
-# The panel: bg_alt at ~85% alpha, stretched over the left quarter.
-+ image {
- left = 0
- top = 0
- width = 25%
- height = 100%
- file = "panel.png"
-}
-
# %TITLE% is filled in by ./build from /etc/os-release.
+ label {
left = 3%
@@ -75,3 +66,14 @@ terminal-border: "0"
bg_color = "@SURFACE@"
border_color = "@SURFACE@"
}
+
+# The panel: bg_alt at ~85% alpha, stretched over the left quarter. Last on
+# purpose: GRUB paints components in reverse order, so the last one declared
+# is the bottom layer. Declared first, it covers the menu and dims it.
++ image {
+ left = 0
+ top = 0
+ width = 25%
+ height = 100%
+ file = "panel.png"
+}