# 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-.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 = ``` 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.