m7-primary.sh raw

#!/usr/bin/env bash
# M7 primary-selection test under quibble:
#   1. drag-selecting in a field populates the compositor's primary
#      selection (read externally via wl-paste --primary),
#   2. middle-click pastes the primary selection app-to-app,
#   3. an externally set primary selection (wl-copy --primary) pastes into
#      the app by middle-click,
#   4. double-click selects a word into the primary selection,
#   5. idle renders only the caret-blink cadence.
set -euo pipefail
cd "$(dirname "$0")/.."

cargo build -p guiduck --example m6_form

workdir=$(mktemp -d)
sock="$workdir/quibble.sock"
cleanup() {
    quibblectl --socket "$sock" quit >/dev/null 2>&1 || true
    rm -rf "$workdir"
}
trap cleanup EXIT

# Layout at 480x360 (same as m6-textinput.sh): first field y~52 h~32,
# second field y~96, button y~140.
FIRST_Y=68; SECOND_Y=112
BUTTON_X=76; BUTTON_Y=158
BTN_LEFT=272; BTN_MIDDLE=274
# evdev keycodes
KEY_LEFTCTRL=29; KEY_A=30; KEY_BACKSPACE=14

GUIDUCK_BACKEND=software GUIDUCK_TRACE_FRAMES=1 \
    quibble --socket "$sock" --width 480 --height 360 \
    target/debug/examples/m6_form \
    >"$workdir/out.log" 2>"$workdir/err.log" &
for _ in $(seq 1 50); do
    quibblectl --socket "$sock" ping >/dev/null 2>&1 && break
    sleep 0.2
done
quibblectl --socket "$sock" wait-window --timeout-ms 30000 >/dev/null
sleep 1
# quibble announces its Wayland socket on stdout; wl-copy/wl-paste use it.
display=$(grep -m1 '^WAYLAND_DISPLAY=' "$workdir/out.log" | cut -d= -f2)
[ -n "$display" ] || { echo "FAIL: no WAYLAND_DISPLAY from quibble" >&2; exit 1; }

frames() { grep -c '^guiduck-frame' "$workdir/err.log" || true; }
qc() { quibblectl --socket "$sock" "$@" >/dev/null; }
combo() { # ctrl+<key>
    qc key $KEY_LEFTCTRL press
    qc key "$1" press
    qc key "$1" release
    qc key $KEY_LEFTCTRL release
}
click_at() { # <x> <y> [button]
    qc pointer-move "$1" "$2"
    qc click "${3:-$BTN_LEFT}"
    sleep 0.3
}
print_values() {
    click_at $BUTTON_X $BUTTON_Y
    sleep 0.3
    grep '^first=' "$workdir/out.log" | tail -1
}
fail=0

# Setup: clear the prefilled second field, then put text in the first.
# (Select-all in a non-empty field itself syncs the primary selection;
# doing this first keeps the later assertions unambiguous.)
click_at 130 $SECOND_Y
combo $KEY_A
qc key $KEY_BACKSPACE press
qc key $KEY_BACKSPACE release
click_at 130 $FIRST_Y
qc type "primary text"
sleep 0.4

# 1: drag-select the first field; the compositor's primary selection sees it.
qc pointer-move 20 $FIRST_Y
qc pointer-button press $BTN_LEFT
qc pointer-move 250 $FIRST_Y
qc pointer-button release $BTN_LEFT
sleep 0.4
p=$(WAYLAND_DISPLAY="$display" wl-paste --primary --no-newline 2>/dev/null || echo "<none>")
if [ "$p" = "primary text" ]; then
    echo "drag -> primary: compositor sees '$p'"
else
    echo "FAIL: drag -> primary; compositor sees '$p'" >&2; fail=1
fi

# 2: middle-click the (empty) second field pastes the drag's selection.
click_at 130 $SECOND_Y $BTN_MIDDLE
v=$(print_values)
if [ "$v" = "first=primary text second=primary text" ]; then
    echo "middle-click paste (app-to-app): ok"
else
    echo "FAIL: middle-click paste; got '$v'" >&2; fail=1
fi

# 3: an externally set primary selection pastes in.
click_at 130 $SECOND_Y
combo $KEY_A
qc key $KEY_BACKSPACE press
qc key $KEY_BACKSPACE release
WAYLAND_DISPLAY="$display" wl-copy --primary "from-outside"
sleep 0.4
click_at 130 $SECOND_Y $BTN_MIDDLE
v=$(print_values)
if [ "$v" = "first=primary text second=from-outside" ]; then
    echo "middle-click paste (external): ok"
else
    echo "FAIL: external primary paste; got '$v'" >&2; fail=1
fi

# 4: double-click selects the word under the pointer into the primary
#    selection ("primary text": the first word ends well before x=80).
qc pointer-move 40 $FIRST_Y
qc click $BTN_LEFT
qc click $BTN_LEFT
sleep 0.4
p=$(WAYLAND_DISPLAY="$display" wl-paste --primary --no-newline 2>/dev/null || echo "<none>")
if [ "$p" = "primary" ]; then
    echo "double-click word: primary sees '$p'"
else
    echo "FAIL: double-click word; primary sees '$p'" >&2; fail=1
fi

# 5: idle with a focused text input = the caret blink cadence, and
# nothing more. ~5-6 toggles in 3s at the 530ms interval; a count outside
# [3, 9] means the blink stalled or something renders beyond it.
q0=$(frames); sleep 3; q1=$(frames)
d=$((q1 - q0))
echo "idle frames: $q0 -> $q1 (blink cadence: $d)"
{ [ "$d" -ge 3 ] && [ "$d" -le 9 ]; } || { echo "FAIL: expected caret-blink cadence while idle, got $d frames" >&2; fail=1; }

[ "$fail" -eq 0 ] && echo "M7 primary-selection test PASSED"
exit $fail