Make the lock screen follow the colour scheme

hyprlock.conf shipped with Tokyo Night Moon hardcoded in six places, so
choosing light mode left the lock screen dark. Every other surface had
been taught to follow the scheme this week -- kitty, GTK, the launcher,
btop, tmux, neovim -- and this was the one left, which is unfortunate,
because it is the screen a user sees most often and the worst possible
place to find a theming bug: you discover it while locked out of the
machine and cannot fix it from there.

It is now generated from a template on every scheme change, the same
shape kitty, GTK, tmux and btop already use, and seeded by link-dotfiles
so the first lock of a fresh install is themed rather than falling back
to hyprlock's bare grey default. hyprlock is launched fresh on each lock
(`pidof hyprlock || hyprlock`), so it picks the file up with no restart.

The dark output is byte-identical to the file it replaces, ignoring
comments -- verified by diff -- so nothing changes for anyone already in
dark mode.

One detail worth recording: hyprlock takes rgba(r, g, b, a) in DECIMAL,
not hex, so the template carries "R, G, B" triples where every other
theme file in this repository uses hex. Two values are the exception,
sitting inside Pango markup where hyprlock wants ##rrggbb. Getting
either wrong is not a parse error -- hyprlock ignores the value and uses
its own default, silently.

Which is why this has a contract. It generates both schemes into a
fixture, never the live config, and checks that no placeholder survives
substitution, that every colour is a well-formed decimal triple, that
the Pango values are well-formed hex, that a light lock screen is
actually light, and that the two schemes differ at all. Verified it
catches a hardcoded colour left in the template and a light mode built
from the dark palette, which is the original bug exactly.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-18 14:36:35 -04:00
parent d87f4b6d6a
commit 2d69ce7648
5 changed files with 204 additions and 15 deletions
+1
View File
@@ -21,3 +21,4 @@ __pycache__/
/config/dot/gtk-3.0/settings.ini /config/dot/gtk-3.0/settings.ini
/config/dot/gtk-4.0/settings.ini /config/dot/gtk-4.0/settings.ini
/config/dot/tmux/current-theme.conf /config/dot/tmux/current-theme.conf
/config/dot/hypr/hyprlock.conf
@@ -1,8 +1,17 @@
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# hyprlock — lock screen # hyprlock — lock screen.
# #
# Tokyo Night Moon, matching quickshell/config/Theme.qml: # GENERATED FILE. Edit hyprlock.conf.template and re-run
# accent #82aaff fg #c8d3f5 dim #828bb8 bg #222436 red #ff757f # quickshell/scripts/panama-theme-apps; editing this copy is overwritten on the
# next colour scheme change.
#
# The colours here follow the desktop's light/dark setting. They used to be
# hardcoded Tokyo Night Moon, which meant the one screen you see most often
# stayed dark when everything else went light.
#
# hyprlock takes rgba(r, g, b, a) in DECIMAL rather than hex, which is why the
# template carries "R, G, B" triples where the rest of Panama uses hex. The two
# Pango markup values are the exception and want ##rrggbb.
# #
# hyprlang syntax, not Lua — hyprlock is a separate project from Hyprland. # hyprlang syntax, not Lua — hyprlock is a separate project from Hyprland.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -45,7 +54,7 @@ background {
vibrancy_darkness = 0.05 vibrancy_darkness = 0.05
# Shown if the screenshot is unavailable. # Shown if the screenshot is unavailable.
color = rgba(34, 36, 54, 1.0) color = rgba(@BG@, 1.0)
zindex = -1 zindex = -1
} }
@@ -54,7 +63,7 @@ background {
label { label {
monitor = monitor =
text = cmd[update:1000] date +"%-I:%M" text = cmd[update:1000] date +"%-I:%M"
color = rgba(200, 211, 245, 1.0) color = rgba(@FG@, 1.0)
font_size = 120 font_size = 120
font_family = Adwaita Sans Light font_family = Adwaita Sans Light
position = 0, 260 position = 0, 260
@@ -65,7 +74,7 @@ label {
label { label {
monitor = monitor =
text = cmd[update:60000] date +"%A, %B %-d" text = cmd[update:60000] date +"%A, %B %-d"
color = rgba(130, 139, 184, 1.0) color = rgba(@MUTED@, 1.0)
font_size = 24 font_size = 24
font_family = Adwaita Sans font_family = Adwaita Sans
position = 0, 160 position = 0, 160
@@ -84,18 +93,18 @@ input-field {
outline_thickness = 2 outline_thickness = 2
rounding = 26 rounding = 26
outer_color = rgba(130, 170, 255, 0.9) outer_color = rgba(@ACCENT@, 0.9)
inner_color = rgba(46, 47, 61, 0.85) inner_color = rgba(@FIELD@, 0.85)
font_color = rgba(200, 211, 245, 1.0) font_color = rgba(@FG@, 1.0)
check_color = rgba(130, 170, 255, 1.0) check_color = rgba(@ACCENT@, 1.0)
fail_color = rgba(255, 117, 127, 1.0) fail_color = rgba(@ERROR@, 1.0)
dots_size = 0.25 dots_size = 0.25
dots_spacing = 0.3 dots_spacing = 0.3
dots_center = true dots_center = true
placeholder_text = <span foreground="##828bb8"><i>Password</i></span> placeholder_text = <span foreground="##@MUTED_HEX@"><i>Password</i></span>
fail_text = <span foreground="##ff757f"><i>$FAIL ($ATTEMPTS)</i></span> fail_text = <span foreground="##@ERROR_HEX@"><i>$FAIL ($ATTEMPTS)</i></span>
fade_on_empty = false fade_on_empty = false
hide_input = false hide_input = false
@@ -105,7 +114,7 @@ input-field {
label { label {
monitor = monitor =
text = $USER text = $USER
color = rgba(200, 211, 245, 0.9) color = rgba(@FG@, 0.9)
font_size = 16 font_size = 16
font_family = Adwaita Sans font_family = Adwaita Sans
position = 0, -110 position = 0, -110
@@ -31,6 +31,59 @@ case "$scheme" in
*) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;; *) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;;
esac esac
# ── hyprlock ─────────────────────────────────────────────────────────────────
# The lock screen. hyprlock is launched fresh on every lock (`pidof hyprlock ||
# hyprlock`), so it reads this file each time and needs no restart.
#
# It takes rgba(r, g, b, a) in DECIMAL, not hex, so the palette is expressed as
# "R, G, B" triples here rather than the hex used everywhere else. The two
# _HEX values are the exception: they sit inside Pango markup, where hyprlock
# wants ##rrggbb.
lock_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hypr"
lock_template="$lock_dir/hyprlock.conf.template"
if [[ "$scheme" == "light" ]]; then
lock_fg="55, 96, 191" # #3760bf
lock_muted="97, 114, 176" # #6172b0
lock_accent="46, 125, 233" # #2e7de9
lock_error="245, 42, 101" # #f52a65
lock_bg="225, 226, 231" # #e1e2e7
lock_field="208, 213, 227" # #d0d5e3
lock_muted_hex="6172b0"
lock_error_hex="f52a65"
else
lock_fg="200, 211, 245" # #c8d3f5
lock_muted="130, 139, 184" # #828bb8
lock_accent="130, 170, 255" # #82aaff
lock_error="255, 117, 127" # #ff757f
lock_bg="34, 36, 54" # #222436
lock_field="46, 47, 61" # #2e2f3d
lock_muted_hex="828bb8"
lock_error_hex="ff757f"
fi
status_hyprlock="skipped"
if [[ -r "$lock_template" ]]; then
# Written atomically: a lock triggered mid-write would otherwise read a
# truncated config and fall back to hyprlock's own defaults, which is a
# bright grey screen with none of this desktop's identity.
if sed -e "s/@FG@/$lock_fg/g" \
-e "s/@MUTED@/$lock_muted/g" \
-e "s/@ACCENT@/$lock_accent/g" \
-e "s/@ERROR@/$lock_error/g" \
-e "s/@BG@/$lock_bg/g" \
-e "s/@FIELD@/$lock_field/g" \
-e "s/@MUTED_HEX@/$lock_muted_hex/g" \
-e "s/@ERROR_HEX@/$lock_error_hex/g" \
"$lock_template" >"$lock_dir/hyprlock.conf.tmp" 2>/dev/null \
&& mv "$lock_dir/hyprlock.conf.tmp" "$lock_dir/hyprlock.conf" 2>/dev/null; then
status_hyprlock="written"
else
rm -f "$lock_dir/hyprlock.conf.tmp"
status_hyprlock="failed"
fi
fi
# ── tmux ───────────────────────────────────────────────────────────────────── # ── tmux ─────────────────────────────────────────────────────────────────────
# Generated like kitty's: tmux.conf sources current-theme.conf, and that file is # Generated like kitty's: tmux.conf sources current-theme.conf, and that file is
# machine state rather than configuration. Running servers are re-sourced so an # machine state rather than configuration. Running servers are re-sourced so an
@@ -146,4 +199,5 @@ jq -cn \
--arg gtk "$status_gtk" \ --arg gtk "$status_gtk" \
--arg btop "$status_btop" \ --arg btop "$status_btop" \
--arg tmux "$status_tmux" \ --arg tmux "$status_tmux" \
'{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux}' --arg hyprlock "$status_hyprlock" \
'{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux, hyprlock: $hyprlock}'
+32
View File
@@ -89,6 +89,38 @@ elif [ -d "$PANAMA_DOT/tmux/themes" ]; then
log "Seeded tmux $tmux_scheme theme ($tmux_name) → $TMUX_THEME" log "Seeded tmux $tmux_scheme theme ($tmux_name) → $TMUX_THEME"
fi fi
# hyprlock.conf is generated from a template on every colour scheme change and
# is not committed. Seed it so the FIRST lock of a fresh install is themed --
# without it hyprlock falls back to its own defaults, which is a bare grey
# screen with none of this desktop's identity, and the first time anyone would
# find out is when they walked away from the machine.
HYPRLOCK_TEMPLATE="$PANAMA_DOT/hypr/hyprlock.conf.template"
HYPRLOCK_CONF="$PANAMA_DOT/hypr/hyprlock.conf"
if [ -e "$HYPRLOCK_CONF" ]; then
log "Keeping existing hyprlock config at $HYPRLOCK_CONF"
elif [ -r "$HYPRLOCK_TEMPLATE" ]; then
lock_scheme="dark"
lock_prefs="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
if [ -r "$lock_prefs" ]; then
lock_stored="$(jq -r '.colorScheme // "dark"' "$lock_prefs" 2>/dev/null || echo dark)"
[ "$lock_stored" = "light" ] && lock_scheme="light"
fi
if [ "$lock_scheme" = "light" ]; then
sed -e "s/@FG@/55, 96, 191/g" -e "s/@MUTED@/97, 114, 176/g" \
-e "s/@ACCENT@/46, 125, 233/g" -e "s/@ERROR@/245, 42, 101/g" \
-e "s/@BG@/225, 226, 231/g" -e "s/@FIELD@/208, 213, 227/g" \
-e "s/@MUTED_HEX@/6172b0/g" -e "s/@ERROR_HEX@/f52a65/g" \
"$HYPRLOCK_TEMPLATE" > "$HYPRLOCK_CONF"
else
sed -e "s/@FG@/200, 211, 245/g" -e "s/@MUTED@/130, 139, 184/g" \
-e "s/@ACCENT@/130, 170, 255/g" -e "s/@ERROR@/255, 117, 127/g" \
-e "s/@BG@/34, 36, 54/g" -e "s/@FIELD@/46, 47, 61/g" \
-e "s/@MUTED_HEX@/828bb8/g" -e "s/@ERROR_HEX@/ff757f/g" \
"$HYPRLOCK_TEMPLATE" > "$HYPRLOCK_CONF"
fi
log "Seeded hyprlock $lock_scheme theme → $HYPRLOCK_CONF"
fi
# btop reads themes from its own config directory, but OWNS btop.conf -- it # btop reads themes from its own config directory, but OWNS btop.conf -- it
# rewrites that file on exit -- so only the theme files are exposed, per file, # rewrites that file on exit -- so only the theme files are exposed, per file,
# and the config itself is left to btop. panama-theme-apps edits the single # and the config itself is left to btop. panama-theme-apps edits the single
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# The lock screen follows the colour scheme.
#
# It did not. hyprlock.conf shipped with Tokyo Night Moon hardcoded in six
# places, so choosing light mode left the one screen a user sees most often
# stubbornly dark. Every other surface -- kitty, GTK, the launcher, btop, tmux,
# neovim -- had been taught to follow the scheme; this was the last one.
#
# It is also the worst place to discover a theming bug, because you find out
# while locked out of the machine and cannot fix it from there. Hence a test.
#
# Generated into a fixture, never the live config: this contract must not
# retheme the lock screen of the desktop it is running on.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
template="$repo_dir/config/dot/hypr/hyprlock.conf.template"
theme_apps="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps"
fail() {
printf 'lock screen theme contract: %s\n' "$1" >&2
exit 1
}
[[ -r "$template" ]] || fail 'hyprlock.conf.template is missing, so nothing generates the lock screen'
# The committed template must carry no literal colours. One left behind is a
# colour that silently stays dark in light mode -- exactly the original bug.
literal="$(grep -vE '^\s*#' "$template" | grep -oE 'rgba\([0-9]+, *[0-9]+, *[0-9]+' || true)"
[[ -z "$literal" ]] \
|| fail "the template still contains hardcoded colours, which will not follow the scheme: $literal"
fixture="$(mktemp -d /tmp/panama-lockscreen.XXXXXX)"
trap 'rm -rf "$fixture"' EXIT
mkdir -p "$fixture/hypr"
cp "$template" "$fixture/hypr/"
for scheme in dark light; do
XDG_CONFIG_HOME="$fixture" "$theme_apps" "$scheme" >/dev/null 2>&1
generated="$fixture/hypr/hyprlock.conf"
[[ -r "$generated" ]] || fail "no hyprlock.conf was generated for $scheme"
# An unsubstituted placeholder is not a parse error to hyprlock; it is an
# invalid colour it quietly ignores, falling back to its own default.
leftover="$(grep -oE '@[A-Z_]+@' "$generated" || true)"
[[ -z "$leftover" ]] \
|| fail "$scheme left placeholders unsubstituted: $leftover"
# Every colour hyprlock is given must be a complete decimal triple. It
# takes rgba(r, g, b, a), NOT hex, and a hex value here is silently ignored.
while read -r colour; do
[[ -n "$colour" ]] || continue
grep -qE '^rgba\([0-9]{1,3}, [0-9]{1,3}, [0-9]{1,3}$' <<<"$colour" \
|| fail "$scheme produced a malformed colour: $colour"
done < <(grep -vE '^\s*#' "$generated" | grep -oE 'rgba\([^)]*' | sed 's/,[^,]*$//')
# The Pango markup values are hex, and hyprlock wants them doubled-hashed.
while read -r pango; do
[[ -n "$pango" ]] || continue
grep -qE '^##[0-9a-f]{6}$' <<<"$pango" \
|| fail "$scheme produced malformed Pango markup colour: $pango"
done < <(grep -vE '^\s*#' "$generated" | grep -oE '##[0-9a-fA-F]{6}')
# The whole background block, not a fixed number of lines after it: the
# colour sits near the end, after the blur and noise settings.
background="$(awk '/^background \{/,/^\}/' "$generated" \
| grep -vE '^\s*#' | grep -oE 'color = rgba\([0-9]+' | grep -oE '[0-9]+$' | head -1)"
[[ -n "$background" ]] || fail "$scheme produced no background colour"
# The point of the whole exercise: a light lock screen must actually be
# light. 128 splits the two cleanly for these palettes.
if [[ "$scheme" == "light" ]]; then
(( background > 128 )) \
|| fail "light mode produced a DARK lock screen background (red channel $background) -- the original bug"
else
(( background < 128 )) \
|| fail "dark mode produced a LIGHT lock screen background (red channel $background)"
fi
done
# The two schemes must actually differ, or the substitution is a no-op that
# passes every check above.
XDG_CONFIG_HOME="$fixture" "$theme_apps" dark >/dev/null 2>&1
dark_hash="$(sha256sum "$fixture/hypr/hyprlock.conf" | cut -d' ' -f1)"
XDG_CONFIG_HOME="$fixture" "$theme_apps" light >/dev/null 2>&1
light_hash="$(sha256sum "$fixture/hypr/hyprlock.conf" | cut -d' ' -f1)"
[[ "$dark_hash" != "$light_hash" ]] \
|| fail 'the light and dark lock screens are byte-identical, so the scheme is not being applied'
printf 'lock screen theme contract: PASS\n'