aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-11 16:30:21 +0200
committerDanilo M. <danix@danix.xyz>2026-09-11 16:30:21 +0200
commit842f25460ba7034e8828e37b9e9431d5a2d11afa (patch)
treea054ed06a6a102fb9dd35123ce1884c3c9aa81bc
parentdc4e324522acfdfbc661ae95089c96854eca6eb0 (diff)
downloadunified-desktop-theme-842f25460ba7034e8828e37b9e9431d5a2d11afa.tar.gz
unified-desktop-theme-842f25460ba7034e8828e37b9e9431d5a2d11afa.zip
feat(accent): generate a QML palette for quickshell
udt-accent now writes ~/.cache/wal/udt-palette.qml alongside the accent, border and colors.json it already produced. Quickshell components read it and watch it, so editing the palette here recolours a running shell without restarting it. Unlike the other outputs this one is not only the accent: it carries the structural colours too. Those are parsed out of rofi/udt/palette.rasi rather than restated in the script, so that file stays the single place the Macchiato values are written down. Hardcoding them a second time here is exactly the drift this is meant to remove: the quickshell components each had their own copy before this. The atomic write that write_border already did by hand is now a helper, write_atomic, used by both. Every generated file is watched by something, and a torn read shows up as a theme that briefly loses its colours. colors.json is untouched by this change, verified by running the script before and after and comparing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G7NRsGyF9jGfPYS4zPqpN7
-rw-r--r--AGENTS.md13
-rw-r--r--README.md5
-rwxr-xr-xbin/udt-accent95
3 files changed, 101 insertions, 12 deletions
diff --git a/AGENTS.md b/AGENTS.md
index bd67064..cf9f914 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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.
diff --git a/README.md b/README.md
index 5d74791..772e969 100644
--- a/README.md
+++ b/README.md
@@ -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)