Files
Panama/tests/quickshell/video-wallpaper-contract
T

232 lines
12 KiB
Bash
Executable File

#!/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.applyCurrentPolicy(' "$service" \
|| fail 'the still policy is not reapplied once hyprpaper returns'
rg -Fq 'Wallpaper.refreshActive()' "$service" \
|| fail 'the fallback readback of hyprpaper state is gone'
# 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'