Files
Panama/config/dot/quickshell/scripts/panama-lock
T
Gabriel Brown cb7c09d208 Give the desktop real themes, video wallpapers, and honest titlebars
Appearance now opens on Themes: light and dark side by side, each
remembering its own choice, over galleries of ten shipped themes —
Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and
Everforest in both modes. A theme is a complete palette: the catalog
lives in themes.json, Theme.qml reads every color token from the
active record, and one render pipeline carries it to kitty, tmux,
btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme
editor builds new ones from four wells — wheel, hex, or eyedropper —
with derived surfaces, a saturation slider, debounced fine-tune, and
effects that save with the theme. Custom edits finally keep GNOME's
accent, kitty's border, and hyprlock in sync.

Wallpapers can be video: mpvpaper per output, hardware-decoded, muted
and looped, supervised and respawned. Panama owns the pausing — games,
battery, and a bar pill for right now — because the compositor
rebuilds full-screen blur for every frame a video wallpaper draws.
The lock screen gets a still frame.

Titlebars stop lying. GNOME apps get close-only on your chosen side,
the maximize and double-click settings are gone, the Settings window
obeys the same rules, and its titlebar can be turned off entirely.
Typography becomes five labeled dropdowns instead of a wall of
samples.

Contracts updated and written throughout (165 now); per the redesign
workflow none were executed — the full sweep runs once at the end.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 23:39:04 -04:00

