Skip to content

macOS: match Ctrl-modified chords on physical key, not IME character (CSAT-10277 / GH#15196) - #15197

Draft
warp-agent-staging[bot] wants to merge 2 commits into
masterfrom
fix/csat-10277-ctrl-j-hangul-ime
Draft

macOS: match Ctrl-modified chords on physical key, not IME character (CSAT-10277 / GH#15196)#15197
warp-agent-staging[bot] wants to merge 2 commits into
masterfrom
fix/csat-10277-ctrl-j-hangul-ime

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Description

On macOS, while a non-Latin input source (e.g. Korean "2-Set" Hangul) is active, Ctrl+J did nothing: it neither inserted a newline in Warp's own input editor nor was forwarded as the 0x0A (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 forwards Ctrl+J correctly under both input sources.

Root cause: macOS Event::KeyDown (crates/warpui/src/platform/mac/event.rs) built keystroke.key from NSEvent.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 the ctrl-j binding, 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 sibling chars field, 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_chord in crates/warpui/src/windowing/winit/event_loop/key_events.rs, GH#9036). Added helpers to warpui_core::platform::keyboard:

  • ctrl_chord_physical_letter: maps a physical KeyCode (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 for Ctrl+<letter> (e.g. j -> 0x0A).
  • resolve_ctrl_chord_key_and_chars: the full, pure decision -- composed from the three helpers above -- that resolves both the keystroke.key (editor keybinding matching) and the PTY-facing chars field, given the event's modifier state, what the input source produced, and what the OS already put in characters(). Only a plain-Ctrl chord (Ctrl held, Alt and Cmd both unheld) takes the chars/C0-byte fallback, aligning with the existing ctrl-only C0 semantics elsewhere in the codebase (keystroke_to_c0_control_code in warp_terminal's escape_sequences.rs); the key fallback isn't restricted this way, since it only affects keybinding matching (which already accounts for every modifier via exact Keystroke equality), not raw byte passthrough.

crates/warpui/src/platform/mac/event.rs's NSEventType::KeyDown conversion is now a thin FFI-extraction wrapper that calls resolve_ctrl_chord_key_and_chars for 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 same Event::KeyDown.keystroke/chars fields.

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 the macos target (cross-compiling warpui for x86_64-apple-darwin from Linux fails independent of this change, since it needs the real macOS SDK headers for Metal shader bindgen).

What I did verify:

  • Extracted the entire key/chars resolution decision (not just the individual helper functions) into resolve_ctrl_chord_key_and_chars in warpui_core::platform::keyboard, and rewrote crates/warpui_core/src/platform/keyboard_tests.rs to exercise it end-to-end (both the key and chars outputs together), covering:
    • Ctrl+J under an empty charactersIgnoringModifiers (Hangul with nothing yet produced) resolves to key "j" and control byte 0x0A.
    • Ctrl+J under a non-ASCII composition character (a Hangul jamo) also falls back correctly.
    • Ctrl+J when the input source already produces 'j' and the OS already places 0x0A in characters() (English/ABC) is left byte-for-byte unchanged.
    • chars is not rewritten when the fallback isn't needed, even if characters() looks unusual for some unrelated reason -- guards against silently altering a chord the OS is already handling.
    • Ctrl+Alt and Ctrl+Cmd chords do not get chars rewritten (the key physical-letter fallback still applies, but the C0 byte does not), matching the plain-Ctrl-only semantics used elsewhere in the codebase.
    • Unmodified-key IME composition and the "nothing usable at all -> drop the event" path are unaffected.
    • I deliberately reverted the fix's 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) chars values, 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, and cargo clippy -p warpui_core --lib --tests --all-features -- -D warnings all pass.
  • Manually reviewed crates/warpui/src/platform/mac/event.rs's NSEventType::KeyDown conversion line-by-line against the pre-existing code paths (including lifetime/type-check reasoning for the unwrap_or pattern reused from the original code) since it cannot be compiled in this environment.
  • Confirmed both consumers of Event::KeyDown.chars (app/src/terminal/alt_screen/alt_screen_element.rs and app/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.
  • Checked that is_composing/IMEOpen gating (host_view.m marks composing only via hasMarkedText, 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+J inserts a newline in Warp's input editor and is forwarded to claude/codex as 0x0A, 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> and Ctrl+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/presubmit on 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

  • Warp Agent Mode - This PR was created via Warp's AI 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.

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>
@cla-bot cla-bot Bot added the cla-signed label Aug 16, 2026
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

@/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>

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.rs was 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 NSEvent behavior.

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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant