Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions lib/input-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,14 +288,19 @@ export class InputHandler {
this.inputElement.addEventListener('beforeinput', this.beforeInputListener);
}

// Attach composition events to inputElement (textarea) if available.
// IME composition events fire on the focused element, and when using a hidden
// textarea for input (as ghostty-web does), the textarea receives focus,
// not the container. This fixes Korean/Chinese/Japanese IME input.
const compositionTarget = this.inputElement || this.container;
this.compositionStartListener = this.handleCompositionStart.bind(this);
this.container.addEventListener('compositionstart', this.compositionStartListener);
compositionTarget.addEventListener('compositionstart', this.compositionStartListener);

this.compositionUpdateListener = this.handleCompositionUpdate.bind(this);
this.container.addEventListener('compositionupdate', this.compositionUpdateListener);
compositionTarget.addEventListener('compositionupdate', this.compositionUpdateListener);

this.compositionEndListener = this.handleCompositionEnd.bind(this);
this.container.addEventListener('compositionend', this.compositionEndListener);
compositionTarget.addEventListener('compositionend', this.compositionEndListener);

// Mouse event listeners (for terminal mouse tracking)
this.mousedownListener = this.handleMouseDown.bind(this);
Expand Down Expand Up @@ -1059,18 +1064,20 @@ export class InputHandler {
this.beforeInputListener = null;
}

// Remove composition listeners from the same element they were attached to
const compositionTarget = this.inputElement || this.container;
if (this.compositionStartListener) {
this.container.removeEventListener('compositionstart', this.compositionStartListener);
compositionTarget.removeEventListener('compositionstart', this.compositionStartListener);
this.compositionStartListener = null;
}

if (this.compositionUpdateListener) {
this.container.removeEventListener('compositionupdate', this.compositionUpdateListener);
compositionTarget.removeEventListener('compositionupdate', this.compositionUpdateListener);
this.compositionUpdateListener = null;
}

if (this.compositionEndListener) {
this.container.removeEventListener('compositionend', this.compositionEndListener);
compositionTarget.removeEventListener('compositionend', this.compositionEndListener);
this.compositionEndListener = null;
}

Expand Down
7 changes: 6 additions & 1 deletion lib/selection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ export class SelectionManager {
if (char.trim()) {
lastNonEmpty = lineText.length;
}
} else {
} else if (!cell || cell.width !== 0) {
// Only add space for truly empty cells, not wide character continuation cells.
// Wide characters (like CJK) occupy 2 terminal cells:
// - First cell: has codepoint, width=2
// - Second cell: codepoint=0, width=0 (continuation marker)
// We skip continuation cells to avoid inserting spaces between characters.
lineText += ' ';
}
}
Expand Down
24 changes: 15 additions & 9 deletions lib/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,15 +717,21 @@ export class Terminal implements ITerminalCore {
* Focus terminal input
*/
focus(): void {
if (this.isOpen && this.element) {
// Focus immediately for immediate keyboard/wheel event handling
this.element.focus();

// Also schedule a delayed focus as backup to ensure it sticks
// (some browsers may need this if DOM isn't fully settled)
setTimeout(() => {
this.element?.focus();
}, 0);
if (this.isOpen) {
// 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;
Comment on lines +754 to +758
Copy link
Copy Markdown

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 👍 / 👎.

if (target) {
target.focus();
Comment on lines +758 to +760
Copy link
Copy Markdown

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 👍 / 👎.


// Also schedule a delayed focus as backup to ensure it sticks
// (some browsers may need this if DOM isn't fully settled)
setTimeout(() => {
target?.focus();
}, 0);
}
}
}

Expand Down