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
402 lines
14 KiB
Bash
Executable File
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
|