Skip to content

iPad: Shift+Return and Shift+Tab reach the Mac with the shift - #110

Merged
Saffsanity merged 5 commits into
Saffsanity:mainfrom
grub-basket:fix/ipad-shift-return
Oct 19, 2026
Merged

Saffsanity merged 5 commits into
Saffsanity:mainfrom
grub-basket:fix/ipad-shift-return

Conversation

@grub-basket

Copy link
Copy Markdown
Contributor

What happens

With a hardware keyboard on the iPad, Shift+Return in a text box on the Mac acts as a plain Return. In Claude's desktop app that sends the message instead of starting a new line. Shift+Tab moves focus forward instead of back. The key row's latched shift followed by the software keyboard's return or tab does the same.

Why

ForwardedKeys.goesAsKey (iOSClient/KeyChords.swift) sends a key to the Mac as a key only if it's one of keyUsages, or if ⌘, ⌃ or ⌥ is held. Everything else, shift included, goes through the text system and arrives as insertText. For Return and Tab, the text system hands over "\n" and "\t" without the shift. The host then types those as a plain Return (virtual key 36) and Tab with the modifier flags cleared.

The change

  • ForwardedKeys.shiftedKeyUsages (return 0x28, tab 0x2B): with shift held, these go as keys, carrying shift, like a hardware keyboard on the Mac. Alone, or with caps lock only, they still go through the text system, so an IME's Return still confirms its composition.
  • InputOverlay.insertText: the key row's latched shift followed by the software keyboard's return or tab now sends Shift+Return or Shift+Tab as a key chord (sendLatched), not as text.
  • The host needs no change: KeyStrokes already maps 0x28 to Return and posts a key's down with the device's modifiers.

Tests

  • Tests/checks/key-strokes:
    • The check that said shifted return and tab go to the text system now says they go as keys, and not without shift.
    • New checks: ⇧Return's down carries shift, and its up follows.
    • 333 checks, all pass. 2 new mutants, 74 of 74 caught. README row updated.
  • On a loopback synthetic host with SILL_TEST_INPUT_LOG=1:
    • The kind 8 this now sends (key 0x28, modifiers shift) posts key 36 down (shift).
    • The old path (text "\n") posts key 36 down (none).
  • The iOS app compiles with this change. The Mac it was built on has only Xcode 27.0, so the build was done in a scratch copy with DuoPostureReader's iOS 27.1 calls stubbed out.
  • Not tested on a device.

🤖 Generated with Claude Code

grub-basket and others added 2 commits October 14, 2026 22:16
Reported on a device: Shift+Return on the iPad's keyboard sent a
message in Claude's desktop app on the Mac instead of starting a new
line.

The overlay splits hardware keys between two paths
(ForwardedKeys.goesAsKey): keys the text system never delivers, and
any key with command, control or option held, go to the Mac as keys;
everything else, shift included, goes through the text system and
arrives as insertText. For Return and Tab the text system hands over
"\n" and "\t" with the shift gone, and the host types those as a plain
Return (virtual key 36) and Tab with the modifier flags cleared. So
Shift+Return was a plain Return on the Mac, and Shift+Tab moved focus
forward.

- ForwardedKeys.shiftedKeyUsages (return 0x28, tab 0x2B): with shift
  held they go as keys, carrying shift, like a hardware keyboard on the
  Mac. Alone, or with caps lock only, they still go through the text
  system, so an IME's Return still confirms its composition.
- InputOverlay.insertText: the key row's latched shift followed by the
  software keyboard's return or tab sends Shift+Return or Shift+Tab as
  a key chord (sendLatched), not "\n" or "\t" as text.
- The host needs no change: KeyStrokes already maps 0x28 to Return and
  posts a key's down with the device's modifiers.

Tests/checks/key-strokes: the check that asserted shifted return and
tab go to the text system now asserts they go as keys (and not without
shift); new checks for the down carrying shift and its up following
(333 checks); 2 new mutants (74 of 74 caught); README row updated.

