macOS: match Ctrl-modified chords on physical key, not IME character (CSAT-10277 / GH#15196) - #15197
macOS: match Ctrl-modified chords on physical key, not IME character (CSAT-10277 / GH#15196)#15197warp-agent-staging[bot] wants to merge 2 commits into
Conversation
Fixes Ctrl+J (and other Ctrl+letter chords) being dropped or failing to reach the PTY on macOS when a non-Latin input source (e.g. Korean/Hangul) is active. Root cause: `Event::KeyDown` on macOS built `keystroke.key` from `NSEvent.charactersIgnoringModifiers`, the character the active input source produces. Under Hangul, that string is empty or a Hangul jamo, so the keystroke matcher never matched `ctrl-j` and the PTY passthrough path (which relies on the OS placing a C0 byte in `chars`) got nothing either. A Ctrl-modified key press is never IME composition input, so it should resolve to the same physical key regardless of the active input source. Add `ctrl_chord_physical_letter`/`ctrl_chord_needs_physical_key_fallback`/ `ctrl_letter_to_control_char` to `warpui_core::platform::keyboard` (mirroring the existing Windows non-Latin-layout chord fallback in `windowing/winit/event_loop/key_events.rs`, GH#9036) and use them in the macOS `KeyDown` conversion to recover the physical key -- and its C0 control byte -- when the input source doesn't produce a usable ASCII character for a Ctrl chord. One conversion fix covers both the editor keybinding match and the raw-mode PTY passthrough, since both consumers read the same `Event::KeyDown` fields. Unmodified-key IME composition (e.g. typing Hangul syllables) is untouched; only Ctrl-modified chords take the physical-key fallback path. Fixes GH#15196. CSAT-10277 Co-Authored-By: Warp <agent@warp.dev>
|
@/tmp/pr/attribution.md |
… full resolution Review findings on the initial fix: 1. The PTY chars rewrite was gated only on ctrl_held plus a physical letter, not on ctrl_chord_needs_physical_key_fallback or modifier exclusivity. That meant a Ctrl+letter whose characters() wasn't already all-control got rewritten to a C0 byte even when charactersIgnoringModifiers already produced a usable ASCII key, and Ctrl+Alt / Ctrl+Cmd chords were forced through the plain-Ctrl mapping. 2. The regression tests only exercised the three pure helpers in isolation, so they would still pass if the macOS from_native wiring were deleted or wired up incorrectly. Fixes: - Extracted the full key/chars resolution (previously inlined across two separate branches in event.rs) into a single, pure resolve_ctrl_chord_key_and_chars() in warpui_core::platform::keyboard. It gates the chars rewrite on the same ctrl_chord_needs_physical_key_fallback predicate used for key, and additionally requires a plain-Ctrl chord (Alt and Cmd both unheld) before rewriting chars, aligning with the existing ctrl-only C0 semantics used elsewhere (escape_sequences.rs's keystroke_to_c0_control_code). The key fallback is intentionally left unrestricted by Alt/Cmd, since it only affects keybinding matching, which already accounts for every modifier via exact Keystroke equality. - crates/warpui/src/platform/mac/event.rs's NSEventType::KeyDown arm is now a thin FFI-extraction wrapper around that function. - Rewrote keyboard_tests.rs to test the full resolution (key AND chars outputs, across the Hangul-empty, Hangul-jamo, already-working, fallback-not-needed, Ctrl+Alt, Ctrl+Cmd, unmodified-IME, and nothing-usable cases), instead of only the three helper functions in isolation. Verified these tests actually fail against the pre-fix (under-gated) chars logic by temporarily reverting to it locally, observing 3 of the new tests fail with the expected wrong values, then restoring the fix. Co-Authored-By: Warp <agent@warp.dev>
There was a problem hiding this comment.
Overview
Derives the key and PTY control byte for Ctrl-modified chords on macOS from the physical key when the active input source produces nothing usable, fixing Ctrl+J under a Hangul input source for both the editor binding and raw-mode PTY passthrough. The logic and its regression tests hold up on inspection; what remains is verification that requires a macOS host, plus one scoping decision.
Concerns
- No visual proof of the fixed behavior on the real path. This is user-visible keyboard behavior and the reported symptom has never been observed as fixed — every check so far is a unit test of the extracted decision function. Someone with a macOS host and a Korean input source needs to confirm Ctrl+J in the command editor and in a raw-mode TUI (
claude/codex), and that unmodified Hangul composition still works. - The macOS-gated
crates/warpui/src/platform/mac/event.rswas never compiled. It cannot be built from Linux (the Metal shader bindgen needs the real macOS SDK), so the AppKit-side wiring was reviewed by inspection only. Confirm a macOS build covers this file before merging. - Non-regression for ASCII-producing non-US layouts (Dvorak, AZERTY, Cyrillic, Greek) is unverified for the same reason. The fallback predicate is written to leave them alone, but that has not been exercised against real
NSEventbehavior.
Verdict
Checks: build pass (warpui_core only; macOS target not built), tests pass (329 in warpui_core, new cases confirmed failing pre-fix), CI green, visual proof missing
Found: 0 critical, 1 important, 0 suggestions, 0 nits, 1 question
Two earlier findings — an under-gated chars rewrite that could rewrite already-working Ctrl chords and force Ctrl+Alt/Ctrl+Cmd through the plain-Ctrl mapping, and tests that only covered the pure helpers rather than the event resolution — were addressed in the follow-up commit and are not repeated here.
| ) -> CtrlChordKeyResolution { | ||
| let needs_fallback = ctrl_chord_needs_physical_key_fallback(ctrl_held, ime_first_char); | ||
|
|
||
| let key = if let Some(letter) = physical_letter.filter(|_| needs_fallback) { |
There was a problem hiding this comment.
The chars rewrite is restricted to a plain-Ctrl chord, but the key fallback is not, so Ctrl+Alt+ and Ctrl+Cmd+ under a non-Latin input source now resolve keystroke.key to the physical letter where they previously resolved to the IME character (or dropped the event). The rationale given is that Keystroke equality already accounts for every modifier, which is sound, but it does mean chords that never matched anything under Hangul can now match a binding. Confirm that is the intended scope, or apply is_plain_ctrl_chord to the key branch as well.
Description
On macOS, while a non-Latin input source (e.g. Korean "2-Set" Hangul) is active,
Ctrl+Jdid nothing: it neither inserted a newline in Warp's own input editor nor was forwarded as the0x0A(LF) control byte to raw-mode TUI apps (Claude Code, Codex) running inside Warp. Switching back to English/ABC made it work again. macOS Terminal.app forwardsCtrl+Jcorrectly under both input sources.Root cause: macOS
Event::KeyDown(crates/warpui/src/platform/mac/event.rs) builtkeystroke.keyfromNSEvent.charactersIgnoringModifiers-- the character the active input source produces -- rather than from the physical key. Under Hangul, that string is empty or a Hangul jamo, so the keystroke never matched thectrl-jbinding, and the PTY passthrough path (alt_screen_element.rs/block_list_element.rs), which relies on the OS having placed a C0 control byte in the siblingcharsfield, got nothing either. Both symptoms trace back to the same conversion.Fix: A Ctrl-modified key press is never IME composition input, so it should resolve to the same physical key regardless of the active input source -- mirroring the existing Windows non-Latin-layout chord fallback (
us_qwerty_fallback_for_chordincrates/warpui/src/windowing/winit/event_loop/key_events.rs, GH#9036). Added helpers towarpui_core::platform::keyboard:ctrl_chord_physical_letter: maps a physicalKeyCode(e.g.KeyJ) to its US-QWERTY letter, ignoring the active layout/input source.ctrl_chord_needs_physical_key_fallback: true when Ctrl is held and the input source didn't produce a usable ASCII character.ctrl_letter_to_control_char: the C0 control byte forCtrl+<letter>(e.g.j->0x0A).resolve_ctrl_chord_key_and_chars: the full, pure decision -- composed from the three helpers above -- that resolves both thekeystroke.key(editor keybinding matching) and the PTY-facingcharsfield, given the event's modifier state, what the input source produced, and what the OS already put incharacters(). Only a plain-Ctrl chord (Ctrl held, Alt and Cmd both unheld) takes thechars/C0-byte fallback, aligning with the existing ctrl-only C0 semantics elsewhere in the codebase (keystroke_to_c0_control_codeinwarp_terminal'sescape_sequences.rs); thekeyfallback isn't restricted this way, since it only affects keybinding matching (which already accounts for every modifier via exactKeystrokeequality), not raw byte passthrough.crates/warpui/src/platform/mac/event.rs'sNSEventType::KeyDownconversion is now a thin FFI-extraction wrapper that callsresolve_ctrl_chord_key_and_charsfor the actual decision, so that decision is fully unit-testable outside of a macOS host. One fix at the conversion layer covers both halves, since Warp's editor keybinding match and the PTY passthrough both read the sameEvent::KeyDown.keystroke/charsfields.Scope is intentionally narrow: only Ctrl-modified letter chords on macOS take the physical-key fallback, and the PTY control-byte rewrite only fires for a plain-Ctrl chord that the input source genuinely couldn't resolve. Unmodified-key IME composition (typing Hangul syllables) is untouched, and chords that already work (ASCII-producing layouts, or Ctrl+Alt/Ctrl+Cmd combinations) are left exactly as they were. The broader layout-independent keybinding rewrite for all chords/layouts remains tracked separately as CORE-2749 (GH#341).
Linked Issue
Testing
This is a macOS-native (
NSEvent/AppKit) code path. I do not have access to a macOS host or a Korean input source, so I could not run the reporter's manual repro or compile/run this crate for themacostarget (cross-compilingwarpuiforx86_64-apple-darwinfrom Linux fails independent of this change, since it needs the real macOS SDK headers for Metal shader bindgen).What I did verify:
resolve_ctrl_chord_key_and_charsinwarpui_core::platform::keyboard, and rewrotecrates/warpui_core/src/platform/keyboard_tests.rsto exercise it end-to-end (both thekeyandcharsoutputs together), covering:Ctrl+Junder an emptycharactersIgnoringModifiers(Hangul with nothing yet produced) resolves to key"j"and control byte0x0A.Ctrl+Junder a non-ASCII composition character (a Hangul jamo) also falls back correctly.Ctrl+Jwhen the input source already produces'j'and the OS already places0x0Aincharacters()(English/ABC) is left byte-for-byte unchanged.charsis not rewritten when the fallback isn't needed, even ifcharacters()looks unusual for some unrelated reason -- guards against silently altering a chord the OS is already handling.Ctrl+AltandCtrl+Cmdchords do not getcharsrewritten (thekeyphysical-letter fallback still applies, but the C0 byte does not), matching the plain-Ctrl-only semantics used elsewhere in the codebase.chars-rewrite gating back to the original (under-gated) form locally, reran the test suite, and confirmed exactly the 3 tests targeting that gap failed with the wrong (rewritten)charsvalues, then restored the fix and reran to confirm all tests pass again -- see the PR's commit history for this revision.cargo test -p warpui_core --lib(329 passed),cargo fmt --check, andcargo clippy -p warpui_core --lib --tests --all-features -- -D warningsall pass.crates/warpui/src/platform/mac/event.rs'sNSEventType::KeyDownconversion line-by-line against the pre-existing code paths (including lifetime/type-check reasoning for theunwrap_orpattern reused from the original code) since it cannot be compiled in this environment.Event::KeyDown.chars(app/src/terminal/alt_screen/alt_screen_element.rsandapp/src/terminal/block_list_element.rs) share the identical!chars.is_empty() && chars.chars().all(|c| c.is_control())gate, so the single conversion-layer fix reaches both the alt-screen and normal block-list PTY paths.is_composing/IMEOpengating (host_view.mmarks composing only viahasMarkedText, i.e. actual pending marked text) is orthogonal to this fix and unaffected by it.What a macOS reviewer should manually check before merging:
The reporter's repro: with "Korean - 2-Set" active,
Ctrl+Jinserts a newline in Warp's input editor and is forwarded toclaude/codexas0x0A, matching Terminal.app.Hangul syllable composition for unmodified keys is unaffected (composing/committing jamo still works normally).
Other Ctrl+letter chords (e.g.
Ctrl+G,Ctrl+D,Ctrl+W) continue to work under both English and Hangul input sources.Non-Latin, non-Hangul input sources (e.g. Cyrillic, Greek) that already worked for Ctrl chords are not regressed.
Ctrl+Alt+<letter>andCtrl+Cmd+<letter>chords (if any are bound) behave exactly as before this PR under both English and Hangul input sources -- this PR does not intend to change their PTY byte behavior.cargo build/./script/presubmiton macOS, since this crate could not be compiled in this (Linux) environment.I have manually tested my changes locally with
./script/run-- N/A, no macOS host available in this environment; see "Testing" above for what was verified instead.Screenshots / Videos
N/A -- macOS-native input-handling fix with no UI screenshot to capture in this environment. A prior reviewer flagged the absence of visual proof for this user-visible keyboard change; that requires a macOS host with a Korean input source, which is not available to me, so it's left for a human reviewer with such a host.
Agent Mode
CHANGELOG-BUG-FIX: Fixed
Ctrl+J(and other Ctrl-modified chords) not working on macOS while a non-Latin input source (e.g. Korean) is active.