fix(input): route IME composition events to the hidden textarea#11
Conversation
There was a problem hiding this comment.
Code Review
This pull request improves IME support for CJK languages by redirecting focus to a hidden textarea and refining composition event handling. Key changes include moving event listeners to the textarea, removing contenteditable from the container to prevent direct text insertion, and implementing a queuing mechanism for keys that terminate composition. Feedback identifies that the compositionJustEnded flag is not correctly updated, special keys are not mapped to terminal escape sequences, and de-duplication logic is missing in the new processPendingKeyAfterComposition method.
| private processPendingKeyAfterComposition(): void { | ||
| if (this.pendingKeyAfterComposition) { | ||
| const key = this.pendingKeyAfterComposition; | ||
| this.pendingKeyAfterComposition = null; | ||
| // Output the key that ended composition | ||
| this.onDataCallback(key); | ||
| } | ||
| } |
There was a problem hiding this comment.
There are three issues in this method:
- The
compositionJustEndedflag is never set totrue, which makes the logic inhandleKeyDown(line 390) that blocks duplicate events ineffective. - Storing and sending
event.keydirectly is incorrect for special keys likeEnter,Tab, orBackspace. In a terminal context, these must be mapped to their respective escape sequences (e.g.,\r,\t,\x7F). - This method should call
recordKeyDownDatato ensure proper de-duplication against subsequentbeforeinputevents, maintaining consistency with the rest of the input handling logic.
private processPendingKeyAfterComposition(): void {
if (this.pendingKeyAfterComposition) {
const key = this.pendingKeyAfterComposition;
this.pendingKeyAfterComposition = null;
this.compositionJustEnded = true;
// Map common terminating keys to their terminal sequences.
// For printable characters (like Space or Period), the key is the data.
let data = key;
if (key === 'Enter') data = '\r';
else if (key === 'Tab') data = '\t';
else if (key === 'Backspace') data = '\x7F';
else if (key === 'Escape') data = '\x1B';
// Output the key that ended composition
this.onDataCallback(data);
this.recordKeyDownData(data);
}
}IME composition events (compositionstart / compositionupdate / compositionend) fire on the focused element. ghostty-web focuses a hidden textarea for keyboard input, but composition listeners were attached to the container element — so every Korean / Chinese / Japanese input event was missed. This commit: - Moves composition listeners from `container` to `inputElement` (textarea) when the input element is available. Detach is also retargeted to the same element so disposal is symmetric. - Adds a state machine to handle the "terminating key" of an IME composition (space, period, etc.). The key is queued during composition and replayed after compositionend so the composed text appears before the terminator. - Removes `contenteditable="true"` from the parent container. Having contenteditable on the container caused IME text to be inserted as text nodes in the container, bypassing the textarea entirely. The textarea is itself a real input element, so most browser extensions (Vimium, etc.) leave it alone — this should not regress the motivation behind coder#78, but needs verification in real browsers. - Sets `tabindex="-1"` on the parent so it is no longer click/tab focusable. Redirects parent mousedown and focus events to the textarea so any focus eventually lands on the input element. - Updates `Terminal.focus()` to target the textarea instead of the container, with the same delayed-focus backup behaviour. Differences from upstream PR coder#120 (deliberate): - The composition-preview overlay (a div with hardcoded Korean text "조합중:" and `#ffcc00` on dark background) is intentionally NOT ported. Native browsers already render IME composition feedback, and the upstream overlay was both untranslated and theme-hostile. - The selection-manager wide-char fix from that PR was already shipped separately as #120a. Co-authored-by: Seungwoo Hong <ai.baryon.ai@gmail.com> Inspired-by: coder#120
fe081ad to
5162a1b
Compare
|
…routing PR #11 routes focus to the hidden textarea inside the container rather than to the container element itself. Accept either as valid. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>



Summary
Ports the IME-routing subset of upstream PR #120 with two deliberate deviations.
Root cause being fixed: composition events fire on the focused element. ghostty-web focuses a hidden textarea for keyboard input, but composition listeners were attached to the container element — every CJK input event was missed.
What changed
compositionendso composed text appears before the terminatorcontenteditable="true"removed from the parent container (was causing IME text to be inserted into the container as text nodes, bypassing the textarea)tabindex="-1"on parent; parent mousedown/focus now redirect to textareaTerminal.focus()targets textarea instead of containerDeviations from upstream coder#120
"조합중:"and inline#ffcc00styling. Native browsers already render IME feedback, and the overlay was untranslated + theme-hostile.#120a).Regression risk (extensions)
Removing
contenteditablewas originally added by #78 to stop Vimium-style extensions from intercepting keys. The textarea is itself a real input element so most extensions should leave it alone — but this needs verification in a real browser with extensions installed. If the regression appears, the fallback is to keepcontenteditableonly while no composition is active.Attribution
Thanks to @hongsw for the original implementation.
Test plan
bun run fmt && bun run lint && bun run typecheckbun test— 331 tests pass (no new tests; existing keyboard suite unchanged), 0 failbun run build:libbun run build:wasm— WASM rebuilt locally with Zig 0.15.2bun run dev:Risk
Medium. Removes contenteditable (may resurrect coder#78's extension regression — needs browser verification). Big-picture: IME is broken today for CJK users; this is a real functional win at the cost of one regression risk that we can quickly verify.