#!/bin/bash # Install udt by symlink, so the repo stays canonical. # Copyright (C) 2026 Danilo M. # 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 # /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-machine-name 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 grub_theme='/boot/grub/themes/udt/theme.txt' echo if diff -rq "$grub_repo/dist/udt" /boot/grub/themes/udt >/dev/null 2>&1; then echo "grub theme: up to date" elif grep -q "^GRUB_THEME=\"$grub_theme\"" /etc/default/grub 2>/dev/null; then echo "grub theme: refresh as root" echo " rm -rf /boot/grub/themes/udt && cp -r \"$grub_repo/dist/udt\" /boot/grub/themes/" else 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=\"$grub_theme\" in /etc/default/grub, then" echo " grub-mkconfig -o /boot/grub/grub.cfg" fi # The panel title falls back to the OS name until one is set. [ -n "$("$repo/bin/udt-machine-name")" ] || \ echo " # optional, the panel title: sudo \"$repo/bin/udt-machine-name\" \"My Machine\"" 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"