diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-16 09:16:46 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-16 09:16:46 +0200 |
| commit | 3df07af56c286a823b28ebe83cd82d90f72f9f12 (patch) | |
| tree | 0a22240181e0675aa1266d2c030b617fb5e2ee51 | |
| parent | b970a0deaad3a6723fe5727a3db7b653ebb3e7a3 (diff) | |
| download | unified-desktop-theme-3df07af56c286a823b28ebe83cd82d90f72f9f12.tar.gz unified-desktop-theme-3df07af56c286a823b28ebe83cd82d90f72f9f12.zip | |
docs: add the GTK palette-consumer implementation plan
GTK was listed in AGENTS.md as a covered consumer but nothing generated or
installed a theme for it. Every GTK app sat on a hardcoded upstream
catppuccin-macchiato-lavender while the rest of the desktop ran tokyo-night.
The plan templates the upstream stylesheets and assets the way Kvantum
already is, rendering into ~/.themes/udt under one fixed name per scheme.
Substitution is whole-file rather than confined to the @define-color block:
338 palette hexes in GTK3 and 328 in GTK4 sit outside it, which was measured
before the approach was chosen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | docs/superpowers/plans/2026-09-16-gtk-palette-consumer.md | 831 |
1 files changed, 831 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-09-16-gtk-palette-consumer.md b/docs/superpowers/plans/2026-09-16-gtk-palette-consumer.md new file mode 100644 index 0000000..1bc05ef --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-gtk-palette-consumer.md @@ -0,0 +1,831 @@ +# GTK as a Palette Consumer 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:** Make GTK3 and GTK4 follow the selected scheme, by generating `~/.themes/udt` from the palette instead of pointing at the hardcoded upstream `catppuccin-macchiato-lavender-standard+default`. + +**Architecture:** The same shape as the existing Kvantum consumer. The upstream theme's two stylesheets and 67 SVG assets become templates with `@ROLE@` placeholders where a palette colour appears; `udt-palette` renders them via new `TARGETS` entries; `install.sh` writes the result to `~/.themes/udt` under one fixed name for every scheme, so gsettings and the GTK4 symlink are set once and never change on a scheme switch. Neutral greys are left exactly as upstream drew them, the rule Kvantum already follows. + +**Tech Stack:** Python 3 (`bin/udt-palette`), bash (`install.sh`), GTK3/GTK4 CSS, gsettings. + +--- + +## Background the engineer needs + +The upstream theme is `~/.themes/catppuccin-macchiato-lavender-standard+default`, from `catppuccin/gtk` v1.0.3 (archived upstream; we are knowingly forking it). It contains: + +- `gtk-3.0/gtk.css` (8539 lines) and `gtk-3.0/gtk-dark.css`, which are **byte-identical**. +- `gtk-4.0/gtk.css` (8502 lines) and `gtk-4.0/gtk-dark.css`, also byte-identical. +- `gtk-3.0/assets/` and `gtk-4.0/assets/`, 67 SVGs each, the two directories **byte-identical**. +- `index.theme`, a metatheme descriptor. +- Other desktop dirs (`cinnamon/`, `gnome-shell/`, `metacity-1/`, `xfwm4/`, `plank/`) that Hyprland never reads. **Do not template these**; they are not copied to `~/.themes/udt` at all. + +Colours appear in two forms, both of which must be substituted: + +1. Hex, e.g. `#b7bdf8`. 11 distinct palette hexes in GTK3, 12 in GTK4. +2. Decimal triples inside `rgba(...)`, **with spaces**, e.g. `rgba(239, 241, 245, 0.5)`. Note `udt-palette`'s existing `rgba()` helper emits *without* spaces, so it cannot be reused here; substitution is on the literal triple text. + +A `@define-color` block sits at the tail of each file (GTK3 lines 8407-8539, GTK4 lines 7017-8502), but **the body above it also hardcodes palette colours** (338 occurrences in GTK3, 328 in GTK4). Substitution is therefore whole-file, not block-scoped. This was verified; do not assume the body routes through the variables. + +### The colour map + +Applies to both stylesheets and the assets. Left column is the literal text in the upstream file; right column is the role placeholder to write in the template. + +| Upstream | Placeholder | Role | Note | +| --- | --- | --- | --- | +| `#eff1f5` | `@FG@` | `fg` | Latte `text`. Upstream's near-white foreground. Mapped to `fg` so GTK text matches rofi, kitty and waybar. | +| `239, 241, 245` | `@FG_RGB@` | `fg` | Same colour, decimal form. | +| `#24273a` | `@BG@` | `bg` | | +| `36, 39, 58` | `@BG_RGB@` | `bg` | | +| `#b7bdf8` | `@ACCENT@` | `accent` | | +| `183, 189, 248` | `@ACCENT_RGB@` | `accent` | | +| `#1e2030` | `@BG_ALT@` | `bg_alt` | | +| `18, 19, 29` | `@BG_ALT_RGB@` | `bg_alt` | Near-miss of mantle; upstream rounding. Treated as `bg_alt`. | +| `#181926` | `@BG_DEEP@` | `bg_deep` | | +| `24, 25, 38` | `@BG_DEEP_RGB@` | `bg_deep` | | +| `17, 17, 27` | `@BG_DEEP_RGB@` | `bg_deep` | Latte `crust`, used as text-on-accent. | +| `#363a4f` | `@SURFACE@` | `surface` | | +| `54, 58, 79` | `@SURFACE_RGB@` | `surface` | | +| `#494d64` | `@SURFACE_ALT@` | `surface_alt` | GTK4 only. | +| `#ed8796` | `@CRITICAL@` | `critical` | | +| `237, 135, 150` | `@CRITICAL_RGB@` | `critical` | | +| `182, 106, 119` | `@CRITICAL_RGB@` | `critical` | Darkened red, close-button hover. | +| `#eed49f` | `@WARNING@` | `warning` | | +| `238, 212, 159` | `@WARNING_RGB@` | `warning` | | +| `#a6da95` | `@SUCCESS@` | `success` | | +| `166, 218, 149` | `@SUCCESS_RGB@` | `success` | | +| `#c6a0f6` | `@MAUVE@` | `highlight` | Uses the existing `[conky] highlight` role. | +| `#91d7e3` | `@INFO@` | `info` | | +| `#8aadf4` | `@BLUE@` | `blue` | GTK4 only. Uses `[terminal] blue`. | +| `#f5a97f` | `@PEACH@` | `peach` | GTK4 only. See Task 1 note. | +| `245, 169, 127` | `@PEACH_RGB@` | `peach` | GTK4 only. | + +**Left alone deliberately** (neutral shading, not theme colour, same rule as the Kvantum SVG): `#3e4152`, `#2e3143`, `#2a2d40`, `#383b4d`, `#393b49`, `#3b3c47`, `#424556`, `#494c60`, `#52535c`, `#616472`, `#6e7297`, `0, 0, 0`, `255, 255, 255`, `#ffffff`, `#666666`, `#424242`, `#07080c`, and every `*_500`/`*_700`/`*_900` Material swatch constant (`BLUEBERRY_500`, `STRAWBERRY_500`, `GRAPE_500`, etc). Also left alone: `#e55267`, `#eea2ae`, `#eedbb5`, `#eaca89`, `#e5bd6b`, `#ea7183`, `#b8e0ad`, `#a0a8f6`, `#bfc5f8`, `#d3d7fb`, `#e5e8fd`, `#94A6FF`, `#6A7CE0`, and the other derived tints, which are upstream-computed shades of colours we do substitute. + +**`peach` is not currently a role.** Task 1 adds it. Every scheme must define it or `--selftest` fails, which is the point of that check. + +--- + +## File Structure + +**Created:** +- `templates/gtk/gtk3.css.in` — GTK3 stylesheet template (~8539 lines, from upstream, placeholders substituted). +- `templates/gtk/gtk4.css.in` — GTK4 stylesheet template (~8502 lines). +- `templates/gtk/index.theme.in` — metatheme descriptor template. +- `templates/gtk/assets/*.svg.in` — 67 asset templates (only 36 actually contain a placeholder; the rest are copied through unchanged for simplicity of the install step). +- `templates/gtk/README.md` — why this exists, what is left alone, the archived-upstream caveat. +- `docs/superpowers/plans/2026-09-16-gtk-palette-consumer.md` — this file. + +**Modified:** +- `palette/roles-*.conf` (all nine) — add `peach` to `[ui]`. +- `bin/udt-palette` — add `rgb_triple()` helper, `GTK_ROLES`, `gen_gtk()`, `gen_gtk_asset()`, four `TARGETS` entries, and a selftest assertion. +- `install.sh` — install `~/.themes/udt`, set gsettings, repoint the GTK4 symlink, write `gtk-3.0/settings.ini`'s theme name. +- `AGENTS.md` — GTK is now a real consumer; correct the reload table and the "Qt and GTK" section. +- `templates/qt-gtk/README.md` — GTK half is now generated; Qt half unchanged. +- `.gitignore` — ignore the generated `templates/gtk/gtk3.css`, `gtk4.css`, `index.theme`, `assets/*.svg`. + +**Generated (gitignored):** `templates/gtk/gtk3.css`, `templates/gtk/gtk4.css`, `templates/gtk/index.theme`, `templates/gtk/assets/*.svg`. + +--- + +### Task 1: Add the `peach` role to all nine schemes + +GTK4 uses `#f5a97f` (macchiato `peach`) for a handful of chart and badge colours. No role currently exposes it. The selftest enforces an identical role set across schemes, so all nine role maps must gain it together. + +**Files:** +- Modify: `palette/roles-macchiato.conf`, `roles-frappe.conf`, `roles-mocha.conf`, `roles-tokyo-night.conf`, `roles-nord.conf`, `roles-dracula.conf`, `roles-material-ocean.conf`, `roles-material-palenight.conf`, `roles-material-darker.conf` + +- [ ] **Step 1: Check what each scheme calls its orange** + +```bash +cd ~/Programming/GIT/unified-desktop-theme +for f in palette/*.conf; do + case "$f" in *roles*) continue;; esac + echo "=== $f ===" + grep -iE "^(peach|orange|amber|yellow) " "$f" +done +``` + +Expected: each palette file names an orange-ish colour. Catppuccin schemes call it `peach`; tokyo-night, nord, dracula and the material schemes use their own names. Note the name each one uses — Step 2 needs it. + +- [ ] **Step 2: Add the role to every roles file** + +In each `palette/roles-<scheme>.conf`, inside the `[ui]` section, immediately after the `accent = ...` line, add: + +```ini + +# GTK uses an orange for chart fills and badge backgrounds. Named here rather +# than reused from [terminal] because [ui] is what a non-terminal consumer +# reads, and a flat namespace means one definition serves both. +peach = <that scheme's orange name> +``` + +For `roles-macchiato.conf`, `roles-frappe.conf` and `roles-mocha.conf` the value is `peach`. For the others use the name found in Step 1. + +- [ ] **Step 3: Run the selftest to verify every scheme resolves** + +Run: `./bin/udt-palette --selftest` +Expected: PASS, with every scheme now reporting **69 roles** (was 68), and the line `selftest OK`. If one scheme reports 68, that file was missed. + +- [ ] **Step 4: Commit** + +```bash +git add palette/roles-*.conf +git commit -m "feat(palette): add a peach role for GTK's chart and badge colours" +``` + +--- + +### Task 2: Add the `rgb_triple` helper and the GTK role list + +**Files:** +- Modify: `bin/udt-palette` (helper near `rgb_array`, around line 109; role list near `KVANTUM_ROLES`, around line 332) + +- [ ] **Step 1: Add the helper** + +Immediately after the `rgb_array` function (around line 113), add: + +```python +def rgb_triple(c): + """`r, g, b` with spaces, which is how GTK's CSS writes an rgba() colour. + + The existing rgba() helper emits without spaces and wraps in rgb()/rgba(), + neither of which matches what the upstream GTK theme contains, so the + substitution is on the bare triple and the template keeps the rgba() call. + """ + r, g, b, _ = c + return f"{r}, {g}, {b}" +``` + +- [ ] **Step 2: Add the role list** + +Immediately after `KVANTUM_ROLES` (around line 335), add: + +```python +# The roles GTK's stylesheets reference. Both a hex and a decimal-triple form +# of each is substituted, because the upstream theme writes colours both ways. +GTK_ROLES = ["fg", "bg", "bg_alt", "bg_deep", "surface", "surface_alt", + "accent", "critical", "warning", "success", "highlight", "info", + "blue", "peach"] +``` + +- [ ] **Step 3: Verify it imports cleanly** + +Run: `python3 -c "import sys; sys.path.insert(0, 'bin'); exec(open('bin/udt-palette').read().split('def main')[0]); print(rgb_triple((239, 241, 245, 100)))"` +Expected: `239, 241, 245` + +- [ ] **Step 4: Commit** + +```bash +git add bin/udt-palette +git commit -m "feat(palette): add an rgb_triple helper and the GTK role list" +``` + +--- + +### Task 3: Build the stylesheet templates + +This is a mechanical transformation of two large upstream files. It is done with a script so it is reproducible and reviewable, not by hand. + +**Files:** +- Create: `templates/gtk/gtk3.css.in`, `templates/gtk/gtk4.css.in` + +- [ ] **Step 1: Write the conversion script** + +Create `/tmp/mkgtk.py`: + +```python +#!/usr/bin/env python3 +"""Turn an upstream catppuccin/gtk stylesheet into a udt template. + +Order matters: the longest literals are replaced first, so a shorter one that +is a prefix of a longer one cannot corrupt it. Hex matching is case-insensitive +because upstream mixes cases; decimal triples are matched literally. +""" +import re +import sys + +HEX = [ + ("#eff1f5", "@FG@"), + ("#24273a", "@BG@"), + ("#b7bdf8", "@ACCENT@"), + ("#1e2030", "@BG_ALT@"), + ("#181926", "@BG_DEEP@"), + ("#363a4f", "@SURFACE@"), + ("#494d64", "@SURFACE_ALT@"), + ("#ed8796", "@CRITICAL@"), + ("#eed49f", "@WARNING@"), + ("#a6da95", "@SUCCESS@"), + ("#c6a0f6", "@MAUVE@"), + ("#91d7e3", "@INFO@"), + ("#8aadf4", "@BLUE@"), + ("#f5a97f", "@PEACH@"), +] + +TRIPLE = [ + ("239, 241, 245", "@FG_RGB@"), + ("183, 189, 248", "@ACCENT_RGB@"), + ("237, 135, 150", "@CRITICAL_RGB@"), + ("238, 212, 159", "@WARNING_RGB@"), + ("166, 218, 149", "@SUCCESS_RGB@"), + ("182, 106, 119", "@CRITICAL_RGB@"), + ("245, 169, 127", "@PEACH_RGB@"), + ("36, 39, 58", "@BG_RGB@"), + ("18, 19, 29", "@BG_ALT_RGB@"), + ("24, 25, 38", "@BG_DEEP_RGB@"), + ("17, 17, 27", "@BG_DEEP_RGB@"), + ("54, 58, 79", "@SURFACE_RGB@"), +] + +text = open(sys.argv[1]).read() +for literal, placeholder in HEX: + text = re.sub(re.escape(literal), placeholder, text, flags=re.IGNORECASE) +for literal, placeholder in TRIPLE: + text = text.replace(literal, placeholder) +open(sys.argv[2], "w").write(text) + +left = re.findall(r"#(?:b7bdf8|24273a|eff1f5|1e2030|181926|363a4f|494d64" + r"|ed8796|eed49f|a6da95|c6a0f6|91d7e3|8aadf4|f5a97f)", + text, re.IGNORECASE) +print(f"{sys.argv[2]}: {text.count('@')} placeholders, {len(left)} palette hexes left") +``` + +- [ ] **Step 2: Generate both templates** + +```bash +cd ~/Programming/GIT/unified-desktop-theme +mkdir -p templates/gtk +T=~/.themes/catppuccin-macchiato-lavender-standard+default +python3 /tmp/mkgtk.py $T/gtk-3.0/gtk.css templates/gtk/gtk3.css.in +python3 /tmp/mkgtk.py $T/gtk-4.0/gtk.css templates/gtk/gtk4.css.in +``` + +Expected: two lines reporting a few hundred placeholders each and **0 palette hexes left**. A non-zero count means a colour is missing from the map; add it and re-run. + +- [ ] **Step 3: Verify no palette colour survived in either form** + +```bash +grep -ciE "eff1f5|24273a|b7bdf8|1e2030|181926|363a4f|494d64|ed8796|eed49f|a6da95|c6a0f6|91d7e3|8aadf4|f5a97f" templates/gtk/gtk3.css.in templates/gtk/gtk4.css.in +grep -c "239, 241, 245\|183, 189, 248\|36, 39, 58\|17, 17, 27" templates/gtk/gtk3.css.in templates/gtk/gtk4.css.in +``` + +Expected: `0` for every file on both commands. + +- [ ] **Step 4: Verify the neutrals were NOT touched** + +```bash +grep -c "3e4152\|2e3143\|BLUEBERRY_500\|rgba(0, 0, 0" templates/gtk/gtk3.css.in +``` + +Expected: a non-zero count. These must survive; a `0` means the script over-matched. + +- [ ] **Step 5: Commit** + +```bash +git add templates/gtk/gtk3.css.in templates/gtk/gtk4.css.in +git commit -m "feat(gtk): template the upstream GTK3 and GTK4 stylesheets" +``` + +--- + +### Task 4: Build the asset and index.theme templates + +**Files:** +- Create: `templates/gtk/assets/*.svg.in` (67 files), `templates/gtk/index.theme.in` + +- [ ] **Step 1: Generate the asset templates** + +The 67 SVGs contain only three palette colours: `#24273a` (38 uses), `#b7bdf8` (36), `#1e2030` (2). The gtk-3.0 and gtk-4.0 asset directories are byte-identical, so one set serves both. + +```bash +cd ~/Programming/GIT/unified-desktop-theme +mkdir -p templates/gtk/assets +T=~/.themes/catppuccin-macchiato-lavender-standard+default +for f in $T/gtk-3.0/assets/*.svg; do + sed -e 's/#24273a/@BG@/gI' -e 's/#b7bdf8/@ACCENT@/gI' -e 's/#1e2030/@BG_ALT@/gI' \ + "$f" > "templates/gtk/assets/$(basename "$f").in" +done +ls templates/gtk/assets/ | wc -l +``` + +Expected: `67` + +- [ ] **Step 2: Verify the substitution and the surviving neutrals** + +```bash +grep -rlc "24273a\|b7bdf8\|1e2030" templates/gtk/assets/ | wc -l +grep -rl "@ACCENT@" templates/gtk/assets/ | wc -l +grep -rl "#666666\|#ffffff" templates/gtk/assets/ | wc -l +``` + +Expected: `0` palette hexes remaining; `36` files carrying `@ACCENT@`; a non-zero count of files still carrying the neutral greys. + +- [ ] **Step 3: Write the index.theme template** + +Create `templates/gtk/index.theme.in`: + +```ini +[Desktop Entry] +Type=X-GNOME-Metatheme +Name=udt +Comment=Generated by udt-palette. Do not edit. +Encoding=UTF-8 + +[X-GNOME-Metatheme] +GtkTheme=udt +MetacityTheme=udt +IconTheme=Material-Black-Plum-Suru +CursorTheme=hypr_bibata-modern-amber +ButtonLayout=close,minimize,maximize:menu +``` + +Note the icon and cursor themes differ from upstream's (`Tela-circle-Dark` / `Macchiato-cursors`), which this desktop does not use. This file has no placeholders; it is a template only so it lives with the rest and is installed by the same code path. + +- [ ] **Step 4: Commit** + +```bash +git add templates/gtk/assets templates/gtk/index.theme.in +git commit -m "feat(gtk): template the GTK widget assets and metatheme descriptor" +``` + +--- + +### Task 5: Wire GTK into the generator + +**Files:** +- Modify: `bin/udt-palette` (generator function near `gen_kvantum` around line 340; `TARGETS` around line 566) + +- [ ] **Step 1: Add the generator functions** + +Immediately after `gen_kvantum` (around line 353), add: + +```python +def gen_gtk(res, scheme, palette, template): + """GTK3/GTK4: substitute the palette into the upstream stylesheet. + + Derived from catppuccin/gtk v1.0.3, which is archived upstream. The whole + file is substituted rather than just its @define-color block, because the + 8400 lines above that block hardcode palette colours too. + + Two forms per role: the hex, and the bare `r, g, b` triple that the theme + writes inside rgba(). Neutral greys and upstream's derived tints are left + exactly as drawn, the same rule the Kvantum SVG follows. + """ + out = template + for role in GTK_ROLES: + out = out.replace(f"@{role.upper()}_RGB@", rgb_triple(res[role])) + out = out.replace(f"@{role.upper()}@", hex6(res[role])) + # The mauve placeholder reads from the conky highlight role, which is the + # only role that names it; spelling it MAUVE keeps the template readable + # against the upstream colours it replaced. + out = out.replace("@MAUVE@", hex6(res["highlight"])) + return out + + +def gen_gtk_asset(res, scheme, palette, template): + """One GTK widget asset. Only three palette colours appear in the SVGs.""" + out = template + for role in ("bg", "bg_alt", "accent"): + out = out.replace(f"@{role.upper()}@", hex6(res[role])) + return out +``` + +**Important:** `_RGB@` is replaced before the bare `@ROLE@`, or `@FG@` would match the start of `@FG_RGB@` and corrupt it. Keep that order. + +- [ ] **Step 2: Add the stylesheet and index targets** + +In `TARGETS` (around line 566), after the kvantum entries, add: + +```python + ("templates/gtk/gtk3.css", gen_gtk, "templates/gtk/gtk3.css.in"), + ("templates/gtk/gtk4.css", gen_gtk, "templates/gtk/gtk4.css.in"), + ("templates/gtk/index.theme", gen_gtk, "templates/gtk/index.theme.in"), +``` + +- [ ] **Step 3: Add the assets to TARGETS** + +`TARGETS` is a static list, but the 67 assets are discovered from disk. Immediately after the `TARGETS = [...]` closing bracket, add: + +```python +# The GTK widget assets, one target each. Discovered rather than listed: there +# are 67 of them and they arrive as a set from upstream, so a hand-written list +# would silently miss one added by a future theme refresh. +TARGETS += [ + (f"templates/gtk/assets/{p.name[:-3]}", gen_gtk_asset, + f"templates/gtk/assets/{p.name}") + for p in sorted((REPO / "templates" / "gtk" / "assets").glob("*.svg.in")) +] +``` + +- [ ] **Step 4: Render and verify** + +Run: `./bin/udt-palette` +Expected: the file count rises by 70 (2 stylesheets + index.theme + 67 assets). Then: + +```bash +grep -c "@" templates/gtk/gtk3.css | head -1 +grep -oE "#[0-9a-f]{6}" templates/gtk/gtk3.css | sort -u | head -5 +``` + +Expected: no `@ROLE@` placeholders left (the `@` count reflects only `@define-color`, `@import` and `@media`, which are CSS syntax), and the hexes present are the current scheme's, not macchiato's. + +- [ ] **Step 5: Verify the current scheme actually landed** + +```bash +grep -m1 "@define-color theme_fg_color" templates/gtk/gtk3.css +grep -m1 "@define-color theme_selected_bg_color" templates/gtk/gtk3.css +``` + +Expected under tokyo-night: `#c0caf5` for the fg and `#7aa2f7` for the selected background. Under macchiato it would be `#cad3f5` and `#b7bdf8`. + +- [ ] **Step 6: Run the selftest** + +Run: `./bin/udt-palette --selftest` +Expected: PASS, `selftest OK`, every scheme at 69 roles. + +- [ ] **Step 7: Gitignore the generated files** + +Add to `.gitignore`, in the generated-files block: + +```gitignore +/templates/gtk/gtk3.css +/templates/gtk/gtk4.css +/templates/gtk/index.theme +/templates/gtk/assets/*.svg +``` + +- [ ] **Step 8: Commit** + +```bash +git add bin/udt-palette .gitignore +git commit -m "feat(palette): add GTK3 and GTK4 as palette consumers" +``` + +--- + +### Task 6: Add a selftest assertion for the GTK templates + +The existing selftest catches a role a scheme cannot supply, and catches a rofi theme referencing a name the generator does not emit. The equivalent GTK failure is a template placeholder with no matching role: it would render literally into the CSS and GTK would silently drop the declaration. + +**Files:** +- Modify: `bin/udt-palette` (inside `selftest`, after the role-set comparison) + +- [ ] **Step 1: Write the assertion** + +In `selftest()`, after the `missing`/`extra` role-set loop, add: + +```python + # Every placeholder in the GTK templates must correspond to a role, or it + # renders literally and GTK drops the declaration without an error. The + # stylesheets are too large to eyeball, so this is the only thing standing + # between a typo and a silently half-themed desktop. + gtk_dir = REPO / "templates" / "gtk" + known = {f"@{r.upper()}@" for r in GTK_ROLES} + known |= {f"@{r.upper()}_RGB@" for r in GTK_ROLES} + known.add("@MAUVE@") + for tpl in sorted(gtk_dir.glob("*.in")) + sorted((gtk_dir / "assets").glob("*.in")): + used = set(re.findall(r"@[A-Z][A-Z_]*@", tpl.read_text())) + unknown = used - known + assert not unknown, f"{tpl.name} uses unknown placeholders: {sorted(unknown)}" +``` + +- [ ] **Step 2: Run it** + +Run: `./bin/udt-palette --selftest` +Expected: PASS, `selftest OK`. + +- [ ] **Step 3: Prove the assertion actually fires** + +```bash +sed -i 's/@ACCENT@/@NOSUCHROLE@/' templates/gtk/index.theme.in +./bin/udt-palette --selftest; echo "exit: $?" +``` + +Expected: FAIL with `index.theme.in uses unknown placeholders: ['@NOSUCHROLE@']` and a non-zero exit. (`index.theme.in` has no `@ACCENT@`, so if the sed is a no-op, use `templates/gtk/assets/checkbox-checked.svg.in` instead.) Then revert: + +```bash +git checkout templates/gtk/index.theme.in +./bin/udt-palette --selftest +``` + +Expected: PASS again. + +- [ ] **Step 4: Commit** + +```bash +git add bin/udt-palette +git commit -m "test(palette): assert every GTK template placeholder maps to a role" +``` + +--- + +### Task 7: Install the generated theme + +**Files:** +- Modify: `install.sh` (a new block after the Kvantum block, around line 122) + +- [ ] **Step 1: Back up the current GTK wiring** + +```bash +cp -a ~/.config/gtk-3.0/settings.ini ~/.config/gtk-3.0/settings.ini.pre-udt-gtk +gsettings get org.gnome.desktop.interface gtk-theme > ~/.config/gtk-3.0/gtk-theme.pre-udt-gtk +ls -la ~/.config/gtk-4.0/theme +``` + +Expected: the backup files exist and the symlink currently points into `catppuccin-macchiato-lavender-standard+default`. + +- [ ] **Step 2: Add the install block** + +In `install.sh`, immediately after the Kvantum block (after the `printf '[General]\ntheme=udt\n'` line), add: + +```bash +# GTK3 and GTK4. Installed under one fixed name for every scheme, like Kvantum: +# the theme name is set in gsettings and in a symlink, so a per-scheme name +# would mean rewriting both on every switch and leaving stale themes behind. +# +# Only the two GTK directories are installed. The upstream theme also ships +# cinnamon, gnome-shell, metacity, xfwm4 and plank directories, none of which +# anything on this desktop reads. +gtk_theme="$HOME/.themes/udt" +install -Dm644 "$repo/templates/gtk/index.theme" "$gtk_theme/index.theme" +for v in 3.0 4.0; do + src="gtk3.css" + [ "$v" = "4.0" ] && src="gtk4.css" + install -Dm644 "$repo/templates/gtk/$src" "$gtk_theme/gtk-$v/gtk.css" + # gtk-dark.css is byte-identical to gtk.css upstream; apps ask for one or + # the other depending on the prefer-dark setting, so both must exist. + install -Dm644 "$repo/templates/gtk/$src" "$gtk_theme/gtk-$v/gtk-dark.css" + rm -rf "$gtk_theme/gtk-$v/assets" + mkdir -p "$gtk_theme/gtk-$v/assets" + cp "$repo"/templates/gtk/assets/*.svg "$gtk_theme/gtk-$v/assets/" +done + +# GTK3 reads the theme name from gsettings on Wayland, not settings.ini: the +# file is written too, but gsettings is what actually wins. +sed -i 's/^gtk-theme-name=.*/gtk-theme-name=udt/' "$HOME/.config/gtk-3.0/settings.ini" +if command -v gsettings >/dev/null 2>&1; then + gsettings set org.gnome.desktop.interface gtk-theme 'udt' 2>/dev/null || true +fi + +# GTK4 ignores gtk-theme-name entirely; its stylesheet is imported by gtk.css +# through this symlink. +ln -sfn "$gtk_theme/gtk-4.0" "$HOME/.config/gtk-4.0/theme" +``` + +- [ ] **Step 3: Run the installer** + +Run: `./install.sh` +Expected: exit 0, and the existing `reloaded:` line unchanged (GTK apps are not signalled; they reread on restart). + +- [ ] **Step 4: Verify what landed on disk** + +```bash +ls -la ~/.themes/udt/ ~/.themes/udt/gtk-3.0/ | head -20 +ls ~/.themes/udt/gtk-3.0/assets | wc -l +grep -m1 "@define-color theme_fg_color" ~/.themes/udt/gtk-3.0/gtk.css +gsettings get org.gnome.desktop.interface gtk-theme +readlink ~/.config/gtk-4.0/theme +grep "^gtk-theme-name" ~/.config/gtk-3.0/settings.ini +``` + +Expected: `index.theme` plus `gtk-3.0/` and `gtk-4.0/`, each with `gtk.css`, `gtk-dark.css` and 67 assets; the fg matching the current scheme; gsettings reporting `'udt'`; the symlink pointing at `~/.themes/udt/gtk-4.0`; and `gtk-theme-name=udt`. + +- [ ] **Step 5: Verify a GTK app actually renders** + +Launch any GTK3 app that is not already running (`gtk3-widget-factory` if installed, otherwise a file chooser from a restarted app) and confirm it is not macchiato lavender. GTK apps only reread a theme at startup, so anything already open keeps the old colours until restarted. Ask the user to confirm visually; a screenshot of a transient dialog is a race. + +- [ ] **Step 6: Commit** + +```bash +git add install.sh +git commit -m "feat(install): install the generated GTK theme as ~/.themes/udt" +``` + +--- + +### Task 8: Verify a scheme switch actually moves GTK + +This is the whole point of the change: the bug was that a scheme switch left GTK behind. + +- [ ] **Step 1: Record the current GTK colours** + +```bash +grep -m1 "@define-color theme_fg_color" ~/.themes/udt/gtk-3.0/gtk.css +grep -m1 "@define-color theme_selected_bg_color" ~/.themes/udt/gtk-3.0/gtk.css +``` + +Note both values. + +- [ ] **Step 2: Switch to a different scheme and reinstall** + +```bash +cp ~/.config/udt/roles.conf /tmp/roles.conf.bak +sed -i 's/^scheme = .*/scheme = nord/' ~/.config/udt/roles.conf +./install.sh +``` + +Expected: exit 0. + +- [ ] **Step 3: Verify GTK moved** + +```bash +grep -m1 "@define-color theme_fg_color" ~/.themes/udt/gtk-3.0/gtk.css +grep -m1 "@define-color theme_selected_bg_color" ~/.themes/udt/gtk-3.0/gtk.css +grep -c "b7bdf8\|cad3f5" ~/.themes/udt/gtk-3.0/gtk.css +``` + +Expected: both values differ from Step 1 and are Nord colours; the macchiato-hex count is `0`. + +- [ ] **Step 4: Verify the assets moved too** + +```bash +grep -l "$(grep -m1 -oE '#[0-9a-f]{6}' <<< "$(grep -m1 'theme_selected_bg_color' ~/.themes/udt/gtk-3.0/gtk.css)")" ~/.themes/udt/gtk-3.0/assets/*.svg | wc -l +``` + +Expected: `36`, the accent-bearing assets, now carrying Nord's accent. + +- [ ] **Step 5: Restore the original scheme** + +```bash +cp /tmp/roles.conf.bak ~/.config/udt/roles.conf +./install.sh +grep -m1 "@define-color theme_fg_color" ~/.themes/udt/gtk-3.0/gtk.css +``` + +Expected: back to the Step 1 value. + +- [ ] **Step 6: No commit** + +This task changes nothing tracked. `git status` should be clean. + +--- + +### Task 9: Documentation + +**Files:** +- Create: `templates/gtk/README.md` +- Modify: `AGENTS.md`, `templates/qt-gtk/README.md` + +- [ ] **Step 1: Write the GTK README** + +Create `templates/gtk/README.md`: + +```markdown +# GTK3 and GTK4 + +Generated from the palette, installed as `~/.themes/udt`. One fixed name for +every scheme, like Kvantum: the name is referenced by gsettings and by the +GTK4 symlink, so a per-scheme name would mean rewriting both on every switch. + +Derived from `catppuccin/gtk` v1.0.3, which is **archived upstream**. Forking +it is deliberate: it is the theme this desktop already ran, and it will not +track future GTK4/libadwaita changes. Replacing it means replacing these +templates, not editing a consumer. + +## What is substituted + +Colours appear in two forms and both are replaced: the hex (`#b7bdf8`) and the +bare decimal triple GTK writes inside `rgba()` (`183, 189, 248`). Substitution +is whole-file, not confined to the `@define-color` block at the tail, because +the 8400 lines above it hardcode palette colours too. That was measured, not +assumed: 338 occurrences in GTK3 and 328 in GTK4 sit outside the block. + +`#eff1f5` is upstream's foreground, which is Latte `text`, a near-white it +pairs with a Macchiato background. It maps to the `fg` role so GTK text matches +rofi, kitty and waybar rather than being brighter than all of them. + +## What is left alone + +Neutral greys and upstream's derived tints: `#3e4152`, `#2e3143`, the +`rgba(0, 0, 0, ...)` shadows, `#ffffff`, and the `*_500`/`*_700`/`*_900` +Material swatch constants, which ship with every build and are not theme +colour. Same rule as the Kvantum SVG: shading is not palette. + +Two derived values are mapped anyway, because they are semantic rather than +shading: `17, 17, 27` (Latte crust, used as text on the accent) becomes +`bg_deep`, and `182, 106, 119` (a darkened red on the close button) becomes +`critical`. + +## Assets + +67 SVGs, carrying only three palette colours. The upstream `gtk-3.0/assets` +and `gtk-4.0/assets` directories are byte-identical, so one templated set is +rendered and copied into both. + +## Gotchas + +- **GTK3 takes its theme name from gsettings on Wayland**, not `settings.ini`. + The file is written too, but gsettings wins; it was once pinned to `Breeze`, + silently overriding the file for who knows how long. +- **GTK4 ignores `gtk-theme-name` entirely.** Its stylesheet is imported by + `~/.config/gtk-4.0/gtk.css` through the `theme` symlink. +- **`gtk-dark.css` must exist** alongside `gtk.css`. Apps request one or the + other depending on the prefer-dark setting; upstream ships them identical. +- **GTK apps only reread a theme at startup**, like Qt. A running app keeps the + old colours, which is why the accent here is the fixed fallback rather than + the wallpaper-tracking one. +``` + +- [ ] **Step 2: Correct AGENTS.md's Qt/GTK section** + +In `AGENTS.md`, find the paragraph beginning `Qt and GTK apps reach the palette through Kvantum.` and replace its first sentence with: + +```markdown +Qt apps reach the palette through Kvantum; GTK has its own generated theme. +``` + +Then, after that paragraph, add: + +```markdown +**GTK is generated, not installed from upstream.** It used to sit on a +hardcoded `catppuccin-macchiato-lavender` from `catppuccin/gtk`, which meant +every GTK app stayed macchiato while the rest of the desktop moved to another +scheme. The stylesheets are now templates rendered into `~/.themes/udt`; see +`templates/gtk/README.md` for what is substituted and what is deliberately +left as upstream drew it. +``` + +- [ ] **Step 3: Correct the reload table** + +In `AGENTS.md`, the reload table's `Qt / GTK apps` row is still accurate (both reread only on restart). Leave it. Verify by reading the table. + +- [ ] **Step 4: Correct the qt-gtk README** + +In `templates/qt-gtk/README.md`, replace the `## Upstream themes` table's GTK row and the sentence above it, so it reads: + +```markdown +## Upstream themes + +Qt's theme is installed from upstream. GTK's is generated by this repo; see +`templates/gtk/README.md`. + +| | Theme | Source | +| --- | --- | --- | +| Qt5 + Qt6 | `udt` | generated, derived from `catppuccin/Kvantum` | +| GTK3 + GTK4 | `udt` | generated, derived from `catppuccin/gtk` v1.0.3 | +``` + +And replace the `## Reproducing` gsettings line for the theme with: + +```bash + gsettings set org.gnome.desktop.interface gtk-theme 'udt' +``` + +- [ ] **Step 5: Verify the docs match reality** + +```bash +grep -n "catppuccin-macchiato-lavender-standard" AGENTS.md templates/qt-gtk/README.md +``` + +Expected: no hits outside a historical note. Any remaining reference describes the old wiring and must be corrected or marked as history. + +- [ ] **Step 6: Commit** + +```bash +git add templates/gtk/README.md AGENTS.md templates/qt-gtk/README.md +git commit -m "docs(gtk): record GTK as a generated consumer" +``` + +--- + +### Task 10: Final verification + +- [ ] **Step 1: Full selftest** + +Run: `./bin/udt-palette --selftest` +Expected: PASS, nine schemes at 69 roles each, `selftest OK`. + +- [ ] **Step 2: Clean install from scratch** + +```bash +rm -rf ~/.themes/udt +./install.sh +ls ~/.themes/udt/gtk-3.0/assets | wc -l +``` + +Expected: exit 0, and `67`. This proves the install block creates everything it needs rather than relying on what an earlier run left behind. + +- [ ] **Step 3: Confirm the working tree is clean and nothing generated is tracked** + +```bash +git status --short +git ls-files templates/gtk/ | grep -v "\.in$" | grep -v README +``` + +Expected: clean tree; the second command returns nothing (only `.in` templates and the README are tracked). + +- [ ] **Step 4: Ask the user to confirm visually** + +GTK apps must be restarted to pick up the theme. Ask the user to open a GTK3 app and a GTK4/libadwaita app and confirm both match the current scheme. This is the one check that cannot be automated, and the same reason the Sublime chrome needed eyes. + +--- + +## Self-Review + +**Spec coverage.** The ask was: make GTK follow the scheme, whole-file substitution, accept the archived-upstream risk. Task 3 does the stylesheets, Task 4 the assets and metatheme, Task 5 the generator wiring, Task 7 the install and rewiring, Task 8 proves a scheme switch moves GTK. Task 1 adds the one missing role. Task 6 adds the regression guard. Task 9 corrects the two documents that currently state the opposite. + +**Placeholders.** Every code step contains the actual code; every verification step names the command and the expected output. The one judgement left to execution is which colour name each non-Catppuccin scheme uses for its orange, which Task 1 Step 1 discovers rather than guesses. + +**Type consistency.** `GTK_ROLES` (Task 2) is consumed by `gen_gtk` (Task 5) and the selftest (Task 6). `rgb_triple` (Task 2) is called only by `gen_gtk`. The `_RGB@`-before-`@ROLE@` ordering is stated in Task 5 Step 1 and matters; `@FG@` is a prefix of `@FG_RGB@`. `gen_gtk_asset` handles only the three roles the SVGs contain, which Task 4 Step 2 verifies. + +**Known risk.** `@MAUVE@` is spelled for the upstream colour it replaces but reads the `highlight` role; `gen_gtk` handles it after the loop and the selftest whitelists it explicitly. If a future role named `mauve` appears, that special case should go. |
