aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md')
-rw-r--r--docs/superpowers/specs/2026-09-11-unified-desktop-theme-design.md131
1 files changed, 92 insertions, 39 deletions
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.