diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-22 17:25:09 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-22 17:25:09 +0200 |
| commit | 78d7d285a980513101210188ab3da8a4c3b512e8 (patch) | |
| tree | 3e2d2f395c16afa70bd2f48af0f135c9569d88f2 | |
| parent | 6b6ac80c924325f1229bf7f230fd80cde0ce7744 (diff) | |
| download | unified-desktop-theme-78d7d285a980513101210188ab3da8a4c3b512e8.tar.gz unified-desktop-theme-78d7d285a980513101210188ab3da8a4c3b512e8.zip | |
install.sh was one linear script, so the only way to re-theme kitty after
it drifted was to reinstall everything. Each consumer is now a c_<name>
function: `./install.sh kitty gtk` runs only those, no argument runs all of
them in definition order, and `--list` prints name, software|hardware and
what it themes, read off the function lines themselves. quickshell's Theme
status page lists and re-applies consumers through those two calls, so a
new consumer is a new function and nothing else.
udt-accent now runs before the consumers with a new --no-helpers flag.
Before it, every install ran it last and it started homepage, GRUB,
keyboard, LED strip, bulbs and TV detached, so re-applying conky also
repainted the strip. Those helpers are consumers of their own here, run in
the foreground so a re-apply reports how it went; wallp still starts them
all on a wallpaper change. Running the accent first means c_sublime fills
the template's @ACCENT@ itself, from udt-accent.rasi.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | AGENTS.md | 16 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rwxr-xr-x | bin/udt-accent | 12 | ||||
| -rwxr-xr-x | install.sh | 158 |
4 files changed, 151 insertions, 40 deletions
@@ -45,6 +45,22 @@ overridden, so every highlight group follows the scheme without restating one. ## One palette, many consumers +**Each consumer is a `c_<name>` function in `install.sh`**, run in the order +it is defined, with what it themes in a comment on the same line (prefixed +`hardware:` for a device). `./install.sh kitty gtk` installs only those, +`./install.sh --list` prints `name<TAB>software|hardware<TAB>what`, +read off the script itself, and no argument runs them all. quickshell's Theme +status page lists and re-applies consumers through those two calls, so adding +a consumer is adding a function and nothing else. Palette generation and then +`udt-accent --no-helpers` always run first: udt-palette resets conky's, GRUB's +and the homepage's accent lines to the scheme's, and the hardware consumers +send what udt-accent writes. `--no-helpers` stops it starting homepage, GRUB, +keyboard, strip, bulbs and TV detached, since each of those is a consumer of +its own here, run in the foreground; so re-applying conky leaves the LED strip +alone. A wallpaper change, through wallp, still starts them all. Sublime's +`@ACCENT@` is filled by `c_sublime` from `udt-accent.rasi`. A consumer reloads +what it themes itself (kitty, Hyprland, conky). + Colours are written down once. `palette/<scheme>.conf` holds them under the scheme's own names and `palette/roles-<scheme>.conf` says what each is for. `bin/udt-palette` renders that pair into every consumer's own syntax; @@ -23,6 +23,11 @@ Both are installed by symlink, so the repo holds the canonical copies: ./install.sh +One consumer at a time, e.g. after an app rewrote its own theme: + + ./install.sh --list + ./install.sh kitty + ## Design See `docs/superpowers/specs/` for the design spec and the reasoning behind diff --git a/bin/udt-accent b/bin/udt-accent index ebbac91..aabbdec 100755 --- a/bin/udt-accent +++ b/bin/udt-accent @@ -5,7 +5,7 @@ # Licensed under the GNU General Public License v2 only. """Extract a wallpaper's signature color and snap it to one of the scheme's accents. -Usage: udt-accent <wallpaper-path> +Usage: udt-accent [--no-helpers] <wallpaper-path> udt-accent --selftest udt-accent --help @@ -624,7 +624,7 @@ def write_sddm(name, image): mode=0o644, dir_mode=0o755) -def main(image): +def main(image, helpers=True): try: color = signature_color(image) name = snap(color) if color else FALLBACK @@ -657,7 +657,8 @@ def main(image): # The LED strip waits up to 20 s for its address broadcast, the bulbs # up to 5 s per bulb for the gateway. All of them read files written above; # udt-tv is a single UDP datagram to the TV launcher. - for helper in ("udt-homepage", "udt-grub", "udt-keyboard", "udt-ledstrip", "udt-bulb", "udt-tv"): + # --no-helpers is install.sh's: it runs each of them as a consumer itself. + for helper in ("udt-homepage", "udt-grub", "udt-keyboard", "udt-ledstrip", "udt-bulb", "udt-tv") if helpers else (): subprocess.Popen([Path(__file__).resolve().parent / helper], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) @@ -917,12 +918,15 @@ def selftest(): if __name__ == "__main__": args = sys.argv[1:] + helpers = args[:1] != ["--no-helpers"] + if not helpers: + args = args[1:] if args == ["--selftest"]: selftest() elif args in (["-h"], ["--help"]): print(__doc__) elif len(args) == 1 and not args[0].startswith("-") and os.path.isfile(args[0]): - main(args[0]) + main(args[0], helpers) else: # An unknown option or a missing file would otherwise be taken as the # wallpaper, and its path written into SDDM's config and colors.json. @@ -7,6 +7,27 @@ set -euo pipefail repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" target="$HOME/.config/rofi/udt" +# Each consumer is a c_<name> function below, run in the order it is defined, +# with what it themes in a comment on the same line, prefixed `hardware:` for +# a device rather than an app. The list is read off the script itself, so +# adding a consumer is adding a function: quickshell's theme page asks for it +# with --list, which prints name<TAB>software|hardware<TAB>description. +listing() { + awk '/^c_[a-z0-9_]+\(\) \{ *#/ { + n = $1; sub(/^c_/, "", n); sub(/\(\).*/, "", n) + d = $0; sub(/^[^#]*# */, "", d) + k = "software"; if (sub(/^hardware: */, "", d)) k = "hardware" + print n "\t" k "\t" d + }' "${BASH_SOURCE[0]}" +} +consumers() { listing | cut -f1; } +if [ "${1:-}" = "--list" ]; then listing; exit 0; fi +# Names are checked before anything runs, palette generation included. +all="$(consumers)" +for c in "$@"; do + grep -qx "$c" <<< "$all" || { echo "unknown consumer: $c (have: $(echo $all))" >&2; exit 2; } +done + # Everything this prints also becomes the install tab of quickshell's steps # window when there is something to do, from whichever caller: the usual one # is the appearance panel, not a terminal, so the hints are assumed wanted on @@ -82,6 +103,28 @@ fi # early, and pipefail would read the SIGPIPE as a failed generation. "$repo/bin/udt-palette" | sed -n 1p +# The rofi menus' scripts live in the sibling rofi-scripts repo, which links +# its own. +for f in udt-accent udt-grub udt-keyboard udt-ledstrip udt-bulb udt-tv udt-steps udt-palette udt-machine-name; do + ln -sfn "$repo/bin/$f" "$HOME/bin/$f" +done + +reloaded="" + +# udt-accent regenerates the accent. It needs a wallpaper to read; without one +# the accent keeps its current value. Quickshell watches the palette it writes, +# so notifications and the rest of the shell follow without a restart. It runs +# before every consumer, since udt-palette above has just rewritten conky's, +# GRUB's and the homepage's accent lines with the scheme's own, and the +# hardware consumers below send what it writes. --no-helpers keeps it from +# starting those itself: each is a consumer here, so re-applying conky does +# not also repaint the LED strip. +if [ -e "$HOME/.cache/udt/wpaper" ]; then + "$repo/bin/udt-accent" --no-helpers "$HOME/.cache/udt/wpaper" >/dev/null 2>&1 && + reloaded="$reloaded accent" +fi + +c_rofi() { # launcher and menus # Theme files are symlinked individually; accent.rasi is NOT, it points at the # generated file in the wal cache instead. for f in palette.rasi common.rasi menu.rasi list.rasi grid.rasi powermenu.rasi confirm.rasi screenshot.rasi ssh.rasi; do @@ -102,7 +145,9 @@ if [ ! -f "$HOME/.cache/udt/udt-accent.rasi" ]; then cp "$repo/rofi/udt/accent.rasi" "$HOME/.cache/udt/udt-accent.rasi" fi ln -sfn "$HOME/.cache/udt/udt-accent.rasi" "$target/accent.rasi" +} +c_hyprlock() { # lock screen # hyprlock. The palette is scheme-level, so it is copied like the other # generated configs. The border is wallpaper-level: seeded from the static # fallback so the lock parses before any wallpaper is processed, then rewritten @@ -115,7 +160,9 @@ if [ ! -f "$HOME/.cache/udt/hyprlock-border.conf" ]; then cp "$repo/templates/hyprlock/hyprlock-border.conf" \ "$HOME/.cache/udt/hyprlock-border.conf" fi +} +c_firefox() { # Firefox, through pywalfox # Firefox's bridge is the one consumer that cannot follow: pywalfox hardcodes # <cache>/wal/colors.json and honours only XDG_CACHE_HOME, which would move # every cache on the system. So udt-accent writes the real file with the rest @@ -123,33 +170,38 @@ fi # nothing but the link. mkdir -p "$HOME/.cache/wal" ln -sfn "$HOME/.cache/udt/colors.json" "$HOME/.cache/wal/colors.json" - -# The rofi menus' scripts live in the sibling rofi-scripts repo, which links -# its own. -for f in udt-accent udt-grub udt-keyboard udt-ledstrip udt-bulb udt-tv udt-steps udt-palette udt-machine-name; do - ln -sfn "$repo/bin/$f" "$HOME/bin/$f" -done +} # The generated configs for everything that is not rofi. These are copied # rather than symlinked: each app owns its config file and mixes theme keys # with settings that are none of this repo's business. -scheme="$(sed -n 's/^scheme *= *//p' "$selector")" +c_kitty() { # terminal # Written through, not replaced: current-theme.conf is a symlink into the # kitty-conf repo, and install -D would unlink it and leave a plain file behind. mkdir -p "$HOME/.config/kitty" cat "$repo/templates/terminal/kitty-theme.conf" \ > "$HOME/.config/kitty/current-theme.conf" +# kitty rereads its config on a signal. Guarded, like every reload here, +# because it is not necessarily running, and a missing one is not an error. +if pgrep -x kitty >/dev/null 2>&1 && pkill -USR1 -x kitty 2>/dev/null; then + reloaded="$reloaded kitty" +fi +} +c_sublime() { # Sublime Text editor and UI # Sublime Text. Copied into Packages/User, which Sublime watches, so writing it -# reaches a running editor. The file still carries @ACCENT@, which udt-accent -# substitutes below; selecting it is a separate step because color_scheme lives +# reaches a running editor. The template carries @ACCENT@, filled here from the +# accent udt-accent wrote above, and replaced on later wallpaper changes by +# udt-accent itself, matching the line; selecting it is a separate step because color_scheme lives # in Preferences.sublime-settings alongside settings that are none of our # business, so only that one key is rewritten, and only if it is not ours yet. subl="$HOME/.config/sublime-text/Packages/User" if [ -d "$subl" ]; then install -Dm644 "$repo/templates/sublime/udt.sublime-color-scheme" \ "$subl/udt.sublime-color-scheme" + accent="$(sed -n 's/.*accent: \(#[0-9a-fA-F]\{6\}\).*/\1/p' "$HOME/.cache/udt/udt-accent.rasi" 2>/dev/null)" + [ -n "$accent" ] && sed -i "s|@ACCENT@|$accent|" "$subl/udt.sublime-color-scheme" # The UI chrome is an override, and Sublime merges .sublime-theme files by # FILENAME across packages with User last. So it has to be installed under # the name of the theme it layers over, not under a name of its own: called @@ -167,7 +219,9 @@ if [ -d "$subl" ]; then echo "sublime: color_scheme switched to udt" fi fi +} +c_kvantum() { # Qt widget style # Kvantum wants a directory per theme, named after itself, and qt5ct/qt6ct # reach Qt apps through it. The theme is installed under one fixed name rather # than one per scheme: Kvantum caches by name, so reusing it means a switch @@ -176,7 +230,9 @@ kv="$HOME/.config/Kvantum/udt" install -Dm644 "$repo/templates/kvantum/theme.kvconfig" "$kv/udt.kvconfig" install -Dm644 "$repo/templates/kvantum/theme.svg" "$kv/udt.svg" printf '[General]\ntheme=udt\n' > "$HOME/.config/Kvantum/kvantum.kvconfig" +} +c_kde() { # KDE Frameworks apps (kcalc and friends) # KDE Frameworks apps (kcalc and friends) bypass qt6ct twice over off Plasma: # KStyleManager forces Breeze unless kdeglobals names a widgetStyle, and # KColorSchemeManager applies BreezeLight unless kdeglobals names a scheme. @@ -187,7 +243,9 @@ if command -v kwriteconfig6 >/dev/null 2>&1; then kwriteconfig6 --file kdeglobals --group UiSettings --key ColorScheme udt kwriteconfig6 --file kdeglobals --group KDE --key widgetStyle kvantum-dark fi +} +c_gtk() { # GTK3 and GTK4 # GTK3 and GTK4. Installed under one fixed name for every scheme, like Kvantum: # the theme name is set in gsettings and in a symlink, so a per-scheme name # would mean rewriting both on every switch and leaving stale themes behind. @@ -232,12 +290,16 @@ elif ! grep -q '@import url("theme/gtk.css")' "$gtk4_css"; then cat "$gtk4_css.udt-tmp" > "$gtk4_css" rm -f "$gtk4_css.udt-tmp" fi +} +c_qt() { # qt5ct and qt6ct # qt5ct and qt6ct hand Qt apps to Kvantum. Theme keys only: both files also # hold settings and personal lists that are none of this repo's business. ini_merge "$repo/templates/qt-gtk/qt5ct.conf" "$HOME/.config/qt5ct/qt5ct.conf" ini_merge "$repo/templates/qt-gtk/qt6ct.conf" "$HOME/.config/qt6ct/qt6ct.conf" +} +c_nvim() { # neovim colorscheme # neovim. The colorscheme is generated, and selected with one line in # init.vim, rewritten only if it names something else. Like Sublime's # color_scheme key: the rest of init.vim is none of this repo's business. @@ -248,12 +310,16 @@ if [ -f "$nvim_init" ] && ! grep -qx 'colorscheme udt' "$nvim_init"; then sed -i 's/^colorscheme .*/colorscheme udt/' "$nvim_init" grep -qx 'colorscheme udt' "$nvim_init" && echo "nvim: colorscheme switched to udt" fi +} +c_apps() { # Obsidian, Typora, btop, Joplin, Feishin # Obsidian and Typora need more than a copy: the Obsidian snippet has to be # enabled in each vault's appearance.json without disturbing the snippets # already listed there. Skipped silently when neither app is installed. "$repo/bin/udt-appthemes" || true +} +c_conky() { # desktop dashboard # conky rereads its config when it is written in place, but the links below # have to exist for it to find the file at all, and the restart further down # covers a first install and a config replaced by rename. @@ -271,33 +337,6 @@ if [ -d "$conky_repo" ]; then done fi -# Hyprland. hypr-theme is the whole of ~/.config/hypr, linked as one -# directory, and it reads the border this repo generates. A real directory -# there is someone's config: it is left alone rather than replaced. -hypr_repo="$(realpath -m "$repo/../hypr-theme")" -if [ -d "$hypr_repo" ]; then - if [ -L "$HOME/.config/hypr" ] || [ ! -e "$HOME/.config/hypr" ]; then - ln -sfn "$hypr_repo" "$HOME/.config/hypr" - elif [ "$(realpath "$HOME/.config/hypr")" != "$hypr_repo" ]; then - echo "hypr: ~/.config/hypr is a real directory, left alone; move it aside to link hypr-theme" >&2 - fi -fi - -# Reload what is running. Writing the configs is not enough: kitty and -# Hyprland reread on a signal, conky on an in-place write, and rofi -# reads its theme per launch so it needs nothing. Each is guarded, because none -# of them is necessarily running, and a missing one is not an error. -reloaded="" -pgrep -x kitty >/dev/null 2>&1 && pkill -USR1 -x kitty 2>/dev/null && reloaded="$reloaded kitty" -command -v hyprctl >/dev/null 2>&1 && hyprctl reload >/dev/null 2>&1 && reloaded="$reloaded hyprland" - -# udt-accent regenerates the accent. It needs a wallpaper to read; without one -# the accent keeps its current value. Quickshell watches the palette it writes, -# so notifications and the rest of the shell follow without a restart. -if command -v udt-accent >/dev/null 2>&1 && [ -e "$HOME/.cache/udt/wpaper" ]; then - udt-accent "$HOME/.cache/udt/wpaper" >/dev/null 2>&1 && reloaded="$reloaded accent" -fi - # conky reloads an in-place write on its own, but its inotify watch is on the # inode, so a first install or a config replaced by rename goes unseen, and a # widget module stays cached until a restart. Nothing respawns it, so it is @@ -323,7 +362,24 @@ if pgrep -x conky >/dev/null 2>&1; then fi reloaded="$reloaded conky" fi +} + +c_hypr() { # Hyprland, window border +# Hyprland. hypr-theme is the whole of ~/.config/hypr, linked as one +# directory, and it reads the border this repo generates. A real directory +# there is someone's config: it is left alone rather than replaced. +hypr_repo="$(realpath -m "$repo/../hypr-theme")" +if [ -d "$hypr_repo" ]; then + if [ -L "$HOME/.config/hypr" ] || [ ! -e "$HOME/.config/hypr" ]; then + ln -sfn "$hypr_repo" "$HOME/.config/hypr" + elif [ "$(realpath "$HOME/.config/hypr")" != "$hypr_repo" ]; then + echo "hypr: ~/.config/hypr is a real directory, left alone; move it aside to link hypr-theme" >&2 + fi +fi +command -v hyprctl >/dev/null 2>&1 && hyprctl reload >/dev/null 2>&1 && reloaded="$reloaded hyprland" +} +c_sddm() { # login greeter, root steps # SDDM. The theme code is root-installed, but the palette the greeter reads has # to stay live, so the root block below symlinks the generated file in. Never # run here: /usr/share/sddm is root-owned and this script is not. Only the @@ -347,12 +403,42 @@ $sddm_steps" echo "sddm theme: ${sddm_steps:-up to date}" printf '%s' "$sddm_steps" | "$repo/bin/udt-steps" sddm fi +} +c_grub() { # boot menu, root steps # GRUB. Same shape as SDDM: udt-palette has rendered theme.txt, and udt-grub # builds the theme as the user and prints the root half. udt-accent also runs # it, detached, so a wallpaper change alone rebuilds it too. echo "$repo/bin/udt-grub" +} + +# The rest are what udt-accent starts detached on a wallpaper change. Here they +# run in the foreground, so a re-apply reports how it went; each is a no-op +# without its device or its config, and none of them is fatal to the install. +c_homepage() { # dashboard host, staged for a root copy +"$repo/bin/udt-homepage" || true +} + +c_keyboard() { # hardware: Ajazz AK820 Pro lighting +"$repo/bin/udt-keyboard" || true +} + +c_ledstrip() { # hardware: Tuya LED strip +"$repo/bin/udt-ledstrip" || true +} + +c_bulb() { # hardware: IKEA TRADFRI colour bulbs +"$repo/bin/udt-bulb" || true +} + +c_tv() { # hardware: TV launcher focus highlight +"$repo/bin/udt-tv" || true +} + +# No arguments installs every consumer; names install only those, which is how +# the theme page re-applies one. +for c in ${*:-$all}; do "c_$c"; done echo [ -n "$reloaded" ] && echo "reloaded:$reloaded" |
