diff --git a/.gitignore b/.gitignore index a041459..a2d024d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ __pycache__/ /config/dot/gtk-3.0/settings.ini /config/dot/gtk-4.0/settings.ini /config/dot/tmux/current-theme.conf +/config/dot/hypr/hyprlock.conf diff --git a/config/dot/hypr/hyprlock.conf b/config/dot/hypr/hyprlock.conf.template similarity index 74% rename from config/dot/hypr/hyprlock.conf rename to config/dot/hypr/hyprlock.conf.template index f662b64..2d84113 100644 --- a/config/dot/hypr/hyprlock.conf +++ b/config/dot/hypr/hyprlock.conf.template @@ -1,8 +1,17 @@ # ───────────────────────────────────────────────────────────────────────────── -# hyprlock — lock screen +# hyprlock — lock screen. # -# Tokyo Night Moon, matching quickshell/config/Theme.qml: -# accent #82aaff fg #c8d3f5 dim #828bb8 bg #222436 red #ff757f +# GENERATED FILE. Edit hyprlock.conf.template and re-run +# 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. # ───────────────────────────────────────────────────────────────────────────── @@ -45,7 +54,7 @@ background { vibrancy_darkness = 0.05 # Shown if the screenshot is unavailable. - color = rgba(34, 36, 54, 1.0) + color = rgba(@BG@, 1.0) zindex = -1 } @@ -54,7 +63,7 @@ background { label { monitor = text = cmd[update:1000] date +"%-I:%M" - color = rgba(200, 211, 245, 1.0) + color = rgba(@FG@, 1.0) font_size = 120 font_family = Adwaita Sans Light position = 0, 260 @@ -65,7 +74,7 @@ label { label { monitor = text = cmd[update:60000] date +"%A, %B %-d" - color = rgba(130, 139, 184, 1.0) + color = rgba(@MUTED@, 1.0) font_size = 24 font_family = Adwaita Sans position = 0, 160 @@ -84,18 +93,18 @@ input-field { outline_thickness = 2 rounding = 26 - outer_color = rgba(130, 170, 255, 0.9) - inner_color = rgba(46, 47, 61, 0.85) - font_color = rgba(200, 211, 245, 1.0) - check_color = rgba(130, 170, 255, 1.0) - fail_color = rgba(255, 117, 127, 1.0) + outer_color = rgba(@ACCENT@, 0.9) + inner_color = rgba(@FIELD@, 0.85) + font_color = rgba(@FG@, 1.0) + check_color = rgba(@ACCENT@, 1.0) + fail_color = rgba(@ERROR@, 1.0) dots_size = 0.25 dots_spacing = 0.3 dots_center = true - placeholder_text = Password - fail_text = $FAIL ($ATTEMPTS) + placeholder_text = Password + fail_text = $FAIL ($ATTEMPTS) fade_on_empty = false hide_input = false @@ -105,7 +114,7 @@ input-field { label { monitor = text = $USER - color = rgba(200, 211, 245, 0.9) + color = rgba(@FG@, 0.9) font_size = 16 font_family = Adwaita Sans position = 0, -110 diff --git a/config/dot/quickshell/scripts/panama-theme-apps b/config/dot/quickshell/scripts/panama-theme-apps index 23be064..a6fc474 100755 --- a/config/dot/quickshell/scripts/panama-theme-apps +++ b/config/dot/quickshell/scripts/panama-theme-apps @@ -31,6 +31,59 @@ case "$scheme" in *) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;; 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 ───────────────────────────────────────────────────────────────────── # 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 @@ -146,4 +199,5 @@ jq -cn \ --arg gtk "$status_gtk" \ --arg btop "$status_btop" \ --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}' diff --git a/setup/scripts/link-dotfiles b/setup/scripts/link-dotfiles index ad29e48..a562f38 100755 --- a/setup/scripts/link-dotfiles +++ b/setup/scripts/link-dotfiles @@ -89,6 +89,38 @@ elif [ -d "$PANAMA_DOT/tmux/themes" ]; then log "Seeded tmux $tmux_scheme theme ($tmux_name) → $TMUX_THEME" 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 # 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 diff --git a/tests/quickshell/lock-screen-theme-contract.sh b/tests/quickshell/lock-screen-theme-contract.sh new file mode 100755 index 0000000..e5afc8a --- /dev/null +++ b/tests/quickshell/lock-screen-theme-contract.sh @@ -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'