38078fe7ee
ci / rust (push) Successful in 1m54s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m2s
windows-host / package (push) Successful in 6m43s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m12s
ci / bench (push) Successful in 4m47s
apple / swift (push) Successful in 1m9s
android / android (push) Successful in 3m33s
deb / build-publish (push) Successful in 4m36s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
decky / build-publish (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 51s
release / apple (push) Successful in 8m30s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 53s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m15s
flatpak / build-publish (push) Successful in 4m4s
apple / screenshots (push) Successful in 5m31s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 9m48s
docker / deploy-docs (push) Successful in 24s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m20s
A controller-driven, chrome-less library launcher for the Steam Deck flow
(the Decky plugin's "Open library on screen" + pinned games, 8470419):
`--browse host[:port]` opens a paired host's game library as a coverflow
over a drifting aurora — A streams the focused title (the id rides the
Hello), session end returns to the launcher, B quits back to Gaming Mode.
`--connect` gains `--launch <id>` for direct-to-game starts; `--mgmt`
overrides the library port. Scope is deliberately library-only: host
selection/settings stay in the touch UI, pairing stays in the plugin (no
dialog can map under gamescope — every state renders in-page).
- gamepad.rs menu mode: the worker holds the active pad open while idle
(WITHOUT the Valve HIDAPI drivers — Deck lizard mode survives) and
translates it through a pure MenuNav state machine: edge-triggered
buttons, held-state snapshot on entry/detach (the escape chord that ends
a stream can't ghost-fire in the menu), 380/160 ms stick/dpad repeat,
menu rumble ticks. Keyboard fallback (arrows/Enter/Esc) drives the same
handler — fully usable with no pad, no host (PUNKTFUNK_FAKE_LIBRARY).
- Coverflow: ±38° corridor-facing tilt under per-card perspective
(gsk rotate_3d), dense overlapping side shelves with paint-order
restacking (gtk::Fixed draws in child order), opaque card faces + a
darkening veil for the recede (translucency would bleed the stack
through). The strip lives in an External-policy ScrolledWindow because
a bare gtk::Fixed measures its TRANSFORMED children and inflates the
page min-width past the window.
- Spring-driven motion: semi-implicit Euler in ≤8 ms substeps (a raw
50 ms frame leaves the stiff recoil spring ringing at ω·dt ≈ 1.2 —
regression-tested), ζ≈0.85 cursor chase + ζ≈0.55 boundary wobble;
velocity carries across retargets so held-repeat scrolling glides.
- Shot scene `gamepad-library` (GTK animations force-disabled in shot mode
— nav transitions froze mid-slide in headless captures); shared poster
fetch extracted to library::spawn_art_fetch.
Verified here: 21 unit tests (MenuNav, cursor stepping, spring
convergence/stability), clippy -D warnings clean, screenshot scene
pixel-checked, --browse smoke runs (fake-library + unpaired) on the
headless session. On-Deck validation pending (virtual-pad input, lizard
mode, rumble via Steam Input, full Decky→browse→stream→launcher loop).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
154 lines
5.4 KiB
Bash
Executable File
154 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Capture host-free UI screenshots of the native Linux client. Mirrors the iOS harness
|
|
# (clients/apple/tools/screenshots.sh): one app launch per scene (PUNKTFUNK_SHOT_SCENE),
|
|
# the app renders a mock-populated REAL view and — when the binary supports it — CAPTURES
|
|
# ITSELF (PUNKTFUNK_SHOT_OUT: widget snapshot → gsk render → PNG) before printing
|
|
# `PF_SHOT_READY`. Self-capture needs no Xvfb/ImageMagick and runs under a live Wayland
|
|
# session too; the X11 root-grab path is kept as a fallback for old binaries. No host,
|
|
# GPU, or live stream — only the chrome scenes (the stream page needs a live connector).
|
|
#
|
|
# cargo build --release -p punktfunk-client-linux
|
|
# bash clients/linux/tools/screenshots.sh # → clients/linux/screenshots/<scene>.png
|
|
# bash clients/linux/tools/screenshots.sh hosts pair # a subset
|
|
#
|
|
# Env knobs: BIN (client binary), OUT (output dir), GEOMETRY (Xvfb WxHxDepth),
|
|
# SETTLE (extra seconds after PF_SHOT_READY, X11-fallback only), SHOT_DISPLAY (X display),
|
|
# GSK_RENDERER (gl|ngl|cairo — cairo is the safe headless/no-GPU choice), FORCE_XVFB=1
|
|
# (ignore a live Wayland session and go through Xvfb anyway).
|
|
set -euo pipefail
|
|
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # clients/linux
|
|
BIN="${BIN:-$here/../../target/release/punktfunk-client}"
|
|
OUT="${OUT:-$here/screenshots}"
|
|
# X11 fallback only: the client window maps at its 1200x780 default; with no WM under
|
|
# Xvfb it lands at the top-left, so keep the root just larger so the full window (incl.
|
|
# its CSD shadow) is captured by a root grab with only a thin margin to crop.
|
|
GEOMETRY="${GEOMETRY:-1380x860x24}"
|
|
SETTLE="${SETTLE:-1.2}"
|
|
SHOT_DISPLAY="${SHOT_DISPLAY:-:99}"
|
|
|
|
if [ "$#" -gt 0 ]; then SCENES=("$@"); else SCENES=(hosts settings trust pair addhost shortcuts library gamepad-library); fi
|
|
|
|
[ -x "$BIN" ] || {
|
|
echo "client binary not found: $BIN (build it first: cargo build --release -p punktfunk-client-linux)" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Isolated scratch HOME: the client generates its identity here on first run, and the
|
|
# saved-hosts grid is read from client-known-hosts.json, so seed mock hosts for the
|
|
# `hosts` scene (the dialogs/settings build their own mock state in-app). `last_used`
|
|
# on the first entry renders the most-recent accent bar.
|
|
WORK="$(mktemp -d)"
|
|
export HOME="$WORK"
|
|
mkdir -p "$HOME/.config/punktfunk"
|
|
cat >"$HOME/.config/punktfunk/client-known-hosts.json" <<'JSON'
|
|
{
|
|
"hosts": [
|
|
{ "name": "Living Room PC", "addr": "192.168.1.42", "port": 9777,
|
|
"fp_hex": "9f8e7d6c5b4a39281706f5e4d3c2b1a0998877665544332211ffeeddccbbaa00",
|
|
"paired": true, "last_used": 1780000000 },
|
|
{ "name": "Office", "addr": "192.168.1.50", "port": 9777,
|
|
"fp_hex": "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00",
|
|
"paired": false }
|
|
]
|
|
}
|
|
JSON
|
|
|
|
XVFB_PID=""
|
|
cleanup() {
|
|
if [ -n "$XVFB_PID" ]; then kill "$XVFB_PID" 2>/dev/null || true; fi
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
if [ -n "${WAYLAND_DISPLAY:-}" ] && [ -z "${FORCE_XVFB:-}" ]; then
|
|
# Live Wayland session: self-capture only (there is no root grab on Wayland). The
|
|
# window flashes up briefly per scene — this is a dev harness, not CI polish.
|
|
MODE=wayland
|
|
export GDK_BACKEND=wayland
|
|
else
|
|
# Software-rendered X session — no GPU/Wayland needed. GL/llvmpipe runs the real NGL
|
|
# renderer (cairo is documented-incomplete for 3D-transformed content / libadwaita
|
|
# transitions).
|
|
MODE=x11
|
|
unset WAYLAND_DISPLAY
|
|
export DISPLAY="$SHOT_DISPLAY"
|
|
export GDK_BACKEND=x11
|
|
export LIBGL_ALWAYS_SOFTWARE=1
|
|
export GALLIUM_DRIVER="${GALLIUM_DRIVER:-llvmpipe}"
|
|
export GSK_RENDERER="${GSK_RENDERER:-gl}"
|
|
|
|
Xvfb "$SHOT_DISPLAY" -screen 0 "$GEOMETRY" -nolisten tcp >"$WORK/xvfb.log" 2>&1 &
|
|
XVFB_PID=$!
|
|
# Wait for the display to accept connections.
|
|
for _ in $(seq 1 50); do
|
|
if command -v xdpyinfo >/dev/null 2>&1; then
|
|
xdpyinfo -display "$SHOT_DISPLAY" >/dev/null 2>&1 && break
|
|
else
|
|
[ -e "/tmp/.X11-unix/X${SHOT_DISPLAY#:}" ] && break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
fi
|
|
|
|
# X11 root grab — the fallback for binaries without self-capture.
|
|
capture_x11() {
|
|
local out="$1"
|
|
if command -v import >/dev/null 2>&1; then
|
|
import -silent -window root "$out"
|
|
elif command -v scrot >/dev/null 2>&1; then
|
|
scrot -o "$out"
|
|
else
|
|
echo "no screenshot tool — install imagemagick or scrot" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
mkdir -p "$OUT"
|
|
rc=0
|
|
for scene in "${SCENES[@]}"; do
|
|
: >"$WORK/log"
|
|
rm -f "$OUT/$scene.png"
|
|
PUNKTFUNK_SHOT_SCENE="$scene" PUNKTFUNK_SHOT_OUT="$OUT/$scene.png" \
|
|
"$BIN" >"$WORK/log" 2>&1 &
|
|
pid=$!
|
|
ready=0
|
|
for _ in $(seq 1 200); do # up to ~20s
|
|
if grep -q "PF_SHOT_READY" "$WORK/log"; then
|
|
ready=1
|
|
break
|
|
fi
|
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
# Self-capture binaries exit(0) right after READY — check the log once more.
|
|
grep -q "PF_SHOT_READY" "$WORK/log" && ready=1
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
if [ "$ready" = 1 ]; then
|
|
if [ -f "$OUT/$scene.png" ]; then
|
|
echo "✓ $scene → $OUT/$scene.png (self-capture)"
|
|
elif [ "$MODE" = x11 ]; then
|
|
# Old binary (no PUNKTFUNK_SHOT_OUT support) — grab the X root instead.
|
|
sleep "$SETTLE"
|
|
if capture_x11 "$OUT/$scene.png"; then
|
|
echo "✓ $scene → $OUT/$scene.png (x11 grab)"
|
|
else
|
|
rc=1
|
|
fi
|
|
else
|
|
echo "✗ $scene: no PNG (self-capture failed — see log)" >&2
|
|
sed 's/^/ /' "$WORK/log" >&2 || true
|
|
rc=1
|
|
fi
|
|
else
|
|
echo "✗ $scene: client never signalled PF_SHOT_READY" >&2
|
|
sed 's/^/ /' "$WORK/log" >&2 || true
|
|
rc=1
|
|
fi
|
|
kill "$pid" 2>/dev/null || true
|
|
wait "$pid" 2>/dev/null || true
|
|
done
|
|
|
|
exit "$rc"
|