1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
|
#!/bin/bash
# Install udt by symlink, so the repo stays canonical.
# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
# Licensed under the GNU General Public License v2 only.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
target="$HOME/.config/rofi/udt"
mkdir -p "$target" "$HOME/bin" "$HOME/.cache/udt"
# Merge an ini template into a live file key by key: every key the template
# sets is written, every other line of the live file is kept as it was. Keys
# missing from a section are appended to it, missing sections to the end. The
# live file is written through rather than replaced, so a symlink survives.
ini_merge() {
local tmpl="$1" live="$2"
mkdir -p "$(dirname "$live")"
[ -e "$live" ] || : > "$live"
awk '
function flush( i, p) {
for (i = 1; i <= n; i++) {
split(order[i], p, SUBSEP)
if (p[1] == sec && !(order[i] in done)) { print want[order[i]]; done[order[i]] = 1 }
}
for (; blank > 0; blank--) print ""
}
FNR == NR {
if (/^\[/) ts = $0
else if (/^[^#;].*=/) { k = ts SUBSEP substr($0, 1, index($0, "=") - 1); want[k] = $0; order[++n] = k }
next
}
/^[[:space:]]*$/ { blank++; next }
/^\[/ { flush(); sec = $0; print; next }
{ for (; blank > 0; blank--) print "" }
index($0, "=") && ((sec SUBSEP substr($0, 1, index($0, "=") - 1)) in want) {
k = sec SUBSEP substr($0, 1, index($0, "=") - 1)
print want[k]; done[k] = 1; next
}
{ print }
END {
flush()
for (i = 1; i <= n; i++) if (!(order[i] in done)) {
split(order[i], p, SUBSEP)
if (!(p[1] in hdr)) { print p[1]; hdr[p[1]] = 1 }
print want[order[i]]
}
}' "$tmpl" "$live" > "$live.udt-tmp"
cat "$live.udt-tmp" > "$live"
rm -f "$live.udt-tmp"
}
# Generate every themed config from the selected scheme first: the rofi palette
# symlinked below is one of its outputs, so this has to run before anything is
# linked. It fails loudly if a role names a colour the scheme does not define,
# which is the whole point of keeping the colours in one place.
selector="$HOME/.config/udt/roles.conf"
if [ ! -f "$selector" ]; then
install -Dm644 "$repo/palette/roles.conf.default" "$selector"
echo "seeded $selector"
fi
"$repo/bin/udt-palette"
# 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; do
ln -sfn "$repo/rofi/udt/$f" "$target/$f"
done
# launcher.rasi is generated rather than symlinked: its wallpaper path comes
# from the wal cache and cannot be committed. Regenerating it is why re-running
# this script after moving the repo is safe.
rm -f "$target/launcher.rasi"
sed "s|@WPAPER@|$HOME/.cache/udt/wpaper|" \
"$repo/rofi/udt/launcher.rasi" > "$target/launcher.rasi"
# Seed the generated accent if it does not exist yet, so themes parse on a
# fresh install before any wallpaper has been set.
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"
# 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
# by udt-accent below. Both are sourced by hyprlock.conf, which carries inline
# lavender fallbacks too, so a wiped cache degrades to lavender rather than
# failing to lock.
install -Dm644 "$repo/templates/hyprlock/hyprlock-palette.conf" \
"$HOME/.cache/udt/hyprlock-palette.conf"
if [ ! -f "$HOME/.cache/udt/hyprlock-border.conf" ]; then
cp "$repo/templates/hyprlock/hyprlock-border.conf" \
"$HOME/.cache/udt/hyprlock-border.conf"
fi
# 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
# of the palette and this points pywalfox at it, leaving ~/.cache/wal holding
# nothing but the link.
mkdir -p "$HOME/.cache/wal"
ln -sfn "$HOME/.cache/udt/colors.json" "$HOME/.cache/wal/colors.json"
# The rofi menus are bound by name in hypr-theme's keybindings, so they keep
# the names they had when they lived loose in ~/bin.
for f in udt-accent udt-palette udt-powermenu.sh udt-launcher.sh qar-scrotmenu.sh; 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")"
# waybar. The bar is waybar-theme-udt, a sibling checkout with its own
# install.sh, and that script is called here rather than having this one copy
# the stylesheet into place.
#
# The reason is that the palette reaches the bar as THREE generated files, not
# one: styles/theme.css, dots.colors for the two dot scripts, which are bash
# and cannot read @define-color, and two tints of the Slackware mark, which
# has to contrast with the accent pill at rest and the dark pill on hover.
# Copying only the stylesheet left the dots and the mark on the previous
# scheme's colours after a switch, and a second writer for those files here
# would drift from the one over there.
#
# Called with the palette already generated, and never fatal: a failure in the
# bar's install must not stop kitty, Kvantum or Sublime from being themed. The
# same reasoning as udt-appthemes below.
#
# The stock ~/.config/waybar/ is deliberately left alone. Nothing launches it
# any more, and its stylesheets still reference the per-module role names this
# generator no longer emits, so writing a new theme in there would leave it
# referencing colours that do not exist rather than simply sitting unused.
waybar_repo="$repo/../waybar-theme-udt"
if [ -x "$waybar_repo/install.sh" ]; then
UDT_PALETTE_DONE=1 "$waybar_repo/install.sh" >/dev/null || \
echo "waybar: its install.sh failed, bar left on the old colours" >&2
fi
# 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"
# 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
# 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"
# 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
# udt.sublime-theme it would be an unselected theme that never applies.
# Renaming it here rather than in the repo keeps the source name obvious.
behave="$subl/Material Behave.sublime-theme"
if [ -f "$behave" ] && ! grep -q 'Generated by udt-palette' "$behave"; then
cp "$behave" "$behave.pre-udt"
echo "sublime: kept your theme override as $(basename "$behave").pre-udt"
fi
install -Dm644 "$repo/templates/sublime/udt.sublime-theme" "$behave"
prefs="$subl/Preferences.sublime-settings"
if [ -f "$prefs" ] && ! grep -q '"color_scheme".*udt\.sublime-color-scheme' "$prefs"; then
sed -i 's|"color_scheme":[[:space:]]*"[^"]*"|"color_scheme": "Packages/User/udt.sublime-color-scheme"|' "$prefs"
echo "sublime: color_scheme switched to udt"
fi
fi
# 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
# needs no change to kvantum.kvconfig and no second theme left behind.
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"
# 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.
# kdeglobals is edited in place, two keys only, like the gsettings calls below.
install -Dm644 "$repo/templates/kde/udt.colors" \
"$HOME/.local/share/color-schemes/udt.colors"
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
# 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.
#
# Only the two GTK directories are installed. The upstream theme this is
# derived from also ships cinnamon, gnome-shell, metacity, xfwm4 and plank
# directories, none of which anything on this desktop reads.
gtk_theme="$HOME/.themes/udt"
install -Dm644 "$repo/templates/gtk/index.theme" "$gtk_theme/index.theme"
for v in 3.0 4.0; do
src="gtk3.css"
[ "$v" = "4.0" ] && src="gtk4.css"
install -Dm644 "$repo/templates/gtk/$src" "$gtk_theme/gtk-$v/gtk.css"
# gtk-dark.css is byte-identical to gtk.css upstream; apps ask for one or
# the other depending on the prefer-dark setting, so both must exist.
install -Dm644 "$repo/templates/gtk/$src" "$gtk_theme/gtk-$v/gtk-dark.css"
rm -rf "$gtk_theme/gtk-$v/assets"
mkdir -p "$gtk_theme/gtk-$v/assets"
cp "$repo"/templates/gtk/assets/*.svg "$gtk_theme/gtk-$v/assets/"
done
# GTK3 reads the theme name from gsettings on Wayland, not settings.ini: the
# file is written too, but gsettings is what actually wins. It was once pinned
# to Breeze, silently overriding the file. Every scheme is dark, so
# prefer-dark is set too; the portal passes it to libadwaita apps.
ini_merge "$repo/templates/qt-gtk/gtk3-settings.ini" "$HOME/.config/gtk-3.0/settings.ini"
if command -v gsettings >/dev/null 2>&1; then
gsettings set org.gnome.desktop.interface gtk-theme 'udt' 2>/dev/null || true
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' 2>/dev/null || true
fi
# GTK4 ignores gtk-theme-name entirely; its stylesheet is imported by
# ~/.config/gtk-4.0/gtk.css through this symlink. gtk.css is seeded when
# missing, and otherwise only gets the import prepended if it lacks one.
mkdir -p "$HOME/.config/gtk-4.0"
ln -sfn "$gtk_theme/gtk-4.0" "$HOME/.config/gtk-4.0/theme"
gtk4_css="$HOME/.config/gtk-4.0/gtk.css"
if [ ! -f "$gtk4_css" ]; then
cp "$repo/templates/qt-gtk/gtk4-gtk.css" "$gtk4_css"
elif ! grep -q '@import url("theme/gtk.css")' "$gtk4_css"; then
{ echo '@import url("theme/gtk.css");'; cat "$gtk4_css"; } > "$gtk4_css.udt-tmp"
cat "$gtk4_css.udt-tmp" > "$gtk4_css"
rm -f "$gtk4_css.udt-tmp"
fi
# 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"
# 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.
mkdir -p "$HOME/.config/nvim/colors"
ln -sfn "$repo/templates/nvim/udt.lua" "$HOME/.config/nvim/colors/udt.lua"
nvim_init="$HOME/.config/nvim/init.vim"
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
# 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
# conky reads one config at startup and never rereads it, so the generated file
# has to be in place before the restart below.
#
# The Lua dashboard lives in its own repo; udt-palette renders its colours into
# conky.conf there. Both the config and the Lua it loads are linked, since
# lua_load points at ~/.config/conky/dashboard.lua.
# A sibling checkout, like waybar-theme-udt, and skipped when absent.
conky_repo="$(realpath -m "$repo/../conky-theme-udt")"
if [ -d "$conky_repo" ]; then
mkdir -p "$HOME/.config/conky"
# bin/ holds the weather fetch script, which conky.text runs on an execi.
for f in conky.conf dashboard.lua lib widgets bin; do
ln -sfn "$conky_repo/$f" "$HOME/.config/conky/$f"
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: waybar, kitty and
# Hyprland reread on a signal, conky only on a restart, 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 waybar >/dev/null 2>&1 && killall -SIGUSR2 waybar 2>/dev/null && reloaded="$reloaded waybar"
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 has no hot-reload and nothing respawns it, so it is restarted only if
# it was already running.
if pgrep -x conky >/dev/null 2>&1; then
pkill -x conky 2>/dev/null
sleep 0.5
# Name the config explicitly rather than relying on conky's default lookup.
# It resolves to the same file, but the dashboard's own docs tell the user
# to launch it this way from autostart.lua, and two launch paths that
# disagree in form are how they later drift apart in effect.
# `[workspace special:dash silent]`: without it the dashboard's own window
# rule puts it on that workspace but cannot suppress the switch, so every
# reinstall pops the dashboard open over whatever is on screen. Matches
# how hypr-theme's autostart launches it.
# Launched THROUGH hyprctl so the workspace rule applies; falls back to a
# plain exec where Hyprland is not running, since conky itself does not
# need it.
if command -v hyprctl >/dev/null 2>&1 && [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]; then
hyprctl dispatch 'hl.dsp.exec_cmd("[workspace special:dash silent] conky -c ~/.config/conky/conky.conf -d")' >/dev/null 2>&1
else
(conky -c "$HOME/.config/conky/conky.conf" -d >/dev/null 2>&1 &)
fi
reloaded="$reloaded conky"
fi
# 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. Printed,
# never run: /usr/share/sddm is root-owned and this script is not.
sddm_repo="$repo/../sddm-theme-udt"
if [ -d "$sddm_repo/theme" ]; then
echo
echo "sddm theme: run these as root to install or refresh it"
echo " install -d /usr/share/sddm/themes/udt"
echo " cp -r \"$sddm_repo/theme/.\" /usr/share/sddm/themes/udt/"
echo " ln -sfn \"$HOME/.local/share/udt/sddm-theme.conf\" /usr/share/sddm/themes/udt/theme.conf.user"
echo " printf '[Theme]\\nCurrent=udt\\n' > /etc/sddm.conf.d/udt.conf"
fi
# GRUB. Same shape as SDDM: udt-palette has rendered theme.txt, the sibling
# repo's build assembles the theme as the user, and the root half is printed.
# A copy, not a link: GRUB reads /boot before /home is mounted. The background
# is a snapshot of the wallpaper, so the copy needs rerunning after a scheme
# or wallpaper change; grub-mkconfig only when GRUB_THEME changes.
grub_repo="$(realpath -m "$repo/../grub-theme-udt")"
if [ -x "$grub_repo/build" ]; then
if "$grub_repo/build"; then
echo
echo "grub theme: run these as root to install or refresh it"
echo " rm -rf /boot/grub/themes/udt && cp -r \"$grub_repo/dist/udt\" /boot/grub/themes/"
echo " # once: GRUB_THEME=\"/boot/grub/themes/udt/theme.txt\" in /etc/default/grub, then"
echo " grub-mkconfig -o /boot/grub/grub.cfg"
else
echo "grub: its build failed, theme not refreshed" >&2
fi
fi
[ -n "$reloaded" ] && echo "reloaded:$reloaded"
echo "installed:"
ls -l "$target" "$HOME/bin/udt-accent"
|