diff options
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.md | 131 |
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. |
