diff --git a/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts b/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts index a8b5a65c06a..6b14675e366 100644 --- a/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts +++ b/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.test.ts @@ -113,4 +113,74 @@ describe("createTerminalKeyEventHandler", () => { expect(handler(event)).toBe(true); expect(xterm.input).not.toHaveBeenCalled(); }); + + describe("non-Latin IME Ctrl+ 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+ 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(); + }); + }); }); diff --git a/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts b/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts index 1b7edf8f10f..39c20606f92 100644 --- a/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts +++ b/apps/desktop/src/renderer/lib/terminal/terminal-key-event-handler.ts @@ -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+ + // control byte from event.key, so it silently drops Ctrl+O, Ctrl+K, + // and every other Ctrl+ 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) { + event.preventDefault(); + const charCode = codeMatch[1].charCodeAt(0) - 64; // A→1 … Z→26 + terminal.input(String.fromCharCode(charCode), false); + return false; + } + } + return true; }; }