Files
playpen/shot.sh
T
Greyson Parrelli 738cead680 vtabs: terminal with vertical tabs on libghostty-vt + GTK4
Uses libghostty-vt (the API Ghostty documents for external embedders) for
the terminal core. Ghostty's other C API, ghostty.h, exposes a full terminal
surface but only supports macOS and iOS platform tags, so it cannot be
embedded on Linux.

We supply the layers libghostty-vt deliberately leaves out: PTY and process
management, a Cairo/Pango cell renderer, and a GTK4/libadwaita UI with a
Zen-style vertical tab sidebar.

Nix pins the whole toolchain (Zig 0.16 via zig-overlay, GTK 4.22, libadwaita)
so no host setup is needed.
2026-08-11 08:36:10 -04:00

118 lines
3.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run vtabs 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/vtabs-shot.png}"
KEYS="${2:-}"
SETTLE="${3:-2}"
RUNDIR="$(mktemp -d)"
trap 'rm -rf "$RUNDIR"' EXIT
cat > "$RUNDIR/sway.conf" <<CONF
output HEADLESS-1 resolution 1200x800
default_border none
exec "'$RUNDIR/launch.sh' >'$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 vtabs, 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 "$VTABS_BIN"
LAUNCH
chmod +x "$RUNDIR/launch.sh"
export VTABS_BIN="$PWD/zig-out/bin/vtabs"
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
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
# The nix dev shell points SHELL at a minimal bash (no readline, no
# programmable completion), which makes screenshots look broken for reasons
# that have nothing to do with vtabs. Use the system shell instead.
export SHELL="${VTABS_SHOT_SHELL:-/bin/bash}"
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 "${VTABS_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