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
parent
25c9e69ad2
commit
d2c0c1ae1f
|
|
@ -1,2 +1,7 @@
|
||||||
preload = ~/Pictures/background.jpg
|
# template for ~/.config/wallpaper.conf — sourced by hypr/hyprpaper.conf,
|
||||||
wallpaper = , ~/Pictures/background.jpg
|
# rewritten by ~/.config/scripts/wallpaper-picker
|
||||||
|
wallpaper {
|
||||||
|
monitor =
|
||||||
|
path = ~/Pictures/background.jpg
|
||||||
|
fit_mode = cover
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
splash = false
|
splash = false
|
||||||
|
|
||||||
|
# fallback if no per-monitor entries exist (first boot)
|
||||||
wallpaper {
|
wallpaper {
|
||||||
monitor =
|
monitor =
|
||||||
path = ~/Pictures/background.jpg
|
path = ~/Pictures/background.jpg
|
||||||
fit_mode = cover
|
fit_mode = cover
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# per-monitor state, written by ~/.config/scripts/wallpaper-picker
|
||||||
|
source = ~/.config/wallpaper.conf
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Per-monitor wallpaper picker for hyprpaper.
|
||||||
|
# IPC docs: https://wiki.hypr.land/Hypr-Ecosystem/hyprpaper/
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
VERBOSE=0
|
VERBOSE=0
|
||||||
DIR=""
|
DIR=""
|
||||||
|
STATE_FILE="${HOME}/.config/wallpaper.conf"
|
||||||
|
FIT_MODE="cover"
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-v|--verbose) VERBOSE=1 ;;
|
-v|--verbose) VERBOSE=1 ;;
|
||||||
|
--fit) shift; FIT_MODE="$1" ;;
|
||||||
|
--state) shift; STATE_FILE="$1" ;;
|
||||||
*) DIR="$1" ;;
|
*) DIR="$1" ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
|
|
@ -14,6 +22,7 @@ done
|
||||||
DIR="${DIR:-$HOME/Pictures}"
|
DIR="${DIR:-$HOME/Pictures}"
|
||||||
LOG=""
|
LOG=""
|
||||||
(( VERBOSE )) && LOG="/tmp/wallpaper-picker-$$.log"
|
(( VERBOSE )) && LOG="/tmp/wallpaper-picker-$$.log"
|
||||||
|
vlog() { (( VERBOSE )) && printf '%s\n' "$*" >> "$LOG"; }
|
||||||
|
|
||||||
IMAGES=()
|
IMAGES=()
|
||||||
while IFS= read -r -d '' f; do
|
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; }
|
[[ ${#IMAGES[@]} -eq 0 ]] && { echo "No images found in $DIR"; exit 1; }
|
||||||
|
|
||||||
INDEX=0
|
MONITORS=()
|
||||||
CURRENT_WALLPAPER=""
|
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; }
|
icat() { kitty +kitten icat --stdin=no --silent --transfer-mode=memory "$@" 2>/dev/null; }
|
||||||
clear_images() { icat --clear; }
|
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() {
|
draw() {
|
||||||
local cols rows
|
local cols rows
|
||||||
cols=$(tput cols); rows=$(tput lines)
|
cols=$(tput cols); rows=$(tput lines)
|
||||||
|
|
@ -43,7 +135,10 @@ draw() {
|
||||||
local center_w=$(( cols - 2 * (side_w + gap) ))
|
local center_w=$(( cols - 2 * (side_w + gap) ))
|
||||||
local center_x=$(( side_w + gap ))
|
local center_x=$(( side_w + gap ))
|
||||||
local next_x=$(( center_x + center_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 n=${#IMAGES[@]}
|
||||||
|
|
||||||
local prev=$(( (INDEX - 1 + n) % n ))
|
local prev=$(( (INDEX - 1 + n) % n ))
|
||||||
|
|
@ -52,59 +147,92 @@ draw() {
|
||||||
printf '\033[2J\033[H'
|
printf '\033[2J\033[H'
|
||||||
clear_images
|
clear_images
|
||||||
|
|
||||||
icat --place "${side_w}x${img_h}@0x0" "${IMAGES[$prev]}"
|
tput cup 0 0
|
||||||
icat --place "${center_w}x${img_h}@${center_x}x0" "${IMAGES[$INDEX]}"
|
printf 'target: %s' "$(draw_targets)"
|
||||||
icat --place "${side_w}x${img_h}@${next_x}x0" "${IMAGES[$next]}"
|
|
||||||
|
|
||||||
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]}")"
|
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]"
|
(( VERBOSE )) && hints+=" [log: $LOG]"
|
||||||
printf '%s' "$hints"
|
printf '%s' "$hints"
|
||||||
}
|
}
|
||||||
|
|
||||||
set_wallpaper() {
|
write_state() {
|
||||||
local path="$1" prev="$CURRENT_WALLPAPER"
|
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
|
local out rc
|
||||||
|
|
||||||
vlog "=== set_wallpaper: $path ==="
|
(( ${#SELECTED[@]} == 0 )) && return 2
|
||||||
|
|
||||||
out=$(hyprctl hyprpaper preload "$path" 2>&1); rc=$?
|
if [[ -z "${LOADED[$path]:-}" ]]; then
|
||||||
vlog "preload exit=$rc out=$out"
|
out=$(hyprctl hyprpaper preload "$path" 2>&1); rc=$?
|
||||||
# Don't bail on preload failure — image may already be preloaded
|
vlog "preload exit=$rc out=$out"
|
||||||
|
LOADED[$path]=1
|
||||||
|
fi
|
||||||
|
|
||||||
local monitors_out ok=0
|
local mon ok=0
|
||||||
monitors_out=$(hyprctl monitors 2>&1)
|
for mon in "${!SELECTED[@]}"; do
|
||||||
vlog "monitors: $monitors_out"
|
out=$(hyprctl hyprpaper wallpaper "$mon, $path" 2>&1); rc=$?
|
||||||
|
|
||||||
while IFS= read -r mon; do
|
|
||||||
out=$(hyprctl hyprpaper wallpaper "$mon,$path" 2>&1); rc=$?
|
|
||||||
vlog "wallpaper $mon exit=$rc out=$out"
|
vlog "wallpaper $mon exit=$rc out=$out"
|
||||||
(( rc == 0 )) && ok=1
|
if (( rc == 0 )); then
|
||||||
done < <(printf '%s\n' "$monitors_out" | awk '/^Monitor /{print $2}')
|
CURRENT[$mon]="$path"
|
||||||
|
ok=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
if (( ok == 0 )); then
|
(( ok == 0 )) && return 1
|
||||||
vlog "FAILED: no monitor set"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CURRENT_WALLPAPER="$path"
|
declare -A in_use
|
||||||
if [[ -n "$prev" && "$prev" != "$path" ]]; then
|
for mon in "${MONITORS[@]}"; do
|
||||||
out=$(hyprctl hyprpaper unload "$prev" 2>&1); rc=$?
|
[[ -n "${CURRENT[$mon]:-}" ]] && in_use[${CURRENT[$mon]}]=1
|
||||||
vlog "unload prev exit=$rc out=$out"
|
done
|
||||||
fi
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KEY=""
|
||||||
read_key() {
|
read_key() {
|
||||||
local key
|
KEY=""
|
||||||
IFS= read -rsn1 key
|
IFS= read -rsn1 KEY
|
||||||
if [[ $key == $'\x1b' ]]; then
|
if [[ $KEY == $'\x1b' ]]; then
|
||||||
local rest
|
local rest=""
|
||||||
IFS= read -rsn2 -t 0.1 rest
|
IFS= read -rsn2 -t 0.1 rest || true
|
||||||
key+="$rest"
|
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
|
fi
|
||||||
printf '%s' "$key"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
old_stty=$(stty -g)
|
old_stty=$(stty -g)
|
||||||
|
|
@ -115,23 +243,43 @@ tput civis
|
||||||
draw
|
draw
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
key=$(read_key)
|
read_key
|
||||||
case "$key" in
|
case "$KEY" in
|
||||||
h|$'\x1b[D')
|
h|$'\x1b[D')
|
||||||
INDEX=$(( (INDEX - 1 + ${#IMAGES[@]}) % ${#IMAGES[@]} ))
|
INDEX=$(( (INDEX - 1 + ${#IMAGES[@]}) % ${#IMAGES[@]} ))
|
||||||
draw ;;
|
draw ;;
|
||||||
l|$'\x1b[C')
|
l|$'\x1b[C')
|
||||||
INDEX=$(( (INDEX + 1) % ${#IMAGES[@]} ))
|
INDEX=$(( (INDEX + 1) % ${#IMAGES[@]} ))
|
||||||
draw ;;
|
draw ;;
|
||||||
$'\r'|$'\n')
|
a|A)
|
||||||
if set_wallpaper "${IMAGES[$INDEX]}"; then
|
if (( ${#SELECTED[@]} == ${#MONITORS[@]} )); then
|
||||||
tput cup $(( $(tput lines) - 1 )) 0
|
SELECTED=()
|
||||||
printf ' Wallpaper set: %s' "$(basename "${IMAGES[$INDEX]}")"
|
|
||||||
else
|
else
|
||||||
tput cup $(( $(tput lines) - 1 )) 0
|
SELECTED=()
|
||||||
printf ' Failed — is hyprpaper running?'
|
for mon in "${MONITORS[@]}"; do SELECTED[$mon]=1; done
|
||||||
(( VERBOSE )) && printf ' [see %s]' "$LOG"
|
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 ;;
|
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 ;;
|
q|Q) break ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue