aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-29 09:55:45 +0200
committerDanilo M. <danix@danix.xyz>2026-09-29 09:55:45 +0200
commit1e4d2b4be7ad20dd108503fb65c264673ca6457b (patch)
tree8d76604d4bdebe5fa4b4e60be2447df61475789f
parenta06d8f948c4d5a60bff992c3d6f26b5fb54f80cb (diff)
downloadunified-desktop-theme-1e4d2b4be7ad20dd108503fb65c264673ca6457b.tar.gz
unified-desktop-theme-1e4d2b4be7ad20dd108503fb65c264673ca6457b.zip
feat(install): track rofi scripts, wire nvim and hypr-theme, merge qt/gtk keys
The rofi menus' launch scripts lived loose in ~/bin, so the themes were tracked and the only things that open them were not. They now live in bin/ and are symlinked under the names hypr-theme's keybindings call. neovim gets a generated colorscheme: catppuccin/nvim with all 26 palette colours overridden from the active scheme, selected by rewriting only the colorscheme line of init.vim. hypr-theme is linked as the whole of ~/.config/hypr, leaving a real directory there alone. The qt-gtk templates were reference copies nothing installed. They now hold theme keys only and ini_merge writes them into the live qt5ct, qt6ct and gtk3 files key by key, keeping every other line; tests/ini-merge.sh checks it. The stale kvantum.kvconfig template is gone, install.sh writes that file. Fresh-machine fixes: settings.ini and ~/.config/gtk-4.0 are created when missing instead of aborting under set -e, and conky is found as a sibling checkout and skipped when absent, in udt-palette as well as install.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--.gitignore1
-rw-r--r--AGENTS.md25
-rw-r--r--TODO-icons.md7
-rwxr-xr-xbin/qar-scrotmenu.sh32
-rwxr-xr-xbin/udt-launcher.sh22
-rwxr-xr-xbin/udt-palette49
-rwxr-xr-xbin/udt-powermenu.sh108
-rwxr-xr-xinstall.sh111
-rw-r--r--templates/qt-gtk/gtk3-settings.ini23
-rw-r--r--templates/qt-gtk/gtk4-gtk.css4
-rw-r--r--templates/qt-gtk/kvantum.kvconfig2
-rw-r--r--templates/qt-gtk/qt5ct.conf10
-rw-r--r--templates/qt-gtk/qt6ct.conf11
-rwxr-xr-xtests/ini-merge.sh27
14 files changed, 374 insertions, 58 deletions
diff --git a/.gitignore b/.gitignore
index e37b5b6..4dce151 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ __pycache__/
/templates/conky.conf
/templates/waybar/theme.css
/templates/terminal/kitty-theme.conf
+/templates/nvim/udt.lua
# The lock border is NOT ignored: it is the static seed install.sh copies on a
# fresh cache, and udt-accent rewrites it per wallpaper at runtime.
/templates/hyprlock/hyprlock-palette.conf
diff --git a/AGENTS.md b/AGENTS.md
index 67e3440..3acdc98 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -14,6 +14,27 @@ the working config cannot drift from what is committed. Everything outside
`~/.config/rofi/udt/` and `~/bin/` was edited in place; `docs/MIGRATION.md` is
the record of what changed and where the backups are.
+The rofi menus' launch scripts (`udt-powermenu.sh`, `udt-launcher.sh`,
+`qar-scrotmenu.sh`) live in `bin/` and are symlinked into `~/bin/` under the
+names hypr-theme's keybindings call. They used to sit loose in `~/bin/`, so the
+themes were tracked and the only things that open them were not.
+
+Sibling checkouts are wired from here, each skipped when not cloned:
+`waybar-theme-udt` (its own `install.sh`), `conky-theme-udt` (five links into
+`~/.config/conky/`) and `hypr-theme`, which is the whole of `~/.config/hypr` as
+one symlink. A real `~/.config/hypr` directory is left alone with a warning.
+
+Live files that mix theme keys with anything else are merged, never replaced:
+`install.sh`'s `ini_merge` writes every key in `templates/qt-gtk/qt5ct.conf`,
+`qt6ct.conf` and `gtk3-settings.ini` into the live file and keeps every other
+line. So those templates hold theme keys only; icons and cursor are the
+appearance drawer's. `tests/ini-merge.sh` checks the merge.
+
+neovim runs `colorscheme udt`, a generated `templates/nvim/udt.lua` linked into
+`~/.config/nvim/colors/`. It is catppuccin/nvim with all 26 palette colours
+overridden, so every highlight group follows the scheme without restating one.
+`install.sh` rewrites only the `colorscheme` line of `init.vim`.
+
## One palette, many consumers
Colours are written down once. `palette/<scheme>.conf` holds them under the
@@ -45,7 +66,8 @@ The generated set is `rofi/udt/palette.rasi`,
`templates/sublime/udt.sublime-color-scheme`,
`templates/sublime/udt.sublime-theme`, `templates/gtk/gtk{3,4}.css`,
`templates/gtk/index.theme`, `templates/gtk/assets/*.svg`,
-`templates/hyprlock/hyprlock-palette.conf` and `bin/udt_colors.py`.
+`templates/hyprlock/hyprlock-palette.conf`, `templates/nvim/udt.lua` and
+`bin/udt_colors.py`.
Sublime is two outputs, not one. The **colour scheme** is the editor pane and
tracks the wallpaper accent. The **theme** is the UI chrome, and is an override
@@ -280,4 +302,5 @@ The table is what it does, and what to run if you change a config by hand.
| kitty | `pkill -USR1 -x kitty` |
| Sublime Text | nothing; it watches `Packages/User` and reloads both the scheme and the theme |
| conky | restart it; no hot-reload |
+| neovim | `:colorscheme udt`, or restart it |
| Qt / GTK apps | restart the app |
diff --git a/TODO-icons.md b/TODO-icons.md
index 0c50c7a..30722bd 100644
--- a/TODO-icons.md
+++ b/TODO-icons.md
@@ -19,10 +19,9 @@ drawer's value now simply stands.
## Not a conflict
-`templates/qt-gtk/{gtk3-settings.ini,qt5ct.conf,qt6ct.conf}` are reference
-copies that nothing installs: no `install.sh` reference and `git log -S` finds
-none ever existed. `install.sh` only seds `gtk-theme-name` into the live
-`gtk-3.0/settings.ini`, never an icon or cursor key. Each file now says so at
+`templates/qt-gtk/{gtk3-settings.ini,qt5ct.conf,qt6ct.conf}` are merged key
+by key into the live files by `install.sh`'s `ini_merge`, and carry no icon or
+cursor key, so the drawer's values are never overwritten. Each file says so at
the top.
## Consumers: now wired to gsettings
diff --git a/bin/qar-scrotmenu.sh b/bin/qar-scrotmenu.sh
new file mode 100755
index 0000000..b60cbc8
--- /dev/null
+++ b/bin/qar-scrotmenu.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# Screenshot menu, rofi themed by udt.
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+# Licensed under the GNU General Public License v2 only.
+
+# Glyph size follows the monitor rofi opens on (the focused one), in points,
+# since rofi cannot size a font by percentage. Each cell gets half the width,
+# so a quarter of the smaller of half-width and height fits both a wide and a
+# rotated portrait monitor. Odd transforms are 90/270 degree rotations, which
+# swap the reported width and height.
+size=$(hyprctl monitors -j | jq -r '.[] | select(.focused) | .scale as $s
+ | (if .transform % 2 == 1 then [.height, .width] else [.width, .height] end)
+ | [.[0] / 2, .[1]] | min / $s / 4 | floor' 2>/dev/null)
+size=${size:-135}
+rofi_command=(rofi -theme ~/.config/rofi/udt/screenshot.rasi
+ -theme-str "element-text { font: \"Inconsolata Nerd Font Mono $size\"; }")
+
+### Options ###
+screen=""
+area=""
+# Variable passed to rofi
+options="$screen\n$area"
+
+chosen="$(echo -e "$options" | "${rofi_command[@]}" -dmenu -selected-row 1)"
+case $chosen in
+ $screen)
+ grimblast -w 3 -n -t png copysave active
+ ;;
+ $area)
+ grimblast -n -t png copysave area
+ ;;
+esac
diff --git a/bin/udt-launcher.sh b/bin/udt-launcher.sh
new file mode 100755
index 0000000..d0a8013
--- /dev/null
+++ b/bin/udt-launcher.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+## Author : Aditya Shakya (adi1090x)
+## Github : @adi1090x
+## Derived from adi1090x/rofi, GPL-3.0 like upstream. Modifications
+## Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+## Rofi : Launcher (Modi Drun, Run, File Browser, Window)
+#
+## Available Styles
+#
+## style-1 style-2 style-3 style-4 style-5
+## style-6 style-7 style-8 style-9 style-10
+## style-11 style-12 style-13 style-14 style-15
+
+dir="$HOME/.config/rofi/udt"
+theme='launcher'
+
+## Run
+rofi \
+ -show drun \
+ -theme ${dir}/${theme}.rasi
diff --git a/bin/udt-palette b/bin/udt-palette
index 234d97a..6a15a52 100755
--- a/bin/udt-palette
+++ b/bin/udt-palette
@@ -28,7 +28,8 @@ SELECTOR = Path.home() / ".config" / "udt" / "roles.conf"
SELECTOR_SEED = PALETTE_DIR / "roles.conf.default"
# The Lua dashboard lives in its own repo; UDT only renders its colours.
-CONKY_REPO = Path("~/Programming/GIT/conky-theme-udt").expanduser()
+# A sibling checkout, like waybar-theme-udt; skipped when it is not cloned.
+CONKY_REPO = REPO.parent / "conky-theme-udt"
def parse_conf(path):
@@ -267,6 +268,47 @@ def gen_kitty(res, scheme, palette):
return "\n".join(lines) + "\n"
+# Catppuccin's 26 names, and the role that stands in for each when the scheme
+# does not define that name itself.
+NVIM_NAMES = [
+ ("rosewater", "cursor"), ("flamingo", "cursor"), ("pink", "magenta"),
+ ("mauve", "highlight"), ("red", "red"), ("maroon", "bright_red"),
+ ("peach", "peach"), ("yellow", "yellow"), ("green", "green"),
+ ("teal", "cyan"), ("sky", "bright_cyan"), ("sapphire", "bright_blue"),
+ ("blue", "blue"), ("lavender", "accent"),
+ ("text", "fg"), ("subtext1", "fg_bright"), ("subtext0", "fg_dim"),
+ ("overlay2", "mid_high"), ("overlay1", "mid_low"), ("overlay0", "fg_faint"),
+ ("surface2", "surface_high"), ("surface1", "surface_alt"),
+ ("surface0", "surface"),
+ ("base", "bg"), ("mantle", "bg_alt"), ("crust", "bg_deep"),
+]
+
+
+def gen_nvim(res, scheme, palette):
+ """neovim: a colorscheme that is catppuccin/nvim with every colour replaced.
+
+ catppuccin's highlight groups are written against its 26 palette names, so
+ overriding all of them re-skins the whole editor without restating a single
+ group. The Catppuccin schemes define those names and pass straight through;
+ the rest are filled from roles, so a few near-duplicate hues (maroon, sky,
+ sapphire, flamingo) collapse onto their neighbours there.
+ """
+ rows = "\n".join(
+ f' {name} = "{palette[name].lower() if name in palette else hex6(res[role])}",'
+ for name, role in NVIM_NAMES)
+ return (
+ f"-- {BANNER.format(scheme=scheme)}\n"
+ "-- Selected with `colorscheme udt`. Needs the catppuccin/nvim plugin.\n"
+ f"local colors = {{\n{rows}\n}}\n\n"
+ "require(\"catppuccin\").setup({\n"
+ " flavour = \"macchiato\",\n"
+ " color_overrides = { macchiato = colors },\n"
+ "})\n"
+ "vim.cmd.colorscheme(\"catppuccin-macchiato\")\n"
+ "vim.g.colors_name = \"udt\"\n"
+ )
+
+
def gen_hyprlock(res, scheme, palette):
"""hyprlock: the lock screen's scheme-level palette, as hyprlang variables.
@@ -743,6 +785,7 @@ TARGETS = [
("templates/waybar/theme.css", gen_waybar, None),
("templates/terminal/kitty-theme.conf", gen_kitty, None),
("templates/hyprlock/hyprlock-palette.conf", gen_hyprlock, None),
+ ("templates/nvim/udt.lua", gen_nvim, None),
(str(CONKY_REPO / "conky.conf"), gen_conky, str(CONKY_REPO / "conky.conf.in")),
("bin/udt_colors.py", gen_accent_py, None),
("templates/homepage/custom.css", gen_homepage, None),
@@ -774,6 +817,10 @@ def generate(roles_path, out_dir, scheme=None):
scheme, palette, res, snap_names = load(roles_path, scheme=scheme)
written = []
for target, fn, template in TARGETS:
+ # A sibling repo that is not cloned here is not an error: its
+ # template is missing too, and there is nothing to theme.
+ if Path(target).is_absolute() and not Path(target).parent.is_dir():
+ continue
# A target outside this repo (the conky dashboard) is an absolute path,
# and `out_dir / absolute` discards out_dir entirely. For the real
# render that is what we want, since the file genuinely belongs at that
diff --git a/bin/udt-powermenu.sh b/bin/udt-powermenu.sh
new file mode 100755
index 0000000..a8524b4
--- /dev/null
+++ b/bin/udt-powermenu.sh
@@ -0,0 +1,108 @@
+#!/usr/bin/env bash
+
+## Author : Aditya Shakya (adi1090x)
+## Github : @adi1090x
+## Derived from adi1090x/rofi, GPL-3.0 like upstream. Modifications
+## Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+## Rofi : Power Menu
+#
+## Available Styles
+#
+## style-1 style-2 style-3 style-4 style-5
+
+# Current Theme
+dir="$HOME/.config/rofi/udt"
+theme='powermenu'
+
+# CMDs
+uptime="`uptime -p | sed -e 's/up //g'`"
+host=`hostname`
+
+# Options
+shutdown=''
+reboot=''
+logout=''
+sleep='󰤄'
+yes='󰗠'
+no='󰅙'
+
+# Rofi CMD
+rofi_cmd() {
+ rofi -dmenu \
+ -p "'ndi vidimu ${USER}" \
+ -mesg "Uptime: $uptime" \
+ -theme ${dir}/${theme}.rasi
+}
+
+# Confirmation glyph size follows the focused monitor, as in
+# qar-scrotmenu.sh: a quarter of the smaller of half-width and height, with
+# 90/270 degree rotations (odd transforms) swapping width and height.
+confirm_size() {
+ hyprctl monitors -j | jq -r '.[] | select(.focused) | .scale as $s
+ | (if .transform % 2 == 1 then [.height, .width] else [.width, .height] end)
+ | [.[0] / 2, .[1]] | min / $s / 4 | floor' 2>/dev/null
+}
+
+# Confirmation CMD
+confirm_cmd() {
+ local size
+ size=$(confirm_size)
+ rofi -dmenu \
+ -p 'Confirmation' \
+ -selected-row 1 \
+ -mesg 'Simu Sicuri?' \
+ -theme "$HOME/.config/rofi/udt/confirm.rasi" \
+ -theme-str "element-text { font: \"Inconsolata Nerd Font Mono ${size:-135}\"; }"
+}
+
+# Ask for confirmation
+confirm_exit() {
+ echo -e "$yes\n$no" | confirm_cmd
+}
+
+# Pass variables to rofi dmenu
+run_rofi() {
+ echo -e "$shutdown\n$reboot\n$sleep\n$logout" | rofi_cmd
+}
+
+# Execute Command
+run_cmd() {
+ selected="$(confirm_exit)"
+ if [[ "$selected" == "$yes" ]]; then
+ if [[ $1 == '--shutdown' ]]; then
+ notify-send 'system shutdown' 'the system is going to shutdown now'
+ sleep 1
+ /usr/bin/loginctl poweroff
+ elif [[ $1 == '--reboot' ]]; then
+ notify-send 'system reboot' 'the system is going to reboot now'
+ sleep 1
+ /usr/bin/loginctl reboot
+ elif [[ $1 == '--sleep' ]]; then
+ notify-send 'suspend to ram' 'the system is going to sleep now'
+ sleep 1
+ /usr/bin/loginctl suspend
+ elif [[ $1 == '--logout' ]]; then
+ hyprctl dispatch 'hl.dsp.exit()'
+ fi
+ else
+ exit 0
+ fi
+}
+
+# Actions
+chosen="$(run_rofi)"
+case ${chosen} in
+ $shutdown)
+ run_cmd --shutdown
+ ;;
+ $reboot)
+ run_cmd --reboot
+ ;;
+ $sleep)
+ run_cmd --sleep
+ ;;
+ $logout)
+ run_cmd --logout
+ ;;
+esac
diff --git a/install.sh b/install.sh
index 36903ea..9626431 100755
--- a/install.sh
+++ b/install.sh
@@ -9,6 +9,47 @@ 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,
@@ -61,8 +102,11 @@ fi
mkdir -p "$HOME/.cache/wal"
ln -sfn "$HOME/.cache/udt/colors.json" "$HOME/.cache/wal/colors.json"
-ln -sfn "$repo/bin/udt-accent" "$HOME/bin/udt-accent"
-ln -sfn "$repo/bin/udt-palette" "$HOME/bin/udt-palette"
+# 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
@@ -171,15 +215,43 @@ 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.
-sed -i 's/^gtk-theme-name=.*/gtk-theme-name=udt/' "$HOME/.config/gtk-3.0/settings.ini"
+# 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.
+# ~/.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
@@ -192,14 +264,27 @@ ln -sfn "$gtk_theme/gtk-4.0" "$HOME/.config/gtk-4.0/theme"
# 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.
-conky_repo="$HOME/Programming/GIT/conky-theme-udt"
-mkdir -p "$HOME/.config/conky"
-ln -sfn "$conky_repo/conky.conf" "$HOME/.config/conky/conky.conf"
-ln -sfn "$conky_repo/dashboard.lua" "$HOME/.config/conky/dashboard.lua"
-ln -sfn "$conky_repo/lib" "$HOME/.config/conky/lib"
-ln -sfn "$conky_repo/widgets" "$HOME/.config/conky/widgets"
-# bin/ holds the weather fetch script, which conky.text runs on an execi.
-ln -sfn "$conky_repo/bin" "$HOME/.config/conky/bin"
+# 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
diff --git a/templates/qt-gtk/gtk3-settings.ini b/templates/qt-gtk/gtk3-settings.ini
index 7287705..f0fe1d8 100644
--- a/templates/qt-gtk/gtk3-settings.ini
+++ b/templates/qt-gtk/gtk3-settings.ini
@@ -1,24 +1,7 @@
-# Reference copy, not installed. install.sh never writes this file; it
-# only seds gtk-theme-name into the live ~/.config/gtk-3.0/settings.ini.
-# The icon and cursor keys below are the appearance drawer's to set.
+# Merged into ~/.config/gtk-3.0/settings.ini by install.sh, key by key: every
+# key here is set, every other key in the live file is left alone. Icons,
+# cursor, modules and sounds are the appearance drawer's to set.
[Settings]
gtk-application-prefer-dark-theme=true
-gtk-button-images=1
-gtk-cursor-theme-name=Oxygen_Zion
-gtk-cursor-theme-size=24
-gtk-decoration-layout=icon:minimize,maximize,close
-gtk-enable-animations=true
gtk-font-name=Noto Sans, 10
-gtk-icon-theme-name=Material-Black-Plum-Suru
-gtk-menu-images=1
-gtk-modules=colorreload-gtk-module:window-decorations-gtk-module
-gtk-primary-button-warps-slider=false
gtk-theme-name=udt
-gtk-toolbar-style=GTK_TOOLBAR_BOTH_HORIZ
-gtk-xft-dpi=98304
-gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
-gtk-enable-event-sounds=1
-gtk-enable-input-feedback-sounds=1
-gtk-xft-antialias=1
-gtk-xft-hinting=1
-gtk-xft-hintstyle=hintmedium
diff --git a/templates/qt-gtk/gtk4-gtk.css b/templates/qt-gtk/gtk4-gtk.css
index 9bf52c0..9d66353 100644
--- a/templates/qt-gtk/gtk4-gtk.css
+++ b/templates/qt-gtk/gtk4-gtk.css
@@ -1,6 +1,6 @@
-/* Catppuccin Macchiato (lavender) for GTK4 / libadwaita.
+/* The udt theme for GTK4 / libadwaita. Seeded by install.sh when missing.
* libadwaita ignores gtk-theme-name, so the theme's own stylesheet is imported
- * here instead. Assets are symlinked next to this file. */
+ * here instead, through the theme symlink install.sh keeps next to this file. */
@import url("theme/gtk.css");
/* vte terminal: local override, kept from the previous config. */
diff --git a/templates/qt-gtk/kvantum.kvconfig b/templates/qt-gtk/kvantum.kvconfig
deleted file mode 100644
index aa0ae60..0000000
--- a/templates/qt-gtk/kvantum.kvconfig
+++ /dev/null
@@ -1,2 +0,0 @@
-[General]
-theme=catppuccin-macchiato-lavender
diff --git a/templates/qt-gtk/qt5ct.conf b/templates/qt-gtk/qt5ct.conf
index fd8aced..cb94ab7 100644
--- a/templates/qt-gtk/qt5ct.conf
+++ b/templates/qt-gtk/qt5ct.conf
@@ -1,14 +1,10 @@
-# Reference copy, not installed. install.sh never writes this file.
-# icon_theme is the appearance drawer's to set.
-# Theme-relevant keys only; the live file carries other settings.
+# Merged into ~/.config/qt5ct/qt5ct.conf by install.sh, key by key: every key
+# here is set, every other key in the live file is left alone. icon_theme is
+# the appearance drawer's to set, so it is not here.
[Appearance]
custom_palette=false
-icon_theme=Material-Black-Plum-Suru
standard_dialogs=xdgdesktopportal
style=kvantum-dark
[Fonts]
fixed="Inconsolata Nerd Font Mono,14,-1,5,50,0,0,0,0,0"
general="Noto Sans,14,-1,5,50,0,0,0,0,0,Regular"
-[Interface]
-[SettingsWindow]
-[Troubleshooting]
diff --git a/templates/qt-gtk/qt6ct.conf b/templates/qt-gtk/qt6ct.conf
index 82bb973..4de7efa 100644
--- a/templates/qt-gtk/qt6ct.conf
+++ b/templates/qt-gtk/qt6ct.conf
@@ -1,15 +1,10 @@
-# Reference copy, not installed. install.sh never writes this file.
-# icon_theme is the appearance drawer's to set.
-# Theme-relevant keys only; the live file carries other settings.
+# Merged into ~/.config/qt6ct/qt6ct.conf by install.sh, key by key: every key
+# here is set, every other key in the live file is left alone. icon_theme is
+# the appearance drawer's to set, so it is not here.
[Appearance]
custom_palette=false
-icon_theme=Material-Black-Plum-Suru
standard_dialogs=xdgdesktopportal
style=kvantum-dark
[Fonts]
fixed="Inconsolata Nerd Font Mono,14,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,,0,0"
general="Noto Sans,14,-1,5,400,0,0,0,0,0,0,0,0,0,0,1,Regular,0,0"
-[Interface]
-[QSSEditor]
-[SettingsWindow]
-[Troubleshooting]
diff --git a/tests/ini-merge.sh b/tests/ini-merge.sh
new file mode 100755
index 0000000..b495c9c
--- /dev/null
+++ b/tests/ini-merge.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+# Check install.sh's ini_merge: fresh file, partial file, idempotence.
+# 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)"
+eval "$(sed -n '/^ini_merge() {/,/^}/p' "$repo/install.sh")"
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+
+printf '[A]\nx=1\ny=2\n[B]\nz=3\n' > "$tmp/tmpl"
+
+ini_merge "$tmp/tmpl" "$tmp/fresh"
+diff <(printf '[A]\nx=1\ny=2\n[B]\nz=3\n') "$tmp/fresh"
+
+# Existing key replaced, foreign key kept, missing key lands before the blank
+# line that separates sections, missing section appended.
+printf '[A]\nx=0\nkeep=me\n\n[C]\nw=4\n' > "$tmp/part"
+ini_merge "$tmp/tmpl" "$tmp/part"
+diff <(printf '[A]\nx=1\nkeep=me\ny=2\n\n[C]\nw=4\n[B]\nz=3\n') "$tmp/part"
+
+cp "$tmp/part" "$tmp/again"
+ini_merge "$tmp/tmpl" "$tmp/again"
+diff "$tmp/part" "$tmp/again"
+
+echo "ini-merge OK"