From a395e8c343c8d1d821a7e6c344a3ce0d17136cdc Mon Sep 17 00:00:00 2001 From: Hiroki Kajiwara <74193310+rerun0510@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:24:13 +0900 Subject: [PATCH] fix: prevent Japanese IME confirmation Enter from executing command (#2075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent Japanese IME confirmation Enter from executing command Korean IME commits a syllable and executes on a single Enter, but Japanese/Chinese IME use Enter only to confirm conversion — a second Enter is needed to execute. Restrict the extra Return forwarding in shouldSendCommittedIMEConfirmKey to Korean input sources only. * refactor: use case-insensitive check for Korean input source ID --- Sources/GhosttyTerminalView.swift | 7 ++++++- Sources/KeyboardLayout.swift | 8 ++++++++ cmuxTests/CJKIMEInputTests.swift | 3 +++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index d6c5945a..477c3181 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -5928,7 +5928,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { private func shouldSendCommittedIMEConfirmKey(event: NSEvent, markedTextBefore: Bool) -> Bool { guard markedTextBefore, markedText.length == 0 else { return false } - return event.keyCode == 36 || event.keyCode == 76 + guard event.keyCode == 36 || event.keyCode == 76 else { return false } + // Korean IME: Enter commits the syllable AND executes the command (single step). + // Japanese/Chinese IME: Enter only confirms the conversion; a second Enter executes. + // Only send the extra Return key for Korean input sources. + guard let sourceId = KeyboardLayout.id else { return false } + return sourceId.range(of: "korean", options: .caseInsensitive) != nil } private func ghosttyKeyEvent(for event: NSEvent, surface: ghostty_surface_t) -> ghostty_input_key_s { diff --git a/Sources/KeyboardLayout.swift b/Sources/KeyboardLayout.swift index 42e05b93..40ad5d24 100644 --- a/Sources/KeyboardLayout.swift +++ b/Sources/KeyboardLayout.swift @@ -2,8 +2,16 @@ import AppKit import Carbon class KeyboardLayout { + /// Test-only override for the current input source ID. + #if DEBUG + static var debugInputSourceIdOverride: String? + #endif + /// Return a string ID of the current keyboard input source. static var id: String? { + #if DEBUG + if let override = debugInputSourceIdOverride { return override } + #endif if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(), let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) { let sourceId = Unmanaged.fromOpaque(sourceIdPointer).takeUnretainedValue() diff --git a/cmuxTests/CJKIMEInputTests.swift b/cmuxTests/CJKIMEInputTests.swift index 80c7d8c6..af1000b0 100644 --- a/cmuxTests/CJKIMEInputTests.swift +++ b/cmuxTests/CJKIMEInputTests.swift @@ -1038,6 +1038,8 @@ final class KoreanIMEReturnCommitRegressionTests: XCTestCase { view.setMarkedText("한", selectedRange: NSRange(location: 0, length: 1), replacementRange: NSRange(location: NSNotFound, length: 0)) + // Simulate Korean input source so shouldSendCommittedIMEConfirmKey fires + KeyboardLayout.debugInputSourceIdOverride = "com.apple.inputmethod.Korean.2SetKorean" installCJKIMEInterpretKeyEventsSwizzle() cjkIMEInterpretKeyEventsHook = { candidateView, _ in guard candidateView === view else { return false } @@ -1045,6 +1047,7 @@ final class KoreanIMEReturnCommitRegressionTests: XCTestCase { return true } defer { + KeyboardLayout.debugInputSourceIdOverride = nil cjkIMEInterpretKeyEventsHook = nil }