From c690ee60978732ebb3088150b11a3b47bfaf4e27 Mon Sep 17 00:00:00 2001 From: kunsanglee <85242378+kunsanglee@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:23:42 +0900 Subject: [PATCH] fix(terminal): tell a Windows IME shifted jamo apart from the committing Enter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows reports every IME-consumed keydown as key 'Process' with keyCode 229, so isTerminalImeProcessEnter also matched a shifted jamo (ㅃ, ㄲ, ㅆ) typed mid-composition. keyboard-handlers then rewrote that keydown to key: 'Enter', the Shift+Enter rule matched, and \x1b\r reached the PTY right behind the committed syllable. Only `code` separates the two shapes, and the predicate never read it. The repo's own fixtures already record both: the committing press as Process/Enter/229, and an observed IBus jamo keydown as Process/KeyG/229. --- .../keyboard-handlers-ime.test.tsx | 28 +++++++++++++++++++ .../terminal-ime-deferred-newline.test.ts | 10 +++++-- .../terminal-ime-deferred-newline.ts | 8 +++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx b/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx index 92b11c13e90..fb34e030095 100644 --- a/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx +++ b/src/renderer/src/components/terminal-pane/keyboard-handlers-ime.test.tsx @@ -295,6 +295,34 @@ describe('Windows IME keyboard ownership', () => { hook.unmount() harness.dispose() }) + + // Windows gives a shifted jamo the same Process/229/shiftKey shape as the committing + // Enter, so treating it as Enter injects a Shift+Enter newline into ordinary Hangul. + it.each([ + { code: 'KeyQ', jamo: 'ㅃ' }, + { code: 'KeyE', jamo: 'ㄸ' } + ])('does not read a shifted $jamo as the committing Enter', ({ code, jamo }) => { + const harness = createHarness() + const hook = renderHook(() => useTerminalKeyboardShortcuts(harness.deps)) + harness.startComposition() + const shiftedJamo = keyboardEvent('keydown', { + key: 'Process', + code, + keyCode: 229, + timeStamp: 10, + isComposing: true, + shiftKey: true + }) + + harness.terminalInput.dispatchEvent(shiftedJamo) + harness.endComposition(jamo) + vi.advanceTimersByTime(250) + + expect(harness.ptyWrites).toEqual([jamo]) + expect(shiftedJamo.defaultPrevented).toBe(false) + hook.unmount() + harness.dispose() + }) }) // The committing press repeats and the direct shortcut write races xterm's flush for diff --git a/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts b/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts index fe7860e35fe..4d960cbf34d 100644 --- a/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.test.ts @@ -325,6 +325,7 @@ describe('isTerminalImeProcessEnter', () => { const event = (overrides: Partial = {}) => ({ key: 'Process', + code: 'Enter', keyCode: 229, metaKey: false, ctrlKey: false, @@ -333,7 +334,7 @@ describe('isTerminalImeProcessEnter', () => { ...overrides }) as KeyboardEvent - it.each([{ shiftKey: true }, { shiftKey: false, ctrlKey: true }])( + it.each([{ shiftKey: true }, { shiftKey: false, ctrlKey: true }, { code: 'NumpadEnter' }])( 'recognizes a Windows IME modifier Enter reported as Process', (modifiers) => { expect(isTerminalImeProcessEnter(event(modifiers))).toBe(true) @@ -345,7 +346,12 @@ describe('isTerminalImeProcessEnter', () => { { keyCode: 13 }, { shiftKey: false }, { ctrlKey: true }, - { altKey: true } + { altKey: true }, + // Windows hands every IME-consumed keydown the same Process/229 shape, so a shifted + // jamo (ㅃ on KeyQ) differs from the committing Enter only by `code`. + { code: 'KeyQ' }, + { code: 'Digit3' }, + { code: '' } ])('rejects a non-IME or ambiguous Process key', (override) => { expect(isTerminalImeProcessEnter(event(override))).toBe(false) }) diff --git a/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts b/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts index c16c3bb4977..806371f77ea 100644 --- a/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts +++ b/src/renderer/src/components/terminal-pane/terminal-ime-deferred-newline.ts @@ -108,11 +108,17 @@ export function getTerminalImeModifiedEnterKind( } export function isTerminalImeProcessEnter( - event: Pick + event: Pick< + KeyboardEvent, + 'key' | 'code' | 'keyCode' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey' + > ): boolean { + // Why: Windows reports every IME-consumed keydown as Process/229, so a shifted jamo + // (ㅃ, ㄲ) matches the modifier check too; only `code` tells it apart from Enter. return ( event.key === 'Process' && event.keyCode === 229 && + (event.code === 'Enter' || event.code === 'NumpadEnter') && getTerminalImeModifiedEnterKind(event) !== null ) }