#!/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