diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-29 10:22:53 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-29 10:22:53 +0200 |
| commit | 27c22afb02a819c8d9e28c9232e515716e92f263 (patch) | |
| tree | bcf726a42a271563131044d0283b6b735e457403 | |
| parent | 37d5d83c5b833394d909728ab19f9a1a37c94803 (diff) | |
| download | grub-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.md | 8 | ||||
| -rwxr-xr-x | preview | 94 | ||||
| -rw-r--r-- | theme.txt.in | 20 |
3 files changed, 111 insertions, 11 deletions
@@ -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 @@ -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" +} |
