From d2c0c1ae1ffe3c66c7f7702f3aa0b397a6a5e46b Mon Sep 17 00:00:00 2001 From: The_miro Date: Sat, 9 May 2026 18:25:48 +0200 Subject: [PATCH] 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 --- desktopenvs/hyprland/hypr-usr/wallpaper.conf | 9 +- desktopenvs/hyprland/hypr/hyprpaper.conf | 7 +- desktopenvs/hyprland/scripts/wallpaper-picker | 244 ++++++++++++++---- 3 files changed, 209 insertions(+), 51 deletions(-) diff --git a/desktopenvs/hyprland/hypr-usr/wallpaper.conf b/desktopenvs/hyprland/hypr-usr/wallpaper.conf index 24604b8..b852451 100644 --- a/desktopenvs/hyprland/hypr-usr/wallpaper.conf +++ b/desktopenvs/hyprland/hypr-usr/wallpaper.conf @@ -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 +} diff --git a/desktopenvs/hyprland/hypr/hyprpaper.conf b/desktopenvs/hyprland/hypr/hyprpaper.conf index eff33e2..22a683b 100644 --- a/desktopenvs/hyprland/hypr/hyprpaper.conf +++ b/desktopenvs/hyprland/hypr/hyprpaper.conf @@ -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 diff --git a/desktopenvs/hyprland/scripts/wallpaper-picker b/desktopenvs/hyprland/scripts/wallpaper-picker index c489c56..ac7bba9 100755 --- a/desktopenvs/hyprland/scripts/wallpaper-picker +++ b/desktopenvs/hyprland/scripts/wallpaper-picker @@ -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