aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 10:11:18 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 10:11:18 +0200
commitdf0639ef214d21a621a3893afed25f1923898730 (patch)
tree0a8e6b1106bb1ca5031f41bc007b4df2f21106ed
parent26298e6bcca0ece78a76f735fcd4feba21895abb (diff)
downloadgrub-theme-udt-df0639ef214d21a621a3893afed25f1923898730.tar.gz
grub-theme-udt-df0639ef214d21a621a3893afed25f1923898730.zip
Add implementation plan; output to dist/udt
The script is named build, so its output directory cannot also be build/ at the repo root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--.gitignore2
-rw-r--r--docs/superpowers/plans/2026-09-29-grub-theme-udt.md555
-rw-r--r--docs/superpowers/specs/2026-09-29-grub-theme-udt-design.md12
3 files changed, 562 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore
index 3529f47..9de4449 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
theme.txt
-build/
+dist/
diff --git a/docs/superpowers/plans/2026-09-29-grub-theme-udt.md b/docs/superpowers/plans/2026-09-29-grub-theme-udt.md
new file mode 100644
index 0000000..f8d7544
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-29-grub-theme-udt.md
@@ -0,0 +1,555 @@
+# grub-theme-udt Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** A udt-coloured GRUB theme: `theme.txt.in` rendered by udt-palette, plus a `build` script that assembles an installable `dist/udt/`.
+
+**Architecture:** udt-palette (other repo, already done in its commit 0e2aefc) renders `theme.txt.in` to `theme.txt`. `build` (stdlib Python, one file) reads `theme.txt`, writes PNG slices with its own encoder, builds `.pf2` fonts with `grub-mkfont`, converts the wallpaper with `magick`, and swaps the finished tree into `dist/udt/`. Root copies that tree to `/boot/grub/themes/udt`.
+
+**Tech Stack:** Python 3 stdlib, GRUB 2.14 gfxmenu, `grub-mkfont`, `fc-match`, `magick`.
+
+Spec: `docs/superpowers/specs/2026-09-29-grub-theme-udt-design.md`.
+
+Contract with udt (fixed, do not change without messaging the udt session):
+- placeholders `@SCHEME@ @BG@ @BG_ALT@ @BG_DEEP@ @SURFACE@ @FG@ @FG_DIM@ @FG_FAINT@ @ACCENT@`, substituted as `#rrggbb`;
+- no other `@WORD@` token anywhere in `theme.txt.in`;
+- `build` is executable, at the repo root, independent of cwd; udt's `install.sh` runs it and prints the root commands only on exit 0.
+
+Verified facts: `grub-mkfont` records names `Noto Sans Regular 24`, `Noto Sans Bold 32`, `Inconsolata Nerd Font Mono Regular 18`; `fc-match` on a missing family returns a fallback (Liberation Sans), so the family must be checked.
+
+---
+
+### Task 1: theme template
+
+**Files:**
+- Create: `theme.txt.in`
+
+- [ ] **Step 1: Write `theme.txt.in`**
+
+```
+# GRUB theme for the unified-desktop-theme family, scheme @SCHEME@.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+#
+# udt-palette renders this to theme.txt; ./build assembles dist/udt/ from
+# that. The next line is read by ./build for the PNG colours; GRUB skips it.
+# udt: bg_alt=@BG_ALT@ bg_deep=@BG_DEEP@ surface=@SURFACE@ accent=@ACCENT@
+
+title-text: ""
+desktop-image: "background.jpg"
+desktop-image-scale-method: "crop"
+desktop-color: "@BG@"
+terminal-font: "Inconsolata Nerd Font Mono Regular 18"
+terminal-box: "terminal_box_*.png"
+terminal-left: "0"
+terminal-top: "0"
+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%
+ top = 8%
+ width = 19%
+ text = "%TITLE%"
+ font = "Noto Sans Bold 32"
+ color = "@FG@"
+}
+
++ boot_menu {
+ left = 2%
+ top = 18%
+ width = 21%
+ height = 60%
+ item_font = "Noto Sans Regular 24"
+ item_color = "@FG_DIM@"
+ selected_item_color = "@FG@"
+ icon_width = 0
+ icon_height = 0
+ item_icon_space = 0
+ item_height = 48
+ item_padding = 12
+ item_spacing = 6
+ selected_item_pixmap_style = "select_*.png"
+ scrollbar = false
+}
+
++ label {
+ left = 3%
+ top = 82%
+ width = 19%
+ id = "__timeout__"
+ text = "Booting in %d seconds"
+ font = "Noto Sans Regular 24"
+ color = "@FG_FAINT@"
+}
+
++ progress_bar {
+ id = "__timeout__"
+ left = 3%
+ top = 87%
+ width = 19%
+ height = 4
+ fg_color = "@ACCENT@"
+ bg_color = "@SURFACE@"
+ border_color = "@SURFACE@"
+}
+```
+
+- [ ] **Step 2: Check the only `@WORD@` tokens are the contract's**
+
+Run: `grep -oE '@[A-Z_]+@' theme.txt.in | sort -u`
+Expected exactly: `@ACCENT@ @BG@ @BG_ALT@ @BG_DEEP@ @FG@ @FG_DIM@ @FG_FAINT@ @SCHEME@ @SURFACE@` (one per line).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add theme.txt.in
+git commit -m "Add theme.txt template"
+```
+
+### Task 2: PNG encoder with selftest
+
+**Files:**
+- Create: `build` (mode 755)
+
+- [ ] **Step 1: Write the skeleton with the selftest first**
+
+```python
+#!/usr/bin/env python3
+# build: assemble the installable GRUB theme into dist/udt/.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+"""Assemble dist/udt/ from the theme.txt udt-palette rendered.
+
+Usage: build [--selftest]
+
+Writes the PNG slices, the .pf2 fonts and a GRUB-readable copy of the
+current wallpaper next to theme.txt. Everything is staged in a temporary
+directory and swapped in at the end, so a failed run leaves the previous
+dist/udt/ untouched. Never touches /boot: root copies dist/udt/ there.
+"""
+import struct
+import sys
+import zlib
+
+PNG_SIG = b"\x89PNG\r\n\x1a\n"
+
+
+def selftest():
+ w, h, px = unpng(png(3, 2, (0x12, 0x34, 0x56, 0xd9)))
+ assert (w, h, px) == (3, 2, {(0x12, 0x34, 0x56, 0xd9)}), (w, h, px)
+ print("selftest ok")
+
+
+def main():
+ if sys.argv[1:] == ["--selftest"]:
+ return selftest()
+
+
+if __name__ == "__main__":
+ main()
+```
+
+- [ ] **Step 2: Run, expect failure**
+
+Run: `chmod +x build && ./build --selftest`
+Expected: `NameError: name 'unpng' is not defined` (or `png`).
+
+- [ ] **Step 3: Add the encoder and the decoder (decoder is test-only) above `selftest`**
+
+```python
+def chunk(tag, data):
+ return (struct.pack(">I", len(data)) + tag + data
+ + struct.pack(">I", zlib.crc32(tag + data)))
+
+
+def png(w, h, rgba):
+ """A w x h PNG of one colour: 8-bit RGBA, not interlaced, which GRUB reads."""
+ row = b"\0" + bytes(rgba) * w # filter byte 0, then the pixels
+ return (PNG_SIG
+ + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0))
+ + chunk(b"IDAT", zlib.compress(row * h))
+ + chunk(b"IEND", b""))
+
+
+def unpng(data):
+ """Decode what png() writes: (width, height, set of RGBA pixels)."""
+ assert data[:8] == PNG_SIG
+ pos, idat = 8, b""
+ while pos < len(data):
+ n, tag = struct.unpack(">I4s", data[pos:pos + 8])
+ body = data[pos + 8:pos + 8 + n]
+ assert struct.unpack(">I", data[pos + 8 + n:pos + 12 + n])[0] == zlib.crc32(tag + body)
+ if tag == b"IHDR":
+ w, h = struct.unpack(">II", body[:8])
+ elif tag == b"IDAT":
+ idat += body
+ pos += 12 + n
+ raw = zlib.decompress(idat)
+ stride = 1 + 4 * w
+ px = set()
+ for y in range(h):
+ row = raw[y * stride:(y + 1) * stride]
+ assert row[0] == 0
+ px |= {tuple(row[1 + 4 * x:5 + 4 * x]) for x in range(w)}
+ return w, h, px
+```
+
+- [ ] **Step 4: Run, expect pass**
+
+Run: `./build --selftest`
+Expected: `selftest ok`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add build
+git commit -m "Add build script with PNG encoder"
+```
+
+### Task 3: reading the rendered theme
+
+**Files:**
+- Modify: `build`
+
+- [ ] **Step 1: Extend `selftest` (append before the `print`)**
+
+```python
+ good = "# udt: bg_alt=#1e2030 accent=#b7bdf8\ncolor = \"#cad3f5\"\n"
+ assert read_colours(good) == {"bg_alt": (0x1e, 0x20, 0x30), "accent": (0xb7, 0xbd, 0xf8)}
+ for bad in (good + 'color = "@FG@"\n', 'color = "#cad3f5"\n'):
+ try:
+ read_colours(bad)
+ except SystemExit:
+ pass
+ else:
+ raise AssertionError(f"accepted: {bad!r}")
+ tpl = (REPO / "theme.txt.in").read_text()
+ assert set(re.findall(r"@[A-Z_]+@", tpl)) <= PLACEHOLDERS, "template token udt does not fill"
+ for _, _, name in FONTS.values():
+ assert f'"{name}"' in tpl, f"template does not use font {name}"
+```
+
+- [ ] **Step 2: Run, expect failure**
+
+Run: `./build --selftest`
+Expected: `NameError: name 'read_colours' is not defined`.
+
+- [ ] **Step 3: Implement**
+
+Imports become:
+
+```python
+import re
+import struct
+import sys
+import zlib
+from pathlib import Path
+```
+
+Constants, after `PNG_SIG`:
+
+```python
+REPO = Path(__file__).resolve().parent
+PLACEHOLDERS = {f"@{r}@" for r in (
+ "SCHEME", "BG", "BG_ALT", "BG_DEEP", "SURFACE", "FG", "FG_DIM", "FG_FAINT", "ACCENT")}
+# file name: (fc-match pattern, size, the name grub-mkfont records and theme.txt uses)
+FONTS = {
+ "noto-sans-regular-24.pf2": ("Noto Sans:regular", 24, "Noto Sans Regular 24"),
+ "noto-sans-bold-32.pf2": ("Noto Sans:bold", 32, "Noto Sans Bold 32"),
+ "inconsolata-18.pf2": ("Inconsolata Nerd Font Mono", 18,
+ "Inconsolata Nerd Font Mono Regular 18"),
+}
+```
+
+Functions, before `selftest`:
+
+```python
+def die(msg):
+ sys.exit(f"build: {msg}")
+
+
+def read_colours(text):
+ """The colours on the '# udt:' line, as RGB tuples, from a rendered theme."""
+ left = sorted(set(re.findall(r"@[A-Z_]+@", text)))
+ if left:
+ die(f"theme.txt still has placeholders {' '.join(left)}; rerun udt-palette")
+ m = re.search(r"^# udt: (.+)$", text, re.M)
+ if not m:
+ die("theme.txt has no '# udt:' colour line")
+ return {k: tuple(bytes.fromhex(v.lstrip("#")))
+ for k, v in (kv.split("=") for kv in m.group(1).split())}
+```
+
+- [ ] **Step 4: Run, expect pass**
+
+Run: `./build --selftest`
+Expected: `selftest ok`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add build
+git commit -m "build: read colours from rendered theme.txt"
+```
+
+### Task 4: assembling dist/udt
+
+**Files:**
+- Modify: `build`
+
+- [ ] **Step 1: Add imports**
+
+```python
+import os
+import shutil
+import subprocess
+import tempfile
+```
+
+- [ ] **Step 2: Add constants after `FONTS`**
+
+```python
+RENDERED = REPO / "theme.txt"
+BUILD = REPO / "dist"
+OUT = BUILD / "udt"
+FONT_CACHE = BUILD / "fonts" # fonts do not follow the scheme, so built once
+WALLPAPER = Path.home() / ".cache/udt/wpaper"
+PANEL_ALPHA = 0xd9 # ~85%, enough to read text over any wallpaper
+MAX_SIDE = 2560 # widest mode in use; GRUB crops, a bigger image only costs /boot
+```
+
+- [ ] **Step 3: Add the builders before `selftest`**
+
+```python
+def run(*cmd):
+ p = subprocess.run(cmd, capture_output=True, text=True)
+ if p.returncode:
+ die(f"{cmd[0]} failed:\n{p.stderr.strip()}")
+ return p.stdout
+
+
+def pf2_name(path):
+ """The NAME section of a .pf2, which is what theme.txt refers to."""
+ data = path.read_bytes()
+ pos = 0
+ while pos < len(data):
+ tag, n = struct.unpack(">4sI", data[pos:pos + 8])
+ if tag == b"NAME":
+ return data[pos + 8:pos + 8 + n].rstrip(b"\0").decode()
+ pos += 8 + n
+ die(f"{path.name} has no NAME section")
+
+
+def font(fname, pattern, size, name):
+ """A cached .pf2, built from the TTF fontconfig resolves the pattern to."""
+ cached = FONT_CACHE / fname
+ if cached.exists():
+ return cached
+ family = pattern.split(":")[0]
+ # fc-match never fails: a missing family comes back as a fallback font.
+ if family not in run("fc-match", "-f", "%{family}", pattern).split(","):
+ die(f"font {family!r} is not installed")
+ ttf = run("fc-match", "-f", "%{file}", pattern)
+ FONT_CACHE.mkdir(parents=True, exist_ok=True)
+ tmp = cached.with_suffix(".tmp")
+ run("grub-mkfont", "-s", str(size), "-o", str(tmp), ttf)
+ if pf2_name(tmp) != name:
+ die(f"{fname} is named {pf2_name(tmp)!r}, theme.txt expects {name!r}")
+ tmp.rename(cached)
+ return cached
+
+
+def os_name():
+ for line in Path("/etc/os-release").read_text().splitlines():
+ if line.startswith("NAME="):
+ return line[5:].strip('"')
+ return "GNU/Linux"
+
+
+def assemble(stage, text, c, wallpaper, fonts):
+ (stage / "theme.txt").write_text(text.replace("%TITLE%", os_name()))
+ slices = {
+ "panel.png": (8, 8, (*c["bg_alt"], PANEL_ALPHA)),
+ # The selected item: a 4px accent bar on the left, surface behind.
+ "select_w.png": (4, 1, (*c["accent"], 255)),
+ "select_c.png": (1, 1, (*c["surface"], 255)),
+ "select_e.png": (8, 1, (*c["surface"], 255)),
+ }
+ for s in ("nw", "n", "ne", "w", "c", "e", "sw", "s", "se"):
+ slices[f"terminal_box_{s}.png"] = (1, 1, (*c["bg_deep"], 255))
+ for fname, (w, h, rgba) in slices.items():
+ (stage / fname).write_bytes(png(w, h, rgba))
+ for f in fonts:
+ shutil.copy(f, stage / f.name)
+ # [0]: first frame only. Baseline JPEG, alpha flattened: GRUB reads
+ # neither progressive JPEG nor webp, and has no use for transparency here.
+ bg = "#%02x%02x%02x" % c["bg_deep"]
+ run("magick", f"{wallpaper}[0]", "-auto-orient",
+ "-background", bg, "-alpha", "remove",
+ "-resize", f"{MAX_SIDE}x{MAX_SIDE}>", "-strip",
+ "-interlace", "none", "-quality", "90", str(stage / "background.jpg"))
+```
+
+- [ ] **Step 4: Replace `main`**
+
+```python
+def main():
+ if sys.argv[1:] == ["--selftest"]:
+ return selftest()
+ for tool in ("magick", "grub-mkfont", "fc-match"):
+ if not shutil.which(tool):
+ die(f"{tool} not found")
+ if not RENDERED.exists():
+ die("theme.txt missing; run udt-palette first")
+ text = RENDERED.read_text()
+ c = read_colours(text)
+ try:
+ wallpaper = WALLPAPER.resolve(strict=True)
+ except FileNotFoundError:
+ die(f"no wallpaper at {WALLPAPER}")
+ fonts = [font(f, *spec) for f, spec in FONTS.items()]
+ BUILD.mkdir(exist_ok=True)
+ stage = Path(tempfile.mkdtemp(dir=BUILD, prefix=".udt-"))
+ try:
+ assemble(stage, text, c, wallpaper, fonts)
+ os.chmod(stage, 0o755) # mkdtemp makes it 0700; root copies it as is
+ shutil.rmtree(OUT, ignore_errors=True)
+ stage.rename(OUT)
+ finally:
+ shutil.rmtree(stage, ignore_errors=True)
+ print(f"build: {OUT}")
+```
+
+- [ ] **Step 5: Selftest still passes**
+
+Run: `./build --selftest`
+Expected: `selftest ok`
+
+- [ ] **Step 6: Failure path leaves nothing behind**
+
+Run: `rm -f theme.txt; ./build; echo "exit $?"; ls -A dist 2>/dev/null`
+Expected: `build: theme.txt missing; run udt-palette first`, `exit 1`, and no `.udt-*` directory listed.
+
+- [ ] **Step 7: Real render through udt, then build**
+
+Run: `../unified-desktop-theme/bin/udt-palette` (renders every consumer including ours; it writes only gitignored outputs), then `./build`.
+If udt-palette needs arguments, use whatever `../unified-desktop-theme/install.sh` passes it; do not edit that repo.
+Expected: `build: /home/.../grub-theme-udt/dist/udt`, then:
+
+Run: `ls dist/udt; file dist/udt/background.jpg dist/udt/panel.png; grep -c '@' dist/udt/theme.txt; grep -n 'Slackware\|%TITLE%' dist/udt/theme.txt`
+Expected: 19 files (theme.txt, background.jpg, 3 .pf2, panel, 3 select, 9 terminal_box); JPEG "baseline"; PNG "8-bit/color RGBA, non-interlaced"; the `@` count is 1 (the copyright email); the title line shows the OS name, no `%TITLE%`.
+
+- [ ] **Step 8: Second run reuses cached fonts**
+
+Run: `ls -l --time-style=+%T dist/fonts; ./build; ls -l --time-style=+%T dist/fonts`
+Expected: font timestamps unchanged.
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add build
+git commit -m "build: assemble dist/udt with fonts and wallpaper"
+```
+
+### Task 5: README
+
+**Files:**
+- Create: `README.md`
+
+- [ ] **Step 1: Write `README.md`**
+
+````markdown
+# grub-theme-udt
+
+A GRUB 2 theme for the unified-desktop-theme (udt) family. The background is a
+snapshot of the current wallpaper; a translucent panel on the left carries the
+menu, in the active udt scheme's colours.
+
+## How it is wired
+
+`theme.txt.in` is a template. udt's `udt-palette` renders it to `theme.txt`
+with the scheme's colours, then udt's `install.sh` runs `./build`, which
+assembles the complete theme in `dist/udt/`: `theme.txt`, the PNG slices in
+the same colours, the `.pf2` fonts and `background.jpg`, a GRUB-readable copy
+of `~/.cache/udt/wpaper`. Both outputs are gitignored.
+
+The accent is the scheme's fixed `accent` role, not the wallpaper-tracking
+one: nothing at boot can read that.
+
+## Install
+
+GRUB reads `/boot` before anything else is mounted, so the theme is copied,
+not linked. In a root shell:
+
+ rm -rf /boot/grub/themes/udt && cp -r <this repo>/dist/udt /boot/grub/themes/
+
+Once, set this in `/etc/default/grub`:
+
+ GRUB_THEME="/boot/grub/themes/udt/theme.txt"
+
+then regenerate the config:
+
+ grub-mkconfig -o /boot/grub/grub.cfg
+
+udt's `install.sh` prints these commands after a successful build; it never
+runs them.
+
+## After a scheme or wallpaper change
+
+Rerun udt's `install.sh`, then the `cp` line above. `grub-mkconfig` is only
+needed when `GRUB_THEME` changes. The theme on `/boot` stays as it was until
+root copies the new one.
+
+## Requirements
+
+GRUB 2.14 or later (for `desktop-image-scale-method`), Python 3, `grub-mkfont`,
+`fc-match`, ImageMagick's `magick`, and the fonts Noto Sans (Regular, Bold)
+and Inconsolata Nerd Font Mono.
+
+## Test
+
+ ./build --selftest
+
+There is no GRUB emulator here, so the visual check is a reboot. Keep the
+previous `GRUB_THEME` line to go back to.
+
+## License
+
+GPLv2 only. See `LICENSE`.
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.
+````
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add README.md
+git commit -m "Add README"
+```
+
+### Task 6: end to end through udt
+
+- [ ] **Step 1: Run udt's installer**
+
+Run: `../unified-desktop-theme/install.sh`
+Expected: our `build: .../dist/udt` line, then the three root lines printed. Nothing run as root.
+
+- [ ] **Step 2: Report to the user** the printed root commands, and that `grub-mkconfig` also picks up the GFXMODE fix they made. The reboot is theirs.
diff --git a/docs/superpowers/specs/2026-09-29-grub-theme-udt-design.md b/docs/superpowers/specs/2026-09-29-grub-theme-udt-design.md
index f8bc517..ed97584 100644
--- a/docs/superpowers/specs/2026-09-29-grub-theme-udt-design.md
+++ b/docs/superpowers/specs/2026-09-29-grub-theme-udt-design.md
@@ -51,10 +51,10 @@ entry class; adding them later is additive.
|---|---|---|
| `theme.txt.in` | yes | the template, `@ROLE@` placeholders |
| `theme.txt` | no | rendered by udt-palette |
-| `build` | yes | stdlib Python script, assembles `build/udt/` |
-| `build/udt/` | no | the complete theme, what root copies |
+| `build` | yes | stdlib Python script, assembles `dist/udt/` |
+| `dist/udt/` | no | the complete theme, what root copies |
| `README.md`, `LICENSE` | yes | GPLv2 only |
-| `.gitignore` | yes | `theme.txt`, `build/` |
+| `.gitignore` | yes | `theme.txt`, `dist/` |
## Placeholders
@@ -80,7 +80,7 @@ GRUB ignores comment lines.
3. `build`:
- reads `theme.txt`, fails if any `@...@` remains or the `# udt:` line is
missing;
- - substitutes the title from `/etc/os-release` into `build/udt/theme.txt`;
+ - substitutes the title from `/etc/os-release` into `dist/udt/theme.txt`;
- writes `panel.png`, `select_{w,c,e}.png`, `terminal_box_{nw,n,ne,w,c,e,sw,s,se}.png`
with an in-script PNG encoder (`zlib` + `struct`, 8-bit RGBA, not
interlaced);
@@ -92,7 +92,7 @@ GRUB ignores comment lines.
side, metadata stripped.
4. udt's `install.sh` prints the root commands:
- rm -rf /boot/grub/themes/udt && cp -r <repo>/build/udt /boot/grub/themes/
+ rm -rf /boot/grub/themes/udt && cp -r <repo>/dist/udt /boot/grub/themes/
# once: set GRUB_THEME="/boot/grub/themes/udt/theme.txt" in /etc/default/grub
grub-mkconfig -o /boot/grub/grub.cfg
@@ -116,7 +116,7 @@ names against the template.
`build` exits non-zero with a one-line message when: `theme.txt` is missing
or unrendered, the wallpaper link is missing, `magick`/`grub-mkfont`/`fc-match`
is absent, or `fc-match` resolves to a fallback font rather than the requested
-family. Nothing is written to `build/udt/` until all inputs are valid, so a
+family. Nothing is written to `dist/udt/` until all inputs are valid, so a
failed build never leaves a half theme for root to copy.
## Testing