aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-19 16:42:07 +0200
committerDanilo M. <danix@danix.xyz>2026-09-19 16:42:07 +0200
commita25905dfaeceff258b482007e8b6eb9f9a6d7063 (patch)
tree0cd17519482c816a97e9a2e780fef1d3e0801942
parent63fd47668a66ff546e330d22b64b7ce6e184a404 (diff)
downloadunified-desktop-theme-a25905dfaeceff258b482007e8b6eb9f9a6d7063.tar.gz
unified-desktop-theme-a25905dfaeceff258b482007e8b6eb9f9a6d7063.zip
feat: theme hyprlock from the UDT palette and accent
udt-palette renders the scheme-level palette to templates/hyprlock/hyprlock-palette.conf ($panel, $input_bg, $text, $subtext, $fail); udt-accent writes the wallpaper accent gradient to ~/.cache/udt/hyprlock-border.conf as a hyprlang variable. The hyprlock config sources both, so a scheme switch recolours the lock and a wallpaper change recolours its border. install.sh copies the palette and seeds the border from the committed fallback on a fresh cache. Both selftests cover the new outputs.
-rw-r--r--.gitignore3
-rw-r--r--AGENTS.md27
-rwxr-xr-xbin/udt-accent35
-rwxr-xr-xbin/udt-palette42
-rwxr-xr-xinstall.sh13
-rw-r--r--templates/hyprlock/hyprlock-border.conf6
6 files changed, 114 insertions, 12 deletions
diff --git a/.gitignore b/.gitignore
index f415982..e19c1d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@ __pycache__/
/templates/conky.conf
/templates/waybar/theme.css
/templates/terminal/kitty-theme.conf
+# 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
/bin/udt_colors.py
/templates/kvantum/theme.kvconfig
/templates/kvantum/theme.svg
diff --git a/AGENTS.md b/AGENTS.md
index c0de696..fc7cbfc 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,8 +6,8 @@ Guidance for agents working in this repository.
One visual identity for a Hyprland desktop: Catppuccin Macchiato, Noto Sans for
UI text, Inconsolata Nerd Font Mono for monospace. It covers rofi, Hyprland
-window borders, conky, Qt5/Qt6, GTK3/GTK4, waybar, kitty, Sublime Text and
-neovim.
+window borders, conky, Qt5/Qt6, GTK3/GTK4, waybar, kitty, Sublime Text,
+hyprlock and neovim.
The repo holds the canonical copies. `./install.sh` symlinks them into place, so
the working config cannot drift from what is committed. Everything outside
@@ -44,8 +44,8 @@ The generated set is `rofi/udt/palette.rasi`,
`templates/kvantum/theme.{kvconfig,svg}`,
`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` and
-`bin/udt_colors.py`.
+`templates/gtk/index.theme`, `templates/gtk/assets/*.svg`,
+`templates/hyprlock/hyprlock-palette.conf` 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
@@ -126,11 +126,12 @@ One colour moves: `@accent`. `bin/udt-accent` reads the current wallpaper,
extracts its signature colour, and snaps it to the nearest of ten Catppuccin
accents by perceptual hue in CIELAB. `wallp` calls it on every wallpaper change.
-It writes five things: `~/.cache/udt/udt-accent.rasi` (rofi),
-`~/.cache/udt/udt-border.lua` (Hyprland), `~/.cache/wal/colors.json`
-(Firefox, via pywalfox), `~/.cache/udt/udt-palette.qml` (quickshell), and
-the accent substitution in Sublime's installed
-`Packages/User/udt.sublime-color-scheme`.
+It writes six things: `~/.cache/udt/udt-accent.rasi` (rofi),
+`~/.cache/udt/udt-border.lua` (Hyprland), `~/.cache/udt/hyprlock-border.conf`
+(hyprlock, the same three hues as the window border, as a hyprlang gradient),
+`~/.cache/wal/colors.json` (Firefox, via pywalfox),
+`~/.cache/udt/udt-palette.qml` (quickshell), and the accent substitution in
+Sublime's installed `Packages/User/udt.sublime-color-scheme`.
**Sublime's accent is replaced, not substituted into a placeholder.** Its
colour scheme cannot cascade, so the accent has to live in the one generated
@@ -173,9 +174,10 @@ write lands after `wallp` has already run `wal --theme`, so it wins; reorder
those two and pywal's wallpaper-derived colours come back.
Not everything tracks the wallpaper. rofi, quickshell, Hyprland borders,
-Sublime's
-colour scheme and Firefox do; conky, Qt, GTK, waybar and Sublime's UI chrome sit
-on fixed lavender, which is also the fallback
+hyprlock's border (through `~/.cache/udt/hyprlock-border.conf`), Sublime's
+colour scheme and Firefox do; conky, Qt, GTK, waybar, hyprlock's panel palette
+(from `templates/hyprlock/hyprlock-palette.conf`) and Sublime's UI chrome sit
+on the scheme's accent, lavender by default, which is also the fallback
when a wallpaper is too grey to snap. Qt and GTK apps only reread a theme on restart,
so a moving accent there would leave running apps disagreeing with new ones.
@@ -258,6 +260,7 @@ The table is what it does, and what to run if you change a config by hand.
| quickshell | nothing; it watches the generated palette and re-reads it |
| Firefox | `udt-accent` runs `pywalfox update` |
| Hyprland | `hyprctl reload` |
+| hyprlock | nothing; it reads its config per lock, so the next lock picks it up |
| waybar | `killall -SIGUSR2 waybar` |
| kitty | `pkill -USR1 -x kitty` |
| Sublime Text | nothing; it watches `Packages/User` and reloads both the scheme and the theme |
diff --git a/bin/udt-accent b/bin/udt-accent
index a862ae9..6462aae 100755
--- a/bin/udt-accent
+++ b/bin/udt-accent
@@ -34,6 +34,9 @@ MIN_CHROMA = 10.0
CACHE = Path.home() / ".cache" / "udt"
OUTPUT = CACHE / "udt-accent.rasi"
BORDER_OUTPUT = CACHE / "udt-border.lua"
+# hyprlock cannot read the Lua border table, so the same three hues are also
+# written as a hyprlang variable the lock config sources. See write_hyprlock.
+HYPRLOCK_BORDER_OUTPUT = CACHE / "hyprlock-border.conf"
SUBLIME = (Path.home() / ".config" / "sublime-text" / "Packages" / "User"
/ "udt.sublime-color-scheme")
QML_OUTPUT = CACHE / "udt-palette.qml"
@@ -208,6 +211,28 @@ def write_border(name):
write_atomic(BORDER_OUTPUT, content)
+def hyprlock_border_content(name):
+ """The hyprlock border gradient, as a hyprlang variable assignment.
+
+ The lock panel, its avatar ring and the password outline all take this
+ gradient: the same three hues the Hyprland window border rotates through,
+ so a wallpaper change recolours the lock with the rest of the desktop.
+ One `rgb(hex) rgb(hex) rgb(hex) <angle>` line, which is the syntax the
+ input-field already used before it followed the theme.
+ """
+ before, after = hue_neighbours(name)
+ stops = " ".join(f"rgb({ACCENTS[n].lstrip('#')})" for n in (before, name, after))
+ return (
+ "# Generated by udt-accent. Do not edit.\n"
+ f"$border = {stops} 45deg\n"
+ )
+
+
+def write_hyprlock_border(name):
+ """Write the hyprlock border gradient, atomically."""
+ write_atomic(HYPRLOCK_BORDER_OUTPUT, hyprlock_border_content(name))
+
+
def read_palette():
"""Parse palette.rasi into {name: "#rrggbb"}.
@@ -429,6 +454,7 @@ def main(image):
write_accent(name)
write_border(name)
+ write_hyprlock_border(name)
write_qml(name)
write_sublime(name)
write_sddm(name, image)
@@ -485,6 +511,15 @@ def selftest():
delta = abs(_hue(ACCENTS[neighbour]) - _hue(ACCENTS[name]))
assert min(delta, 2 * math.pi - delta) < math.pi, (name, neighbour)
+ # The lock border carries the same three hues as the window border, in
+ # hyprlang gradient syntax: three rgb() stops and an angle. Get it wrong and
+ # the panel border, avatar ring and password outline all draw hyprlock's
+ # default cyan instead of the accent.
+ lock = hyprlock_border_content(FALLBACK)
+ assert lock.count("rgb(") == 3, lock
+ assert lock.rstrip().endswith("45deg"), lock
+ assert "$border = " in lock, lock
+
# The SDDM theme reads this file through a symlink, so it has to carry every
# key the theme asks for. The names are spelled out here rather than read
# from SDDM_KEYS: deriving both the input and the check from the same
diff --git a/bin/udt-palette b/bin/udt-palette
index 9bd3838..fbe53e6 100755
--- a/bin/udt-palette
+++ b/bin/udt-palette
@@ -260,6 +260,39 @@ def gen_kitty(res, scheme, palette):
return "\n".join(lines) + "\n"
+def gen_hyprlock(res, scheme, palette):
+ """hyprlock: the lock screen's scheme-level palette, as hyprlang variables.
+
+ Only the values that move with the scheme. The accent border is a separate
+ generated file, written by udt-accent, because it follows the wallpaper
+ rather than the scheme; hyprlock.conf sources both.
+
+ The panel alpha is a design constant, not a role: it matches the drawer's
+ Qt.alpha(Theme.base, 0.72), so the lock panel and the drawer read as one
+ surface.
+ """
+ def with_alpha(role, alpha):
+ r, g, b, _ = res[role]
+ return f"rgba({r},{g},{b},{alpha})"
+
+ rows = [
+ ("panel", with_alpha("bg", 0.72)),
+ ("input_bg", rgba(res["surface"])),
+ ("text", rgba(res["fg"])),
+ ("subtext", rgba(res["fg_dim"])),
+ ("fail", rgba(res["critical"])),
+ ]
+ body = "\n".join(f"${name} = {value}" for name, value in rows)
+
+ return (
+ f"# {BANNER.format(scheme=scheme)}\n"
+ "#\n"
+ "# hyprlock's static palette. The accent border is generated separately\n"
+ "# by udt-accent, at ~/.cache/udt/hyprlock-border.conf.\n"
+ f"{body}\n"
+ )
+
+
def gen_obsidian(res, scheme, palette):
"""Obsidian: a CSS snippet overriding the theme variables.
@@ -659,6 +692,7 @@ TARGETS = [
("rofi/udt/palette.rasi", gen_rofi, None),
("templates/waybar/theme.css", gen_waybar, None),
("templates/terminal/kitty-theme.conf", gen_kitty, None),
+ ("templates/hyprlock/hyprlock-palette.conf", gen_hyprlock, 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),
@@ -782,6 +816,14 @@ def selftest():
assert not missing, f"{scheme} is missing roles: {sorted(missing)}"
assert not extra, f"{scheme} has roles no other scheme has: {sorted(extra)}"
+ # The hyprlock palette must name every variable the lock config references.
+ # A missing one renders a literal `$var`, and hyprlock then falls back to a
+ # default colour with nothing to say so.
+ _, _, res, _ = load(None, scheme=names[0])
+ lock = gen_hyprlock(res, names[0], None)
+ for var in ("panel", "input_bg", "text", "subtext", "fail"):
+ assert f"${var} = " in lock, f"hyprlock palette is missing ${var}"
+
# Every placeholder in the GTK templates must correspond to a role, or it
# renders literally and GTK drops the declaration without an error. The
# stylesheets are too large to eyeball, so this is the only thing standing
diff --git a/install.sh b/install.sh
index 49c26c0..85d8216 100755
--- a/install.sh
+++ b/install.sh
@@ -40,6 +40,19 @@ if [ ! -f "$HOME/.cache/udt/udt-accent.rasi" ]; then
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
diff --git a/templates/hyprlock/hyprlock-border.conf b/templates/hyprlock/hyprlock-border.conf
new file mode 100644
index 0000000..d1ee17e
--- /dev/null
+++ b/templates/hyprlock/hyprlock-border.conf
@@ -0,0 +1,6 @@
+# Fallback lock border, seeded by install.sh when the cache has no live copy.
+# udt-accent rewrites ~/.cache/udt/hyprlock-border.conf on every wallpaper
+# change. The lavender-centred triple is the same fallback sections/
+# look_and_feel.lua uses before the first wallpaper is processed, and lavender
+# is the desktop-wide fallback accent.
+$border = rgb(c6a0f6) rgb(b7bdf8) rgb(8aadf4) 45deg