#!/bin/sh # Owns the visibility of the eww now-playing widget. Installed to # /usr/local/bin/fullscreen-watcher, started from configs/sway/config. # # The rule this enforces: the widget may only appear for *audio-only* playback with # nothing visual on screen. A status overlay on top of a film or a Steam Link stream is # worse than no status overlay at all, so every ambiguous case resolves to "hidden". # # Two independent signals, because neither alone is sufficient: # # 1. The sway tree. Catches fullscreen anything (mpv, Steam Link, a fullscreen # Firefox video) via fullscreen_mode, and catches a *windowed* visual app via its # app_id/class. mpv is the interesting case here: playing an audio-only file it # opens no window at all, so "an mpv window exists in the tree" is already a # reliable proxy for "mpv has a video stream". # # 2. mpv's IPC socket, queried for the `video-format` property. This is the direct # answer to "does the current mpv track have video", and covers the case where # mpv was started with --force-window and so has a window even for audio. It is # best-effort: if the socket is absent (mpv not running, or configs/mpv/mpv.conf # not installed) signal 1 still stands on its own. set -eu WINDOW=now-playing LOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/fullscreen-watcher.pid" # exec_always in the sway config re-runs this on every `swaymsg reload`; without this # guard each reload would leave another subscription and another swaymsg behind. if [ -f "$LOCK" ] && kill -0 "$(cat "$LOCK" 2>/dev/null)" 2>/dev/null; then exit 0 fi echo $$ > "$LOCK" if ! command -v eww >/dev/null 2>&1; then echo "fullscreen-watcher: eww is not installed, no now-playing widget on this image." >&2 echo " See live-build/config/hooks/normal/0800-eww-widget.hook.chroot." >&2 exit 0 fi eww daemon >/dev/null 2>&1 || true # Anything whose presence on screen means "do not draw over this". Steam Link runs # under Xwayland so it appears as an X11 class, not a Wayland app_id — both are # checked. This is a fixed list in this file; nothing external feeds it. VISUAL_MATCH='^(mpv|steam|steamlink|steam_app_.*|org\.videolan\.VLC)$' visual_on_screen() { swaymsg -t get_tree 2>/dev/null | jq -e --arg m "$VISUAL_MATCH" ' [ recurse(.nodes[]?, .floating_nodes[]?) | select(.type == "con" or .type == "floating_con") | select( ((.fullscreen_mode // 0) != 0) or ((.visible // false) and (((.app_id // "") | ascii_downcase | test($m)) or ((.window_properties.class // "") | ascii_downcase | test($m)))) ) ] | length > 0 ' >/dev/null 2>&1 } mpv_has_video() { for sock in "${HOME:-/root}/.mpv-socket" "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/mpv.sock"; do [ -S "$sock" ] || continue python3 - "$sock" <<'PY' && return 0 import json, socket, sys try: conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) conn.settimeout(1.0) conn.connect(sys.argv[1]) conn.sendall(b'{"command":["get_property","video-format"]}\n') reply = json.loads(conn.recv(4096).splitlines()[0]) except Exception: sys.exit(1) sys.exit(0 if reply.get("data") else 1) PY done return 1 } apply() { if visual_on_screen || mpv_has_video; then eww close "$WINDOW" >/dev/null 2>&1 || true else eww open "$WINDOW" >/dev/null 2>&1 || true fi } cleanup() { rm -f "$LOCK" eww close "$WINDOW" >/dev/null 2>&1 || true } trap cleanup EXIT INT TERM apply # The event payloads are not parsed: any window or workspace change simply triggers a # fresh look at the whole tree. That is one extra `swaymsg -t get_tree` per event and # it removes a whole class of bugs around which fields a given sway version puts in a # "fullscreen_mode" event. swaymsg -t subscribe -m '["window","workspace"]' 2>/dev/null | while read -r _event; do apply done