Hold synthesized iPad presses long enough for the host to see them - #39
Merged
Merged
Conversation
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
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mouseDown/mouseUp, which carry real human dwell. iPad is the only platform that synthesizes a press, and it synthesized one of zero duration.SynthesizedTapSequencerholds each press for 50 ms before releasing. It queues rather than merges, becauseinsertTextsynthesizes 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 firstdownstays synchronous, so press latency is unchanged.MARKETING_VERSIONto 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
pressesCancelledflushes a pending release so an interrupted sequence can't leave a key held.Test plan
test_virtualKeyIsHeldRatherThanReleasedInTheSameInstant, which drives the realKeyboardCaptureUIViewand takes 0.051 s — confirming the hold happens end-to-end rather than only in the unit under testswift test --package-path KVMCore— 166/166 passinsertTextIf clicks come back but keys stay flaky (or vice versa), raise
SynthesizedTapSequencer.defaultPressDuration— both paths share it.🤖 Generated with Claude Code