aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-22 17:25:09 +0200
committerDanilo M. <danix@danix.xyz>2026-10-22 17:25:09 +0200
commit78d7d285a980513101210188ab3da8a4c3b512e8 (patch)
tree3e2d2f395c16afa70bd2f48af0f135c9569d88f2
parent6b6ac80c924325f1229bf7f230fd80cde0ce7744 (diff)
downloadunified-desktop-theme-master.tar.gz
unified-desktop-theme-master.zip
install: one function per consumer, runnable aloneHEADmaster
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.md16
-rw-r--r--README.md5
-rwxr-xr-xbin/udt-accent12
-rwxr-xr-xinstall.sh158
4 files changed, 151 insertions, 40 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 068587e..7b31cdd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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;
diff --git a/README.md b/README.md
index 9d46d54..282bdf3 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/install.sh b/install.sh
index e25e4ec..849d770 100755
--- a/install.sh
+++ b/install.sh
@@ -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"