Files

413 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.
#
# Lowercased first, because panama-video-wallpaper scans with -iname: a
# HOLIDAY.MP4 it happily offers would otherwise be handed to the lock screen
# as an image and render as nothing at all.
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