diff options
| -rw-r--r-- | AGENTS.md | 13 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rwxr-xr-x | bin/udt-accent | 95 |
3 files changed, 101 insertions, 12 deletions
@@ -19,10 +19,17 @@ One colour moves: `@accent`. `bin/udt-accent` reads the current wallpaper, extracts its signature colour, and snaps it to the nearest of ten Catppuccin accents by perceptual hue in CIELAB. `wallp` calls it on every wallpaper change. -It writes four things: `~/.cache/wal/udt-accent.rasi` (rofi), +It writes five things: `~/.cache/wal/udt-accent.rasi` (rofi), `~/.cache/wal/udt-border.lua` (Hyprland), the accent substitution in -`~/.cache/wal/dunstrc`, and `~/.cache/wal/colors.json` (Firefox, via -pywalfox). +`~/.cache/wal/dunstrc`, `~/.cache/wal/colors.json` (Firefox, via +pywalfox), and `~/.cache/wal/udt-palette.qml` (quickshell). + +The QML palette is the one output that is not only the accent: it carries +the whole structural palette too, parsed out of `rofi/udt/palette.rasi` +rather than duplicated in the script. That file stays the single place the +Macchiato values are written down. Quickshell components watch the generated +file and re-read it in place, so a palette edit reaches a running shell +without restarting it; see the `quickshell` repo alongside this one. **Extraction is deliberately isolated from the pywal cache.** It calls `pywal.backends.colorz.get()` directly, which returns a list and writes nothing. @@ -9,6 +9,11 @@ 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. +Quickshell reads the palette rather than restating it: `udt-accent` writes +`~/.cache/wal/udt-palette.qml` from `rofi/udt/palette.rasi`, and the +components in the `quickshell` repo watch that file, so editing the palette +here recolours them without a restart. + ## Layout rofi/udt/ the theme files diff --git a/bin/udt-accent b/bin/udt-accent index 703f483..4294347 100755 --- a/bin/udt-accent +++ b/bin/udt-accent @@ -15,6 +15,7 @@ including the terminal's ANSI colors. See the design spec for why that matters. import json import math import os +import re import subprocess import sys import tempfile @@ -44,6 +45,12 @@ MIN_CHROMA = 10.0 OUTPUT = Path.home() / ".cache" / "wal" / "udt-accent.rasi" BORDER_OUTPUT = Path.home() / ".cache" / "wal" / "udt-border.lua" DUNSTRC = Path.home() / ".cache" / "wal" / "dunstrc" +QML_OUTPUT = Path.home() / ".cache" / "wal" / "udt-palette.qml" + +# The installed palette, which is a symlink back into this repo. Read rather +# than duplicated: palette.rasi is the one place the structural colours are +# written down, and a second copy here would drift the first time one changed. +PALETTE_RASI = Path.home() / ".config" / "rofi" / "udt" / "palette.rasi" COLORS_JSON = Path.home() / ".cache" / "wal" / "colors.json" # Catppuccin Macchiato, in pywal's slot order. Firefox is themed from this @@ -59,6 +66,25 @@ MACCHIATO = { } +def write_atomic(path, content): + """Write a file atomically, so no reader ever sees it half-written. + + Every generated file is watched by something: rofi rereads on launch, + quickshell watches with a FileView. A torn read shows up as a theme that + briefly loses its colours. + """ + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp") + try: + with os.fdopen(fd, "w") as handle: + handle.write(content) + os.replace(tmp, path) + except BaseException: + if os.path.exists(tmp): + os.unlink(tmp) + raise + + 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)) @@ -168,16 +194,66 @@ def write_border(name): f"return {{ {stops} }}\n" ) - BORDER_OUTPUT.parent.mkdir(parents=True, exist_ok=True) - fd, tmp = tempfile.mkstemp(dir=str(BORDER_OUTPUT.parent), suffix=".tmp") + write_atomic(BORDER_OUTPUT, content) + + +def read_palette(): + """Parse palette.rasi into {name: "#rrggbb"}. + + rofi writes colours as #rrggbbaa; QML wants #rrggbb, and every entry in + that file is fully opaque, so the alpha pair is dropped rather than + converted. Returns an empty dict if the file is missing, which leaves the + QML palette to fall back to its own defaults. + """ try: - with os.fdopen(fd, "w") as handle: - handle.write(content) - os.replace(tmp, BORDER_OUTPUT) - except BaseException: - if os.path.exists(tmp): - os.unlink(tmp) - raise + text = PALETTE_RASI.read_text() + except OSError: + return {} + + found = {} + for key, value in re.findall(r"(\w+):\s*#([0-9a-fA-F]{6,8})\s*;", text): + found[key] = "#" + value[:6] + return found + + +def write_qml(name): + """Write the palette and current accent as a QML singleton. + + Emitted as QML rather than parsed from palette.rasi by quickshell itself: + the accent has to reach it anyway, so one generated file carrying both + means a component needs a single FileView and no rasi parser. It is + regenerated on every wallpaper change along with the other outputs. + """ + palette = read_palette() + if not palette: + print("udt-accent: palette.rasi unreadable, skipping QML palette", + file=sys.stderr) + return + + rows = "\n".join( + f' readonly property color {key}: "{value}"' + for key, value in sorted(palette.items()) + ) + + content = ( + "// Generated by udt-accent. Do not edit.\n" + "//\n" + "// The Catppuccin Macchiato palette from palette.rasi, plus the accent\n" + "// currently snapped from the wallpaper. Import it from a quickshell\n" + "// component and watch this file to follow theme changes.\n" + "pragma Singleton\n" + "\n" + "import QtQuick\n" + "\n" + "QtObject {\n" + f' readonly property color accent: "{ACCENTS[name]}"\n' + f' readonly property string accentName: "{name}"\n' + "\n" + f"{rows}\n" + "}\n" + ) + + write_atomic(QML_OUTPUT, content) def write_colors_json(name, image): @@ -262,6 +338,7 @@ def main(image): write_accent(name) write_border(name) + write_qml(name) write_dunst(name) write_colors_json(name, image) |
