Two feature bodies accumulated in the working tree:
1. Pinch to change the app font size, continuously (no snapping):
- internal/ui/pinch_tracker.go: logic-free touch state machine.
Two-mover formation (the resting palm can land first or last;
movement is the only signal valid for both), pair = the mover
pair whose distance changed most, baseline = press distance
(formDist), lazy pending releases, survivor-scroll forwarding
after a pair break. Robust to ~1 fps frames: a whole pinch can
land in one drain (formDist/brokeFactor/lazy releases).
- render.go: pinch probe (raw pointer events) + grab lifecycle so
the pair is exclusive (scroll sees nothing of the pair) and the
survivor's finger keeps working as a scroll after the pinch.
- state.go/logic.go/session.go/frame.go: app-local float font
scale, content-point pin (buffer byte + offset from baseline,
not a layout point, so rewrap keeps the same character under
the center), restore/font pins, session persistence.
- pinch_test.go, pinch_font_test.go, tag_identity_test.go,
real_draw_probe_test.go: unit + real-Renderer/real-Router tests.
2. Soft keyboard must not shift content:
- Root cause: gioui.org/app calls Router.RevealFocus on any frame
the viewport shrinks (IME open under adjustResize) and
synthesizes a pointer.Scroll nudge aimed at the focused field's
stale pre-resize bounds; gesture.Scroll consumed it -> a 32 dp
content jump.
- Fix: main.go flags the shrink frame; render.go drains that one
synthetic scroll for the gesture's tag before Update (scroll-
range clamping cannot work: the router UNIONs ranges across
frames). Finger scroll (pointer.Drag) and the flinger are
untouched. reveal_focus_drain_test.go reproduces RevealFocus at
the router level and verifies the drain + zero delta.
Also: tools/touchinject (platform-signed emulator multi-touch
injection harness + e2e script, adb has no two-finger input),
docs (spec 2.2 + development_plan 18-20), .gitignore, gofmt.
277 lines
9.5 KiB
Bash
Executable File
277 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Emulator + on-device debug helpers for Pad (machine-local workstation).
|
|
#
|
|
# Usage:
|
|
# scripts/emu.sh up # start headless AVD, wait for boot
|
|
# scripts/emu.sh down # stop the emulator (clean; saves snapshot)
|
|
# scripts/emu.sh status # adb / boot / top-activity state
|
|
# scripts/emu.sh app start|stop|restart
|
|
# scripts/emu.sh shot [FILE] # screenshot (default /tmp/pad_shot_<ts>.png)
|
|
# scripts/emu.sh log [LINES] # last N lines of app logcat (default 20)
|
|
# scripts/emu.sh tap X Y # tap at SCREEN px (1080x2400 space).
|
|
# # Tap-what-you-see rule: read (x,y) off
|
|
# # the PNG you are viewing and multiply
|
|
# # both by 1.2 (vision displays
|
|
# # screenshots at 900x2000). Note: the
|
|
# # first tap right after launch/open is
|
|
# # sometimes swallowed — re-tap.
|
|
# scripts/emu.sh type TEXT # type into the focused field (spaces ok)
|
|
# scripts/emu.sh cmd <top|bottom|frac F|dp N|pinch F|fontsize F>
|
|
# # one-shot editor debug command
|
|
# # (pinch F = relative app font scale
|
|
# # anchored at the region center;
|
|
# # fontsize F = absolute, top-anchored)
|
|
# scripts/emu.sh perf on|off # enable/disable the in-app profiler
|
|
# scripts/emu.sh perf pull [FILE] # pull logic_frames.csv (default ./logic_frames.csv)
|
|
# scripts/emu.sh push FILE # push FILE -> /storage/emulated/0/Notes/
|
|
# scripts/emu.sh pull NAME # pull Notes/NAME -> ./
|
|
#
|
|
# Machine-local facts encoded here (see doc/README.md for context):
|
|
# - AVD pad_avd (Pixel 6 profile, API 35, x86_64); headless + swiftshader.
|
|
# Boot is fast (snapshot restore ~10 s, cold boot ~20-30 s warm).
|
|
# - App pad.pad / org.gioui.GioActivity.
|
|
# - Profiler: enable marker + CSV live under /storage/emulated/0/PadPerf/;
|
|
# the CSV is truncated on app relaunch, so pull it before restarting the
|
|
# app if you are accumulating data.
|
|
# - One-shot editor debug commands are read from /storage/emulated/0/PadPerf/cmd
|
|
# (scroll: top | bottom | "frac 0.5" | "dp 1234"; app font scale:
|
|
# "pinch 1.1" relative | "fontsize 1.5" absolute).
|
|
# - Test files go under /storage/emulated/0/Notes/.
|
|
# - The emulator OOMs above ~2.5 GB RSS on this VM; kill it if it wedges.
|
|
#
|
|
# Snapshots: the AVD auto-saves a "default_boot" instant-boot snapshot on
|
|
# clean shutdown. A corrupted snapshot makes the emulator segfault during
|
|
# restore (device never comes online). `up` detects that (process death or
|
|
# no adb device within PAD_ONLINE_TIMEOUT seconds) and falls back to a cold
|
|
# boot with -no-snapshot-load; the next clean `down` re-saves a healthy
|
|
# snapshot, so this self-heals. Always stop with `down` (clean kill saves
|
|
# the snapshot) — SIGKILLing the emulator corrupts the snapshot.
|
|
#
|
|
# Env overrides: PAD_AVD, PAD_ONLINE_TIMEOUT (default 60 s),
|
|
# PAD_BOOT_TIMEOUT (default 300 s), EMU_LOG path via ANDROID_HOME-adjacent.
|
|
set -euo pipefail
|
|
|
|
AVD=${PAD_AVD:-pad_avd}
|
|
PKG=pad.pad
|
|
ACTIVITY=org.gioui.GioActivity
|
|
NOTES=/storage/emulated/0/Notes
|
|
PERFDIR=/storage/emulated/0/PadPerf
|
|
EMU_LOG=${PAD_EMU_LOG:-/tmp/emulator.log}
|
|
ONLINE_TIMEOUT=${PAD_ONLINE_TIMEOUT:-60}
|
|
BOOT_TIMEOUT=${PAD_BOOT_TIMEOUT:-300}
|
|
|
|
export ANDROID_HOME=${ANDROID_HOME:-$HOME/android-sdk}
|
|
export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
|
|
adb_state() { adb get-state 2>/dev/null || echo offline; }
|
|
|
|
booted() {
|
|
[ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]
|
|
}
|
|
|
|
EMU_PID=""
|
|
|
|
# Find the actual qemu process for this AVD: comm is qemu-system-* and the
|
|
# command line contains " -avd $AVD". The comm check excludes the emulator's
|
|
# crashpad/netsimd children (their command lines carry the AVD name too) and
|
|
# any unrelated process that merely mentions the AVD. Prints the pid.
|
|
# Returns 0 if found, 1 otherwise.
|
|
find_emu_qemu() {
|
|
local p
|
|
for p in $(pgrep -f " -avd $AVD" 2>/dev/null); do
|
|
case "$(ps -o comm= -p "$p" 2>/dev/null)" in
|
|
qemu-system-*) echo "$p"; return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
start_emulator() {
|
|
nohup emulator -avd "$AVD" -no-window -no-audio -no-boot-anim \
|
|
-gpu swiftshader_indirect "$@" >"$EMU_LOG" 2>&1 &
|
|
EMU_PID=$!
|
|
}
|
|
|
|
# Wait until the adb device is online.
|
|
# Returns: 0 online, 1 emulator process died, 2 timeout.
|
|
wait_device_online() {
|
|
local deadline=${1:-$ONLINE_TIMEOUT} i=0
|
|
while [ "$i" -lt "$deadline" ]; do
|
|
[ "$(adb_state)" = "device" ] && return 0
|
|
# Give the launcher time to exec into qemu before declaring death.
|
|
if [ "$i" -ge 10 ] && ! find_emu_qemu >/dev/null; then
|
|
return 1
|
|
fi
|
|
sleep 3
|
|
i=$((i + 3))
|
|
done
|
|
return 2
|
|
}
|
|
|
|
# Wait for boot_completed on an already-online device.
|
|
# Returns: 0 booted, 1 not booted (device lost or timeout).
|
|
wait_boot_completed() {
|
|
local deadline=${1:-$BOOT_TIMEOUT} i=0
|
|
while [ "$i" -lt "$deadline" ]; do
|
|
booted && return 0
|
|
[ "$(adb_state)" = "device" ] || return 1
|
|
sleep 5
|
|
i=$((i + 5))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
cmd_up() {
|
|
if [ "$(adb_state)" = "device" ] && booted; then
|
|
echo "emulator already up and booted"
|
|
return 0
|
|
fi
|
|
if [ "$(adb_state)" = "device" ]; then
|
|
echo "emulator present; waiting for boot..."
|
|
wait_boot_completed "$BOOT_TIMEOUT" || die "boot failed; see $EMU_LOG"
|
|
echo "booted"
|
|
return 0
|
|
fi
|
|
echo "starting AVD $AVD (headless)..."
|
|
# Clear any orphaned qemu left by a previously force-killed run: it would
|
|
# hold the AVD lock and make the new launch fail silently.
|
|
local qp
|
|
if qp=$(find_emu_qemu); then
|
|
echo "WARN: orphaned emulator (pid $qp) still running; killing it first"
|
|
kill -9 "$qp" 2>/dev/null || true
|
|
sleep 2
|
|
fi
|
|
start_emulator "$@"
|
|
local rc=0
|
|
wait_device_online "$ONLINE_TIMEOUT" || rc=$?
|
|
if [ "$rc" -ne 0 ]; then
|
|
if [ "$rc" -eq 1 ]; then
|
|
echo "WARN: emulator process died during startup (corrupt snapshot?) — $EMU_LOG"
|
|
else
|
|
echo "WARN: device never came online within ${ONLINE_TIMEOUT}s — $EMU_LOG"
|
|
fi
|
|
echo "retrying with -no-snapshot-load (cold boot; next 'down' re-saves a good snapshot)..."
|
|
# Kill the old instance (launcher and/or qemu). SIGKILL: the snapshot
|
|
# is already broken, there is nothing to save, and a half-written
|
|
# snapshot is harmless (next clean 'down' re-saves).
|
|
qp=$(find_emu_qemu || true)
|
|
if [ -n "$qp" ]; then kill -9 "$qp" 2>/dev/null || true; fi
|
|
kill -9 "$EMU_PID" 2>/dev/null || true
|
|
# Wait until it is truly gone: the old device and AVD lock must be
|
|
# released before the replacement starts, or the new waits can latch
|
|
# onto the dying instance and report false success.
|
|
local j=0
|
|
while [ "$j" -lt 15 ]; do
|
|
find_emu_qemu >/dev/null 2>&1 || break
|
|
sleep 1
|
|
j=$((j + 1))
|
|
done
|
|
start_emulator -no-snapshot-load "$@"
|
|
# Note: full BOOT_TIMEOUT here — a cold boot legitimately takes far
|
|
# longer than the ONLINE_TIMEOUT used for the first (snapshot) attempt.
|
|
wait_device_online "$BOOT_TIMEOUT" || { tail -n 5 "$EMU_LOG" >&2; die "device never came online (cold boot); see $EMU_LOG"; }
|
|
fi
|
|
wait_boot_completed "$BOOT_TIMEOUT" || die "boot failed; see $EMU_LOG"
|
|
echo "booted"
|
|
}
|
|
|
|
cmd_down() {
|
|
adb emu kill 2>/dev/null || die "no emulator running"
|
|
echo "stopped"
|
|
}
|
|
|
|
cmd_status() {
|
|
echo "adb: $(adb_state)"
|
|
if [ "$(adb_state)" = "device" ]; then
|
|
echo "boot_completed: $(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
|
|
local top
|
|
top=$(adb shell dumpsys activity activities 2>/dev/null | grep -m1 "topResumedActivity" || true)
|
|
echo "top: ${top:-none}"
|
|
fi
|
|
}
|
|
|
|
cmd_app() {
|
|
case "${1:-}" in
|
|
start) adb shell am start -n "$PKG/$ACTIVITY" >/dev/null; echo "started" ;;
|
|
stop) adb shell am force-stop "$PKG"; echo "stopped" ;;
|
|
restart)
|
|
adb shell am force-stop "$PKG"
|
|
sleep 1
|
|
adb shell am start -n "$PKG/$ACTIVITY" >/dev/null
|
|
echo "restarted"
|
|
;;
|
|
*) die "app: start|stop|restart" ;;
|
|
esac
|
|
}
|
|
|
|
cmd_shot() {
|
|
local out="${1:-/tmp/pad_shot_$(date +%Y%m%d_%H%M%S).png}"
|
|
adb exec-out screencap -p >"$out"
|
|
echo "$out"
|
|
}
|
|
|
|
cmd_log() {
|
|
adb logcat -d -s "$PKG" | tail -n "${1:-20}"
|
|
}
|
|
|
|
cmd_tap() {
|
|
[ $# -eq 2 ] || die "tap X Y"
|
|
adb shell input tap "$1" "$2"
|
|
}
|
|
|
|
cmd_type() {
|
|
[ $# -ge 1 ] || die "type TEXT"
|
|
# input text takes one arg; encode spaces as %s.
|
|
local text=${*// /%s}
|
|
adb shell input text "$text"
|
|
}
|
|
|
|
cmd_cmd() {
|
|
# Commands are constrained (top|bottom|frac F|dp N) — no shell metachars.
|
|
[ $# -ge 1 ] || die "cmd <top|bottom|frac F|dp N>"
|
|
adb shell "mkdir -p $PERFDIR && printf '%s\n' \"$*\" > $PERFDIR/cmd"
|
|
echo "sent editor debug command: $*"
|
|
}
|
|
|
|
cmd_perf() {
|
|
case "${1:-}" in
|
|
on) adb shell "mkdir -p $PERFDIR && touch $PERFDIR/enable"; echo "profiler on" ;;
|
|
off) adb shell "rm -f $PERFDIR/enable"; echo "profiler off" ;;
|
|
pull)
|
|
local out="${2:-./logic_frames.csv}"
|
|
adb pull "$PERFDIR/logic_frames.csv" "$out"
|
|
;;
|
|
*) die "perf on|off|pull [FILE]" ;;
|
|
esac
|
|
}
|
|
|
|
cmd_push() {
|
|
[ $# -ge 1 ] || die "push FILE"
|
|
adb push "$1" "$NOTES/"
|
|
}
|
|
|
|
cmd_pull() {
|
|
[ $# -ge 1 ] || die "pull NAME"
|
|
adb pull "$NOTES/$1" "./"
|
|
}
|
|
|
|
[ $# -ge 1 ] || die "no subcommand (see header)"
|
|
main="${1}"; shift
|
|
case "$main" in
|
|
up) cmd_up "$@" ;;
|
|
down) cmd_down "$@" ;;
|
|
status) cmd_status "$@" ;;
|
|
app) cmd_app "$@" ;;
|
|
shot) cmd_shot "$@" ;;
|
|
log) cmd_log "$@" ;;
|
|
tap) cmd_tap "$@" ;;
|
|
type) cmd_type "$@" ;;
|
|
cmd) cmd_cmd "$@" ;;
|
|
perf) cmd_perf "$@" ;;
|
|
push) cmd_push "$@" ;;
|
|
pull) cmd_pull "$@" ;;
|
|
*) die "unknown subcommand: $main" ;;
|
|
esac
|