diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-11 09:27:58 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-11 09:27:58 +0200 |
| commit | 2ee3b2f7fa6ab225ebbd5a8826d8cf5022c90a33 (patch) | |
| tree | 8d4fd01844ff48d2d069f43e4999b8e06bbfa065 | |
| parent | 4737f3511b8dd9d3f9eb683cb15802e31e188f98 (diff) | |
| download | unified-desktop-theme-2ee3b2f7fa6ab225ebbd5a8826d8cf5022c90a33.tar.gz unified-desktop-theme-2ee3b2f7fa6ab225ebbd5a8826d8cf5022c90a33.zip | |
feat: extract wallpaper accent without touching the pywal cache
Calls pywal.backends.colorz.get() directly rather than running `wal -i`. That
function returns a list and writes nothing, so the preset-driven cache that
kitty, dunst and neovim read is left alone. `wal -i` has no isolation flag and
would rewrite all of ~/.cache/wal, reintroducing the unreadable-terminal
problem the preset was adopted to fix.
Writes atomically, so a rofi launch concurrent with a wallpaper change cannot
read a half-written theme. A missing or broken image falls back to mauve rather
than leaving no accent at all.
Verified across six wallpapers with colors.json byte-identical afterward.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6aeE37K4GHBsaM51yLTTq
| -rwxr-xr-x | bin/udt-accent | 58 |
1 files changed, 58 insertions, 0 deletions
diff --git a/bin/udt-accent b/bin/udt-accent index e459a35..a47460e 100755 --- a/bin/udt-accent +++ b/bin/udt-accent @@ -13,7 +13,10 @@ including the terminal's ANSI colors. See the design spec for why that matters. """ import math +import os import sys +import tempfile +from pathlib import Path # The nine candidate accents. rosewater and flamingo are excluded as # near-neutral tints that capture saturated inputs; maroon, sapphire and @@ -33,6 +36,8 @@ ACCENTS = { FALLBACK = "mauve" MIN_CHROMA = 10.0 +OUTPUT = Path.home() / ".cache" / "wal" / "udt-accent.rasi" + def _to_lab(hexval): """Convert #rrggbb to CIELAB. sRGB D65, the standard conversion.""" @@ -80,6 +85,54 @@ def snap(hexval): return min(ACCENTS, key=distance) +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]}") + + def selftest(): # Every accent must snap to itself, or the metric is not self-consistent. for name, hexval in ACCENTS.items(): @@ -99,3 +152,8 @@ def selftest(): 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) |
