wallpaper-picker: per-monitor support

- default selection = monitor under the mouse (cursorpos + geometry math)
- 1-9 toggle individual monitors, a toggles all
- Space/Enter both apply
- persist per-monitor state to ~/.config/wallpaper.conf using new wallpaper{} syntax
- hyprpaper.conf sources ~/.config/wallpaper.conf so picks survive reboot
main
The_miro 2026-05-09 18:25:48 +02:00
parent 25c9e69ad2
commit d2c0c1ae1f
3 changed files with 209 additions and 51 deletions

View File

@ -1,2 +1,7 @@
preload = ~/Pictures/background.jpg
wallpaper = , ~/Pictures/background.jpg
# template for ~/.config/wallpaper.conf — sourced by hypr/hyprpaper.conf,
# rewritten by ~/.config/scripts/wallpaper-picker
wallpaper {
monitor =
path = ~/Pictures/background.jpg
fit_mode = cover
}

View File

@ -1,6 +1,11 @@
splash = false
# fallback if no per-monitor entries exist (first boot)
wallpaper {
monitor =
monitor =
path = ~/Pictures/background.jpg
fit_mode = cover
}
# per-monitor state, written by ~/.config/scripts/wallpaper-picker
source = ~/.config/wallpaper.conf

View File

@ -1,11 +1,19 @@
#!/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
@ -14,6 +22,7 @@ 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
@ -26,14 +35,97 @@ done < <(find "$DIR" -maxdepth 1 -type f \
[[ ${#IMAGES[@]} -eq 0 ]] && { echo "No images found in $DIR"; exit 1; }
INDEX=0
CURRENT_WALLPAPER=""
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; }
vlog() { (( VERBOSE )) && printf '%s\n' "$*" >> "$LOG"; }
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)
@ -43,7 +135,10 @@ draw() {
local center_w=$(( cols - 2 * (side_w + gap) ))
local center_x=$(( side_w + gap ))
local next_x=$(( center_x + center_w + gap ))
local img_h=$(( rows - 3 ))
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 ))
@ -52,59 +147,92 @@ draw() {
printf '\033[2J\033[H'
clear_images
icat --place "${side_w}x${img_h}@0x0" "${IMAGES[$prev]}"
icat --place "${center_w}x${img_h}@${center_x}x0" "${IMAGES[$INDEX]}"
icat --place "${side_w}x${img_h}@${next_x}x0" "${IMAGES[$next]}"
tput cup 0 0
printf 'target: %s' "$(draw_targets)"
tput cup $(( rows - 2 )) 0
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]}")"
local hints=' h/←: prev l/→: next Enter: set wallpaper q: quit'
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"
}
set_wallpaper() {
local path="$1" prev="$CURRENT_WALLPAPER"
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
vlog "=== set_wallpaper: $path ==="
(( ${#SELECTED[@]} == 0 )) && return 2
out=$(hyprctl hyprpaper preload "$path" 2>&1); rc=$?
vlog "preload exit=$rc out=$out"
# Don't bail on preload failure — image may already be preloaded
if [[ -z "${LOADED[$path]:-}" ]]; then
out=$(hyprctl hyprpaper preload "$path" 2>&1); rc=$?
vlog "preload exit=$rc out=$out"
LOADED[$path]=1
fi
local monitors_out ok=0
monitors_out=$(hyprctl monitors 2>&1)
vlog "monitors: $monitors_out"
while IFS= read -r mon; do
out=$(hyprctl hyprpaper wallpaper "$mon,$path" 2>&1); rc=$?
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"
(( rc == 0 )) && ok=1
done < <(printf '%s\n' "$monitors_out" | awk '/^Monitor /{print $2}')
if (( rc == 0 )); then
CURRENT[$mon]="$path"
ok=1
fi
done
if (( ok == 0 )); then
vlog "FAILED: no monitor set"
return 1
fi
(( ok == 0 )) && return 1
CURRENT_WALLPAPER="$path"
if [[ -n "$prev" && "$prev" != "$path" ]]; then
out=$(hyprctl hyprpaper unload "$prev" 2>&1); rc=$?
vlog "unload prev exit=$rc out=$out"
fi
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() {
local key
IFS= read -rsn1 key
if [[ $key == $'\x1b' ]]; then
local rest
IFS= read -rsn2 -t 0.1 rest
key+="$rest"
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
printf '%s' "$key"
}
old_stty=$(stty -g)
@ -115,23 +243,43 @@ tput civis
draw
while true; do
key=$(read_key)
case "$key" in
read_key
case "$KEY" in
h|$'\x1b[D')
INDEX=$(( (INDEX - 1 + ${#IMAGES[@]}) % ${#IMAGES[@]} ))
draw ;;
l|$'\x1b[C')
INDEX=$(( (INDEX + 1) % ${#IMAGES[@]} ))
draw ;;
$'\r'|$'\n')
if set_wallpaper "${IMAGES[$INDEX]}"; then
tput cup $(( $(tput lines) - 1 )) 0
printf ' Wallpaper set: %s' "$(basename "${IMAGES[$INDEX]}")"
a|A)
if (( ${#SELECTED[@]} == ${#MONITORS[@]} )); then
SELECTED=()
else
tput cup $(( $(tput lines) - 1 )) 0
printf ' Failed — is hyprpaper running?'
(( VERBOSE )) && printf ' [see %s]' "$LOG"
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