From 178c400eeb4e9cd878d95c8117914ffde43e6fe0 Mon Sep 17 00:00:00 2001 From: Harry Date: Thu, 8 Oct 2026 19:38:35 +0200 Subject: [PATCH] fix: share reliable virtual key delivery across Linux rigs --- docs/validation.md | 31 ++++++++++++++++++++++++++++++ scripts/rig/keys.sh | 14 ++++++++++++++ scripts/validate/linux-clips.sh | 5 +++-- scripts/validate/linux-playback.sh | 19 ++++++------------ 4 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 scripts/rig/keys.sh diff --git a/docs/validation.md b/docs/validation.md index 9af3a31..665a579 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -466,3 +466,34 @@ The headless e2e restores keyboard focus after virtual-pointer commands and holds virtual keys briefly: instantaneous virtual input can disappear before GPUI/Wayland dispatch. Keyboard actions update the editor entity directly, avoiding a nested window borrow during event dispatch. + +After merging clips and playback at `dd016f7`, the clips rig intermittently +reported “desktop did not honour Quit”. A fresh-window probe on 2026-10-08 +missed 3 of 8 instantaneous `wtype` Ctrl+Q presses; two missed presses occurred +while Clipbin was focused. Temporary action/GUI-exit/worker-join markers showed +that the missed presses never reached the Quit handler. All 8 held-key attempts +reached the handler and completed shutdown. The temporary markers were removed; +no application shutdown change was needed. + +Both scripts now source `scripts/rig/keys.sh`: it restores keyboard focus after +virtual-pointer input, gives the virtual keymap 200 ms to settle, and holds each +key for 100 ms. Modified keys also give the modifier 100 ms to settle. Delivery +has a 10-second timeout; each script still requires the actual desktop process +to exit successfully. Serial reruns passed with +`env PATH=/usr/bin:/bin:$HOME/.cargo/bin`: + +- `scripts/validate/linux-clips.sh /tmp/clipbin-clips-quit-fixed`: H.264 retained, + 360 CFR frames over a six-second aligned clip, continuous audio, zero decode + errors, original press range, bookmark protection, tiny-budget cleanup, + bookmark re-export after adjacent removal, and both Quit exits. +- `scripts/validate/linux-playback.sh /tmp/clipbin-playback-quit-fixed`: moving + preview (4.1% changed pixels), device audio RMS 0.08045, thumbnails, active + capture, seek/gap/boundary behaviour, volume/mute, frame stepping and both Quit + exits. Decode median/p95 were 2.578/4.073 ms; GPUI upload median/p95 were + 0.241/0.408 ms. + +Logs are `/tmp/clipbin-clips-quit-fixed.log` and +`/tmp/clipbin-playback-quit-fixed.log`; screenshots are in their corresponding +output directories. `bash -n`, `git diff --check` and `cargo fmt --check` also +passed. This follow-up changes only the Linux rig scripts and documentation; +Windows runtime and real Hyprland validation limits above remain unchanged. diff --git a/scripts/rig/keys.sh b/scripts/rig/keys.sh new file mode 100644 index 0000000..e215919 --- /dev/null +++ b/scripts/rig/keys.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Source inside the headless rig; accepts a key and an optional modifier. +rig_key() { + local key="$1" modifier="${2:-}" + # Destroying rig-pointer's virtual seat can clear keyboard focus in Sway. + swaymsg '[app_id="dev.clipbin.Clipbin"] focus' >/dev/null + sleep .2 + # Let the virtual keymap settle, then hold the key through a UI dispatch. + local args=(-s 200) + if [[ -n "${modifier}" ]]; then args+=(-M "${modifier}" -s 100); fi + args+=(-P "${key}" -s 100 -p "${key}") + if [[ -n "${modifier}" ]]; then args+=(-m "${modifier}"); fi + timeout 10 wtype "${args[@]}" +} diff --git a/scripts/validate/linux-clips.sh b/scripts/validate/linux-clips.sh index da36432..773dd85 100755 --- a/scripts/validate/linux-clips.sh +++ b/scripts/validate/linux-clips.sh @@ -10,6 +10,7 @@ export CLIPBIN_CLIPS_REPO="${repo}" CLIPBIN_CLIPS_OUT="${out}" "${repo}/scripts/rig/headless.sh" bash <<'INNER' set -euo pipefail repo="${CLIPBIN_CLIPS_REPO}"; out="${CLIPBIN_CLIPS_OUT}" +source "${repo}/scripts/rig/keys.sh" export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" mkdir -p "${XDG_CONFIG_HOME}/clipbin" cat > "${XDG_CONFIG_HOME}/clipbin/settings.json" <<'JSON' @@ -34,12 +35,12 @@ quit_app() { read -r qx qy qw qh < <(geometry) click "$((qx+qw-30))" "$((qy+100))" sleep 0.5 - wtype -M ctrl -k q -m ctrl + rig_key q ctrl for attempt in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then wait "${app}"; return; fi if ((attempt % 4 == 0)); then swaymsg '[app_id="dev.clipbin.Clipbin"] focus' >/dev/null - wtype -M ctrl -k q -m ctrl + rig_key q ctrl fi sleep 1 done diff --git a/scripts/validate/linux-playback.sh b/scripts/validate/linux-playback.sh index da176a3..e7b07ec 100755 --- a/scripts/validate/linux-playback.sh +++ b/scripts/validate/linux-playback.sh @@ -11,6 +11,7 @@ export CLIPBIN_PLAYBACK_REPO="${repo}" CLIPBIN_PLAYBACK_OUT="${out}" "${repo}/scripts/rig/headless.sh" bash <<'INNER' set -euo pipefail repo="${CLIPBIN_PLAYBACK_REPO}" out="${CLIPBIN_PLAYBACK_OUT}" +source "${repo}/scripts/rig/keys.sh" export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" CLIPBIN_TRACE_FRAMES=1 mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}" # ALSA's PipeWire plugin is cpal's fallback on Linux; there is no hardware card. @@ -21,12 +22,6 @@ ctl.!default { type pipewire } ALSA export ALSA_CONFIG_PATH="${out}/alsa.conf" swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null -send_key() { - # Destroying rig-pointer's virtual seat can clear keyboard focus in Sway. - swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null - sleep .2 - timeout 10 wtype -s 200 -P "$1" -s 100 -p "$1" -} # Timeline bounds come from GPUI's trace rather than guessed screen positions. pointer_time() { python3 - "$1" "$2" "$3" "${XDG_DATA_HOME}" <<'PYPOINTER' @@ -62,7 +57,7 @@ grim "${out}/editor-open.png" pointer_time "${out}/active.log" 1 click | "${repo}/target/debug/rig-pointer" 1920 1080 # GPUI/Wayland needs the virtual keyboard keymap to settle and a held key; # instantaneous wtype press/release occasionally disappears before dispatch. -send_key space +rig_key space sleep 1 grim "${out}/editor-playing-1.png" sleep 2 @@ -76,8 +71,7 @@ print('PASS playback left recording active and generated real thumbnails') PY kill "${game}"; wait "${game}" 2>/dev/null || true sleep 3 -swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null -timeout 10 wtype -s 150 -M ctrl -s 100 -P q -s 100 -p q -m ctrl +rig_key q ctrl for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi; sleep 1; done if kill -0 "${app}" 2>/dev/null; then echo 'FAIL Ctrl+Q did not exit' >&2; exit 1; fi wait "${app}" @@ -110,12 +104,11 @@ grim "${out}/editor-gap.png" pointer_time "${out}/playback.log" 1 $'scroll -1\nsleep 0.5\nscroll 1' | "${repo}/target/debug/rig-pointer" 1920 1080 pointer_time "${out}/playback.log" 2.5 click | "${repo}/target/debug/rig-pointer" 1920 1080 sleep .3 -send_key Right +rig_key Right sleep .3 -send_key Left +rig_key Left sleep .3 -swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null -timeout 10 wtype -s 150 -M ctrl -s 100 -P q -s 100 -p q -m ctrl +rig_key q ctrl for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi; sleep 1; done if kill -0 "${app}" 2>/dev/null; then echo 'FAIL Ctrl+Q did not exit' >&2; exit 1; fi wait "${app}"