Tested: the check and its mutants; on a loopback synthetic host with
SILL_TEST_INPUT_LOG, the kind 8 this sends now (key 0x28, modifiers
shift) posts "key 36 down (shift)", while the old path (text "\n")
posts "key 36 down (none)". The iOS app type-checks in a scratch copy
with DuoPostureReader's iOS 27.1 calls stubbed out: this Mac has only
the iOS 27.0 SDK, and the project needs 27.1 for the iPhone Duo.
Not tested on a device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	Tests/checks/README.md
#	Tests/checks/key-strokes/mutants.py
@grub-basket

Copy link
Copy Markdown
Contributor Author

Merged main in (2b4b279), with no rebase. #80 didn't touch this path: goesAsKey still sent shifted Return and Tab to the text system, so the fix still applies. The only conflicts were Tests/checks/key-strokes/mutants.py (both blocks kept) and the README row, now 351 checks and 83 of 83 mutants. run-all.sh passes, and the iOS app builds.

@Saffsanity

Copy link
Copy Markdown
Owner

From a pass over all the open pull requests (nothing is decided yet): #147 (Chinese input and dictation through UITextInput) rewrites the end of InputOverlayView.insertText, where this PR adds the latched-shift Return and Tab branch, so whichever lands second keeps both, with that branch behind !composition.isComposing so that ⇧Return stays with an input method while it composes. The key-strokes row of Tests/checks/README.md also changes in #66 (and #72 and #73, built on it), so the second to land just updates the counts.

Saffsanity and others added 2 commits October 20, 2026 00:41
…y#142)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ow the change

The change sends Return and Tab with shift as keys (ForwardedKeys.shiftedKeyUsages), from a
hardware keyboard and from the key row's latched shift, but InputOverlayView's comments still said
a shift latch always sends plain text and that the text path owns return and tab. They now say what
the code does: a shift latch sends plain text as ordinary typing, but return and tab, whose "\n"
and "\t" carry no shift, go out as ⇧Return and ⇧Tab keys, as a hardware keyboard's do; the raw path
owns return and tab with shift; and the latched-shift branch takes any return or tab the text
system delivers (the software keyboard's, or a hardware one's without shift). KeyChords.swift's
header names the latched shift's return and tab. Comments only in the app; no code changes.

