#!/usr/bin/env bash # Generates Panama's hyprlock configuration into the state directory. The # tracked config is never rewritten and remains the fallback if generation # fails, so a malformed preference can never leave the session without a # working locker. set -euo pipefail 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.$$" settings_valid=false 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'* ]] } 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 wallpaper_mode="$(read_string wallpaperMode single)" [[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \ || wallpaper_mode=single wallpaper_path="$(read_string wallpaperPath '')" if ! valid_path "$wallpaper_path"; then wallpaper_path="$HOME/Pictures/Wallpapers/faroe_islands.jpg" fi 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 if [[ "$color_scheme" == light ]]; then background_color='rgba(245, 246, 250, 1.0)' foreground_color='rgba(55, 63, 87, 1.0)' dim_color='rgba(111, 119, 151, 1.0)' field_color='rgba(220, 223, 232, 0.88)' else background_color='rgba(34, 36, 54, 1.0)' foreground_color='rgba(200, 211, 245, 1.0)' dim_color='rgba(130, 139, 184, 1.0)' field_color='rgba(46, 47, 61, 0.85)' fi } 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 valid_path "$candidate"; then assigned="$candidate" 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 = rgba(130, 170, 255, 0.9)\n' printf ' inner_color = %s\n' "$field_color" printf ' font_color = %s\n' "$foreground_color" printf ' check_color = rgba(130, 170, 255, 1.0)\n' printf ' fail_color = rgba(255, 117, 127, 1.0)\n' printf ' dots_size = 0.25\n' printf ' dots_spacing = 0.3\n' printf ' dots_center = true\n' printf ' placeholder_text = Password\n' printf ' fail_text = $FAIL ($ATTEMPTS)\n' 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() { load_preferences 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 "" } 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