diff options
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/plans/2026-09-11-rofi-unified-theme.md | 1125 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md | 131 |
2 files changed, 1217 insertions, 39 deletions
diff --git a/docs/superpowers/plans/2026-09-11-rofi-unified-theme.md b/docs/superpowers/plans/2026-09-11-rofi-unified-theme.md new file mode 100644 index 0000000..ede0c05 --- /dev/null +++ b/docs/superpowers/plans/2026-09-11-rofi-unified-theme.md @@ -0,0 +1,1125 @@ +# Rofi Unified Theme 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:** Replace five inconsistent rofi theme systems with one Catppuccin Macchiato palette, three shared layouts, and a wallpaper-driven accent that cannot produce an unreadable result. + +**Architecture:** A fixed Macchiato palette file plus a single generated `@accent` variable. `udt-accent` extracts the wallpaper's signature color by calling pywal's colorz backend directly (which writes nothing, leaving the terminal's preset colors untouched) and snaps it to one of nine Macchiato accents by perceptual hue in CIELAB. Three layout files import a shared `common.rasi`; eleven call sites are repointed at them. + +**Tech Stack:** rofi 1.7.3 rasi themes, Python 3 (stdlib `math`/`sys`/`pathlib` plus `pywal.backends.colorz`), bash. + +**Repo note:** This project holds the canonical copies under `rofi/` and `bin/`. Installation is by symlink into `~/.config/rofi/udt/` and `~/bin/`, so the working config and the repo never diverge. Tasks 1-8 build and verify; Task 9 installs; Tasks 10-12 migrate call sites. + +**Design spec:** `docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md` + +**On verification:** every rofi step in this plan opens a real window and is +judged by eye. Rofi needs a display and cannot be usefully checked from a +headless shell, where it exits non-zero for reasons unrelated to the theme. Run +these steps in a terminal on the actual desktop session. A theme that fails to +parse prints `Failed to parse theme:` with a line number; that message, not the +exit code, is the failure signal. + +--- + +### Task 1: Project scaffolding and license + +**Files:** +- Create: `LICENSE`, `README.md`, `.gitignore` + +- [ ] **Step 1: Fetch the GPLv2 text** + +```bash +cd ~/Programming/unified-desktop-theme +curl -sL https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt -o LICENSE +head -3 LICENSE +``` + +Expected: prints ` GNU GENERAL PUBLIC LICENSE` and the version line. + +- [ ] **Step 2: Create .gitignore** + +```bash +cat > .gitignore <<'EOF' +HANDOFF.md +__pycache__/ +*.pyc +EOF +``` + +- [ ] **Step 3: Write README.md** + +```markdown +# unified-desktop-theme + +A single visual identity for a Hyprland desktop: Catppuccin Macchiato as the +fixed base, Noto Sans for UI text, Inconsolata Nerd Font Mono for monospace. + +The accent color follows the current wallpaper, but is always snapped to a real +Macchiato accent, so it can never render as unreadable. + +Phase 1 covers rofi. Later phases extend the same palette to waybar, dunst, +conky, hyprland and quickshell. + +## Layout + + rofi/udt/ the theme files + bin/udt-accent wallpaper accent extractor + +Both are installed by symlink, so the repo holds the canonical copies: + + ./install.sh + +## Design + +See `docs/superpowers/specs/` for the design spec and the reasoning behind +each decision, including why accent extraction is deliberately isolated from +the pywal cache. + +## 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 4: Commit** + +```bash +git add LICENSE README.md .gitignore +git commit -m "chore: add GPLv2 license, README and gitignore" +``` + +--- + +### Task 2: The Macchiato palette file + +**Files:** +- Create: `rofi/udt/palette.rasi` + +- [ ] **Step 1: Write the palette** + +Values are from the official `catppuccin/palette` repository. Create `rofi/udt/palette.rasi`: + +```css +/* + * Catppuccin Macchiato palette for rofi. + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * Licensed under the GNU General Public License v2 only. + * + * Source: https://github.com/catppuccin/palette + * Structural colors only. The accent lives in accent.rasi and is generated. + */ + +* { + base: #24273aff; + mantle: #1e2030ff; + crust: #181926ff; + + text: #cad3f5ff; + subtext1: #b8c0e0ff; + subtext0: #a5adcbff; + + overlay2: #939ab7ff; + overlay1: #8087a2ff; + overlay0: #6e738dff; + + surface2: #5b6078ff; + surface1: #494d64ff; + surface0: #363a4fff; + + rosewater: #f4dbd6ff; + flamingo: #f0c6c6ff; + pink: #f5bde6ff; + mauve: #c6a0f6ff; + red: #ed8796ff; + maroon: #ee99a0ff; + peach: #f5a97fff; + yellow: #eed49fff; + green: #a6da95ff; + teal: #8bd5caff; + sky: #91d7e3ff; + sapphire: #7dc4e4ff; + blue: #8aadf4ff; + lavender: #b7bdf8ff; +} +``` + +- [ ] **Step 2: Verify rofi parses it** + +```bash +mkdir -p /tmp/udt-check +printf '@import "%s/rofi/udt/palette.rasi"\n* { background-color: @base; text-color: @text; }\n' "$PWD" > /tmp/udt-check/t.rasi +echo probe | rofi -dmenu -theme /tmp/udt-check/t.rasi -e "palette parses" +``` + +Expected: a rofi window appears with a dark Macchiato background and no parse error printed to the terminal. Press Escape to dismiss. A parse failure prints `Failed to parse theme:` and is the failure signal. + +- [ ] **Step 3: Commit** + +```bash +git add rofi/udt/palette.rasi +git commit -m "feat: add Catppuccin Macchiato palette for rofi" +``` + +--- + +### Task 3: The accent snapper, hue matching + +This task is TDD. The self-check is written first and must fail before the implementation exists. + +**Files:** +- Create: `bin/udt-accent` + +- [ ] **Step 1: Write the failing self-check** + +Create `bin/udt-accent` containing only the self-check and the constants it needs: + +```python +#!/usr/bin/env python3 +# udt-accent: pick a Catppuccin Macchiato accent matching a wallpaper. +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# Licensed under the GNU General Public License v2 only. +"""Extract a wallpaper's signature color and snap it to a Macchiato accent. + +Usage: udt-accent <wallpaper-path> + udt-accent --selftest + +The extraction deliberately calls pywal's colorz backend directly rather than +running `wal -i`, because `wal -i` rewrites the whole ~/.cache/wal directory +including the terminal's ANSI colors. See the design spec for why that matters. +""" + +import math +import sys + +# The nine candidate accents. rosewater and flamingo are excluded as +# near-neutral tints that capture saturated inputs; maroon, sapphire and +# lavender are excluded as near-duplicate hues of red, sky and mauve. +ACCENTS = { + "pink": "#f5bde6", + "mauve": "#c6a0f6", + "red": "#ed8796", + "peach": "#f5a97f", + "yellow": "#eed49f", + "green": "#a6da95", + "teal": "#8bd5ca", + "sky": "#91d7e3", + "blue": "#8aadf4", +} + +FALLBACK = "mauve" +MIN_CHROMA = 10.0 + + +def selftest(): + # Every accent must snap to itself, or the metric is not self-consistent. + for name, hexval in ACCENTS.items(): + got = snap(hexval) + assert got == name, f"{name} ({hexval}) snapped to {got}" + + # Representative real-world inputs. + assert snap("#ff8800") == "peach", snap("#ff8800") + assert snap("#00cc44") == "green", snap("#00cc44") + + # A near-grey has an unstable hue angle and must take the fallback. + assert snap("#888888") == FALLBACK, snap("#888888") + + print("selftest OK") + + +if __name__ == "__main__": + if len(sys.argv) == 2 and sys.argv[1] == "--selftest": + selftest() +``` + +- [ ] **Step 2: Run it to verify it fails** + +```bash +chmod +x bin/udt-accent +python3 bin/udt-accent --selftest +``` + +Expected: FAIL with `NameError: name 'snap' is not defined`. + +- [ ] **Step 3: Implement the color math** + +Insert these functions into `bin/udt-accent` after the `MIN_CHROMA` constant and before `def selftest():`: + +```python +def _to_lab(hexval): + """Convert #rrggbb to CIELAB. sRGB D65, the standard conversion.""" + r, g, b = (int(hexval[i:i + 2], 16) / 255 for i in (1, 3, 5)) + + def linear(c): + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + r, g, b = linear(r), linear(g), linear(b) + + x = (0.4124 * r + 0.3576 * g + 0.1805 * b) / 0.95047 + y = (0.2126 * r + 0.7152 * g + 0.0722 * b) + z = (0.0193 * r + 0.1192 * g + 0.9505 * b) / 1.08883 + + def f(t): + return t ** (1 / 3) if t > 0.008856 else 7.787 * t + 16 / 116 + + fx, fy, fz = f(x), f(y), f(z) + return (116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)) + + +def _hue(hexval): + """Perceptual hue angle in radians.""" + _, a, b = _to_lab(hexval) + return math.atan2(b, a) + + +def _chroma(hexval): + """Distance from the neutral axis. Near-greys sit close to zero.""" + _, a, b = _to_lab(hexval) + return math.hypot(a, b) + + +def snap(hexval): + """Return the name of the nearest candidate accent by perceptual hue.""" + if _chroma(hexval) < MIN_CHROMA: + return FALLBACK + + target = _hue(hexval) + + def distance(name): + delta = abs(_hue(ACCENTS[name]) - target) + return min(delta, 2 * math.pi - delta) # hue is circular + + return min(ACCENTS, key=distance) +``` + +- [ ] **Step 4: Run the self-check to verify it passes** + +```bash +python3 bin/udt-accent --selftest +``` + +Expected: `selftest OK` + +- [ ] **Step 5: Commit** + +```bash +git add bin/udt-accent +git commit -m "feat: add perceptual hue matching for Macchiato accents" +``` + +--- + +### Task 4: The accent snapper, extraction and output + +**Files:** +- Modify: `bin/udt-accent` + +- [ ] **Step 1: Add extraction, writing, and the CLI** + +Add to `bin/udt-accent`. Put the imports with the existing ones at the top, and the functions before `def selftest():`: + +```python +import os +import tempfile +from pathlib import Path +``` + +```python +OUTPUT = Path.home() / ".cache" / "wal" / "udt-accent.rasi" + + +def signature_color(image): + """Extract the image's most chromatic mid-tone color. + + Calls the colorz backend directly. It returns a list and writes nothing, + which is what keeps the pywal cache (and so the terminal) untouched. + """ + from pywal.backends import colorz + + colors = colorz.get(str(image), 16) + # Slot 0 trends near-black and the upper slots near-white; the signature + # color of an image lives in the middle. + return max(colors[1:7], key=_chroma) + + +def write_accent(name): + """Write the accent rasi file atomically.""" + hexval = ACCENTS[name] + content = ( + "/* Generated by udt-accent. Do not edit. */\n" + f"* {{ accent: {hexval}ff; }}\n" + ) + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + # Write-then-rename: a rofi launch during a wallpaper change must never + # read a half-written file. + fd, tmp = tempfile.mkstemp(dir=str(OUTPUT.parent), suffix=".tmp") + try: + with os.fdopen(fd, "w") as handle: + handle.write(content) + os.replace(tmp, OUTPUT) + except BaseException: + if os.path.exists(tmp): + os.unlink(tmp) + raise + + +def main(image): + try: + name = snap(signature_color(image)) + except Exception as exc: + # A broken image must still leave a working theme. + print(f"udt-accent: {exc}, falling back to {FALLBACK}", file=sys.stderr) + name = FALLBACK + + write_accent(name) + print(f"{name} {ACCENTS[name]}") +``` + +Replace the existing `__main__` block with: + +```python +if __name__ == "__main__": + if len(sys.argv) == 2 and sys.argv[1] == "--selftest": + selftest() + elif len(sys.argv) == 2: + main(sys.argv[1]) + else: + print(__doc__, file=sys.stderr) + sys.exit(2) +``` + +- [ ] **Step 2: Snapshot the pywal cache, then run against real wallpapers** + +The critical property is that this does NOT disturb the terminal colors. + +```bash +cp ~/.cache/wal/colors.json /tmp/udt-check/before.json +for w in $(find ~/Pictures/wallpapers -type f \( -name '*.jpg' -o -name '*.png' \) | head -6); do + printf '%-40s ' "$(basename "$w")" + python3 bin/udt-accent "$w" +done +``` + +Expected: six lines, each naming an accent and its hex, with different wallpapers giving different accents. + +- [ ] **Step 3: Verify the pywal cache was not touched** + +```bash +diff ~/.cache/wal/colors.json /tmp/udt-check/before.json && echo "ISOLATION OK" +``` + +Expected: `ISOLATION OK` with no diff output. **If this prints a diff, stop.** It means the terminal colors were modified, which is the exact regression this design exists to prevent. + +- [ ] **Step 4: Verify the fallback path** + +```bash +python3 bin/udt-accent /nonexistent/image.png +cat ~/.cache/wal/udt-accent.rasi +``` + +Expected: a warning on stderr, `mauve #c6a0f6` on stdout, and the file containing `* { accent: #c6a0f6ff; }`. + +- [ ] **Step 5: Re-run the self-check** + +```bash +python3 bin/udt-accent --selftest +``` + +Expected: `selftest OK` + +- [ ] **Step 6: Commit** + +```bash +git add bin/udt-accent +git commit -m "feat: extract wallpaper accent without touching the pywal cache" +``` + +--- + +### Task 5: The shared common.rasi + +**Files:** +- Create: `rofi/udt/common.rasi` + +- [ ] **Step 1: Write common.rasi** + +This holds everything the three layouts share. Fonts are the Qt/GTK ones, verified to resolve via `fc-match`. + +```css +/* + * Shared identity for all udt rofi layouts. + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * Licensed under the GNU General Public License v2 only. + */ + +@import "palette.rasi" +@import "accent.rasi" + +* { + font: "Noto Sans 11"; + monospace-font: "Inconsolata Nerd Font Mono 11"; + + background-color: transparent; + text-color: @text; + + margin: 0; + padding: 0; + spacing: 0; + + radius: 10px; + bar-height: 36px; +} + +window { + background-color: @base; + border: 2px; + border-color: @accent; + border-radius: @radius; + padding: 16px; +} + +mainbox { + spacing: 12px; +} + +inputbar { + background-color: @surface0; + border-radius: 6px; + padding: 10px 12px; + spacing: 8px; + children: [ prompt, entry ]; +} + +prompt { + text-color: @accent; +} + +entry { + placeholder: "search"; + placeholder-color: @overlay0; + cursor: text; +} + +listview { + scrollbar: false; + cycle: true; + dynamic: true; + spacing: 4px; +} + +element { + border-radius: 6px; + padding: 8px 10px; + spacing: 10px; + cursor: pointer; +} + +element normal.normal { text-color: @text; } +element alternate.normal{ text-color: @text; } +element normal.urgent { text-color: @red; } +element normal.active { text-color: @accent; } + +element selected.normal { + background-color: @accent; + text-color: @base; +} + +element selected.urgent { + background-color: @red; + text-color: @base; +} + +element selected.active { + background-color: @accent; + text-color: @base; +} + +element-icon { + size: 1.2em; + vertical-align: 0.5; + background-color: transparent; +} + +element-text { + vertical-align: 0.5; + background-color: transparent; + text-color: inherit; +} + +message { + background-color: @surface0; + border-radius: 6px; + padding: 10px; +} + +textbox { + text-color: @text; +} + +error-message { + background-color: @base; + text-color: @red; + padding: 12px; +} +``` + +- [ ] **Step 2: Create a placeholder accent so imports resolve** + +`accent.rasi` will be a symlink to the generated file (Task 9), but the file must exist now for the layouts to parse. + +```bash +mkdir -p rofi/udt +python3 bin/udt-accent --selftest >/dev/null && \ + printf '/* Generated by udt-accent. Do not edit. */\n* { accent: #c6a0f6ff; }\n' > rofi/udt/accent.rasi +cat rofi/udt/accent.rasi +``` + +Expected: the file prints with a mauve accent. + +- [ ] **Step 3: Commit** + +```bash +git add rofi/udt/common.rasi rofi/udt/accent.rasi +git commit -m "feat: add shared rofi styling common to all udt layouts" +``` + +--- + +### Task 6: The list layout + +`list.rasi` is built first because it serves eight of the eleven call sites. + +**Files:** +- Create: `rofi/udt/list.rasi` + +- [ ] **Step 1: Write list.rasi** + +```css +/* + * udt list layout: a tall, searchable, single-column list. + * For ssh hosts, passwords, emoji, VMs, repos, windows, clipboard. + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * Licensed under the GNU General Public License v2 only. + */ + +@import "common.rasi" + +window { + width: 680px; + anchor: center; + location: center; +} + +mainbox { + children: [ inputbar, listview ]; +} + +listview { + columns: 1; + lines: 12; + fixed-height: false; +} +``` + +- [ ] **Step 2: Verify it renders** + +```bash +printf 'alpha\nbravo\ncharlie\ndelta\n' | \ + rofi -dmenu -p "list" -theme "$PWD/rofi/udt/list.rasi" +``` + +Expected: a centered 680px window, Macchiato background, accent border, a search bar, and four rows. The selected row has an accent background with dark text. Press Escape. + +- [ ] **Step 3: Commit** + +```bash +git add rofi/udt/list.rasi +git commit -m "feat: add udt list layout" +``` + +--- + +### Task 7: The menu layout + +**Files:** +- Create: `rofi/udt/menu.rasi` + +- [ ] **Step 1: Write menu.rasi** + +A small box for a handful of fixed choices. No search bar: with four options, typing to filter is pointless and the bar only adds bulk. + +```css +/* + * udt menu layout: a small box for a few fixed options. + * For the screenshot menu, notes actions, the power menu. + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * Licensed under the GNU General Public License v2 only. + */ + +@import "common.rasi" + +window { + width: 380px; + anchor: center; + location: center; +} + +mainbox { + children: [ listview ]; +} + +listview { + columns: 1; + lines: 6; + fixed-height: false; +} + +element { + padding: 10px 12px; +} +``` + +- [ ] **Step 2: Verify it renders** + +```bash +printf 'lock\nlogout\nreboot\nshutdown\n' | \ + rofi -dmenu -p "power" -theme "$PWD/rofi/udt/menu.rasi" +``` + +Expected: a compact 380px window with four rows and no search bar, matching the list layout's colors and border. Press Escape. + +- [ ] **Step 3: Commit** + +```bash +git add rofi/udt/menu.rasi +git commit -m "feat: add udt menu layout" +``` + +--- + +### Task 8: The launcher layout + +**Files:** +- Create: `rofi/udt/launcher.rasi` + +- [ ] **Step 1: Write launcher.rasi** + +```css +/* + * udt launcher layout: a searchable icon grid for launching applications. + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * Licensed under the GNU General Public License v2 only. + */ + +@import "common.rasi" + +window { + width: 880px; + anchor: center; + location: center; +} + +mainbox { + children: [ inputbar, listview ]; +} + +listview { + columns: 3; + lines: 6; + fixed-height: false; +} + +element { + padding: 10px; + spacing: 12px; +} + +element-icon { + size: 2.2em; +} +``` + +- [ ] **Step 2: Verify it renders with real applications** + +```bash +rofi -show drun -theme "$PWD/rofi/udt/launcher.rasi" +``` + +Expected: an 880px window, three columns of applications with icons at a larger size, and a working search bar. Typing filters the grid. Press Escape. + +- [ ] **Step 3: Commit** + +```bash +git add rofi/udt/launcher.rasi +git commit -m "feat: add udt launcher layout" +``` + +--- + +### Task 9: Install script and wallp integration + +**Files:** +- Create: `install.sh` +- Modify: `~/bin/wallp` (the `finalize` function) + +- [ ] **Step 1: Write install.sh** + +```bash +#!/bin/bash +# Install udt by symlink, so the repo stays canonical. +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# Licensed under the GNU General Public License v2 only. +set -euo pipefail + +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +target="$HOME/.config/rofi/udt" + +mkdir -p "$target" "$HOME/bin" "$HOME/.cache/wal" + +# Theme files are symlinked individually; accent.rasi is NOT, it points at the +# generated file in the wal cache instead. +for f in palette.rasi common.rasi launcher.rasi menu.rasi list.rasi; do + ln -sfn "$repo/rofi/udt/$f" "$target/$f" +done + +# Seed the generated accent if it does not exist yet, so themes parse on a +# fresh install before any wallpaper has been set. +if [ ! -f "$HOME/.cache/wal/udt-accent.rasi" ]; then + cp "$repo/rofi/udt/accent.rasi" "$HOME/.cache/wal/udt-accent.rasi" +fi +ln -sfn "$HOME/.cache/wal/udt-accent.rasi" "$target/accent.rasi" + +ln -sfn "$repo/bin/udt-accent" "$HOME/bin/udt-accent" + +echo "installed:" +ls -l "$target" "$HOME/bin/udt-accent" +``` + +- [ ] **Step 2: Run it and verify the symlinks** + +```bash +chmod +x install.sh +./install.sh +``` + +Expected: `palette.rasi`, `common.rasi`, `launcher.rasi`, `menu.rasi` and `list.rasi` point into the repo; `accent.rasi` points into `~/.cache/wal/`; `~/bin/udt-accent` points into the repo. + +- [ ] **Step 3: Verify the installed path renders** + +```bash +printf 'one\ntwo\nthree\n' | rofi -dmenu -theme ~/.config/rofi/udt/list.rasi +``` + +Expected: renders exactly as in Task 6, now through the installed path. Press Escape. + +- [ ] **Step 4: Hook udt-accent into wallp** + +`finalize` is currently three lines: + +```bash +finalize() { + update_wpaper + apply_theme "$1" +} +``` + +`update_wpaper` maintains `~/.cache/wal/wpaper` as a symlink to the current +horizontal wallpaper, so that path is the simplest stable handle on the image +and needs no re-resolution. Add the accent refresh after it: + +```bash +finalize() { + update_wpaper + apply_theme "$1" + # Refresh the rofi accent from the wallpaper. Never fatal: a failure here + # must not stop the wallpaper from being set. + if command -v udt-accent >/dev/null 2>&1 && [ -e "$HOME/.cache/wal/wpaper" ]; then + udt-accent "$HOME/.cache/wal/wpaper" >/dev/null 2>&1 || true + fi +} +``` + +- [ ] **Step 5: Verify the integration end to end** + +```bash +cp ~/.cache/wal/colors.json /tmp/udt-check/before-wallp.json +cat ~/.cache/wal/udt-accent.rasi +wallp --restore +echo "--- accent after ---" +cat ~/.cache/wal/udt-accent.rasi +echo "--- terminal colors unchanged? ---" +diff ~/.cache/wal/colors.json /tmp/udt-check/before-wallp.json && echo "ISOLATION OK" +``` + +Expected: the accent file reflects the restored wallpaper, and `ISOLATION OK` confirms the terminal palette did not move. **If the diff is non-empty, stop and investigate before continuing.** + +- [ ] **Step 6: Commit** + +```bash +git add install.sh +git commit -m "feat: add symlink installer and wire accent refresh into wallp" +``` + +Note: `~/bin/wallp` is outside this repo, so it is not part of this commit. Mention the edit in the final summary so it is not lost. + +--- + +### Task 10: Migrate the list-layout call sites + +Eight call sites. Each is a one-line change. `~/bin` is not this repo, so these are edits to the live scripts; no commit here. + +**Files:** +- Modify: `~/bin/blackpearl-sshmenu.sh:4-7` +- Modify: `~/bin/blackpearl-emoji.sh:3` +- Modify: `~/bin/rofi-qemu.sh:68` +- Modify: `~/bin/github-repos.sh:22-23` +- Modify: `~/bin/hypr-windows.sh:5-6,17` +- Modify: `~/bin/rofipass:111` +- Modify: `~/bin/ddgr_search.py` +- Modify: `~/.config/hypr/sections/keybindings.lua` + +- [ ] **Step 1: Back up every file to be edited** + +```bash +mkdir -p /tmp/udt-check/backup +cp ~/bin/blackpearl-sshmenu.sh ~/bin/blackpearl-emoji.sh ~/bin/rofi-qemu.sh \ + ~/bin/github-repos.sh ~/bin/hypr-windows.sh ~/bin/rofipass \ + ~/bin/ddgr_search.py /tmp/udt-check/backup/ +cp ~/.config/hypr/sections/keybindings.lua /tmp/udt-check/backup/ +ls /tmp/udt-check/backup/ +``` + +- [ ] **Step 2: Repoint the three scripts that use the `dir`/`theme` variable pair** + +`blackpearl-sshmenu.sh`, `github-repos.sh` and `hypr-windows.sh` each define `dir=` and `theme=` and then reference `${dir}/${theme}.rasi`. Replace both variable lines in each file so the existing reference resolves to the new theme: + +```bash +for f in ~/bin/blackpearl-sshmenu.sh ~/bin/github-repos.sh ~/bin/hypr-windows.sh; do + sed -i \ + -e 's|^dir=.*|dir="$HOME/.config/rofi/udt"|' \ + -e "s|^theme=.*|theme='list'|" "$f" + echo "--- $f ---" + grep -nE '^dir=|^theme=|\$\{dir\}' "$f" +done +``` + +Expected: each file shows `dir="$HOME/.config/rofi/udt"`, `theme='list'`, and an unchanged `${dir}/${theme}.rasi` reference. + +- [ ] **Step 3: Repoint the scripts with an inline theme path** + +```bash +sed -i 's|-theme darknix/runner.rasi|-theme ~/.config/rofi/udt/list.rasi|' \ + ~/bin/blackpearl-emoji.sh ~/bin/rofi-qemu.sh +sed -i 's|-theme elegantVagrant/elegantvagrant-dark|-theme ~/.config/rofi/udt/list.rasi|' \ + ~/bin/rofipass +grep -n 'udt' ~/bin/blackpearl-emoji.sh ~/bin/rofi-qemu.sh ~/bin/rofipass +``` + +Expected: one match in each of the three files. + +- [ ] **Step 4: Repoint the cliphist keybinding** + +```bash +sed -i 's|-theme ~/.config/rofi/launchers/type-2/style-1.rasi|-theme ~/.config/rofi/udt/list.rasi|' \ + ~/.config/hypr/sections/keybindings.lua +grep -n 'cliphist' ~/.config/hypr/sections/keybindings.lua +``` + +Expected: the binding now references `udt/list.rasi`. + +- [ ] **Step 5: Give ddgr_search.py a theme** + +This script currently passes no `-theme` and uses rofi's default. Read it to find the rofi invocation, then add `-theme ~/.config/rofi/udt/list.rasi` to that command: + +```bash +grep -n 'rofi' ~/bin/ddgr_search.py +``` + +Apply the edit to the line that actually builds the rofi command, not to the docstring at the top that shows example usage. + +- [ ] **Step 6: Verify each one launches** + +Run each and confirm it renders in the new theme. These are interactive; press Escape to dismiss each. + +```bash +~/bin/blackpearl-sshmenu.sh +~/bin/hypr-windows.sh +~/bin/github-repos.sh +~/bin/blackpearl-emoji.sh +~/bin/rofi-qemu.sh +~/bin/rofipass +``` + +Expected: all six show the Macchiato list layout with the accent border. Test the cliphist binding with CTRL+SHIFT+L and the ddgr script per its own usage. + +--- + +### Task 11: Migrate the menu- and launcher-layout call sites + +**Files:** +- Modify: `~/bin/qar-scrotmenu.sh:3` +- Modify: `~/bin/blackpearl-notes.sh:3` +- Modify: `~/.config/rofi/powermenu/type-4/powermenu.sh` +- Modify: `~/bin/blackpearl-appsmenu.sh:5` +- Modify: `~/.config/rofi/launchers/type-1/launcher.sh` + +- [ ] **Step 1: Back up the remaining files** + +```bash +cp ~/bin/qar-scrotmenu.sh ~/bin/blackpearl-notes.sh ~/bin/blackpearl-appsmenu.sh \ + /tmp/udt-check/backup/ +cp ~/.config/rofi/powermenu/type-4/powermenu.sh /tmp/udt-check/backup/powermenu.sh +cp ~/.config/rofi/launchers/type-1/launcher.sh /tmp/udt-check/backup/launcher.sh +``` + +- [ ] **Step 2: Repoint the two menu scripts** + +```bash +sed -i 's|-theme darknix/scrotmenu.rasi|-theme ~/.config/rofi/udt/menu.rasi|' \ + ~/bin/qar-scrotmenu.sh +sed -i 's|-theme darknix/notes.rasi|-theme ~/.config/rofi/udt/menu.rasi|' \ + ~/bin/blackpearl-notes.sh +grep -n 'udt' ~/bin/qar-scrotmenu.sh ~/bin/blackpearl-notes.sh +``` + +Expected: one match in each. + +- [ ] **Step 3: Repoint the appsmenu launcher** + +```bash +sed -i 's|-theme darknix/appmenu.rasi|-theme ~/.config/rofi/udt/launcher.rasi|' \ + ~/bin/blackpearl-appsmenu.sh +grep -n 'udt' ~/bin/blackpearl-appsmenu.sh +``` + +- [ ] **Step 4: Repoint the two adi1090x scripts** + +These build a theme path from their own directory layout rather than taking a simple `-theme` argument. Read each one first: + +```bash +grep -nE 'theme|rasi|dir=' ~/.config/rofi/powermenu/type-4/powermenu.sh | head -20 +grep -nE 'theme|rasi|dir=' ~/.config/rofi/launchers/type-1/launcher.sh | head -20 +``` + +In each, replace the constructed theme path with the fixed new one: `~/.config/rofi/udt/menu.rasi` for the power menu, `~/.config/rofi/udt/launcher.rasi` for the launcher. Do not restructure these scripts, change only the theme path they pass to rofi. + +- [ ] **Step 5: Verify each one launches** + +```bash +~/bin/qar-scrotmenu.sh +~/bin/blackpearl-notes.sh +~/bin/blackpearl-appsmenu.sh +~/.config/rofi/powermenu/type-4/powermenu.sh +~/.config/rofi/launchers/type-1/launcher.sh +``` + +Expected: the two menu scripts and the power menu show the compact menu layout; appsmenu and the ALT+F2 launcher show the icon grid. Press Escape on each. **Take care with the power menu, it contains real shutdown and reboot entries. Dismiss it with Escape rather than selecting a row.** + +--- + +### Task 12: Final verification and documentation + +**Files:** +- Create: `docs/MIGRATION.md` + +- [ ] **Step 1: Confirm nothing still references the old themes** + +```bash +grep -rn 'darknix\|elegantVagrant\|launchers/type-2\|colors/catppuccin' \ + ~/bin/*.sh ~/bin/rofipass ~/bin/ddgr_search.py \ + ~/.config/hypr/sections/keybindings.lua 2>/dev/null | grep -v archive/ +``` + +Expected: no output. Any match is a call site that was missed. Commented-out lines are acceptable but should be noted. + +- [ ] **Step 2: Confirm the accent still tracks wallpapers and isolation holds** + +```bash +cp ~/.cache/wal/colors.json /tmp/udt-check/final.json +for w in $(find ~/Pictures/wallpapers -type f \( -name '*.jpg' -o -name '*.png' \) | head -4); do + printf '%-40s ' "$(basename "$w")" + udt-accent "$w" +done +diff ~/.cache/wal/colors.json /tmp/udt-check/final.json && echo "ISOLATION OK" +python3 ~/bin/udt-accent --selftest +``` + +Expected: four accents, `ISOLATION OK`, and `selftest OK`. + +- [ ] **Step 3: Write docs/MIGRATION.md** + +Record what changed outside this repo, since those edits are not under version control here: + +```markdown +# Migration record, phase 1 (rofi) + +Files edited outside this repository. Backups from the migration run are in +`/tmp/udt-check/backup/` and do not survive a reboot; if these need reverting +later, use the table below rather than the backups. + +## Call sites repointed + +| File | Was | Now | +| --- | --- | --- | +| `~/bin/blackpearl-sshmenu.sh` | `launchers/type-2/style-1` | `udt/list.rasi` | +| `~/bin/github-repos.sh` | `launchers/type-2/style-1` | `udt/list.rasi` | +| `~/bin/hypr-windows.sh` | `launchers/type-2/style-1` | `udt/list.rasi` | +| `~/bin/blackpearl-emoji.sh` | `darknix/runner.rasi` | `udt/list.rasi` | +| `~/bin/rofi-qemu.sh` | `darknix/runner.rasi` | `udt/list.rasi` | +| `~/bin/rofipass` | `elegantVagrant/elegantvagrant-dark` | `udt/list.rasi` | +| `~/bin/ddgr_search.py` | (rofi default) | `udt/list.rasi` | +| `~/.config/hypr/sections/keybindings.lua` | `launchers/type-2/style-1` | `udt/list.rasi` | +| `~/bin/qar-scrotmenu.sh` | `darknix/scrotmenu.rasi` | `udt/menu.rasi` | +| `~/bin/blackpearl-notes.sh` | `darknix/notes.rasi` | `udt/menu.rasi` | +| `~/.config/rofi/powermenu/type-4/powermenu.sh` | own theme dir | `udt/menu.rasi` | +| `~/bin/blackpearl-appsmenu.sh` | `darknix/appmenu.rasi` | `udt/launcher.rasi` | +| `~/.config/rofi/launchers/type-1/launcher.sh` | own theme dir | `udt/launcher.rasi` | + +## Other edits + +- `~/bin/wallp`: `finalize()` now calls `udt-accent` with the horizontal + wallpaper, guarded so a failure cannot stop the wallpaper being set. + +## Not removed + +The old theme directories are still on disk and untouched: +`~/.config/rofi/darknix/`, `elegantVagrant/`, `launchers/`, `applets/`, +`powermenu/`. Deleting them is a separate decision, deferred until the new +themes have been lived with. + +Note that `~/.config/rofi/applets/` still has its own launchers referenced from +waybar (`applets/bin/mpd.sh`) and is therefore still in use. It is out of scope +for phase 1 and will be handled with waybar in a later phase. + +## Out of scope + +`ronema` (rofi NetworkManager applet) was excluded: no longer used, pending +archival. +``` + +- [ ] **Step 4: Commit** + +```bash +git add docs/MIGRATION.md +git commit -m "docs: record phase 1 migration of rofi call sites" +``` + +- [ ] **Step 5: Final review with the user** + +Phase 1 is visual, so the user is the test. Ask them to exercise each menu over normal use and report anything that looks wrong: colors off the Macchiato palette, fonts that are not Noto Sans or Inconsolata, a layout that does not suit its job, or an accent that reads poorly against the background. + +--- + +## Deferred to later phases + +- waybar, dunst, conky, hyprland borders, quickshell (see the spec's "Later phases"). +- Deleting the five superseded rofi theme directories, once the new themes have proven themselves. +- `~/.config/rofi/applets/`, still live via waybar's mpd button, to be handled alongside waybar. +- `~/.config/rofi/config.rasi` still sets `font: "Mono 12"` and an `icon-theme`. The udt themes override the font, so this is harmless, but it is worth cleaning up when rofi's global config is next touched. diff --git a/docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md b/docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md index 8db59e5..3aa46dd 100644 --- a/docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md +++ b/docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md @@ -7,8 +7,8 @@ Status: approved, phase 1 not yet implemented One consistent visual identity across the desktop: Catppuccin Macchiato as the fixed base, Noto Sans as the UI font, Inconsolata Nerd Font Mono as the -monospace font. The accent color follows the wallpaper through pywal, but is -always snapped to a real Macchiato accent so the result is never unreadable. +monospace font. The accent color follows the wallpaper, but is always snapped +to a real Macchiato accent so the result is never unreadable. Phase 1 covers rofi only. Later phases extend the same mechanism to waybar, dunst, kitty, conky, hyprland and quickshell. @@ -37,8 +37,7 @@ background is `#1E1D2F` and its `selected` is `#7AA2F7`, a Tokyo Night blue. ## Decisions -Four decisions were made during brainstorming, each with alternatives -considered: +Five decisions were made during design, each with alternatives considered: 1. **Color model: Macchiato base with a pywal-driven accent.** Structural colors (backgrounds, text) are fixed Macchiato. Exactly one color, the @@ -46,11 +45,11 @@ considered: wallpaper tie-in, and the pywal pipeline already exists); fully pywal-driven (readability not guaranteed). -2. **Accent selection: snap to the nearest Macchiato accent.** pywal's dominant - color is matched by hue against the 14 named Macchiato accents and the - closest one wins. Rejected: raw pywal color (can be muddy or dark against - the base); raw color with a contrast floor (keeps more wallpaper fidelity - but can emit colors outside the palette). +2. **Accent selection: snap to the nearest Macchiato accent**, matched by + perceptual hue in CIELAB against a curated set of nine accents. Rejected: + raw pywal color (can be muddy or dark against the base); raw color with a + contrast floor (keeps more wallpaper fidelity but can emit colors outside + the palette). 3. **Scope: rofi only in phase 1.** Rofi holds the actual inconsistency and exercises every part of the pipeline. Once proven, each further app is a @@ -60,10 +59,14 @@ considered: different jobs and forcing one shape on all of them would push the variation back into scattered `-theme-str` strings. Revisit if three proves wrong. +5. **Accent extraction is isolated from the pywal cache.** The accent is + derived by calling the colorz backend directly on the wallpaper image, not + by running `wal -i`. See "Why extraction is isolated" below: this is what + keeps the change from regressing terminal readability. + ## Architecture ``` -~/.config/wal/templates/udt-accent.rasi # pywal template, emits raw dominant color ~/.config/rofi/udt/ palette.rasi # Macchiato, fixed, hand-written accent.rasi # symlink -> ~/.cache/wal/udt-accent.rasi (generated) @@ -71,21 +74,47 @@ considered: launcher.rasi # grid + search menu.rasi # small, fixed options list.rasi # tall searchable list -~/bin/udt-accent # snaps pywal dominant color to nearest Macchiato accent +~/bin/udt-accent # extracts wallpaper color, snaps to nearest Macchiato accent ``` Data flow on wallpaper change: -1. pywal renders `udt-accent.rasi` into `~/.cache/wal/` with the raw dominant - color. -2. `udt-accent` reads it, snaps to the nearest Macchiato accent, rewrites the - file in place. -3. Next rofi invocation picks it up. Rofi reads its theme per launch, so no +1. `wallp` sets the wallpaper and calls `udt-accent <wallpaper>`. +2. `udt-accent` extracts the image's signature color, snaps it to the nearest + Macchiato accent, and writes `~/.cache/wal/udt-accent.rasi`. +3. The next rofi invocation picks it up. Rofi reads its theme per launch, so no daemon and no reload are needed. -Step 3 is why this needs no running process. `udt-accent` is called from -`wal.sh`, which already performs the symlink-and-restart sequence for dunst and -kitty. +Step 3 is why this needs no running process. + +### Why extraction is isolated + +`wallp` invokes pywal as `wal --backend colorz -nq --theme "$THEME"`, with +`THEME` currently `sexy-splurge`. The colors are therefore from a fixed preset, +not from the wallpaper: `background` and `foreground` are pure black and white, +and `color1`-`color15` never change when the wallpaper does. + +This is deliberate. A previous wallpaper-derived setup made terminal text +unreadable. The cause is structural: the kitty template maps `{color1}` through +`{color15}` onto the terminal's sixteen ANSI slots, and every terminal program +(neovim, ls, git) picks colors by ANSI index. Nothing in a wallpaper-derived +palette guarantees that `color4` stays legible against `color0`, so low-contrast +images produce invisible comment text. Hand-tuned presets do guarantee it. + +Rofi is not exposed to that failure. It uses a single accent as a highlight +against a fixed Macchiato base, and the snapper can only emit one of nine +Macchiato accents, all of which are designed to be readable on `@base`. +Contrast is guaranteed by construction, whatever the input color is. + +So the accent must be derived without touching the pywal cache. `wal -i` has no +isolation flag and rewrites all of `~/.cache/wal/`, including +`colors-kitty.conf`, which would reintroduce exactly the old problem. Instead +`udt-accent` calls `pywal.backends.colorz.get()` directly. That function returns +a list of colors and writes nothing, so the preset-driven cache that kitty, +dunst and neovim depend on is left untouched. + +Verified during design: extracting accents for six different wallpapers left +`~/.cache/wal/colors.json` byte-identical. ## Color contract @@ -105,31 +134,49 @@ blue #8aadf4 lavender #b7bdf8 ``` `accent.rasi` defines exactly one variable, `@accent`, always equal to one of -the 14 accent colors above. +the nine candidate accents listed under "The accent snapper". Themes reference semantic names only, never literal hex. Everything structural is fixed; only `@accent` moves. ## The accent snapper -`~/bin/udt-accent`, Python 3, standard library only (`colorsys`, `re`). +`~/bin/udt-accent`, Python 3. Standard library (`math`, `sys`, `pathlib`) plus +`pywal.backends.colorz`, which is already installed as part of pywal. + +Usage: `udt-accent <wallpaper-path>`. + +- Calls `colorz.get(image, 16)` to extract the image's colors. Writes nothing. +- Takes the most chromatic of slots 1 through 6 as the image's signature color. + Slot 0 and the upper slots tend toward near-black and near-white. +- Snaps it to the nearest of nine candidate accents by perceptual hue. +- Writes `* { accent: #rrggbb; }` to `~/.cache/wal/udt-accent.rasi`, atomically + (write to a temporary file in the same directory, then rename) so a rofi + launch concurrent with a wallpaper change cannot read a half-written file. +- Falls back to `mauve` when the image is missing or unreadable, so a failure + still leaves a working theme rather than a broken one. + +**Matching metric.** Distance is the circular difference of hue angle in +CIELAB, computed as `atan2(b, a)`. Plain HLS hue was tried first and rejected: +it is not perceptually uniform, and it mismatched obvious cases, snapping +orange to `yellow` and saturated red to `flamingo`. + +**Candidate set, nine not fourteen:** pink, mauve, red, peach, yellow, green, +teal, sky, blue. Dropped are `rosewater` and `flamingo` (near-neutral tints +that carry a hue angle but almost no chroma, so they captured saturated inputs), +and `maroon`, `sapphire` and `lavender` (near-duplicate hues of `red`, `sky` +and `mauve`, adding ambiguity but no visible range). -- Reads the raw color from `~/.cache/wal/udt-accent.rasi`. -- Converts it and the 14 candidate accents to HLS. -- Picks the candidate with the smallest circular hue distance. -- Writes `* { accent: #rrggbb; }` back to the same path. -- Falls back to `mauve` when the cache file is missing or unparseable, so a - fresh machine or a failed pywal run still yields a working theme. +**Grey guard.** A near-grey color has an unstable hue angle, so inputs below a +chroma of 10 fall back to `mauve` rather than snapping arbitrarily. -Hue alone is the comparison metric: the Macchiato accents are already -normalized for lightness and saturation against the base, so matching hue is -what selects the perceptually right one. A near-grey wallpaper color has an -unstable hue, so colors below a small saturation threshold fall back to `mauve` -rather than snapping arbitrarily. +**Self-check.** `udt-accent --selftest` asserts that each of the nine accents +snaps to itself, that a mid orange gives `peach`, a mid green gives `green`, +and a grey gives the `mauve` fallback. -Leaves one runnable self-check (`udt-accent --selftest`) asserting that a known -orange input snaps to `peach`, a known green to `green`, and a grey to the -`mauve` fallback. +Known and accepted: fully saturated primaries such as `#ff0000` snap to +`peach` rather than `red`, because Macchiato has no vivid red. Colors that +extreme do not occur in colorz output from real images. ## Layouts @@ -176,7 +223,9 @@ Inconsolata, and the shape suits the job. The accent pipeline is verified separately by changing the wallpaper to images with clearly different dominant hues and confirming the accent tracks and stays -readable. +readable. Critically, it also requires confirming that kitty and neovim colors +do NOT change, since leaving them alone is the whole point of isolating the +extraction. `udt-accent --selftest` covers the snapping logic non-visually. @@ -186,15 +235,19 @@ Each subsequent app reuses `palette.rasi` and `accent.rasi` through a format-appropriate template, and needs no new mechanism: - **waybar**: CSS, consumes a generated `colors.css`. -- **dunst**: already pywal-templated, template gets repointed. -- **kitty**: already pywal-templated, same. +- **dunst**: already pywal-templated, template gets repointed. Note it draws + from the preset theme, so moving it to Macchiato is a real change, not a + repoint of the same colors. +- **kitty**: deliberately last, and possibly never. Its ANSI slots are the + source of the readability problem described above. A Macchiato ANSI mapping + is hand-tunable and safe, but wallpaper-derived values are not. - **conky**: Lua config, reads generated values. - **hyprland**: window borders use the accent. - **quickshell**: new work, to be designed when the user starts on it. ## License -GPLv2 (to be confirmed). Needs `LICENSE`, per-file header notices with +GPLv2 only. Needs `LICENSE`, per-file header notices with `Copyright (C) 2026 Danilo M. <danix@danix.xyz>`, and a License section in the README, added early rather than retrofitted. The README also needs the standard Development Approach section disclosing AI-assisted development. |