The key-strokes check now presses ⇧Return and ⇧Tab through its hardware-keyboard model, as
pressesBegan has them (shift's own key to the text system, then the key): the down carries shift,
the up nothing, and neither this Mac nor one from before is left a modifier. Return and Tab join
the random device sessions' hardware keys, so ⇧Return and ⇧Tab go through pressesBegan there too
(before, those sessions had ⇧Return only as a latched shift's, and no ⇧Tab). 353 checks, and 83 of
83 mutants caught. Tests/checks/README.md's row and docs/DEVELOPMENT.md's keyboard section
describe it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Saffsanity

Copy link
Copy Markdown
Owner

Thanks for the fix. On main, ⇧Return and ⇧Tab from a hardware keyboard, and the on-screen keyboard's return and tab with the key row's shift latched, reach the Mac without the shift; this branch fixes both.

Two commits were added on top of the branch:

  • 3696fee merges main (fix(checks): quote shell variables in build.sh to handle paths with spaces #153 and fix: preserve saved remote destinations #142 had landed since; no conflicts).
  • 921d698 brings the comments, the check and the docs in line with the change (no app code changes):
    • InputOverlay.swift: the comments on the latched-modifier rule, the latched-shift branch and the two keyboard paths still said a shift latch always sends plain text and that the text path owns return and tab. They now describe the new rule: return and tab with shift go out as ⇧Return and ⇧Tab keys, and the latched-shift branch takes any return or tab the text system delivers.
    • KeyChords.swift: the header mentions a latched shift's return and tab.
    • Tests/checks/key-strokes: ⇧Return and ⇧Tab are pressed through the check's hardware-keyboard model, as pressesBegan sees them. The check confirms the down carries shift, the up carries nothing, and neither the current Mac model nor the pre-2026-09-27 one is left holding a modifier. Return and Tab also joined the random device sessions' hardware keys. 353 checks pass and all 83 mutants are caught, including the two this PR added.
    • Tests/checks/README.md (the key-strokes row) and docs/DEVELOPMENT.md (the hardware keyboard section) describe the change.

Verified locally on 921d698: the key-strokes check and its mutants, swift build -c release, and the iOS simulator build as CI runs it, each with only the existing warnings. It merges once CI passes on this head.

The checks README keeps main's installed-apps row and this branch's key-strokes row (353 checks, 83 mutants).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Saffsanity
Saffsanity merged commit 68e880e into Saffsanity:main Oct 19, 2026
2 checks passed
Saffsanity added a commit that referenced this pull request Oct 19, 2026
…pressure-rebased

No conflict: #110 changes InputOverlay.swift's text and key paths (a latched
or held Shift with Return or Tab reaches the Mac as ⇧Return and ⇧Tab keys)
and KeyChords.swift; this branch changes the Pencil's touch and hover paths
and the finger handlers' palm guards in the same file, apart from those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 19, 2026
CLAUDE.md's entry names main's 68e880e as the last merged; the plan's
"Rebased onto main" says #110 merged without a conflict, that the three iOS
builds and the release build ran again after it with the same result, and
that key-strokes, whose cases #110 extended, passes its 353.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 19, 2026
…l-hold

No conflict: #110 changes the hardware keys (InputOverlay, KeyChords, the key-strokes check);
this branch the thumbnails and the Settings panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HunterAhlquist added a commit to HunterAhlquist/sill that referenced this pull request Oct 19, 2026
…to pencil-pressure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to scripsweave/sill that referenced this pull request Oct 19, 2026
Main gained Saffsanity#110 (Shift-Return and Shift-Tab), Saffsanity#136 (apps in folders,
the installed-apps check), Saffsanity#87 (the virtual display's BOOL encoding, the
virtual-display-api check) and Saffsanity#88 (a universal Sill.app on request).
Merged so this branch builds and checks against today's main. Only
ci.yml's one-line mutants matrix conflicted: both sides kept, sorted
(46 checks: main's installed-apps and virtual-display-api beside this
branch's pairing-sharing; ledger has no mutants).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to Code-Tomato/sill that referenced this pull request Oct 20, 2026
…ffsanity#88 beside Send Audio

Main gained Saffsanity#110 (Shift+Return and Shift+Tab from an iPad's keyboard reach
the Mac with the shift: InputOverlay, KeyChords and the key-strokes check),
Saffsanity#87 (VirtualDisplay's BOOL return read as "B" or "c", with its new
virtual-display-api check) and Saffsanity#88 (Sill.app universal with
SILL_UNIVERSAL=1, release.sh checking every slice). Two conflicts, both
lists, both sides kept:
- ci.yml's mutants matrix: the sorted union, 49 checks, which are exactly
  the folders with a mutants.py (the sound's audio-codec,
  audio-packetizer, audio-playout and ledger, and main's
  virtual-display-api).
- Tests/checks/README.md: the opening list names both the sound's checks
  and the virtual display's encodings; the history keeps main's
  virtual-display-api sentence, then the audio branch's.
CLAUDE.md and docs/DEVELOPMENT.md merged by themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 20, 2026
 Intel) into display-stepping

