Shipping met reality tonight, and reality won three rounds before we did. mpvpaper's -f forks it into the background, which a supervisor reads as instant death — the respawn loop repainted the desktop black once a second. The reaper's own kills fired onExited like crashes, so the supervisor ate its young until deliberate deaths got marked. And the video's path shared wallpaperPath with the still pipeline, whose transactional persistence clobbered it — it now lives under its own videoWallpaperPath key, read by the lock screen and the doctor too. Restore is the service's own now, reactive and once per session: a one-shot timer raced the async preference load and availability probe at cold start and silently lost. A video-wallpaper IPC target drives start/stop/pause from a terminal and from the contract sweep to come. Verified live: one player per output, same PID across restarts, VAAPI engaged, 0.2% CPU steady for 1440p30 h264, pause from the bar pill. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
409 lines
15 KiB
Bash
Executable File
409 lines
15 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=""
|
|
# A video wallpaper lives under its own key; when one is set it wins,
|
|
# standing in as its cached still frame via resolve_wallpaper_path.
|
|
video_wallpaper="$(read_string videoWallpaperPath '')"
|
|
if [[ -n "$video_wallpaper" ]]; then
|
|
resolve_wallpaper_path "$video_wallpaper"
|
|
else
|
|
resolve_wallpaper_path "$(read_string wallpaperPath '')"
|
|
fi
|
|
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
|