Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,74 @@ describe("createTerminalKeyEventHandler", () => {
expect(handler(event)).toBe(true);
expect(xterm.input).not.toHaveBeenCalled();
});

describe("non-Latin IME Ctrl+<letter> fix", () => {
it("sends Ctrl+O to PTY when Korean IME maps KeyO to a non-ASCII glyph", () => {
const xterm = terminal();
// Korean 2-set: O key → "ㅐ" (U+1150, charCode 4432)
const event = keyboardEvent({
key: "ㅐ",
code: "KeyO",
ctrlKey: true,
});
const handler = createTerminalKeyEventHandler(xterm, {
platform: "MacIntel",
});

expect(handler(event)).toBe(false);
expect(event.preventDefault).toHaveBeenCalled();
// Ctrl+O = ASCII 15 = \x0f
expect(xterm.input).toHaveBeenCalledWith("\x0f", false);
});

it("sends Ctrl+K to PTY when Korean IME maps KeyK to a non-ASCII glyph", () => {
const xterm = terminal();
// Korean 2-set: K key → "ㅏ"
const event = keyboardEvent({
key: "ㅏ",
code: "KeyK",
ctrlKey: true,
});
const handler = createTerminalKeyEventHandler(xterm, {
platform: "MacIntel",
});

expect(handler(event)).toBe(false);
expect(event.preventDefault).toHaveBeenCalled();
// Ctrl+K = ASCII 11 = \x0b
expect(xterm.input).toHaveBeenCalledWith("\x0b", false);
});

it("does not intercept Ctrl+<letter> when Latin IME is active (event.key is ASCII)", () => {
const xterm = terminal();
const event = keyboardEvent({
key: "o",
code: "KeyO",
ctrlKey: true,
});
const handler = createTerminalKeyEventHandler(xterm, {
platform: "MacIntel",
});

// Let xterm handle it normally
expect(handler(event)).toBe(true);
expect(xterm.input).not.toHaveBeenCalled();
});

it("does not intercept Ctrl+Shift or Ctrl+Alt combos (only bare Ctrl)", () => {
const xterm = terminal();
const event = keyboardEvent({
key: "ㅐ",
code: "KeyO",
ctrlKey: true,
shiftKey: true,
});
const handler = createTerminalKeyEventHandler(xterm, {
platform: "MacIntel",
});

expect(handler(event)).toBe(true);
expect(xterm.input).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ export function createTerminalKeyEventHandler(
return false;
}

// Non-Latin IME (e.g. Korean 2-set) maps physical letter keys to
// non-ASCII glyphs: pressing O while Korean is active yields
// event.key="ㅐ" instead of "o". xterm derives the Ctrl+<letter>
// control byte from event.key, so it silently drops Ctrl+O, Ctrl+K,
// and every other Ctrl+<letter> chord typed while such an IME is
// active. Compute the byte from the physical key position
// (event.code) instead — that value is always layout-agnostic.
if (
event.type === "keydown" &&
event.ctrlKey &&
!event.metaKey &&
!event.altKey &&
!event.shiftKey
) {
const codeMatch = event.code.match(/^Key([A-Z])$/);
if (codeMatch && event.key.charCodeAt(0) > 127) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Possible double-interception when isComposing is true

If an IME composition is in progress (e.g. the user is mid-way through composing a Korean syllable) and presses Ctrl+letter, isComposing may be true. xterm's own handler skips events where isComposing is true, so it would naturally ignore that event. The new block doesn't guard for this state, meaning it would still call terminal.input() and preventDefault() — potentially swallowing an event that should have been ignored and interrupting IME composition unexpectedly. Adding && !event.isComposing to the outer condition would match xterm's own behavior and avoid this edge case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts
Line: 94

Comment:
**Possible double-interception when `isComposing` is true**

If an IME composition is in progress (e.g. the user is mid-way through composing a Korean syllable) and presses Ctrl+letter, `isComposing` may be `true`. xterm's own handler skips events where `isComposing` is true, so it would naturally ignore that event. The new block doesn't guard for this state, meaning it would still call `terminal.input()` and `preventDefault()` — potentially swallowing an event that should have been ignored and interrupting IME composition unexpectedly. Adding `&& !event.isComposing` to the outer condition would match xterm's own behavior and avoid this edge case.

How can I resolve this? If you propose a fix, please make it concise.

event.preventDefault();
const charCode = codeMatch[1].charCodeAt(0) - 64; // A→1 … Z→26
terminal.input(String.fromCharCode(charCode), false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 wasUserInput: false is inconsistent with the rest of the handler

The translateLineEditChord path directly above (line 54) calls terminal.input(translation, true). When wasUserInput is true, xterm scrolls the viewport to the bottom before delivering the bytes — the expected behavior for a real keypress. Passing false here means a user who has scrolled up and then presses Ctrl+K (or any Ctrl+letter) while Korean IME is active won't have the viewport snapped to the current prompt, so the shell's response appears off-screen. Since this block only fires on a genuine keydown, true is the correct value.

Suggested change
terminal.input(String.fromCharCode(charCode), false);
terminal.input(String.fromCharCode(charCode), true);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts
Line: 97

Comment:
**`wasUserInput: false` is inconsistent with the rest of the handler**

The `translateLineEditChord` path directly above (line 54) calls `terminal.input(translation, true)`. When `wasUserInput` is `true`, xterm scrolls the viewport to the bottom before delivering the bytes — the expected behavior for a real keypress. Passing `false` here means a user who has scrolled up and then presses Ctrl+K (or any Ctrl+letter) while Korean IME is active won't have the viewport snapped to the current prompt, so the shell's response appears off-screen. Since this block only fires on a genuine `keydown`, `true` is the correct value.

```suggestion
			terminal.input(String.fromCharCode(charCode), true);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Mark this forwarded control byte as user input so xterm applies normal keypress behavior (including scroll-to-bottom and user-input side effects) consistently with the other keyboard translation path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts, line 97:

<comment>Mark this forwarded control byte as user input so xterm applies normal keypress behavior (including scroll-to-bottom and user-input side effects) consistently with the other keyboard translation path.</comment>

<file context>
@@ -76,6 +76,29 @@ export function createTerminalKeyEventHandler(
+			if (codeMatch && event.key.charCodeAt(0) > 127) {
+				event.preventDefault();
+				const charCode = codeMatch[1].charCodeAt(0) - 64; // A→1 … Z→26
+				terminal.input(String.fromCharCode(charCode), false);
+				return false;
+			}
</file context>
Suggested change
terminal.input(String.fromCharCode(charCode), false);
terminal.input(String.fromCharCode(charCode), true);

return false;
}
}

return true;
};
}
Expand Down