diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | AGENTS.md | 27 | ||||
| -rwxr-xr-x | bin/udt-accent | 35 | ||||
| -rwxr-xr-x | bin/udt-palette | 42 | ||||
| -rwxr-xr-x | install.sh | 13 | ||||
| -rw-r--r-- | templates/hyprlock/hyprlock-border.conf | 6 |
6 files changed, 114 insertions, 12 deletions
@@ -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 @@ -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 @@ -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 |
