Appearance now opens on Themes: light and dark side by side, each remembering its own choice, over galleries of ten shipped themes — Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and Everforest in both modes. A theme is a complete palette: the catalog lives in themes.json, Theme.qml reads every color token from the active record, and one render pipeline carries it to kitty, tmux, btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme editor builds new ones from four wells — wheel, hex, or eyedropper — with derived surfaces, a saturation slider, debounced fine-tune, and effects that save with the theme. Custom edits finally keep GNOME's accent, kitty's border, and hyprlock in sync. Wallpapers can be video: mpvpaper per output, hardware-decoded, muted and looped, supervised and respawned. Panama owns the pausing — games, battery, and a bar pill for right now — because the compositor rebuilds full-screen blur for every frame a video wallpaper draws. The lock screen gets a still frame. Titlebars stop lying. GNOME apps get close-only on your chosen side, the maximize and double-click settings are gone, the Settings window obeys the same rules, and its titlebar can be turned off entirely. Typography becomes five labeled dropdowns instead of a wall of samples. Contracts updated and written throughout (165 now); per the redesign workflow none were executed — the full sweep runs once at the end. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
218 lines
11 KiB
Bash
Executable File
218 lines
11 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", "-f", "-o",' "$service" \
|
|
|| fail 'the mpvpaper invocation is no longer a fixed argv'
|
|
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 'onOutputsChanged: 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'
|