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.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user