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 @@
+
+
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 <