Skip to content

Hold synthesized iPad presses long enough for the host to see them - #39

Merged
thirteen37 merged 3 commits into
mainfrom
fix/ipad-synthesized-press-dwell
Aug 21, 2026
Merged

thirteen37 merged 3 commits into
mainfrom
fix/ipad-synthesized-press-dwell

Conversation

@thirteen37

Copy link
Copy Markdown
Owner

Summary

  • Tapping to click on iPad moved the pointer but never delivered a click; modifier-bar and on-screen keys had the same defect. Both paths emitted their down and up reports in the same instant, and HID reports carry state rather than events — the KVM holds the most recent report and the attached host only samples it when it polls, every few milliseconds. A press and release landing inside one polling window collapse into the released state, so the position change survives and the press never happened.
  • macOS is unaffected: it emits from mouseDown/mouseUp, which carry real human dwell. iPad is the only platform that synthesizes a press, and it synthesized one of zero duration.
  • SynthesizedTapSequencer holds each press for 50 ms before releasing. It queues rather than merges, because insertText synthesizes a whole string's keystrokes in one pass and each needs its own hold — a merge-on-next-tap design would have quietly reintroduced the bug for every character but the last. The first down stays synchronous, so press latency is unchanged.
  • Bumps MARKETING_VERSION to 1.0.4, since 1.0.3 is already tagged and released and the release tag should match.

Two related corrections came with it: the key release now re-reads the builder instead of capturing it at press time, so a hardware modifier pressed during the hold survives; and pressesCancelled flushes a pending release so an interrupted sequence can't leave a key held.

Test plan

  • iPad unit tests pass (18/18), including test_virtualKeyIsHeldRatherThanReleasedInTheSameInstant, which drives the real KeyboardCaptureUIView and takes 0.051 s — confirming the hold happens end-to-end rather than only in the unit under test
  • swift test --package-path KVMCore — 166/166 pass
  • macOS test suite passes (unchanged code path)
  • Needs hardware confirmation: tap-to-click delivers a click on a real KVM host
  • Needs hardware confirmation: modifier-bar keys and on-screen keyboard characters register, including a multi-character string via insertText
  • Verify a one-finger drag still works and isn't disturbed by a tap immediately before it

If clicks come back but keys stay flaky (or vice versa), raise SynthesizedTapSequencer.defaultPressDuration — both paths share it.

🤖 Generated with Claude Code

thirteen37 and others added 3 commits August 21, 2026 20:58
Tapping to click moved the pointer but never clicked, and modifier-bar keys
behaved the same way. Both paths emitted their down and up reports in the same
instant. HID reports carry state rather than events: the KVM holds the most
recent report and the attached host only samples it when it polls, every few
milliseconds, so a press and release landing inside one polling window collapse
into the released state. The position survives; the press never happened.

macOS is unaffected because it emits from mouseDown/mouseUp, which carry real
human dwell. iPad is the only platform that synthesizes a press, and it
synthesized one of zero duration.

SynthesizedTapSequencer holds each press for 50ms before releasing it. It
queues rather than merges, because insertText synthesizes a whole string's
keystrokes in one pass and every one of them needs its own hold. The first
down stays synchronous, so press latency is unchanged.

The key release now re-reads the builder instead of capturing it at press
time, so a hardware modifier pressed during the hold survives, and
pressesCancelled flushes a pending release so an interrupted sequence can't
leave a key held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XU7Ck7AbZ9KpRhdPbRzNj8
1.0.3 is already tagged and released, so shipping this fix needs a new
version for the release tag to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XU7Ck7AbZ9KpRhdPbRzNj8
Deferring the release by 50ms created a window that the previous same-instant
emission made structurally impossible. Six issues lived in it:

- A release emitted after capture was switched off was dropped by the capture
  guards in ViewerViewModel, stranding a held button or key on the host
  forever. Releases now travel on their own channel, sendInputRelease, which
  bypasses the guard by design. This also covers the pre-existing case of
  toggling capture off mid-drag.
- KeyboardCaptureUIView had no flush hook at all, so disabling keyboard
  capture during a hold left the key asserted and auto-repeating. It now
  mirrors the pointer view's didSet.
- The synthesized key release emitted an empty report, releasing any hardware
  key held during the hold and causing a spurious repeat when the next key
  arrived. It now emits the builder's current state, and pressesBegan flushes
  so real and synthesized presses can't interleave.
- A hover during the hold emitted a move that kept the button bit set, turning
  a trackpad click into a drag. Hover is suppressed while a press is held.
- flushPendingRelease discarded the whole queue, silently losing the rest of a
  multi-character insertText. Releasing the held press and dropping the queue
  are now separate operations, and only the drag-takeover and capture-off
  paths drop.
- startNextTap armed the release after running down(), so a re-entrant flush
  could leave a press armed behind its back. State is now armed first and the
  entry guarded on holdTask.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XU7Ck7AbZ9KpRhdPbRzNj8
@thirteen37
thirteen37 merged commit 0cab21b into main Aug 21, 2026
3 checks passed
@thirteen37
thirteen37 deleted the fix/ipad-synthesized-press-dwell branch August 21, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant