Dotfiles/desktopenvs/hyprland/scripts/wallpaper-picker

286 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Per-monitor wallpaper picker for hyprpaper.
# IPC docs: https://wiki.hypr.land/Hypr-Ecosystem/hyprpaper/
set -u
VERBOSE=0
DIR=""
STATE_FILE="${HOME}/.config/wallpaper.conf"
FIT_MODE="cover"
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=1 ;;
--fit) shift; FIT_MODE="$1" ;;
--state) shift; STATE_FILE="$1" ;;
*) DIR="$1" ;;
esac
shift
done
DIR="${DIR:-$HOME/Pictures}"
LOG=""
(( VERBOSE )) && LOG="/tmp/wallpaper-picker-$$.log"
vlog() { (( VERBOSE )) && printf '%s\n' "$*" >> "$LOG"; }
IMAGES=()
while IFS= read -r -d '' f; do
IMAGES+=("$f")
done < <(find "$DIR" -maxdepth 1 -type f \
\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \
-o -iname '*.webp' -o -iname '*.bmp' -o -iname '*.gif' \
-o -iname '*.avif' -o -iname '*.tiff' \) \
-print0 | sort -z)
[[ ${#IMAGES[@]} -eq 0 ]] && { echo "No images found in $DIR"; exit 1; }
MONITORS=()
while IFS= read -r m; do
[[ -n "$m" ]] && MONITORS+=("$m")
done < <(hyprctl monitors 2>/dev/null | awk '/^Monitor /{print $2}')
[[ ${#MONITORS[@]} -eq 0 ]] && { echo "No monitors detected — is Hyprland running?"; exit 1; }
mouse_monitor() {
command -v jq >/dev/null 2>&1 || return 1
local pos cx cy
pos=$(hyprctl cursorpos -j 2>/dev/null) || return 1
cx=$(printf '%s' "$pos" | jq -r '.x // empty')
cy=$(printf '%s' "$pos" | jq -r '.y // empty')
[[ -z "$cx" || -z "$cy" ]] && return 1
hyprctl monitors -j 2>/dev/null | jq -r --argjson cx "$cx" --argjson cy "$cy" '
.[] |
((.transform % 2) == 1) as $rot |
(if $rot then .height else .width end) / .scale as $lw |
(if $rot then .width else .height end) / .scale as $lh |
select(.x <= $cx and $cx < .x + $lw and .y <= $cy and $cy < .y + $lh) |
.name
' 2>/dev/null | head -n1
}
declare -A CURRENT
declare -A LOADED
while IFS= read -r line; do
mon="${line%%: *}"
path="${line#*: }"
[[ -z "$mon" || -z "$path" || "$mon" == "$line" ]] && continue
CURRENT[$mon]="$path"
LOADED[$path]=1
done < <(hyprctl hyprpaper listactive 2>/dev/null)
declare -A SELECTED
default_mon=$(mouse_monitor || true)
if [[ -z "$default_mon" ]]; then
default_mon=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.focused) | .name' 2>/dev/null | head -n1)
fi
[[ -z "$default_mon" ]] && default_mon="${MONITORS[0]}"
SELECTED[$default_mon]=1
INDEX=0
icat() { kitty +kitten icat --stdin=no --silent --transfer-mode=memory "$@" 2>/dev/null; }
clear_images() { icat --clear; }
selected_count() { printf '%d' "${#SELECTED[@]}"; }
target_label() {
local mon out=""
if (( ${#SELECTED[@]} == 0 )); then
printf 'none'; return
fi
if (( ${#SELECTED[@]} == ${#MONITORS[@]} )); then
printf 'all'; return
fi
for mon in "${MONITORS[@]}"; do
[[ -n "${SELECTED[$mon]:-}" ]] && out+="$mon, "
done
printf '%s' "${out%, }"
}
draw_targets() {
local i mon out=""
for (( i=0; i<${#MONITORS[@]}; i++ )); do
mon="${MONITORS[$i]}"
if [[ -n "${SELECTED[$mon]:-}" ]]; then
out+=$'\033[7m'" $((i+1)) ${mon} "$'\033[0m'" "
else
out+=" $((i+1)) ${mon} "
fi
done
if (( ${#SELECTED[@]} == ${#MONITORS[@]} )); then
out+=$'\033[7m'" a all "$'\033[0m'
else
out+=" a all "
fi
printf '%s' "$out"
}
draw_status() {
local mon name marker
for mon in "${MONITORS[@]}"; do
marker=" "
[[ -n "${SELECTED[$mon]:-}" ]] && marker="•"
name="${CURRENT[$mon]:-}"
[[ -n "$name" ]] && name="$(basename "$name")" || name="—"
printf ' %s %-12s %s\n' "$marker" "$mon:" "$name"
done
}
draw() {
local cols rows
cols=$(tput cols); rows=$(tput lines)
local gap=2
local side_w=$(( cols * 18 / 100 ))
local center_w=$(( cols - 2 * (side_w + gap) ))
local center_x=$(( side_w + gap ))
local next_x=$(( center_x + center_w + gap ))
local header_h=2
local footer_h=$(( ${#MONITORS[@]} + 3 ))
local img_h=$(( rows - header_h - footer_h ))
(( img_h < 5 )) && img_h=5
local n=${#IMAGES[@]}
local prev=$(( (INDEX - 1 + n) % n ))
local next=$(( (INDEX + 1) % n ))
printf '\033[2J\033[H'
clear_images
tput cup 0 0
printf 'target: %s' "$(draw_targets)"
icat --place "${side_w}x${img_h}@0x${header_h}" "${IMAGES[$prev]}"
icat --place "${center_w}x${img_h}@${center_x}x${header_h}" "${IMAGES[$INDEX]}"
icat --place "${side_w}x${img_h}@${next_x}x${header_h}" "${IMAGES[$next]}"
local row=$(( header_h + img_h ))
tput cup "$row" 0
printf ' [%d/%d] %s\n' $(( INDEX + 1 )) "$n" "$(basename "${IMAGES[$INDEX]}")"
draw_status
local hints=' h/←: prev l/→: next 1-9: toggle monitor a: toggle all Enter/Space: apply q: quit'
(( VERBOSE )) && hints+=" [log: $LOG]"
printf '%s' "$hints"
}
write_state() {
local mon path tmp
tmp=$(mktemp "${STATE_FILE}.XXXXXX") || return 1
{
printf '# generated by wallpaper-picker — do not edit by hand\n'
for mon in "${MONITORS[@]}"; do
path="${CURRENT[$mon]:-}"
[[ -z "$path" ]] && continue
printf 'wallpaper {\n monitor = %s\n path = %s\n fit_mode = %s\n}\n' \
"$mon" "$path" "$FIT_MODE"
done
} > "$tmp" && mv -f "$tmp" "$STATE_FILE"
vlog "wrote $STATE_FILE"
}
apply() {
local path="$1"
local out rc
(( ${#SELECTED[@]} == 0 )) && return 2
if [[ -z "${LOADED[$path]:-}" ]]; then
out=$(hyprctl hyprpaper preload "$path" 2>&1); rc=$?
vlog "preload exit=$rc out=$out"
LOADED[$path]=1
fi
local mon ok=0
for mon in "${!SELECTED[@]}"; do
out=$(hyprctl hyprpaper wallpaper "$mon, $path" 2>&1); rc=$?
vlog "wallpaper $mon exit=$rc out=$out"
if (( rc == 0 )); then
CURRENT[$mon]="$path"
ok=1
fi
done
(( ok == 0 )) && return 1
declare -A in_use
for mon in "${MONITORS[@]}"; do
[[ -n "${CURRENT[$mon]:-}" ]] && in_use[${CURRENT[$mon]}]=1
done
local p
for p in "${!LOADED[@]}"; do
if [[ -z "${in_use[$p]:-}" ]]; then
out=$(hyprctl hyprpaper unload "$p" 2>&1); rc=$?
vlog "unload $p exit=$rc out=$out"
unset "LOADED[$p]"
fi
done
write_state
return 0
}
KEY=""
read_key() {
KEY=""
IFS= read -rsn1 KEY
if [[ $KEY == $'\x1b' ]]; then
local rest=""
IFS= read -rsn2 -t 0.1 rest || true
KEY+="$rest"
fi
if (( VERBOSE )); then
local hex
hex=$(printf '%s' "$KEY" | od -An -tx1 | tr -d ' \n')
vlog "key: hex=$hex len=${#KEY}"
fi
}
old_stty=$(stty -g)
trap 'stty "$old_stty"; clear_images; tput cnorm; printf "\033[2J\033[H"' EXIT
stty -echo -icanon -icrnl min 1 time 0
tput civis
draw
while true; do
read_key
case "$KEY" in
h|$'\x1b[D')
INDEX=$(( (INDEX - 1 + ${#IMAGES[@]}) % ${#IMAGES[@]} ))
draw ;;
l|$'\x1b[C')
INDEX=$(( (INDEX + 1) % ${#IMAGES[@]} ))
draw ;;
a|A)
if (( ${#SELECTED[@]} == ${#MONITORS[@]} )); then
SELECTED=()
else
SELECTED=()
for mon in "${MONITORS[@]}"; do SELECTED[$mon]=1; done
fi
draw ;;
[1-9])
n=$KEY
if (( n >= 1 && n <= ${#MONITORS[@]} )); then
mon="${MONITORS[$((n-1))]}"
if [[ -n "${SELECTED[$mon]:-}" ]]; then
unset "SELECTED[$mon]"
else
SELECTED[$mon]=1
fi
draw
fi ;;
$'\r'|$'\n'|$'\x1bOM'|' ')
label=$(target_label)
apply "${IMAGES[$INDEX]}"; rc=$?
tput cup $(( $(tput lines) - 1 )) 0
case $rc in
0) printf '\033[2K Set on %s: %s' "$label" "$(basename "${IMAGES[$INDEX]}")" ;;
2) printf '\033[2K No monitors selected — press 1-%d or a' "${#MONITORS[@]}" ;;
*) printf '\033[2K Failed — is hyprpaper running?'
(( VERBOSE )) && printf ' [see %s]' "$LOG" ;;
esac ;;
q|Q) break ;;
esac
done