Compare commits
No commits in common. "2ce38ce9eb1d449085f2e4389e2d4005f74b8bd5" and "f25cef8780bb25ca9e2e0f0290d0e4e2dc8d5bc2" have entirely different histories.
2ce38ce9eb
...
f25cef8780
6 changed files with 278 additions and 847 deletions
94
README.md
94
README.md
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
Animated video wallpapers for [Omarchy](https://omarchy.com) (Arch Linux + Hyprland).
|
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. 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.
|
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.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -31,34 +31,26 @@ The installer handles all dependencies automatically.
|
||||||
|---------|--------|---------|
|
|---------|--------|---------|
|
||||||
| `mpv` | Official repos | Video player engine (decodes and renders video) |
|
| `mpv` | Official repos | Video player engine (decodes and renders video) |
|
||||||
| `jq` | Official repos | Parses monitor info from Hyprland |
|
| `jq` | Official repos | Parses monitor info from Hyprland |
|
||||||
| `gum` | Official repos | TUI toolkit (action menus, monitor picker, file browser) |
|
| `zenity` | Official repos | GUI dialogs (file picker, confirmations) |
|
||||||
| `libnotify` | Official repos | `notify-send` for post-action desktop notifications |
|
|
||||||
| `mpvpaper` | AUR | Wayland wallpaper daemon that uses mpv as its backend |
|
| `mpvpaper` | AUR | Wayland wallpaper daemon that uses mpv as its backend |
|
||||||
|
|
||||||
### Files Created
|
### Files Created
|
||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `~/.local/bin/motion-wallpaper-toggle` | Runtime script (toggle / start / stop / change / status) |
|
| `~/.local/bin/motion-wallpaper-toggle` | Toggle script (on/off switch) |
|
||||||
| `~/.local/share/applications/motion-wallpaper-toggle.desktop` | App launcher entry |
|
| `~/.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
|
## Usage
|
||||||
|
|
||||||
### From App Launcher
|
### From 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.
|
Search for **"Motion Wallpaper"** in Walker or your app launcher.
|
||||||
|
|
||||||
### From Terminal
|
### From Terminal
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
motion-wallpaper-toggle # interactive — toggle, or Stop/Change if running
|
motion-wallpaper-toggle
|
||||||
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
|
### With a Keybind
|
||||||
|
|
@ -71,37 +63,23 @@ 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.
|
> **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
|
## How It Works
|
||||||
|
|
||||||
### Toggle — not running
|
The toggle script works as an on/off switch:
|
||||||
|
|
||||||
1. Detects monitors via `hyprctl monitors -j`.
|
### Toggle ON (no video wallpaper running)
|
||||||
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`.
|
|
||||||
|
|
||||||
### Toggle — already running
|
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
|
||||||
|
|
||||||
Shows a radiolist with two choices:
|
### Toggle OFF (video wallpaper is running)
|
||||||
|
|
||||||
- **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`.
|
1. Shows a confirmation dialog
|
||||||
- **Change video** — pick a new video, keep the same target, swap in place.
|
2. Stops `mpvpaper`
|
||||||
|
3. Restarts `hyprpaper` to restore your normal static wallpaper
|
||||||
|
|
||||||
## Supported Video Formats
|
## Supported Video Formats
|
||||||
|
|
||||||
|
|
@ -130,54 +108,36 @@ Search for "live wallpaper" or "motion desktop" videos. Good sources include:
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
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.
|
mpvpaper uses GPU-accelerated rendering (`--vo=gpu`) so CPU usage is minimal. However:
|
||||||
|
|
||||||
- Higher resolution videos use more VRAM.
|
- Video decoding does use some GPU resources
|
||||||
- Shorter seamless loops (10–30s) use less memory.
|
- Higher resolution videos use more VRAM
|
||||||
- If you still notice impact, toggle the wallpaper off or disable autostart.
|
- If you notice performance impact in games, toggle the wallpaper off first
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
First stop: `~/.cache/motion-wallpaper.log` — both the toggle script and mpvpaper write there.
|
|
||||||
|
|
||||||
**Video wallpaper doesn't appear / shows black**
|
**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`
|
- Make sure hyprpaper and swaybg are not running: `pgrep hyprpaper && pkill hyprpaper`
|
||||||
|
- Try a different video file to rule out codec issues
|
||||||
|
|
||||||
**TUI fails with "gum is not installed"**
|
**File picker doesn't open**
|
||||||
- `sudo pacman -S gum`
|
- Check zenity is installed: `pacman -Qi zenity`
|
||||||
|
|
||||||
**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**
|
**"No monitors detected" error**
|
||||||
- Make sure you're running Hyprland: `echo $XDG_CURRENT_DESKTOP`
|
- Make sure you're running Hyprland: `echo $XDG_CURRENT_DESKTOP`
|
||||||
- Check hyprctl works: `hyprctl monitors`
|
- 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**
|
**Normal wallpaper doesn't come back after toggling off**
|
||||||
- Omarchy: `pkill -x swaybg; setsid uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill &`
|
- Manually restart hyprpaper: `hyprctl dispatch exec hyprpaper`
|
||||||
- Or just cycle the background: `omarchy-theme-bg-next` then back with `SUPER CTRL SPACE`.
|
|
||||||
- Generic Hyprland: `hyprctl dispatch exec hyprpaper`.
|
|
||||||
|
|
||||||
## Uninstalling
|
## Uninstalling
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop and disable autostart if enabled
|
# Remove the toggle script
|
||||||
systemctl --user disable --now motion-wallpaper.service 2>/dev/null || true
|
|
||||||
|
|
||||||
# Remove installed files
|
|
||||||
rm -f ~/.local/bin/motion-wallpaper-toggle
|
rm -f ~/.local/bin/motion-wallpaper-toggle
|
||||||
|
|
||||||
|
# Remove the app launcher entry
|
||||||
rm -f ~/.local/share/applications/motion-wallpaper-toggle.desktop
|
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
|
# Optionally remove packages
|
||||||
sudo pacman -Rns mpvpaper zenity
|
sudo pacman -Rns mpvpaper zenity
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
|
||||||
<title>Motion Wallpaper</title>
|
|
||||||
|
|
||||||
<!-- Rounded square app-tile background -->
|
|
||||||
<rect x="16" y="16" width="224" height="224" rx="44" ry="44" fill="#1e1e2e"/>
|
|
||||||
|
|
||||||
<!-- Monitor frame -->
|
|
||||||
<rect x="44" y="64" width="168" height="116" rx="14" ry="14"
|
|
||||||
fill="#11111b" stroke="#cdd6f4" stroke-width="5"/>
|
|
||||||
|
|
||||||
<!-- Subtle waveform inside screen — signals moving wallpaper -->
|
|
||||||
<path d="M 60 146 Q 84 118 108 146 T 156 146 T 196 146"
|
|
||||||
stroke="#cdd6f4" stroke-width="4" fill="none"
|
|
||||||
stroke-linecap="round" opacity="0.35"/>
|
|
||||||
|
|
||||||
<!-- Play triangle -->
|
|
||||||
<path d="M 108 94 L 108 138 L 148 116 Z" fill="#cba6f7"/>
|
|
||||||
|
|
||||||
<!-- Monitor stand -->
|
|
||||||
<rect x="116" y="180" width="24" height="18" fill="#cdd6f4"/>
|
|
||||||
<rect x="84" y="198" width="88" height="10" rx="5" ry="5" fill="#cdd6f4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 927 B |
|
|
@ -1,536 +0,0 @@
|
||||||
#!/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 <<MSG
|
|
||||||
$APP_NAME: interactive mode requires a terminal.
|
|
||||||
Run from a terminal, or use a non-interactive action:
|
|
||||||
$0 start | stop | status
|
|
||||||
MSG
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
tui_err() {
|
|
||||||
gum style --foreground="$COLOR_ERROR" --bold "ERROR: $1"
|
|
||||||
log "ERROR: $1"
|
|
||||||
# Hold the terminal open so launcher-spawned windows don't flash away.
|
|
||||||
if [ -t 0 ]; then
|
|
||||||
gum input --placeholder="Press enter to close…" >/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=""
|
|
||||||
[ -f "$STATE_FILE" ] || return 0
|
|
||||||
# Parse KEY="VALUE" lines directly instead of `source`, so a maliciously
|
|
||||||
# crafted video path (e.g. containing `";rm -rf …`) can't execute.
|
|
||||||
local key val
|
|
||||||
while IFS='=' read -r key val; do
|
|
||||||
val="${val#\"}"
|
|
||||||
val="${val%\"}"
|
|
||||||
case "$key" in
|
|
||||||
LAST_VIDEO) LAST_VIDEO="$val" ;;
|
|
||||||
LAST_TARGET) LAST_TARGET="$val" ;;
|
|
||||||
LAST_DIR) LAST_DIR="$val" ;;
|
|
||||||
esac
|
|
||||||
done < "$STATE_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
# Atomic write: avoids leaving a truncated file if the process dies mid-cat.
|
|
||||||
local tmp="$STATE_FILE.tmp"
|
|
||||||
cat > "$tmp" <<STATE
|
|
||||||
LAST_VIDEO="$1"
|
|
||||||
LAST_TARGET="$2"
|
|
||||||
LAST_DIR="$dir"
|
|
||||||
STATE
|
|
||||||
mv -f "$tmp" "$STATE_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
is_running() {
|
|
||||||
pgrep -x mpvpaper >/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
|
|
||||||
if ! systemctl --user enable motion-wallpaper.service >/dev/null 2>&1; then
|
|
||||||
tui_err "Failed to enable autostart (systemctl error)."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
log "autostart enabled"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
# Stop may be called twice in quick succession when the TUI stops the
|
|
||||||
# wallpaper and systemd's ExecStop fires right after. Skip if someone
|
|
||||||
# else already put swaybg back so we don't kill-and-respawn (causes a
|
|
||||||
# visible flicker).
|
|
||||||
if pgrep -x swaybg >/dev/null 2>&1 && ! pgrep -x mpvpaper >/dev/null 2>&1; then
|
|
||||||
log "swaybg already running, skipping restore"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
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() {
|
|
||||||
# Serialize stops so the TUI-initiated path and systemd's ExecStop (which
|
|
||||||
# fires when the TUI kills mpvpaper) can't both be mid-restore at once.
|
|
||||||
# `flock -n` → second caller skips cleanly if the first still holds it.
|
|
||||||
(
|
|
||||||
flock -n 9 || { log "stop already in progress, skipping"; exit 0; }
|
|
||||||
kill_watcher
|
|
||||||
pkill -x mpvpaper 2>/dev/null || true
|
|
||||||
# mpv cleans up its IPC socket on exit; a hard kill can leave it dangling.
|
|
||||||
rm -f "$MPV_IPC_SOCK" 2>/dev/null || true
|
|
||||||
restore_static_wallpaper
|
|
||||||
log "stopped"
|
|
||||||
) 9>"$STATE_DIR/.stop.lock"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===== 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."
|
|
||||||
# If autostart is on, a plain stop will let the wallpaper return on
|
|
||||||
# next reboot. Offer to turn autostart off so "stop" means "stop".
|
|
||||||
if autostart_enabled; then
|
|
||||||
if gum confirm "Autostart is still enabled — also disable it so the wallpaper doesn't resume after reboot?"; then
|
|
||||||
autostart_disable
|
|
||||||
tui_ok "Autostart disabled."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"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."
|
|
||||||
# Motion wallpaper is still running at this point. Offer to also stop
|
|
||||||
# it now so the label "turn off" matches visible behaviour.
|
|
||||||
if is_running; then
|
|
||||||
if gum confirm "Motion wallpaper is still running — stop it now too?"; then
|
|
||||||
stop_mpvpaper
|
|
||||||
tui_ok "Stopped. Normal wallpaper restored."
|
|
||||||
notify "Motion wallpaper stopped."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
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 <<USAGE
|
|
||||||
$APP_NAME
|
|
||||||
Usage: ${0##*/} [toggle|start|stop|change|status]
|
|
||||||
|
|
||||||
toggle Interactive gum TUI (default). If running, offers Stop / Change.
|
|
||||||
start Start from saved state without prompting (for systemd).
|
|
||||||
stop Stop mpvpaper and restore the normal wallpaper.
|
|
||||||
change Pick a new video while already running.
|
|
||||||
status Print current state.
|
|
||||||
USAGE
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown action: $1" >&2
|
|
||||||
echo "Try: ${0##*/} --help" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
[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
|
|
||||||
385
wallpaper.sh
385
wallpaper.sh
|
|
@ -2,177 +2,300 @@
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Motion Wallpaper Installer for Omarchy / Hyprland
|
# Motion Wallpaper Installer for Omarchy / Hyprland
|
||||||
#
|
#
|
||||||
# Installs:
|
# This script sets up animated video wallpapers on an Omarchy (Arch Linux +
|
||||||
# ~/.local/bin/motion-wallpaper-toggle runtime script
|
# Hyprland) desktop. It uses mpvpaper, which is a Wayland wallpaper program
|
||||||
# ~/.local/share/applications/motion-wallpaper-toggle.desktop app entry
|
# that plays a video file on the desktop background layer using mpv.
|
||||||
# ~/.config/systemd/user/motion-wallpaper.service optional autostart unit
|
#
|
||||||
|
# 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
|
||||||
#
|
#
|
||||||
# Dependencies:
|
# Dependencies:
|
||||||
# mpv, jq, zenity (pacman)
|
# - mpv: Video player engine (does the actual video decoding/rendering)
|
||||||
# mpvpaper (AUR, via yay or paru)
|
# - mpvpaper: Wayland-native wallpaper daemon that uses mpv as its backend
|
||||||
# libnotify (pacman) — optional, for notify-send
|
# (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)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
echo "=== Motion wallpaper installer for Omarchy / Hyprland ==="
|
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
|
if ! command -v pacman >/dev/null 2>&1; then
|
||||||
echo "This script expects a pacman-based system (Arch/Omarchy). Aborting." >&2
|
echo "This script expects a pacman-based system (Arch/Omarchy). Aborting."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ----- source files ------------------------------------------------------------
|
# 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
|
||||||
|
|
||||||
TOGGLE_SRC="$SCRIPT_DIR/motion-wallpaper-toggle"
|
# Install mpvpaper from the AUR (Arch User Repository).
|
||||||
WATCHER_SRC="$SCRIPT_DIR/motion-wallpaper-watcher"
|
# mpvpaper is not in the official repos because it's a smaller community
|
||||||
UNIT_SRC="$SCRIPT_DIR/motion-wallpaper.service"
|
# project. It needs an AUR helper (yay or paru) to build and install.
|
||||||
ICON_SRC="$SCRIPT_DIR/icons/motion-wallpaper.svg"
|
echo
|
||||||
|
echo "Installing mpvpaper from AUR..."
|
||||||
|
|
||||||
for f in "$TOGGLE_SRC" "$WATCHER_SRC" "$UNIT_SRC" "$ICON_SRC"; do
|
# Look for an AUR helper — yay and paru are the two most common ones.
|
||||||
if [ ! -f "$f" ]; then
|
# Omarchy ships with yay by default.
|
||||||
echo "Missing installer asset: $f" >&2
|
AUR_HELPER=""
|
||||||
exit 1
|
if command -v yay >/dev/null 2>&1; then
|
||||||
fi
|
AUR_HELPER="yay"
|
||||||
done
|
elif command -v paru >/dev/null 2>&1; then
|
||||||
|
AUR_HELPER="paru"
|
||||||
# ----- 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 "✓ Repo dependencies already installed (mpv, jq, gum, socat, libnotify)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v mpvpaper >/dev/null 2>&1; then
|
if [ -n "$AUR_HELPER" ]; then
|
||||||
echo "✓ mpvpaper already installed"
|
|
||||||
else
|
|
||||||
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..."
|
echo "Using $AUR_HELPER to install mpvpaper..."
|
||||||
"$AUR_HELPER" -S --needed mpvpaper
|
$AUR_HELPER -S --needed mpvpaper
|
||||||
|
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..."
|
||||||
|
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
|
else
|
||||||
cat >&2 <<'MSG'
|
echo "ERROR: $1" >&2
|
||||||
|
|
||||||
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
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
if ! command -v mpvpaper >/dev/null 2>&1; then
|
zen_info() {
|
||||||
echo "ERROR: mpvpaper is not installed. Cannot continue." >&2
|
if command -v zenity >/dev/null 2>&1; then
|
||||||
exit 1
|
zenity --info --title="$APP_NAME" --text="$1" || true
|
||||||
|
else
|
||||||
|
echo "$1"
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# ----- install files -----------------------------------------------------------
|
# 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.
|
||||||
|
|
||||||
install -D -m 755 "$TOGGLE_SRC" "$HOME/.local/bin/motion-wallpaper-toggle"
|
# Make sure we're actually running inside Hyprland — hyprctl is how we
|
||||||
install -D -m 755 "$WATCHER_SRC" "$HOME/.local/bin/motion-wallpaper-watcher"
|
# talk to the compositor to find out which monitors are connected.
|
||||||
|
if ! command -v hyprctl >/dev/null 2>&1; then
|
||||||
# Install custom SVG icon into the hicolor theme — Walker and other XDG-aware
|
zen_err "hyprctl not found. Are you running Hyprland?"
|
||||||
# launchers will find it by name (Icon=motion-wallpaper) without needing a
|
exit 1
|
||||||
# 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
|
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"
|
||||||
|
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)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${SELECTED_MON:-}" ]; then
|
||||||
|
# User cancelled
|
||||||
|
exit 0
|
||||||
|
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"
|
mkdir -p "$HOME/.local/share/applications"
|
||||||
cat > "$HOME/.local/share/applications/motion-wallpaper-toggle.desktop" <<EOF
|
|
||||||
|
cat << EOF > "$HOME/.local/share/applications/motion-wallpaper-toggle.desktop"
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Motion Wallpaper
|
Name=Motion Wallpaper
|
||||||
Comment=Toggle animated video wallpaper on/off (TUI)
|
Comment=Toggle animated video wallpaper on/off
|
||||||
Exec=$HOME/.local/bin/motion-wallpaper-toggle
|
Exec=$HOME/.local/bin/motion-wallpaper-toggle
|
||||||
Icon=motion-wallpaper
|
Icon=preferences-desktop-wallpaper
|
||||||
Terminal=true
|
Terminal=false
|
||||||
Categories=Utility;Settings;DesktopSettings;
|
Categories=Utility;Settings;DesktopSettings;
|
||||||
Keywords=wallpaper;video;animated;background;
|
Keywords=wallpaper;video;animated;background;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Poke the desktop database so launchers re-index immediately.
|
|
||||||
if command -v update-desktop-database >/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
|
||||||
echo "=== Install complete ==="
|
echo "=== Install complete ==="
|
||||||
echo
|
echo
|
||||||
echo "✓ motion-wallpaper-toggle installed to ~/.local/bin/"
|
echo "✓ Motion Wallpaper has been added to your application menu"
|
||||||
echo "✓ 'Motion Wallpaper' added to your application menu"
|
echo " Search for 'Motion Wallpaper' in your app launcher"
|
||||||
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
|
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||||
cat <<'MSG'
|
echo
|
||||||
⚠️ ~/.local/bin is not in your PATH. Add to your shell rc:
|
echo "⚠️ NOTE: ~/.local/bin is not in your PATH."
|
||||||
|
echo "Add this to your ~/.bashrc or ~/.zshrc:"
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
echo
|
||||||
|
echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
|
||||||
MSG
|
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"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
echo
|
||||||
Usage:
|
echo "Optional Hyprland keybind (add to ~/.config/hypr/bindings.conf):"
|
||||||
motion-wallpaper-toggle # interactive (toggle / change video)
|
echo
|
||||||
motion-wallpaper-toggle status # print current state
|
echo " NOTE: SUPER+W is already bound to 'Close window' in Omarchy."
|
||||||
motion-wallpaper-toggle stop # stop and restore normal wallpaper
|
echo " Use a different keybind to avoid conflicts, for example:"
|
||||||
|
echo
|
||||||
Tip: drop videos in ~/Videos/Wallpapers/ for quick-pick access.
|
echo " bind = SUPER ALT, W, exec, ~/.local/bin/motion-wallpaper-toggle"
|
||||||
|
echo
|
||||||
Optional Hyprland keybind (avoid SUPER+W — that's Close window in Omarchy):
|
|
||||||
bind = SUPER ALT, W, exec, \$HOME/.local/bin/motion-wallpaper-toggle
|
|
||||||
|
|
||||||
Persist across logins via systemd:
|
|
||||||
systemctl --user enable --now motion-wallpaper.service
|
|
||||||
|
|
||||||
Logs: ~/.cache/motion-wallpaper.log
|
|
||||||
EOF
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue