Skip to content

Comments

fix: Korean/CJK IME composition events not captured#120

Open
hongsw wants to merge 4 commits intocoder:mainfrom
hongsw:fix/korean-ime-composition-events
Open

fix: Korean/CJK IME composition events not captured#120
hongsw wants to merge 4 commits intocoder:mainfrom
hongsw:fix/korean-ime-composition-events

Conversation

@hongsw
Copy link

@hongsw hongsw commented Jan 25, 2026

Summary

  • Fix IME composition events not being captured for Korean, Chinese, Japanese input
  • Fix spaces appearing between CJK characters when copying text from terminal

Fixes #119

Problem

IME composition events (compositionstart, compositionupdate, compositionend) fire on the focused element. When using a hidden textarea for keyboard input (as ghostty-web does), the textarea receives focus, but composition event listeners were attached to the container element. This caused all IME events to be missed.

Current:
  Canvas click → textarea.focus()     (focus goes to textarea)
  IME listeners → attached to container  ❌ MISMATCH

Fixed:
  Canvas click → textarea.focus()
  IME listeners → attached to textarea  ✅

Additionally, when copying CJK text from the terminal, spaces appeared between characters because the continuation cells (width=0) for wide characters were being treated as empty spaces.

Changes

  1. input-handler.ts: Attach composition events to inputElement (textarea) if available
  2. terminal.ts: Focus textarea instead of container in focus() method
  3. selection-manager.ts: Skip wide character continuation cells when extracting text

Test Plan

  • Type Korean text (e.g., "안녕하세요") - characters appear correctly
  • Type Chinese text (e.g., "你好") - characters should appear correctly
  • Type Japanese text (e.g., "こんにちは") - characters should appear correctly
  • Select and copy CJK text - no extra spaces between characters
  • English typing still works normally

Known Limitation

Fast Korean typing may occasionally insert extra spaces due to composition event timing. This is tracked as a separate issue.

🤖 Generated with Claude Code

IME composition events (compositionstart, compositionupdate, compositionend)
fire on the focused element. When using a hidden textarea for input,
the textarea receives focus, but composition event listeners were attached
to the container element, causing the events to be missed.

Changes:
- input-handler.ts: Attach composition events to inputElement (textarea)
  if available, otherwise fall back to container
- terminal.ts: Focus textarea instead of container in focus() method
- selection-manager.ts: Skip wide character continuation cells when
  extracting selection text (fixes spaces between CJK characters)

This fixes Korean, Chinese, Japanese and other IME input methods.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f572fa14f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +721 to +725
// Focus the textarea for keyboard/IME input.
// The textarea is the actual input element that receives keyboard events
// and IME composition events. Focusing the container doesn't work for IME
// because composition events fire on the focused element.
const target = this.textarea || this.element;

Choose a reason for hiding this comment

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

P2 Badge Keep blur behavior aligned with textarea focus

The new focus path now targets the hidden textarea when it exists, but blur() still calls this.element.blur(). When the terminal is open (default case creates the textarea), calling terminal.blur() will leave the textarea focused, so keyboard/IME input continues to be captured even though callers expect blur to release focus. This is a regression introduced by switching focus to the textarea. Consider blurring the same target (this.textarea || this.element) or explicitly blurring the textarea when present.

Useful? React with 👍 / 👎.

@hongsw
Copy link
Author

hongsw commented Jan 25, 2026

#119

aibaryonai-ctrl and others added 3 commits January 26, 2026 04:26
Display a small "조합중: X" indicator in the top-right corner during
IME composition. This helps users see what character is being composed
before it's finalized.

Korean is a compositional writing system where characters are built
step-by-step (e.g., ㅅ → 세 → 셰). This visual feedback makes the
composition process clearer.

Closes #2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When pressing space or period to end Korean IME composition, the keydown
event fires before compositionend. This caused the triggering character
to be output before the composed text (e.g., "세요" becomes "세 요").

Added compositionJustEnded flag that blocks the first keydown after
composition ends, preventing the character order reversal.

Fixes #1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove contenteditable from container (causes IME to insert text as DOM nodes)
- Set container tabindex="-1" so it's not focusable
- Add focus redirection from container to textarea
- Queue composition-ending key (space, period) to process after compositionend
- This ensures correct character order: "세요 " instead of "세 요"

Key changes:
1. input-handler.ts: Add pendingKeyAfterComposition to queue the terminating key
2. terminal.ts: Remove contenteditable, set tabindex="-1", add focus redirection

Fixes Korean, Chinese, and Japanese IME input.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@hongsw
Copy link
Author

hongsw commented Jan 25, 2026

스크린샷 2026-01-26 05-47-46

@sreya
Copy link
Contributor

sreya commented Feb 24, 2026

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3db97718dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +735 to +738
const key = this.pendingKeyAfterComposition;
this.pendingKeyAfterComposition = null;
// Output the key that ended composition
this.onDataCallback(key);

Choose a reason for hiding this comment

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

P1 Badge Encode queued composition key before emitting

processPendingKeyAfterComposition forwards the raw KeyboardEvent.key string to onDataCallback, which is only correct for printable characters. IME sessions are often finalized with non-printable keys (for example Enter), and those keys become literal text like "Enter" instead of terminal control input (\r), so CJK users can end up sending wrong bytes to the PTY when confirming candidates.

Useful? React with 👍 / 👎.

Comment on lines +389 to 392
// Block the key that triggered composition end if we just processed a pending key
if (this.compositionJustEnded) {
this.compositionJustEnded = false;
return;

Choose a reason for hiding this comment

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

P2 Badge Set composition-end dedupe flag when pending key is consumed

The new compositionJustEnded guard can never trigger because this patch only reads/resets the flag and never sets it to true. On browsers that emit a follow-up keydown for the same composition-ending key, that duplicate event is not suppressed, so the terminating character can be inserted twice.

Useful? React with 👍 / 👎.

Comment on lines +758 to +760
const target = this.textarea || this.element;
if (target) {
target.focus();

Choose a reason for hiding this comment

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

P2 Badge Keep focus and blur on the same target element

focus() now prioritizes the hidden textarea, but blur() still blurs only the container. After this change, calling terminal.blur() can leave the textarea focused, so keyboard input continues to flow into the terminal in contexts that expect blur to stop capture (e.g., when opening overlays or switching inputs).

Useful? React with 👍 / 👎.

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.

Korean (Hangul) IME input not working

3 participants