ci.yml's mutants matrix gained virtual-display-api on main and
display-stepping here: both kept, sorted. CLAUDE.md, docs/DEVELOPMENT.md and
Tests/checks/README.md merged by themselves (main's Layout, Build and run and
check rows beside this branch's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to acalatrava/sill that referenced this pull request Oct 20, 2026
…, Saffsanity#87, Saffsanity#88)

Main gained Shift+Return and Shift+Tab from the iPad (Saffsanity#110), the virtual display's BOOL
encodings with their check virtual-display-api (Saffsanity#87), and a universal Sill.app on request (Saffsanity#88).
Three files conflicted, each resolved keeping both sides: ci.yml's mutants matrix as the sorted
union (display-wake and virtual-display-api, every check folder but ledger), and the lists of
what the pure checks decide in Tests/checks/README.md (its intro and provenance note) and
docs/DEVELOPMENT.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 20, 2026
Main gained Saffsanity#110 (Shift+Return and Shift+Tab from a hardware keyboard: InputOverlay, KeyChords and
the key-strokes check), Saffsanity#87 (the virtual display's BOOL encodings, its virtual-display-api check)
and Saffsanity#88 (a universal Sill.app with SILL_UNIVERSAL=1). Only ci.yml's mutants matrix conflicted:
both new checks kept, keyframe-recovery and virtual-display-api, in sorted order. None of it
touches the display path, the host's window commands, kind 44 or the keyframe check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 26, 2026
Main gained Saffsanity#110 (Shift+Return and Shift+Tab), Saffsanity#136 (apps in folders), Saffsanity#87 (the
virtual display's BOOL encoding, the virtual-display-api check) and Saffsanity#88 (a universal
Sill.app on request). Two conflicts, each keeping both sides: ci.yml's mutants matrix
(connection-list and virtual-display-api, sorted) and the checks README's sentence on
where each check came from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 26, 2026
Saffsanity#110 lands first (polished at 921d698). Both change the end of
InputOverlayView.insertText: this branch sends what the text system hands over
through the composition (typed(composition.insert(text))), and Saffsanity#110 adds a
branch that sends the key row's latched shift with a return or tab as a
⇧Return or ⇧Tab key. Both are kept, Saffsanity#110's branch behind
!composition.isComposing, as Noah asked on both pull requests: while an input
method composes, what it hands over replaces its composition and its Return
is its own. Everything else merged by itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 26, 2026
Main gained Saffsanity#136 (the Apps list's installed-apps check). Two textual
conflicts, both lists where each side added an entry: CI's mutants matrix
keeps installed-apps and text-composition, and the checks' README keeps
installed-apps' new row beside key-strokes' row as Saffsanity#110 left it (353 checks,
83 mutants), which this branch carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 26, 2026
Saffsanity#110 merged main after Saffsanity#136 (2814629), the same main this branch merged
in fc02292; the tree is unchanged. Merged so that main's merge of Saffsanity#110 at
that head, and this branch's next merge of main, have nothing left to do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 26, 2026
…affsanity#88)

Main gained Saffsanity#110 (at 2814629, already merged here, so nothing of it is new),
Saffsanity#87 (the virtual display's BOOL encoding, with the virtual-display-api check)
and Saffsanity#88 (a universal Mac app for Intel testers). Two textual conflicts, lists
where each side added an entry: CI's mutants matrix keeps text-composition and
virtual-display-api, and CLAUDE.md's list of checks both. The checks' README
and DEVELOPMENT.md merged by themselves; the sentence listing the pure checks
is wrapped again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
Main gained Saffsanity#110 (Shift+Return and Shift+Tab reach the Mac from an iPad:
InputOverlay, KeyChords and the key-strokes check) and Saffsanity#87 (VirtualDisplay's
BOOL return spelled "B" or "c", with the virtual-display-api check). Neither
touches a file this branch changes but CLAUDE.md's Layout, docs/DEVELOPMENT.md
and Tests/checks/README.md, whose lines merged by themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit to andrewgribben/sill that referenced this pull request Oct 29, 2026
Main has moved 253 commits since this branch's base (4d990e9): among them Reverse Scrolling's
ScrollDirection in InputOverlay's scroll path (Saffsanity#159) and the text input work (Saffsanity#147, Saffsanity#110).
Git merged the four files this branch changes by itself; both sides kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Saffsanity

Copy link
Copy Markdown
Owner

Thank you @grub-basket, your fix is in Sill for iPhone and iPad 0.5.5 (5) on TestFlight: Shift+Return and Shift+Tab reach the Mac with the shift, from a hardware keyboard and from the key row's latched shift with the on-screen keyboard. It works with the Mac app people have now (0.5.1). Please tell me here if anything about it behaves differently in the build!

The newest Sill for Mac is the nightly build on GitHub: https://github.com/Saffsanity/sill/releases/tag/nightly. This change works with Sill for Mac 0.5.1 as well.

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.

2 participants