From f31ff2b152dc4d6c02bc393f485e78ddd529b156 Mon Sep 17 00:00:00 2001 From: 28allday Date: Thu, 23 Apr 2026 20:33:02 +0100 Subject: [PATCH] v2: gum TUI, autostart, auto-pause watcher, Omarchy fixes Major rewrite of the runtime so the entry point is a proper gum TUI instead of zenity dialogs, plus a handful of correctness fixes that make it work on real Omarchy setups. Runtime (motion-wallpaper-toggle, extracted from the installer heredoc): * Full gum TUI: status header, monitor picker (with All monitors), library / filesystem pickers, change-video, autostart toggle. * State file at ~/.config/motion-wallpaper/state remembers last video, target monitor, and last-used directory so Browse reopens where the user was. * Actions: toggle | start | stop | change | status. Autostart: * Ships a systemd user unit (motion-wallpaper.service). * First fresh start prompts the user via gum confirm to enable it. * Running-state menu offers a Turn autostart ON/OFF entry. * Header shows the current autostart state. Auto-pause: * mpvpaper's -p is unreliable on Hyprland 0.54.x, so a small motion-wallpaper-watcher subscribes to Hyprland's socket2 and toggles mpv pause/resume via --input-ipc-server on fullscreen enter/exit. Started/stopped alongside mpvpaper. Omarchy compatibility: * Stop path now respawns swaybg pointed at ~/.config/omarchy/current/background via setsid uwsm-app (the way Omarchy autostarts it), instead of execing hyprpaper which isn't present. Falls back to hyprpaper on non-Omarchy Hyprland setups. * mpvpaper is launched under setsid uwsm-app so it survives the Walker-spawned terminal closing. Install / UX: * Installer only invokes sudo/yay when packages are actually missing, so reinstall is quiet. * Dropped zenity; added gum + socat + libnotify. * Custom SVG icon in the hicolor theme so Walker shows a proper tile. Installer restarts elephant.service so the new entry/icon appear without logout. * .desktop flipped to Terminal=true so launchers spawn a terminal for the TUI. * Watcher lookup falls back to the script's own dir when PATH is minimal (launcher-spawned terminals). Bug fixes: * Monitor picker was sending row-major data to zenity as one cell; fixed (kept the correct form for the new gum picker). * load_state / action_status no longer leak a non-zero exit code from trailing test expressions. * Stop path cleans up stray hyprpaper that would otherwise win the background layer. --- README.md | 98 +++++--- icons/motion-wallpaper.svg | 23 ++ motion-wallpaper-toggle | 490 +++++++++++++++++++++++++++++++++++++ motion-wallpaper-watcher | 63 +++++ motion-wallpaper.service | 14 ++ wallpaper.sh | 391 ++++++++++------------------- 6 files changed, 793 insertions(+), 286 deletions(-) create mode 100644 icons/motion-wallpaper.svg create mode 100644 motion-wallpaper-toggle create mode 100644 motion-wallpaper-watcher create mode 100644 motion-wallpaper.service diff --git a/README.md b/README.md index 89a82c6..9927fc7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Animated video wallpapers for [Omarchy](https://omarchy.com) (Arch Linux + Hyprland). -Uses [mpvpaper](https://github.com/GhostNaN/mpvpaper) to play any video file as your desktop wallpaper, with a simple toggle to switch between video and your normal static wallpaper. +Uses [mpvpaper](https://github.com/GhostNaN/mpvpaper) to play any video file as your desktop wallpaper. Features a [gum](https://github.com/charmbracelet/gum)-powered TUI with Stop / Change-video options, a quick-pick library folder, optional systemd autostart so the wallpaper survives reboots, and pause-on-fullscreen so games and full-screen video don't pay the decode cost. ## Quick Start @@ -31,26 +31,34 @@ The installer handles all dependencies automatically. |---------|--------|---------| | `mpv` | Official repos | Video player engine (decodes and renders video) | | `jq` | Official repos | Parses monitor info from Hyprland | -| `zenity` | Official repos | GUI dialogs (file picker, confirmations) | +| `gum` | Official repos | TUI toolkit (action menus, monitor picker, file browser) | +| `libnotify` | Official repos | `notify-send` for post-action desktop notifications | | `mpvpaper` | AUR | Wayland wallpaper daemon that uses mpv as its backend | ### Files Created | Path | Purpose | |------|---------| -| `~/.local/bin/motion-wallpaper-toggle` | Toggle script (on/off switch) | +| `~/.local/bin/motion-wallpaper-toggle` | Runtime script (toggle / start / stop / change / status) | | `~/.local/share/applications/motion-wallpaper-toggle.desktop` | App launcher entry | +| `~/.config/systemd/user/motion-wallpaper.service` | Optional autostart unit (not enabled by default) | +| `~/.config/motion-wallpaper/state` | Last-used video + target monitor | +| `~/.cache/motion-wallpaper.log` | Runtime log | ## Usage ### From App Launcher -Search for **"Motion Wallpaper"** in Walker or your app launcher. +Search for **"Motion Wallpaper"** in Walker or your app launcher. Because the entry is a TUI, your launcher spawns a terminal window (`Terminal=true` in the `.desktop` entry) and runs the gum interface inside it. The terminal closes automatically when the action finishes. ### From Terminal ```bash -motion-wallpaper-toggle +motion-wallpaper-toggle # interactive — toggle, or Stop/Change if running +motion-wallpaper-toggle change # pick a new video without stopping first +motion-wallpaper-toggle stop # stop and restore the normal wallpaper +motion-wallpaper-toggle status # print current state +motion-wallpaper-toggle start # non-interactive start from saved state (used by systemd) ``` ### With a Keybind @@ -63,23 +71,37 @@ bind = SUPER ALT, W, exec, ~/.local/bin/motion-wallpaper-toggle > **Note**: `SUPER+W` is already bound to "Close window" in Omarchy. Use `SUPER ALT+W` or another free combination. +### Video library folder + +Drop videos in `~/Videos/Wallpapers/` and the picker shows that folder as a quick list instead of opening the full filesystem browser. A **Browse…** entry is always available for picking something outside the library. + +### Persist across reboots (autostart) + +Enable the bundled systemd user unit — it calls `motion-wallpaper-toggle start`, which loads the last video and target monitor from state and starts mpvpaper non-interactively: + +```bash +systemctl --user enable --now motion-wallpaper.service +``` + +Disable with `systemctl --user disable --now motion-wallpaper.service`. If no state has been saved yet, the unit exits cleanly without error. + ## How It Works -The toggle script works as an on/off switch: +### Toggle — not running -### Toggle ON (no video wallpaper running) +1. Detects monitors via `hyprctl monitors -j`. +2. If multiple monitors, offers a picker with an **All monitors** option (passes `*` to mpvpaper). +3. Shows the video library (if any) or a file picker. +4. Stops the current wallpaper daemon (`swaybg` on Omarchy, or `hyprpaper` on generic Hyprland) so mpvpaper is visible, then starts `mpvpaper -f` with `--auto-pause`, `--loop`, `--vo=gpu`, `--profile=high-quality`. +5. Verifies mpvpaper is alive after 0.5s; surfaces failures inline in the TUI and holds the terminal open until you press enter. +6. Saves the video path and target to `~/.config/motion-wallpaper/state`. -1. Detects your connected monitors via `hyprctl monitors -j` -2. If multiple monitors, shows a selection dialog -3. Opens a file picker to choose a video file -4. Stops `hyprpaper` and `swaybg` (Omarchy's default wallpaper daemons) so mpvpaper is visible -5. Starts `mpvpaper` in the background with GPU-accelerated looping playback +### Toggle — already running -### Toggle OFF (video wallpaper is running) +Shows a radiolist with two choices: -1. Shows a confirmation dialog -2. Stops `mpvpaper` -3. Restarts `hyprpaper` to restore your normal static wallpaper +- **Stop motion wallpaper** — kill mpvpaper and restore the previous static wallpaper. On Omarchy this respawns `swaybg -i ~/.config/omarchy/current/background -m fill` via `uwsm-app`, matching how Omarchy autostarts it; on generic Hyprland it re-execs `hyprpaper`. +- **Change video** — pick a new video, keep the same target, swap in place. ## Supported Video Formats @@ -108,36 +130,54 @@ Search for "live wallpaper" or "motion desktop" videos. Good sources include: ## Performance -mpvpaper uses GPU-accelerated rendering (`--vo=gpu`) so CPU usage is minimal. However: +mpvpaper uses GPU-accelerated rendering (`--vo=gpu`) so CPU usage is minimal. `--auto-pause` also pauses playback whenever a fullscreen window covers the wallpaper, so games and full-screen video don't pay the decode cost. -- Video decoding does use some GPU resources -- Higher resolution videos use more VRAM -- If you notice performance impact in games, toggle the wallpaper off first +- Higher resolution videos use more VRAM. +- Shorter seamless loops (10–30s) use less memory. +- If you still notice impact, toggle the wallpaper off or disable autostart. ## Troubleshooting -**Video wallpaper doesn't appear / shows black** -- Make sure hyprpaper and swaybg are not running: `pgrep hyprpaper && pkill hyprpaper` -- Try a different video file to rule out codec issues +First stop: `~/.cache/motion-wallpaper.log` — both the toggle script and mpvpaper write there. -**File picker doesn't open** -- Check zenity is installed: `pacman -Qi zenity` +**Video wallpaper doesn't appear / shows black** +- Check the log. Codec issues and "no such monitor" errors both show up there. +- Make sure hyprpaper and swaybg are not running: `pgrep hyprpaper && pkill hyprpaper` + +**TUI fails with "gum is not installed"** +- `sudo pacman -S gum` + +**Launcher runs it but no terminal opens** +- Make sure your default terminal is XDG-registered. Omarchy's alacritty works out of the box. +- As a fallback, run `motion-wallpaper-toggle` directly from any terminal. **"No monitors detected" error** - Make sure you're running Hyprland: `echo $XDG_CURRENT_DESKTOP` - Check hyprctl works: `hyprctl monitors` +**Autostart unit fails** +- `journalctl --user -u motion-wallpaper.service` +- If the saved video was moved or deleted, the unit exits non-zero. Run the toggle interactively once to save fresh state. + **Normal wallpaper doesn't come back after toggling off** -- Manually restart hyprpaper: `hyprctl dispatch exec hyprpaper` +- Omarchy: `pkill -x swaybg; setsid uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill &` +- Or just cycle the background: `omarchy-theme-bg-next` then back with `SUPER CTRL SPACE`. +- Generic Hyprland: `hyprctl dispatch exec hyprpaper`. ## Uninstalling ```bash -# Remove the toggle script -rm -f ~/.local/bin/motion-wallpaper-toggle +# Stop and disable autostart if enabled +systemctl --user disable --now motion-wallpaper.service 2>/dev/null || true -# Remove the app launcher entry +# Remove installed files +rm -f ~/.local/bin/motion-wallpaper-toggle rm -f ~/.local/share/applications/motion-wallpaper-toggle.desktop +rm -f ~/.local/share/icons/hicolor/scalable/apps/motion-wallpaper.svg +rm -f ~/.config/systemd/user/motion-wallpaper.service +rm -rf ~/.config/motion-wallpaper +rm -f ~/.cache/motion-wallpaper.log +systemctl --user daemon-reload # Optionally remove packages sudo pacman -Rns mpvpaper zenity diff --git a/icons/motion-wallpaper.svg b/icons/motion-wallpaper.svg new file mode 100644 index 0000000..3845da4 --- /dev/null +++ b/icons/motion-wallpaper.svg @@ -0,0 +1,23 @@ + + + Motion Wallpaper + + + + + + + + + + + + + + + + + diff --git a/motion-wallpaper-toggle b/motion-wallpaper-toggle new file mode 100644 index 0000000..fc2b2e4 --- /dev/null +++ b/motion-wallpaper-toggle @@ -0,0 +1,490 @@ +#!/usr/bin/env bash +# ============================================================================== +# Motion Wallpaper — gum-powered TUI for mpvpaper on Omarchy / Hyprland. +# +# Actions: +# toggle (default) Interactive TUI. If running, offers Stop / Change. +# start Non-interactive start from saved state (for systemd). +# stop Stop mpvpaper and restore the normal wallpaper. +# change Pick a new video while already running. +# status Print current state (TUI header via gum). +# +# Files: +# ~/.config/motion-wallpaper/state last video + target monitor +# ~/Videos/Wallpapers/ optional quick-pick library +# ~/.cache/motion-wallpaper.log runtime log +# ============================================================================== + +set -euo pipefail + +APP_NAME="Motion Wallpaper" +STATE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/motion-wallpaper" +STATE_FILE="$STATE_DIR/state" +LOG_DIR="${XDG_CACHE_HOME:-$HOME/.cache}" +LOG_FILE="$LOG_DIR/motion-wallpaper.log" +LIBRARY_DIR="$HOME/Videos/Wallpapers" + +# mpv options forwarded via mpvpaper -o. An input-ipc-server socket is opened +# so our companion watcher can send pause/resume commands when Hyprland +# reports a fullscreen window (mpvpaper's own -p is unreliable on 0.54.x). +RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" +MPV_IPC_SOCK="$RUNTIME_DIR/motion-wallpaper-mpv.sock" +MPV_OPTS="--loop --no-audio --mute=yes --vo=gpu --profile=high-quality --input-ipc-server=$MPV_IPC_SOCK" + +# Catppuccin Mocha-ish accents +COLOR_ACCENT="#cba6f7" +COLOR_ERROR="#f38ba8" +COLOR_OK="#a6e3a1" +COLOR_MUTED="#6c7086" + +BROWSE_SENTINEL="── Browse filesystem… ──" + +mkdir -p "$STATE_DIR" "$LOG_DIR" + +# ===== helpers ================================================================ + +log() { + printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" +} + +require_gum() { + if ! command -v gum >/dev/null 2>&1; then + echo "ERROR: gum is not installed. Run: sudo pacman -S gum" >&2 + exit 1 + fi +} + +require_tty() { + if [ ! -t 0 ] || [ ! -t 1 ]; then + cat >&2 </dev/null 2>&1 || true + fi +} + +tui_ok() { + gum style --foreground="$COLOR_OK" "✓ $1" + log "$1" +} + +notify() { + # Fire-and-forget system notification so the user sees results after the + # TUI terminal closes. + if command -v notify-send >/dev/null 2>&1; then + notify-send "$APP_NAME" "$1" || true + fi + log "$1" +} + +load_state() { + LAST_VIDEO="" + LAST_TARGET="" + LAST_DIR="" + if [ -f "$STATE_FILE" ]; then + # shellcheck source=/dev/null + source "$STATE_FILE" + fi +} + +save_state() { + # $1 video, $2 target, $3 (optional) last dir — defaults to dirname of $1 + # so the filesystem browser re-opens where the user last landed. + local dir="${3:-$(dirname "$1")}" + umask 077 + cat > "$STATE_FILE" </dev/null 2>&1 +} + +# ===== autostart (systemd user unit) ========================================== + +autostart_installed() { + systemctl --user list-unit-files motion-wallpaper.service >/dev/null 2>&1 +} + +autostart_enabled() { + systemctl --user is-enabled motion-wallpaper.service >/dev/null 2>&1 +} + +autostart_enable() { + if ! autostart_installed; then + tui_err "motion-wallpaper.service is not installed. Re-run wallpaper.sh." + return 1 + fi + systemctl --user enable motion-wallpaper.service >/dev/null 2>&1 + log "autostart enabled" +} + +autostart_disable() { + systemctl --user disable motion-wallpaper.service >/dev/null 2>&1 || true + log "autostart disabled" +} + +show_header() { + load_state + local status_line target_line video_line autostart_line + if is_running; then + status_line="status: $(gum style --foreground="$COLOR_OK" running)" + else + status_line="status: $(gum style --foreground="$COLOR_MUTED" stopped)" + fi + target_line="target: ${LAST_TARGET:-$(gum style --foreground="$COLOR_MUTED" --italic '(none)')}" + video_line="video: ${LAST_VIDEO:-$(gum style --foreground="$COLOR_MUTED" --italic '(none)')}" + if autostart_enabled; then + autostart_line="autostart: $(gum style --foreground="$COLOR_OK" enabled)" + else + autostart_line="autostart: $(gum style --foreground="$COLOR_MUTED" disabled)" + fi + gum style --border=rounded --border-foreground="$COLOR_ACCENT" \ + --padding="1 2" --margin="1 0" \ + "$(gum style --bold --foreground="$COLOR_ACCENT" "◐ $APP_NAME")" \ + "" \ + "$status_line" \ + "$target_line" \ + "$video_line" \ + "$autostart_line" +} + +# ===== selection ============================================================== + +get_monitors() { + local mon_json + mon_json="$(hyprctl monitors -j 2>/dev/null || true)" + [ -z "$mon_json" ] && return 1 + printf '%s' "$mon_json" | jq -r '.[].name' +} + +pick_target() { + command -v hyprctl >/dev/null || { tui_err "hyprctl not found. Are you in Hyprland?"; return 1; } + command -v jq >/dev/null || { tui_err "jq is not installed."; return 1; } + + local monitors + monitors="$(get_monitors)" || { tui_err "Could not read monitors from hyprctl."; return 1; } + [ -z "$monitors" ] && { tui_err "No monitors detected."; return 1; } + + local count + count="$(printf '%s\n' "$monitors" | wc -l)" + if [ "$count" -eq 1 ]; then + printf '%s' "$monitors" + return 0 + fi + + local selected + selected=$( { echo "All monitors"; printf '%s\n' "$monitors"; } \ + | gum choose --header="Select a monitor") || return 1 + + if [ "$selected" = "All monitors" ]; then + printf '%s' '*' + else + printf '%s' "$selected" + fi +} + +browse_filesystem() { + # Start gum file at the last directory we successfully picked from, so + # changing videos returns the user to where they were. Falls back to the + # last video's dirname (for state files pre-dating LAST_DIR), then $HOME. + load_state + local start_dir="${LAST_DIR:-}" + if [ -z "$start_dir" ] && [ -n "${LAST_VIDEO:-}" ]; then + start_dir="$(dirname "$LAST_VIDEO")" + fi + if [ -z "$start_dir" ] || [ ! -d "$start_dir" ]; then + start_dir="$HOME" + fi + gum file --height=20 "$start_dir" +} + +pick_video() { + local library=() basenames=() v + if [ -d "$LIBRARY_DIR" ]; then + while IFS= read -r -d '' f; do + library+=("$f") + done < <( + find "$LIBRARY_DIR" -maxdepth 1 -type f \ + \( -iname '*.mp4' -o -iname '*.mkv' -o -iname '*.webm' -o -iname '*.mov' -o -iname '*.avi' \) \ + -print0 2>/dev/null | sort -z + ) + fi + + if [ ${#library[@]} -eq 0 ]; then + browse_filesystem || return 1 + return 0 + fi + + for v in "${library[@]}"; do + basenames+=("$(basename "$v")") + done + + local choice + choice=$( { printf '%s\n' "${basenames[@]}"; echo "$BROWSE_SENTINEL"; } \ + | gum choose --header="Choose a video from $LIBRARY_DIR") || return 1 + + if [ "$choice" = "$BROWSE_SENTINEL" ]; then + browse_filesystem + else + printf '%s' "$LIBRARY_DIR/$choice" + fi +} + +# ===== mpvpaper control ======================================================= + +kill_static_wallpapers() { + pkill -x hyprpaper 2>/dev/null || true + pkill -x swaybg 2>/dev/null || true +} + +kill_watcher() { + pkill -f motion-wallpaper-watcher 2>/dev/null || true +} + +start_watcher() { + local watcher + # PATH in launcher-spawned terminals is unreliable (minimal systemd env etc), + # so fall back to the script's own directory where the installer puts both. + watcher="$(command -v motion-wallpaper-watcher 2>/dev/null || true)" + if [ -z "$watcher" ]; then + local self_dir + self_dir="$(dirname "$(readlink -f "$0")")" + if [ -x "$self_dir/motion-wallpaper-watcher" ]; then + watcher="$self_dir/motion-wallpaper-watcher" + fi + fi + if [ -z "$watcher" ]; then + log "watcher binary not found — auto-pause disabled" + return 0 + fi + kill_watcher + setsid "$watcher" < /dev/null >> "$LOG_FILE" 2>&1 & + disown 2>/dev/null || true + log "watcher spawned via $watcher" +} + +restore_static_wallpaper() { + local omarchy_bg="$HOME/.config/omarchy/current/background" + if [ -e "$omarchy_bg" ]; then + pkill -x hyprpaper 2>/dev/null || true + pkill -x swaybg 2>/dev/null || true + if command -v uwsm-app >/dev/null 2>&1; then + setsid uwsm-app -- swaybg -i "$omarchy_bg" -m fill >/dev/null 2>&1 & + else + setsid swaybg -i "$omarchy_bg" -m fill >/dev/null 2>&1 & + fi + disown 2>/dev/null || true + log "restored swaybg -> $omarchy_bg" + elif command -v hyprpaper >/dev/null 2>&1; then + hyprctl dispatch exec hyprpaper >/dev/null 2>&1 || true + log "restored hyprpaper" + else + log "no known static wallpaper daemon to restore" + fi +} + +start_mpvpaper_fg() { + local target="$1" video="$2" + kill_static_wallpapers + pkill -x mpvpaper 2>/dev/null || true + sleep 0.3 + log "systemd start target=$target video=$video" + start_watcher + # shellcheck disable=SC2086 # intentional word-splitting on MPV_OPTS + exec mpvpaper -o "$MPV_OPTS" "$target" "$video" +} + +start_mpvpaper_bg() { + local target="$1" video="$2" + kill_static_wallpapers + pkill -x mpvpaper 2>/dev/null || true + sleep 0.3 + log "start target=$target video=$video" + # setsid detaches from the controlling terminal so mpvpaper survives the + # TUI terminal closing; uwsm-app parents it to the user systemd scope + # (matches how Omarchy autostarts swaybg). Without both, mpvpaper was + # getting SIGHUP'd when the Walker-spawned terminal exited. + # shellcheck disable=SC2086 # intentional word-splitting on MPV_OPTS + if command -v uwsm-app >/dev/null 2>&1; then + setsid uwsm-app -- mpvpaper -o "$MPV_OPTS" "$target" "$video" \ + < /dev/null >> "$LOG_FILE" 2>&1 & + else + # shellcheck disable=SC2086 + setsid mpvpaper -o "$MPV_OPTS" "$target" "$video" \ + < /dev/null >> "$LOG_FILE" 2>&1 & + fi + disown 2>/dev/null || true + start_watcher + sleep 0.8 + if ! is_running; then + tui_err "mpvpaper failed to start. See $LOG_FILE for details." + return 1 + fi +} + +stop_mpvpaper() { + kill_watcher + pkill -x mpvpaper 2>/dev/null || true + # mpv cleans up the IPC socket on exit, but if we killed mpvpaper hard + # the dangling socket can linger and confuse the next run. + rm -f "$MPV_IPC_SOCK" 2>/dev/null || true + restore_static_wallpaper + log "stopped" +} + +# ===== actions ================================================================ + +action_toggle() { + require_gum + require_tty + + if is_running; then + show_header + local autostart_label + if autostart_enabled; then + autostart_label="Turn autostart OFF" + else + autostart_label="Turn autostart ON" + fi + local choice + choice=$(gum choose --header="What would you like to do?" \ + "Stop motion wallpaper" "Change video" "$autostart_label" "Cancel") || exit 0 + + case "$choice" in + "Stop motion wallpaper") + stop_mpvpaper + tui_ok "Stopped. Normal wallpaper restored." + notify "Motion wallpaper stopped." + ;; + "Change video") + load_state + local video + video=$(pick_video) || exit 0 + [ -z "$video" ] && exit 0 + [ -f "$video" ] || { tui_err "File not found: $video"; exit 1; } + save_state "$video" "$LAST_TARGET" + start_mpvpaper_bg "$LAST_TARGET" "$video" || exit 1 + tui_ok "Swapped to $(basename "$video")." + notify "Motion wallpaper updated." + ;; + "Turn autostart ON") + autostart_enable && tui_ok "Autostart enabled — wallpaper will resume after reboot." + ;; + "Turn autostart OFF") + autostart_disable + tui_ok "Autostart disabled." + ;; + Cancel) exit 0 ;; + esac + return 0 + fi + + show_header + local target video + target=$(pick_target) || exit 0 + [ -z "$target" ] && exit 0 + video=$(pick_video) || exit 0 + [ -z "$video" ] && exit 0 + [ -f "$video" ] || { tui_err "File not found: $video"; exit 1; } + + save_state "$video" "$target" + start_mpvpaper_bg "$target" "$video" || exit 1 + tui_ok "Started $(basename "$video") on $target." + notify "Motion wallpaper started on $target." + + # Offer autostart on first fresh start (skipped silently if already on or + # if the systemd unit isn't installed). + if autostart_installed && ! autostart_enabled; then + if gum confirm "Start motion wallpaper automatically after login / reboot?"; then + autostart_enable && tui_ok "Autostart enabled." + fi + fi +} + +action_change() { + require_gum + require_tty + if ! is_running; then + tui_err "Motion wallpaper is not running." + exit 1 + fi + load_state + local video + video=$(pick_video) || exit 0 + [ -z "$video" ] && exit 0 + [ -f "$video" ] || { tui_err "File not found: $video"; exit 1; } + save_state "$video" "$LAST_TARGET" + start_mpvpaper_bg "$LAST_TARGET" "$video" || exit 1 + tui_ok "Swapped to $(basename "$video")." + notify "Motion wallpaper updated." +} + +action_start() { + load_state + if [ -z "$LAST_VIDEO" ] || [ -z "$LAST_TARGET" ]; then + log "autostart: no saved state, exiting cleanly" + exit 0 + fi + if [ ! -f "$LAST_VIDEO" ]; then + log "autostart: saved video missing ($LAST_VIDEO)" + exit 1 + fi + start_mpvpaper_fg "$LAST_TARGET" "$LAST_VIDEO" +} + +action_stop() { + stop_mpvpaper +} + +action_status() { + if command -v gum >/dev/null 2>&1 && [ -t 1 ]; then + show_header + else + load_state + if is_running; then echo "status: running"; else echo "status: stopped"; fi + if [ -n "${LAST_TARGET:-}" ]; then echo "target: $LAST_TARGET"; fi + if [ -n "${LAST_VIDEO:-}" ]; then echo "video: $LAST_VIDEO"; fi + fi +} + +# ===== main =================================================================== + +case "${1:-toggle}" in + toggle) action_toggle ;; + start) action_start ;; + stop) action_stop ;; + change) action_change ;; + status) action_status ;; + -h|--help) + cat <&2 + echo "Try: ${0##*/} --help" >&2 + exit 1 + ;; +esac diff --git a/motion-wallpaper-watcher b/motion-wallpaper-watcher new file mode 100644 index 0000000..ba7e231 --- /dev/null +++ b/motion-wallpaper-watcher @@ -0,0 +1,63 @@ +#!/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" + +# Hyprland socket2 emits newline-separated "EVENT>>DATA" lines. We only care +# about the fullscreen state. `fullscreen>>1` = entered, `fullscreen>>0` = left. +socat -u "UNIX-CONNECT:$HYPR_SOCK" - | while IFS= read -r line; do + case "$line" in + fullscreen\>\>1) + log "fullscreen entered — pause" + pause_mpv + ;; + fullscreen\>\>0) + log "fullscreen left — resume" + resume_mpv + ;; + esac +done diff --git a/motion-wallpaper.service b/motion-wallpaper.service new file mode 100644 index 0000000..9121da2 --- /dev/null +++ b/motion-wallpaper.service @@ -0,0 +1,14 @@ +[Unit] +Description=Motion wallpaper (mpvpaper) autostart +PartOf=graphical-session.target +After=graphical-session.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/motion-wallpaper-toggle start +ExecStop=%h/.local/bin/motion-wallpaper-toggle stop +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=graphical-session.target diff --git a/wallpaper.sh b/wallpaper.sh index e1f031d..e91c8c5 100644 --- a/wallpaper.sh +++ b/wallpaper.sh @@ -2,300 +2,177 @@ # ============================================================================== # Motion Wallpaper Installer for Omarchy / Hyprland # -# This script sets up animated video wallpapers on an Omarchy (Arch Linux + -# Hyprland) desktop. It uses mpvpaper, which is a Wayland wallpaper program -# that plays a video file on the desktop background layer using mpv. -# -# What this installer does: -# 1. Installs dependencies (mpv, jq, zenity, mpvpaper) -# 2. Creates a toggle script at ~/.local/bin/motion-wallpaper-toggle -# 3. Creates a .desktop entry so it appears in your app launcher -# -# The toggle script works as an on/off switch: -# - If no video wallpaper is running: opens a file picker, starts playback -# - If a video wallpaper IS running: stops it, restores normal wallpaper +# Installs: +# ~/.local/bin/motion-wallpaper-toggle runtime script +# ~/.local/share/applications/motion-wallpaper-toggle.desktop app entry +# ~/.config/systemd/user/motion-wallpaper.service optional autostart unit # # Dependencies: -# - mpv: Video player engine (does the actual video decoding/rendering) -# - mpvpaper: Wayland-native wallpaper daemon that uses mpv as its backend -# (AUR package — renders video on the wl_surface background layer) -# - jq: JSON parser — used to read monitor info from hyprctl -# - zenity: GTK dialog toolkit — provides the file picker and confirmation -# dialogs so the script works without a terminal -# - hyprctl: Hyprland's CLI tool — used to detect connected monitors -# (comes with Hyprland, no separate install needed) +# mpv, jq, zenity (pacman) +# mpvpaper (AUR, via yay or paru) +# libnotify (pacman) — optional, for notify-send # ============================================================================== set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + echo "=== Motion wallpaper installer for Omarchy / Hyprland ===" -# Sanity check — this script uses pacman for package installation, so it -# only works on Arch-based systems. Omarchy is built on Arch Linux. if ! command -v pacman >/dev/null 2>&1; then - echo "This script expects a pacman-based system (Arch/Omarchy). Aborting." + echo "This script expects a pacman-based system (Arch/Omarchy). Aborting." >&2 exit 1 fi -# Install core dependencies from the official Arch repos. -# --needed skips packages that are already installed, so this is safe -# to run multiple times without reinstalling anything unnecessarily. -# - mpv: the video player that mpvpaper uses under the hood -# - jq: parses the JSON output from "hyprctl monitors -j" -# - zenity: provides GUI dialogs (file picker, yes/no prompts) -echo "Installing required packages: mpv jq zenity" -sudo pacman -S --needed mpv jq zenity +# ----- source files ------------------------------------------------------------ -# Install mpvpaper from the AUR (Arch User Repository). -# mpvpaper is not in the official repos because it's a smaller community -# project. It needs an AUR helper (yay or paru) to build and install. -echo -echo "Installing mpvpaper from AUR..." +TOGGLE_SRC="$SCRIPT_DIR/motion-wallpaper-toggle" +WATCHER_SRC="$SCRIPT_DIR/motion-wallpaper-watcher" +UNIT_SRC="$SCRIPT_DIR/motion-wallpaper.service" +ICON_SRC="$SCRIPT_DIR/icons/motion-wallpaper.svg" -# Look for an AUR helper — yay and paru are the two most common ones. -# Omarchy ships with yay by default. -AUR_HELPER="" -if command -v yay >/dev/null 2>&1; then - AUR_HELPER="yay" -elif command -v paru >/dev/null 2>&1; then - AUR_HELPER="paru" -fi +for f in "$TOGGLE_SRC" "$WATCHER_SRC" "$UNIT_SRC" "$ICON_SRC"; do + if [ ! -f "$f" ]; then + echo "Missing installer asset: $f" >&2 + exit 1 + fi +done -if [ -n "$AUR_HELPER" ]; then - echo "Using $AUR_HELPER to install mpvpaper..." - $AUR_HELPER -S --needed mpvpaper +# ----- dependencies ------------------------------------------------------------ + +# Check what's already there so we don't invoke sudo when nothing needs doing. +MISSING_REPO=() +for cmd in mpv jq gum socat notify-send; do + command -v "$cmd" >/dev/null 2>&1 || MISSING_REPO+=("$cmd") +done +# notify-send maps to libnotify; translate for the pacman call below. +MISSING_PKGS=() +for cmd in "${MISSING_REPO[@]}"; do + case "$cmd" in + notify-send) MISSING_PKGS+=("libnotify") ;; + *) MISSING_PKGS+=("$cmd") ;; + esac +done + +if [ "${#MISSING_PKGS[@]}" -gt 0 ]; then + echo "Installing required packages: ${MISSING_PKGS[*]}" + sudo pacman -S --needed "${MISSING_PKGS[@]}" else - echo "⚠️ No AUR helper (yay/paru) found." - echo - echo "Please install mpvpaper manually:" - echo " 1. Install an AUR helper first:" - echo " sudo pacman -S --needed base-devel git" - echo " git clone https://aur.archlinux.org/yay.git" - echo " cd yay && makepkg -si" - echo - echo " 2. Then install mpvpaper:" - echo " yay -S mpvpaper" - echo - read -p "Press Enter after installing mpvpaper to continue..." + echo "✓ Repo dependencies already installed (mpv, jq, gum, socat, libnotify)" fi -# Final check — if mpvpaper still isn't installed at this point (e.g. user -# skipped the AUR step or the build failed), we can't continue because the -# toggle script depends on it. -if ! command -v mpvpaper >/dev/null 2>&1; then - echo "ERROR: mpvpaper is not installed. Cannot continue." - exit 1 -fi - -# Create the directory for user scripts. ~/.local/bin is the standard -# location for user-installed scripts on Linux (follows the XDG spec). -mkdir -p "$HOME/.local/bin" - -# Create the toggle script — this is the main script users interact with. -# It's a self-contained on/off switch for video wallpapers. -# When toggled ON: picks a video file via GUI, stops hyprpaper, starts mpvpaper -# When toggled OFF: stops mpvpaper, restarts hyprpaper to restore normal wallpaper -cat << 'EOF' > "$HOME/.local/bin/motion-wallpaper-toggle" -#!/usr/bin/env bash -set -euo pipefail - -APP_NAME="Motion Wallpaper" - -# Zenity helper functions — these wrap zenity dialogs so the script can -# show GUI pop-ups for errors, info, and yes/no questions. If zenity isn't -# available (shouldn't happen since we install it), they fall back to -# plain text output in the terminal. -zen_err() { - if command -v zenity >/dev/null 2>&1; then - zenity --error --title="$APP_NAME" --text="$1" || true - else - echo "ERROR: $1" >&2 - fi -} - -zen_info() { - if command -v zenity >/dev/null 2>&1; then - zenity --info --title="$APP_NAME" --text="$1" || true - else - echo "$1" - fi -} - -zen_question() { - if command -v zenity >/dev/null 2>&1; then - zenity --question --title="$APP_NAME" --text="$1" - return $? - else - # No zenity, default to "yes" - return 0 - fi -} - -# Toggle logic — check if mpvpaper is already running. -# If it is, this is a "toggle OFF" action: stop the video wallpaper -# and bring back the normal static wallpaper by restarting hyprpaper. -if pgrep -x mpvpaper >/dev/null 2>&1; then - if zen_question "Motion wallpaper is currently running.\n\nDo you want to stop it and return to your normal wallpaper?"; then - pkill mpvpaper || true - # Restart hyprpaper/swaybg so the normal wallpaper comes back - hyprctl dispatch exec hyprpaper >/dev/null 2>&1 || true - zen_info "Motion wallpaper stopped.\nNormal wallpaper restored." - fi - exit 0 -fi - -# If we get here, mpvpaper is NOT running — this is a "toggle ON" action. -# We need to: detect monitors → let user pick one → let user pick a video -# → stop hyprpaper → start mpvpaper. - -# Make sure we're actually running inside Hyprland — hyprctl is how we -# talk to the compositor to find out which monitors are connected. -if ! command -v hyprctl >/dev/null 2>&1; then - zen_err "hyprctl not found. Are you running Hyprland?" - exit 1 -fi - -# Get monitor list from Hyprland as JSON. The -j flag outputs structured -# JSON data which we parse with jq to extract monitor names (e.g. HDMI-A-1, -# DP-1, eDP-1). mpvpaper needs the exact monitor name to know where to -# render the wallpaper. -MON_JSON="$(hyprctl monitors -j 2>/dev/null || true)" -if [ -z "$MON_JSON" ]; then - zen_err "Could not get monitor info from hyprctl." - exit 1 -fi - -if ! command -v jq >/dev/null 2>&1; then - zen_err "jq is not installed. Please install jq and try again." - exit 1 -fi - -MONITORS="$(printf '%s\n' "$MON_JSON" | jq -r '.[].name')" -if [ -z "$MONITORS" ]; then - zen_err "No monitors detected." - exit 1 -fi - -MON_COUNT="$(printf '%s\n' "$MONITORS" | wc -l)" - -# If there's only one monitor, use it automatically. If there are multiple -# monitors, show a selection dialog so the user can pick which one gets -# the video wallpaper. -SELECTED_MON="" -if [ "$MON_COUNT" -eq 1 ]; then - SELECTED_MON="$MONITORS" +if command -v mpvpaper >/dev/null 2>&1; then + echo "✓ mpvpaper already installed" else - # Build a list for Zenity - MON_LIST=$(printf '%s\n' "$MONITORS" | awk '{print NR, $1}') - SELECTED_MON=$(echo "$MON_LIST" | zenity --list \ - --title="$APP_NAME - Select monitor" \ - --column="ID" --column="Monitor" \ - --height=300 \ - --print-column=2) + echo + echo "Installing mpvpaper from AUR..." + + AUR_HELPER="" + if command -v yay >/dev/null 2>&1; then AUR_HELPER="yay" + elif command -v paru >/dev/null 2>&1; then AUR_HELPER="paru" + fi + + if [ -n "$AUR_HELPER" ]; then + echo "Using $AUR_HELPER to install mpvpaper..." + "$AUR_HELPER" -S --needed mpvpaper + else + cat >&2 <<'MSG' + +ERROR: No AUR helper (yay/paru) found. + +Install one first, then re-run this installer: + + sudo pacman -S --needed base-devel git + git clone https://aur.archlinux.org/yay.git + cd yay && makepkg -si + +MSG + exit 1 + fi + + if ! command -v mpvpaper >/dev/null 2>&1; then + echo "ERROR: mpvpaper is not installed. Cannot continue." >&2 + exit 1 + fi fi -if [ -z "${SELECTED_MON:-}" ]; then - # User cancelled - exit 0 +# ----- install files ----------------------------------------------------------- + +install -D -m 755 "$TOGGLE_SRC" "$HOME/.local/bin/motion-wallpaper-toggle" +install -D -m 755 "$WATCHER_SRC" "$HOME/.local/bin/motion-wallpaper-watcher" + +# Install custom SVG icon into the hicolor theme — Walker and other XDG-aware +# launchers will find it by name (Icon=motion-wallpaper) without needing a +# full path in the .desktop entry. +ICON_DIR="$HOME/.local/share/icons/hicolor/scalable/apps" +install -D -m 644 "$ICON_SRC" "$ICON_DIR/motion-wallpaper.svg" + +# Refresh the icon cache if gtk-update-icon-cache is available. Harmless if +# not — launchers that read SVGs directly will pick it up regardless. +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -f -q "$HOME/.local/share/icons/hicolor" 2>/dev/null || true fi -# Open a file picker dialog for the user to choose their video wallpaper. -# Filters to common video formats. If the user clicks Cancel, exit cleanly. -if ! command -v zenity >/dev/null 2>&1; then - zen_err "Zenity is not installed but is required for file selection." - exit 1 -fi - -VIDEO="$(zenity --file-selection \ - --title="$APP_NAME - Choose motion wallpaper video" \ - --file-filter="Video files | *.mp4 *.mkv *.webm *.mov *.avi")" || exit 0 - -if [ -z "$VIDEO" ]; then - # User cancelled - exit 0 -fi - -if [ ! -f "$VIDEO" ]; then - zen_err "Selected file does not exist:\n$VIDEO" - exit 1 -fi - -# 2e) Stop existing wallpaper daemons so mpvpaper is visible -# Omarchy runs hyprpaper (and sometimes swaybg) which render on the same -# background layer as mpvpaper. They must be stopped or mpvpaper will be -# hidden behind them. -pkill -x hyprpaper 2>/dev/null || true -pkill -x swaybg 2>/dev/null || true -sleep 0.3 - -# Start mpvpaper with optimised playback settings: -# --loop: Loop the video forever (it's a wallpaper, not a movie) -# --no-audio: Don't play audio (you don't want wallpaper sounds) -# --vo=gpu: Use GPU-accelerated rendering for minimal CPU usage -# --profile=high-quality: Use mpv's high quality rendering profile -# --keep-open=yes: Keep the window open when video reaches end (before loop) -# -# nohup + & runs it in the background detached from the terminal, so -# closing the terminal won't kill the wallpaper. -nohup mpvpaper -o "--loop --no-audio --vo=gpu --profile=high-quality --keep-open=yes" \ - "$SELECTED_MON" "$VIDEO" >/dev/null 2>&1 & - -zen_info "Motion wallpaper started on $SELECTED_MON." -EOF - -chmod +x "$HOME/.local/bin/motion-wallpaper-toggle" - -# Create a .desktop entry so "Motion Wallpaper" appears in app launchers -# (Walker, Elephant, etc.). This follows the freedesktop.org Desktop Entry -# spec. The Categories and Keywords fields help the launcher index it -# properly so users can find it by searching "wallpaper", "video", etc. mkdir -p "$HOME/.local/share/applications" - -cat << EOF > "$HOME/.local/share/applications/motion-wallpaper-toggle.desktop" +cat > "$HOME/.local/share/applications/motion-wallpaper-toggle.desktop" </dev/null 2>&1; then + update-desktop-database -q "$HOME/.local/share/applications" 2>/dev/null || true +fi + +install -D -m 644 "$UNIT_SRC" "$HOME/.config/systemd/user/motion-wallpaper.service" +systemctl --user daemon-reload >/dev/null 2>&1 || true + +# Walker's data provider (Elephant) caches the desktop index in memory. Nudge +# it so the new entry + icon show up without the user having to log out. +if systemctl --user --quiet is-active elephant.service 2>/dev/null; then + systemctl --user restart elephant.service || true +fi + +# ----- done -------------------------------------------------------------------- + echo echo "=== Install complete ===" echo -echo "✓ Motion Wallpaper has been added to your application menu" -echo " Search for 'Motion Wallpaper' in your app launcher" +echo "✓ motion-wallpaper-toggle installed to ~/.local/bin/" +echo "✓ 'Motion Wallpaper' added to your application menu" +echo "✓ systemd unit installed (not enabled)" +echo -# Check if ~/.local/bin is in PATH — if it's not, the user won't be able -# to run "motion-wallpaper-toggle" directly from the terminal. The .desktop -# entry uses the full path so the app launcher will always work regardless. if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - echo - echo "⚠️ NOTE: ~/.local/bin is not in your PATH." - echo "Add this to your ~/.bashrc or ~/.zshrc:" - echo - echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" - echo - echo "Then reload your shell with: source ~/.bashrc" - echo - echo "For now, run with full path:" - echo " ~/.local/bin/motion-wallpaper-toggle" -else - echo - echo "Run this to toggle motion wallpaper on/off:" - echo - echo " motion-wallpaper-toggle" + cat <<'MSG' +⚠️ ~/.local/bin is not in your PATH. Add to your shell rc: + + export PATH="$HOME/.local/bin:$PATH" + +MSG fi -echo -echo "Optional Hyprland keybind (add to ~/.config/hypr/bindings.conf):" -echo -echo " NOTE: SUPER+W is already bound to 'Close window' in Omarchy." -echo " Use a different keybind to avoid conflicts, for example:" -echo -echo " bind = SUPER ALT, W, exec, ~/.local/bin/motion-wallpaper-toggle" -echo +cat <