Motion-Wallpaper-Omarchy/motion-wallpaper-watcher
28allday 2ce38ce9eb Harden stop path, autostart, and state file
Bug-check pass on top of the v2 rewrite. Five real issues fixed:

* autostart_enable used to return the exit code of the trailing `log`
  call, so a failing `systemctl --user enable` was silently reported
  as success. Now returns 1 with a tui_err on real failure.

* save_state wrote directly to STATE_FILE; a crash mid-write would
  leave a truncated file that load_state would partially parse.
  Switched to an atomic tmp + mv -f pattern.

* load_state used `source "$STATE_FILE"` which is arbitrary code
  execution if a video path ever contained shell metacharacters.
  Replaced with a read-based KEY=VALUE parser that only honours
  LAST_VIDEO / LAST_TARGET / LAST_DIR.

* stop_mpvpaper can be called twice in quick succession (TUI stop
  immediately followed by systemd's ExecStop). Wrapped the whole
  body in a `flock -n` on $STATE_DIR/.stop.lock so the second caller
  no-ops instead of racing against the first.

* Watcher `cleanup` trap used `[ -n VAR ] && kill`, which
  short-circuits to non-zero when VAR is unset and aborts the trap
  before `exit 0` under set -e. Restructured to a proper if/|| true.
2026-04-23 20:52:41 +01:00

79 lines
2.6 KiB
Bash

#!/usr/bin/env bash
# ==============================================================================
# Motion Wallpaper — auto-pause watcher.
#
# Subscribes to Hyprland's event socket (socket2) and toggles mpv pause/resume
# via the IPC socket that mpvpaper was started with. Replaces mpvpaper's own
# --auto-pause / -p flag, which is unreliable on recent Hyprland releases.
#
# This script runs as a background sibling to mpvpaper. The main toggle script
# starts it and kills it.
# ==============================================================================
set -euo pipefail
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
MPV_IPC="$RUNTIME_DIR/motion-wallpaper-mpv.sock"
LOG_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/motion-wallpaper.log"
log() {
printf '[%s] watcher: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
}
if [ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]; then
log "HYPRLAND_INSTANCE_SIGNATURE not set — exiting"
exit 0
fi
HYPR_SOCK="$RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock"
if [ ! -S "$HYPR_SOCK" ]; then
log "hyprland event socket not found ($HYPR_SOCK) — exiting"
exit 0
fi
if ! command -v socat >/dev/null 2>&1; then
log "socat not installed — exiting"
exit 0
fi
send_mpv() {
# Best-effort — silently no-op if mpv's IPC socket isn't up yet.
[ -S "$MPV_IPC" ] || return 0
printf '%s\n' "$1" | socat - "UNIX-CONNECT:$MPV_IPC" >/dev/null 2>&1 || true
}
pause_mpv() { send_mpv '{ "command": ["set_property", "pause", true] }'; }
resume_mpv() { send_mpv '{ "command": ["set_property", "pause", false] }'; }
log "watching $HYPR_SOCK"
# Kill socat child on exit so it doesn't spam "Broken pipe" to the log when
# the watcher is killed by the main toggle script.
cleanup() {
# `[ ] && kill` short-circuits to non-zero when SOCAT_PID is unset, which
# under `set -e` would abort the trap before `exit 0` — wrap safely.
if [ -n "${SOCAT_PID:-}" ]; then
kill "$SOCAT_PID" 2>/dev/null || true
fi
exit 0
}
trap cleanup EXIT INT TERM
# Hyprland socket2 emits newline-separated "EVENT>>DATA" lines. We only care
# about the fullscreen state. `fullscreen>>1` = entered, `fullscreen>>0` = left.
# socat stderr is dropped so EPIPE on shutdown doesn't bloat the log.
coproc SOCAT { socat -u "UNIX-CONNECT:$HYPR_SOCK" - 2>/dev/null; }
# Bash auto-exports SOCAT_PID from `coproc SOCAT`; used in cleanup trap.
while IFS= read -r line <&"${SOCAT[0]}"; do
case "$line" in
fullscreen\>\>1)
log "fullscreen entered — pause"
pause_mpv
;;
fullscreen\>\>0)
log "fullscreen left — resume"
resume_mpv
;;
esac
done