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 /preview | |
| 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>
Diffstat (limited to 'preview')
| -rwxr-xr-x | preview | 94 |
1 files changed, 94 insertions, 0 deletions
@@ -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() |
