#!/usr/bin/env bash # Run playpen inside a throwaway headless Sway and screenshot it. # # This keeps UI checks out of the developer's real Wayland session: nothing # pops up on screen, and it works even while the session is locked. # # Usage: ./shot.sh OUT.png ["keys to type"] [settle-seconds] set -u cd "$(dirname "$0")" OUT="${1:-/tmp/playpen-shot.png}" KEYS="${2:-}" SETTLE="${3:-2}" RUNDIR="$(mktemp -d)" trap 'rm -rf "$RUNDIR"' EXIT cat > "$RUNDIR/sway.conf" <'$RUNDIR/app.log' 2>&1" CONF # The nix dev shell exports bash functions into the environment. Any /bin/sh # the terminal's child spawns tries to import them and spews parse errors that # have nothing to do with playpen, so drop them just before exec. cat > "$RUNDIR/launch.sh" <<'LAUNCH' #!/usr/bin/env bash while read -r fn; do unset -f "$fn" 2>/dev/null; done < <(declare -Fx | awk '{print $3}') exec "$PLAYPEN_BIN" LAUNCH chmod +x "$RUNDIR/launch.sh" export PLAYPEN_BIN="$PWD/zig-out/bin/playpen" export WLR_BACKENDS=headless export WLR_LIBINPUT_NO_DEVICES=1 # No GPU is available to a headless compositor here, so force software # rendering on both the compositor and GTK sides. export WLR_RENDERER=pixman export LIBGL_ALWAYS_SOFTWARE=1 export GSK_RENDERER=cairo # WebKit's web process renders through mesa, which the dev shell has already # pointed at its own copy; here we only force that mesa down to its software # rasterizer, so a web pane paints the same way with or without a GPU. export GALLIUM_DRIVER=llvmpipe export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" export SWAYSOCK="$RUNDIR/sway.sock" # Don't let the app fall back to the real session. unset DISPLAY unset WAYLAND_DISPLAY for sock in "$XDG_RUNTIME_DIR"/wayland-*; do [ -S "$sock" ] && basename "$sock" done > "$RUNDIR/pre-sockets" 2>/dev/null || true sway -c "$RUNDIR/sway.conf" >"$RUNDIR/sway.log" 2>&1 & SWAY_PID=$! # Wait for the compositor socket before talking to it. for _ in $(seq 1 50); do [ -S "$SWAYSOCK" ] && break sleep 0.2 done # Sway picks its own socket name, so discover it by diffing the sockets in # XDG_RUNTIME_DIR against the ones that existed before we started. DISPLAY_NAME="" for _ in $(seq 1 50); do for sock in "$XDG_RUNTIME_DIR"/wayland-*; do case "$sock" in *.lock) continue;; esac [ -S "$sock" ] || continue name="$(basename "$sock")" grep -qx "$name" "$RUNDIR/pre-sockets" && continue DISPLAY_NAME="$name" break done [ -n "$DISPLAY_NAME" ] && break sleep 0.2 done if [ -z "$DISPLAY_NAME" ]; then echo "could not determine wayland display" >&2 cat "$RUNDIR/sway.log" >&2 exit 1 fi export WAYLAND_DISPLAY="$DISPLAY_NAME" sleep "$SETTLE" if [ -n "$KEYS" ]; then # wtype drops the first keystroke of every invocation while the compositor # takes up its freshly uploaded keymap, so burn one on a harmless key. wtype -k Shift_L 2>/dev/null || true sleep 0.3 wtype "$KEYS" 2>/dev/null || true sleep 1.5 fi if [ -n "${PLAYPEN_SHOT_DEBUG:-}" ]; then echo "--- swaymsg get_tree (apps) ---" >&2 swaymsg -t get_tree 2>&1 | grep -E '"(app_id|name|pid)"' | head -30 >&2 echo "--- sway log ---" >&2 cat "$RUNDIR/sway.log" >&2 echo "--- app log ---" >&2 cat "$RUNDIR/app.log" >&2 2>/dev/null || echo "(no app log)" >&2 fi if ! grim -o HEADLESS-1 "$OUT"; then echo "grim -o failed, trying full capture" >&2 grim "$OUT" || echo "grim failed entirely" >&2 fi swaymsg exit >/dev/null 2>&1 || kill "$SWAY_PID" 2>/dev/null wait "$SWAY_PID" 2>/dev/null echo "wrote $OUT" # Surface app-side errors, which sway captures on its stdout. grep -iE "error|warn|panic|segfault" "$RUNDIR/sway.log" | grep -v "portal" | head -20 || true