From 40238a1dbf3e73aca6d29dcbe6a60b5fc8f91249 Mon Sep 17 00:00:00 2001 From: wnduqrla Date: Mon, 25 May 2026 17:58:22 +0900 Subject: [PATCH] fix(terminal): forward Ctrl+ to PTY under non-Latin IME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Korean (and other non-Latin) IME maps physical letter keys to non-ASCII glyphs: pressing O while Korean 2-set is active yields event.key="ㅐ" instead of "o". xterm derives the Ctrl+ control byte from event.key, so it silently drops every Ctrl+ chord typed while such an IME is active — breaking TUI app shortcuts like Claude Code's Ctrl+O (toggle transcript) and Ctrl+K (clear input). The fix intercepts these chords in createTerminalKeyEventHandler before xterm runs: when ctrlKey is set, no other modifier is held, event.code is a letter key, and event.key is non-ASCII (charCode > 127), compute the control byte from the physical key position (event.code) and write it directly to the PTY via terminal.input(). event.code is always layout- and IME-agnostic, so the correct byte is sent regardless of which input source is active. Relates to #3365, extends the event.code approach from #3391 to the xterm key-encoder path. --- .../terminal-key-event-handler.test.ts | 70 +++++++++++++++++++ .../terminal/terminal-key-event-handler.ts | 23 ++++++ 2 files changed, 93 insertions(+) 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; }; }