#!/usr/bin/env bash # Video wallpapers. # # A looping video behind every window is the single most expensive thing a # desktop can draw, and none of the cost is visible: Hyprland rebuilds the # full-screen blur chain on every wallpaper frame while any blur-enabled layer # exists, and an occluded wallpaper keeps compositing unless solitary mode # holds -- which one toast breaks. So Panama owns the pause policy rather than # trusting the compositor with it, and this pins that policy in place. # # What must hold: # # 1. The video pauses for a game, on battery when asked to, and on demand # from the bar. Each reason is independent; the video plays only when # none of them holds. # 2. mpvpaper is told to decode on the GPU, stay muted, loop, and open an # IPC socket. A silent software-decode fallback would burn a core # forever with nothing on screen to explain it. # 3. hyprpaper's service is stopped while a video plays and started again # afterwards. Both claim the background layer and stacking within a layer # is creation order -- a race with no winner worth having. # 4. The picker cannot be flooded, and cannot be handed a file mpvpaper will # not play. # 5. The lock screen gets a still frame, because hyprlock cannot play motion. # # Static only: starting a real mpvpaper would take over the live desktop's # background. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" shell_dir="$repo_dir/config/dot/quickshell" service="$shell_dir/services/VideoWallpaper.qml" wallpaper="$shell_dir/services/Wallpaper.qml" helper="$shell_dir/scripts/panama-video-wallpaper" indicator="$shell_dir/modules/bar/WallpaperIndicator.qml" bar="$shell_dir/modules/bar/Bar.qml" picker="$shell_dir/modules/settings/WallpaperPicker.qml" appearance="$shell_dir/modules/settings/AppearancePage.qml" schema="$shell_dir/config/PreferenceSchema.qml" lock="$shell_dir/scripts/panama-lock" packages="$repo_dir/setup/packages/hyprland-packages" fail() { printf 'video wallpaper contract: %s\n' "$1" >&2 exit 1 } for file in "$service" "$wallpaper" "$helper" "$indicator" "$bar" "$picker" \ "$appearance" "$schema" "$lock" "$packages"; do [[ -f "$file" ]] || fail "missing ${file#"$repo_dir/"}" done [[ -x "$helper" ]] || fail 'panama-video-wallpaper is not executable' bash -n "$helper" || fail 'panama-video-wallpaper does not parse' # ── 1. Three independent pause reasons ────────────────────────────────────── python3 - "$service" <<'PY' || fail 'the pause policy drifted' import re import sys text = open(sys.argv[1], encoding="utf-8").read() stripped = re.sub(r"//.*", "", text) reasons = { "gamePaused": "FocusModes.gameRunning", "batteryPaused": 'DesktopPreferences.get("videoWallpaperPauseOnBattery")', } for prop, source in reasons.items(): block = re.search(r'property bool ' + prop + r':(.*?)\n\s*(readonly )?property', stripped, re.S) if not block or source not in block.group(1): raise SystemExit(f"{prop} no longer follows {source}") battery = re.search(r'property bool batteryPaused:(.*?)\n\s*(readonly )?property', stripped, re.S) for needle in ("Battery.available", "!Battery.acOnline"): if needle not in battery.group(1): raise SystemExit(f"the battery pause reason no longer checks {needle}") if not re.search(r'property bool manuallyPaused: false', stripped): raise SystemExit("the bar pill's manual pause is gone") combined = re.search(r'property bool paused:(.*?)\n', stripped) if not combined: raise SystemExit("there is no combined paused state") for reason in ("root.manuallyPaused", "root.gamePaused", "root.batteryPaused"): if reason not in combined.group(1): raise SystemExit(f"the combined paused state ignores {reason}") # The pause reaches mpv over its JSON IPC socket rather than by killing and # respawning the player, which would restart the video from the first frame. if 'JSON.stringify({ "command": ["set_property", "pause", root.paused] })' not in stripped: raise SystemExit("pausing no longer goes over mpv's JSON IPC") if 'onPausedChanged: pauseSync.restart()' not in stripped: raise SystemExit("a change of pause state does not push to the player") PY # ── 2. What mpvpaper is actually told ─────────────────────────────────────── for option in 'hwdec=vaapi' 'no-audio' 'loop-file=inf' 'input-ipc-server='; do rg -Fq "$option" "$service" || fail "the mpvpaper invocation is missing $option" done rg -Fq '"mpvpaper", "-o",' "$service" \ || fail 'the mpvpaper invocation is no longer a fixed argv' # -f is banned outright: it forks mpvpaper into the background, which reads to # the supervisor as an instant death — the respawn loop it caused flashed the # desktop black once a second until the flag was removed. if rg -Fq '"mpvpaper", "-f"' "$service"; then fail 'mpvpaper is launched with -f again; the supervisor needs it foreground' fi # Restore is the service's own, reactive and once per session — a one-shot # timer raced the async preference load and the availability probe at cold # start and silently lost. for needle in 'function tryRestore' 'restoreConsumed' 'videoWallpaperPath'; do rg -Fq "$needle" "$service" || fail "startup restore lost its $needle" done rg -Fq 'command -v mpvpaper' "$service" \ || fail 'the shell never checks whether mpvpaper is installed' rg -Fq 'mpvpaper' "$packages" \ || fail 'mpvpaper is not in the package list, so a fresh machine has no player' # A dead player while a video is meant to be active is a crash -- mpvpaper has # a known hotplug segfault -- so the whole set respawns rather than being # reasoned about per output. rg -Fq 'playerRespawn.restart();' "$service" \ || fail 'a crashed player is not respawned' rg -Fq 'onOutputSignatureChanged: if (root.active) playerRespawn.restart()' "$service" \ || fail 'a display hotplug does not respawn the players' # ── 3. The hyprpaper handover ─────────────────────────────────────────────── rg -Fq '["systemctl", "--user", "stop", "hyprpaper.service"]' "$service" \ || fail 'starting a video does not stop hyprpaper, so the two race for the layer' rg -Fq '["systemctl", "--user", "start", "hyprpaper.service"]' "$service" \ || fail 'stopping a video does not bring hyprpaper back' rg -Fq 'Wallpaper.refreshActive()' "$service" \ || fail 'the still wallpaper is not reapplied once hyprpaper returns' # Wallpaper.qml routes a video away from the hyprpaper transaction, which would # only fail validation, and gives hyprpaper a beat to come back on the way out. for needle in 'VideoWallpaper.isVideo(path)' 'VideoWallpaper.isVideo(root.configured)' \ 'pendingStillPolicy' 'VideoWallpaper.start(' 'VideoWallpaper.stop()'; do rg -Fq "$needle" "$wallpaper" || fail "Wallpaper.qml lost its video routing: $needle" done # ── 4. Discovery is bounded and typed ─────────────────────────────────────── rg -Fq 'NR <= 60' "$helper" \ || fail 'the video scan is unbounded, so a dumping-ground folder floods the picker' rg -Fq -- '-maxdepth 2' "$helper" \ || fail 'the video scan is no longer bounded in depth' rg -Fq "\\( -iname '*.mp4' -o -iname '*.mkv' -o -iname '*.webm' \\)" "$helper" \ || fail 'the video scan accepts extensions mpvpaper may not play' rg -Fq '/\.(mp4|mkv|webm)$/i' "$service" \ || fail 'the shell and the helper disagree about what counts as a video' rg -Fq 'VideoWallpaper.isVideo(' "$picker" \ || fail 'the picker does not distinguish video tiles from image tiles' # ── 5. The lock screen gets a still ───────────────────────────────────────── rg -Fq 'video-wallpaper-frame.png' "$service" \ || fail 'no still frame is cached for the lock screen' rg -Fq 'video-wallpaper-frame.png' "$lock" \ || fail 'the lock screen does not use the cached still frame' rg -Fq 'ffmpeg -hide_banner' "$helper" \ || fail 'the frame grab no longer runs ffmpeg quietly' rg -Fq '\.(mp4|mkv|webm)$' "$lock" \ || fail 'panama-lock does not recognise a video wallpaper' # ── The bar pill ──────────────────────────────────────────────────────────── rg -Fq 'visible: VideoWallpaper.active' "$indicator" \ || fail 'the bar pill is not conditional on a video actually playing' rg -Fq 'onActivated: VideoWallpaper.togglePause()' "$indicator" \ || fail 'clicking the bar pill does not pause the video' rg -Fq 'Accessible.name: VideoWallpaper.paused' "$indicator" \ || fail 'the bar pill has no spoken name, so it is a glyph and nothing else' rg -Fq 'Paused for game' "$indicator" \ || fail 'the pill does not say why the video paused itself' python3 - "$bar" <<'PY' || fail 'the wallpaper pill is not in the bar' import re import sys text = open(sys.argv[1], encoding="utf-8").read() if 'WallpaperIndicator {' not in text: raise SystemExit("Bar.qml never instantiates WallpaperIndicator") # It belongs in the right-hand status row, beside the other conditional pills. row = re.search(r'// ── Right ─+\n\s*Row \{(.*?)\n \}', text, re.S) if not row or 'WallpaperIndicator {' not in row.group(1): raise SystemExit("WallpaperIndicator is not in the bar's right-hand row") PY if rg -n 'NumberAnimation|ColorAnimation|SequentialAnimation|ParallelAnimation|loops:[[:space:]]*Animation\.Infinite' \ "$indicator"; then fail 'the wallpaper pill animates continuously beside a video that already costs frames' fi # ── Schema and settings surface ───────────────────────────────────────────── python3 - "$schema" <<'PY' || fail 'the video wallpaper schema entries are missing or malformed' import re import sys text = open(sys.argv[1], encoding="utf-8").read() expected = { "videoWallpaperDir": ("string", '"Videos/Wallpapers"', "wallpaper"), "videoWallpaperPauseOnBattery": ("bool", "true", "wallpaper"), } for key, (kind, default, group) in expected.items(): block = re.search(r"\{\s*\n\s*key:\s*\"" + key + r"\".*?\n\s{8}\}", text, re.S) if not block: raise SystemExit(f"missing {key}") body = block.group(0) if not re.search(rf'type:\s*"{kind}"', body): raise SystemExit(f"{key} has wrong type") if not re.search(rf'def:\s*{re.escape(default)}', body): raise SystemExit(f"{key} has wrong default") if not re.search(rf'group:\s*"{group}"', body): raise SystemExit(f"{key} has wrong group") if 'internal: true' in body: raise SystemExit(f"{key} is marked internal, so nobody can find it") PY for needle in 'title: "Video playback"' 'setting: "videoWallpaperDir"' \ 'setting: "videoWallpaperPauseOnBattery"' 'VideoWallpaper.rescan()'; do rg -Fq "$needle" "$appearance" || fail "Appearance is missing $needle" done # Honest about what it cannot do, rather than silently showing a black lock # screen when the wallpaper is a video. rg -Fq 'Still frame' "$appearance" \ || fail 'the settings page does not say the lock screen uses a still frame' # ── The doctor knows about it ─────────────────────────────────────────────── rg -Fq 'input.video-wallpaper' "$shell_dir/scripts/panama-doctor" \ || fail 'panama-doctor has no video wallpaper check' printf 'video wallpaper contract: PASS\n'