402 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Generates Panama's hyprlock configuration into the state directory. The
# gitignored fallback config (rendered from the tracked .template at link
# time) is never rewritten by this script and remains the fallback if
# generation fails, so a malformed preference can never leave the session
# without a working locker.
set -euo pipefail
# The accents and the theme resolver, from the one file that holds them. The
# lock screen renders the same theme panama-theme-apps does, resolved by the
# same function rather than by a second copy of the lookup.
# shellcheck source=./panama-palette
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/panama-palette"
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
state_home="${XDG_STATE_HOME:-$HOME/.local/state}"
settings="$config_home/panama/settings.json"
state_dir="$state_home/panama"
generated="$state_dir/hyprlock.conf"
status_file="$state_dir/hyprlock-status.json"
fallback="$config_home/hypr/hyprlock.conf"
temporary="$generated.tmp.$$"
status_temporary="$status_file.tmp.$$"
shipped_wallpaper="$HOME/Pictures/Wallpapers/faroe_islands.jpg"
settings_valid=false
wallpaper_warning=""
cleanup() {
rm -f "$temporary" "$status_temporary" 2>/dev/null || true
}
trap cleanup EXIT
read_string() {
local key="$1" default="$2" value
if [[ "$settings_valid" != true ]]; then
printf '%s' "$default"
return
fi
value="$(jq -er --arg key "$key" \
'if has($key) and (.[$key] | type) == "string" then .[$key] else empty end' \
"$settings" 2>/dev/null)" || value="$default"
printf '%s' "$value"
}
read_bool() {
local key="$1" default="$2" value
if [[ "$settings_valid" != true ]]; then
printf '%s' "$default"
return
fi
value="$(jq -er --arg key "$key" \
'if has($key) and (.[$key] | type) == "boolean" then (.[$key] | tostring) else empty end' \
"$settings" 2>/dev/null)" || value="$default"
printf '%s' "$value"
}
read_int() {
local key="$1" default="$2" low="$3" high="$4" value
if [[ "$settings_valid" != true ]]; then
printf '%s' "$default"
return
fi
value="$(jq -er --arg key "$key" \
'if has($key) and (.[$key] | type) == "number" and (.[$key] | floor) == .[$key]
then (.[$key] | tostring) else empty end' "$settings" 2>/dev/null)" || value="$default"
if [[ ! "$value" =~ ^-?[0-9]+$ ]] || (( value < low || value > high )); then
value="$default"
fi
printf '%s' "$value"
}
read_object() {
local key="$1"
if [[ "$settings_valid" != true ]]; then
printf '{}'
return
fi
jq -c --arg key "$key" \
'if has($key) and (.[$key] | type) == "object" then .[$key] else {} end' \
"$settings" 2>/dev/null || printf '{}'
}
valid_path() {
[[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* && -f "$1" && -r "$1" ]]
}
resolve_wallpaper_path() {
local candidate="$1"
# A video wallpaper cannot play on the lock screen; VideoWallpaper.qml
# grabs a still frame when one is selected, and that frame stands in.
# A video with no cached frame falls through to the shipped image.
if [[ "$candidate" =~ \.(mp4|mkv|webm)$ ]]; then
local frame="${XDG_STATE_HOME:-$HOME/.local/state}/panama/video-wallpaper-frame.png"
if valid_path "$frame"; then
resolved_wallpaper="$frame"
return
fi
candidate=""
fi
if valid_path "$candidate"; then
resolved_wallpaper="$candidate"
return
fi
if [[ -n "$candidate" ]]; then
wallpaper_warning="One or more lock-screen wallpapers were unavailable; a safe fallback is in use."
fi
if valid_path "$shipped_wallpaper"; then
resolved_wallpaper="$shipped_wallpaper"
else
resolved_wallpaper="screenshot"
fi
}
load_preferences() {
if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
settings_valid=true
else
settings_valid=false
fi
background_mode="$(read_string lockBackgroundMode screenshot)"
[[ "$background_mode" == screenshot || "$background_mode" == wallpaper || "$background_mode" == solid ]] \
|| background_mode=screenshot
blur_level="$(read_int lockBlurLevel 3 0 5)"
show_clock="$(read_bool lockShowClock true)"
show_date="$(read_bool lockShowDate true)"
show_user="$(read_bool lockShowUser true)"
fade_on_empty="$(read_bool lockFadeOnEmpty false)"
use_24_hour="$(read_bool use24Hour false)"
color_scheme="$(read_string colorScheme dark)"
[[ "$color_scheme" == dark || "$color_scheme" == light ]] || color_scheme=dark
accent_name="$(read_string accentName blue)"
case "$accent_name" in
blue|orchid|teal|green|amber|orange|rose|slate) ;;
*) accent_name=blue ;;
esac
wallpaper_mode="$(read_string wallpaperMode single)"
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|| wallpaper_mode=single
wallpaper_warning=""
resolve_wallpaper_path "$(read_string wallpaperPath '')"
wallpaper_path="$resolved_wallpaper"
wallpaper_assignments="$(read_object wallpaperPerMonitor)"
case "$blur_level" in
0) blur_passes=0; blur_size=1 ;;
1) blur_passes=1; blur_size=3 ;;
2) blur_passes=2; blur_size=5 ;;
3) blur_passes=3; blur_size=8 ;;
4) blur_passes=4; blur_size=10 ;;
5) blur_passes=5; blur_size=12 ;;
esac
# The colours come from the ACTIVE THEME, resolved by panama-palette from
# settings.json and config/themes.json -- the same function
# panama-theme-apps renders hyprlock.conf.template through. They used to be
# two hardcoded Tokyo Night tables here, a third copy in panama-theme-apps
# and a fourth in link-dotfiles, which is why choosing any other theme left
# the lock screen in last season's colour with nothing to say why.
eval "$(theme_vars "$(resolve_theme "$color_scheme")")"
# An unreadable themes.json is the only way to arrive here without a
# palette. Refusing is right: the seeded fallback config is themed, and
# hyprlock's own defaults -- which is what a config full of empty colours
# resolves to -- are a bright grey screen.
for token in bg fg fgMuted red bgPanel; do
palette_token="pal_$token"
[[ -n "${!palette_token:-}" ]] || return 1
done
background_color="rgba($(hex_to_rgb "$pal_bg"), 1.0)"
foreground_color="rgba($(hex_to_rgb "$pal_fg"), 1.0)"
dim_color="rgba($(hex_to_rgb "$pal_fgMuted"), 1.0)"
error_color="rgba($(hex_to_rgb "$pal_red"), 1.0)"
field_color="rgba($(hex_to_rgb "$pal_bgPanel"), 0.85)"
dim_hex="$pal_fgMuted"
error_hex="$pal_red"
# The focus ring is the theme's accent. accentName is still read as the
# fallback: a stored custom theme may carry no accent of its own, and the
# curated pair for the recorded name is a better answer than none.
accent_rgb="$(hex_to_rgb "$(hex_or "${theme_accent:-}" "$(accent_hex "$accent_name" "$color_scheme")")")"
accent_color="rgba($accent_rgb, 1.0)"
accent_ring_color="rgba($accent_rgb, 0.9)"
}
monitor_names() {
hyprctl -j monitors 2>/dev/null \
| jq -r '.[]? | .name | select(type == "string") | select(test("^[A-Za-z0-9_.-]+$"))' \
2>/dev/null || true
}
emit_background() {
local monitor="$1" path="$2"
printf 'background {\n'
printf ' monitor = %s\n' "$monitor"
if [[ -n "$path" ]]; then
printf ' path = %s\n' "$path"
fi
printf ' blur_passes = %s\n' "$blur_passes"
printf ' blur_size = %s\n' "$blur_size"
printf ' noise = 0.0117\n'
printf ' contrast = 0.9\n'
printf ' brightness = 0.8\n'
printf ' vibrancy = 0.17\n'
printf ' vibrancy_darkness = 0.05\n'
printf ' color = %s\n' "$background_color"
printf ' zindex = -1\n'
printf '}\n\n'
}
emit_backgrounds() {
local output assigned saw_output=false
case "$background_mode" in
screenshot)
emit_background '' screenshot
;;
solid)
emit_background '' ''
;;
wallpaper)
while IFS= read -r output; do
[[ -n "$output" ]] || continue
saw_output=true
assigned="$wallpaper_path"
if [[ "$wallpaper_mode" == per-monitor ]]; then
candidate="$(jq -r --arg output "$output" \
'if has($output) and (.[$output] | type) == "string" then .[$output] else "" end' \
<<<"$wallpaper_assignments" 2>/dev/null || true)"
if [[ -n "$candidate" ]]; then
resolve_wallpaper_path "$candidate"
assigned="$resolved_wallpaper"
fi
fi
emit_background "$output" "$assigned"
done < <(monitor_names)
if [[ "$saw_output" != true ]]; then
emit_background '' "$wallpaper_path"
fi
;;
esac
}
emit_config() {
printf '# Generated by Panama. Do not edit.\n\n'
printf 'general {\n'
printf ' hide_cursor = true\n'
printf ' fractional_scaling = 2\n'
printf ' screencopy_mode = 0\n'
printf ' fail_timeout = 2000\n'
printf '}\n\n'
printf 'auth {\n'
printf ' pam:enabled = true\n'
printf ' pam:module = hyprlock\n'
printf '}\n\n'
emit_backgrounds
if [[ "$show_clock" == true ]]; then
printf 'label {\n'
printf ' monitor =\n'
if [[ "$use_24_hour" == true ]]; then
printf ' text = cmd[update:1000] date +"%%H:%%M"\n'
else
printf ' text = cmd[update:1000] date +"%%-I:%%M"\n'
fi
printf ' color = %s\n' "$foreground_color"
printf ' font_size = 120\n'
printf ' font_family = Adwaita Sans Light\n'
printf ' position = 0, 260\n'
printf ' halign = center\n'
printf ' valign = center\n'
printf '}\n\n'
fi
if [[ "$show_date" == true ]]; then
printf 'label {\n'
printf ' monitor =\n'
printf ' text = cmd[update:60000] date +"%%A, %%B %%-d"\n'
printf ' color = %s\n' "$dim_color"
printf ' font_size = 24\n'
printf ' font_family = Adwaita Sans\n'
printf ' position = 0, 160\n'
printf ' halign = center\n'
printf ' valign = center\n'
printf '}\n\n'
fi
printf 'input-field {\n'
printf ' monitor =\n'
printf ' size = 340, 52\n'
printf ' position = 0, -40\n'
printf ' halign = center\n'
printf ' valign = center\n'
printf ' outline_thickness = 2\n'
printf ' rounding = 26\n'
printf ' outer_color = %s\n' "$accent_ring_color"
printf ' inner_color = %s\n' "$field_color"
printf ' font_color = %s\n' "$foreground_color"
printf ' check_color = %s\n' "$accent_color"
printf ' fail_color = %s\n' "$error_color"
printf ' dots_size = 0.25\n'
printf ' dots_spacing = 0.3\n'
printf ' dots_center = true\n'
printf ' placeholder_text = <span foreground="##%s"><i>Password</i></span>\n' "$dim_hex"
printf ' fail_text = <span foreground="##%s"><i>$FAIL ($ATTEMPTS)</i></span>\n' "$error_hex"
printf ' fade_on_empty = %s\n' "$fade_on_empty"
printf ' hide_input = false\n'
printf '}\n\n'
if [[ "$show_user" == true ]]; then
printf 'label {\n'
printf ' monitor =\n'
printf ' text = $USER\n'
printf ' color = %s\n' "$foreground_color"
printf ' font_size = 16\n'
printf ' font_family = Adwaita Sans\n'
printf ' position = 0, -110\n'
printf ' halign = center\n'
printf ' valign = center\n'
printf '}\n'
fi
}
write_status() {
local generated_value="$1" path_value="$2" fallback_value="$3" error_value="$4"
mkdir -p "$state_dir" 2>/dev/null || return 0
[[ -w "$state_dir" ]] || return 0
jq -nc --argjson generated "$generated_value" --arg path "$path_value" \
--argjson fallback "$fallback_value" --arg error "$error_value" \
'{generated:$generated,path:$path,fallback:$fallback,error:$error}' \
>"$status_temporary" 2>/dev/null || return 0
mv "$status_temporary" "$status_file" 2>/dev/null || true
}
generate() {
if ! load_preferences; then
write_status false "$fallback" true "The lock-screen colors could not be resolved."
return 1
fi
mkdir -p "$state_dir" || return 1
if ! emit_config >"$temporary"; then
rm -f "$temporary" 2>/dev/null || true
write_status false "$fallback" true "The lock-screen configuration could not be generated."
return 1
fi
if [[ ! -s "$temporary" ]] \
|| ! rg -q '^auth \{' "$temporary" \
|| ! rg -q '^background \{' "$temporary" \
|| ! rg -q '^input-field \{' "$temporary"; then
rm -f "$temporary"
write_status false "$fallback" true "The lock-screen configuration could not be generated."
return 1
fi
if ! mv "$temporary" "$generated"; then
rm -f "$temporary" 2>/dev/null || true
write_status false "$fallback" true "The lock-screen configuration could not be generated."
return 1
fi
write_status true "$generated" false "$wallpaper_warning"
}
status() {
if [[ -r "$status_file" ]] \
&& jq -e '.generated | type == "boolean"' "$status_file" >/dev/null 2>&1 \
&& jq -e '.path | type == "string"' "$status_file" >/dev/null 2>&1; then
jq -c '{generated,path,fallback:(.fallback == true),error:(.error // "")}' "$status_file"
elif [[ -s "$generated" ]]; then
jq -nc --arg path "$generated" '{generated:true,path:$path,fallback:false,error:""}'
else
jq -nc --arg path "$fallback" \
'{generated:false,path:$path,fallback:true,error:"The generated lock-screen configuration is unavailable."}'
fi
}
case "${1:-status}" in
generate)
generate
;;
status)
status
;;
run)
if generate >/dev/null 2>&1; then
exec hyprlock -c "$generated"
fi
write_status false "$fallback" true "The lock-screen configuration could not be generated."
exec hyprlock -c "$fallback"
;;
*)
printf 'usage: panama-lock [generate|status|run]\n' >&2
exit 2
;;
esac