From 49f98c6d8c3f75d5270c621b97d88870cacd8376 Mon Sep 17 00:00:00 2001 From: anthhub Date: Tue, 24 Mar 2026 12:08:26 +0800 Subject: [PATCH 1/5] Normalize keyboard event for menu dispatch under non-Latin IME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Korean or Russian IME is active, event.charactersIgnoringModifiers returns non-ASCII characters (e.g., "ㅅ" instead of "t"), causing NSMenu.performKeyEquivalent to fail matching ASCII-based shortcuts. Now we synthesize a normalized event with ASCII characters derived from the physical key code before dispatching to the main menu. Fixes #1945 Co-Authored-By: Claude Opus 4.6 (1M context) --- Sources/AppDelegate.swift | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 41a57562def9..c7e6b531faa6 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -12605,7 +12605,34 @@ private extension NSWindow { if firstResponderGhosttyView != nil, shouldRouteCommandEquivalentDirectlyToMainMenu(event), let mainMenu = NSApp.mainMenu { - let consumedByMenu = mainMenu.performKeyEquivalent(with: event) + // Normalize event characters for non-Latin IME (Korean, Russian, etc.) + // so NSMenu.performKeyEquivalent can match ASCII-based shortcuts. + // When IME is active, event.charactersIgnoringModifiers may return + // non-ASCII characters (e.g. "ㅅ" instead of "t"), causing menu + // shortcut matching to fail. + var menuEvent = event + if let chars = event.charactersIgnoringModifiers, + !chars.allSatisfy({ $0.isASCII }), + let normalized = KeyboardLayout.character(forKeyCode: event.keyCode) { + menuEvent = NSEvent.keyEvent( + with: event.type, + location: event.locationInWindow, + modifierFlags: event.modifierFlags, + timestamp: event.timestamp, + windowNumber: event.windowNumber, + context: nil, + characters: normalized, + charactersIgnoringModifiers: normalized, + isARepeat: event.isARepeat, + keyCode: event.keyCode + ) ?? event + } +#if DEBUG + if menuEvent !== event { + dlog(" → normalized menu event chars: \(event.charactersIgnoringModifiers ?? "nil") → \(menuEvent.charactersIgnoringModifiers ?? "nil")") + } +#endif + let consumedByMenu = mainMenu.performKeyEquivalent(with: menuEvent) #if DEBUG if browserZoomShortcutTraceCandidate( flags: event.modifierFlags, From 7d832ff833f78f0ff1097ac1ea2040caa08b22fa Mon Sep 17 00:00:00 2001 From: anthhub Date: Tue, 24 Mar 2026 12:26:44 +0800 Subject: [PATCH 2/5] Fix Shift modifier loss and preserve original characters in IME normalization Address review feedback: 1. Pass modifierFlags to KeyboardLayout.character(forKeyCode:) so Shift combinations (Cmd+Shift+[, Cmd+?) work under non-Latin IME 2. Preserve original event.characters instead of overwriting with normalized ASCII to avoid confusing downstream consumers Co-Authored-By: Claude Opus 4.6 (1M context) --- Sources/AppDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index c7e6b531faa6..e9ab3371d52f 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -12613,7 +12613,7 @@ private extension NSWindow { var menuEvent = event if let chars = event.charactersIgnoringModifiers, !chars.allSatisfy({ $0.isASCII }), - let normalized = KeyboardLayout.character(forKeyCode: event.keyCode) { + let normalized = KeyboardLayout.character(forKeyCode: event.keyCode, modifierFlags: event.modifierFlags) { menuEvent = NSEvent.keyEvent( with: event.type, location: event.locationInWindow, @@ -12621,7 +12621,7 @@ private extension NSWindow { timestamp: event.timestamp, windowNumber: event.windowNumber, context: nil, - characters: normalized, + characters: event.characters ?? normalized, charactersIgnoringModifiers: normalized, isARepeat: event.isARepeat, keyCode: event.keyCode From 60a3f38cdfec446ac8c5152f34d450cf9a78afbc Mon Sep 17 00:00:00 2001 From: anthhub Date: Tue, 24 Mar 2026 12:26:45 +0800 Subject: [PATCH 3/5] Add Korean font mapping to CJK font fallback system Korean (Hangul) characters were excluded from the automatic CJK font mapping, falling back to Ghostty's native CTFontCreateForString which ignores user font-family settings. Now Korean language preferences map Hangul ranges to Apple SD Gothic Neo via font-codepoint-map. Fixes #1946 Co-Authored-By: Claude Opus 4.6 (1M context) --- cmuxTests/GhosttyConfigTests.swift | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index d7820ce35ed1..14b8216e4502 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -2103,10 +2103,13 @@ final class GhosttyMouseFocusTests: XCTestCase { XCTAssertFalse(ranges.contains("U+AC00-U+D7AF"), "Should NOT include Hangul") } - func testCJKFontMappingsReturnsNilForKoreanOnly() { - // Korean is not auto-mapped — Ghostty's native CTFontCreateForString - // fallback selects a better-matching font for Hangul. - XCTAssertNil(GhosttyApp.cjkFontMappings(preferredLanguages: ["ko-KR"])) + func testCJKFontMappingsReturnsKoreanMappings() { + let mappings = GhosttyApp.cjkFontMappings(preferredLanguages: ["ko-KR"]) + XCTAssertNotNil(mappings) + let fonts = Set(mappings!.map { $0.1 }) + XCTAssertTrue(fonts.contains("Apple SD Gothic Neo")) + let ranges = mappings!.map { $0.0 } + XCTAssertTrue(ranges.contains("U+AC00-U+D7AF")) } func testCJKFontMappingsReturnsPingFangForChinese() { @@ -2125,17 +2128,12 @@ final class GhosttyMouseFocusTests: XCTestCase { XCTAssertNil(GhosttyApp.cjkFontMappings(preferredLanguages: [])) } - func testCJKFontMappingsMultiLanguageSkipsKorean() { - // When both ja and ko are preferred, only Japanese mappings are generated. - // Korean is left to Ghostty's native CTFontCreateForString fallback. - let mappings = GhosttyApp.cjkFontMappings(preferredLanguages: ["ja-JP", "ko-KR"])! - - let hiraginoRanges = mappings.filter { $0.1 == "Hiragino Sans" }.map(\.0) - - XCTAssertTrue(hiraginoRanges.contains("U+3040-U+309F"), "Hiragana → Hiragino") - XCTAssertTrue(hiraginoRanges.contains("U+4E00-U+9FFF"), "Shared CJK → first lang font") - XCTAssertFalse(mappings.contains { $0.1 == "Apple SD Gothic Neo" }, "No Korean font mapping") - XCTAssertFalse(hiraginoRanges.contains("U+AC00-U+D7AF"), "Hangul NOT in Hiragino") + func testCJKFontMappingsMultiLanguageIncludesKorean() { + let mappings = GhosttyApp.cjkFontMappings(preferredLanguages: ["ja", "ko-KR"]) + XCTAssertNotNil(mappings) + let fonts = Set(mappings!.map { $0.1 }) + XCTAssertTrue(fonts.contains("Hiragino Sans")) + XCTAssertTrue(fonts.contains("Apple SD Gothic Neo")) } // MARK: userConfigContainsCJKCodepointMap From d2f1c55637c376661bbfdc83d04b7892c03ecc11 Mon Sep 17 00:00:00 2001 From: Achieve Date: Thu, 26 Mar 2026 11:57:01 +0800 Subject: [PATCH 4/5] Remove out-of-scope CJK font test changes (belong to #2037) --- cmuxTests/GhosttyConfigTests.swift | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index 14b8216e4502..d7820ce35ed1 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -2103,13 +2103,10 @@ final class GhosttyMouseFocusTests: XCTestCase { XCTAssertFalse(ranges.contains("U+AC00-U+D7AF"), "Should NOT include Hangul") } - func testCJKFontMappingsReturnsKoreanMappings() { - let mappings = GhosttyApp.cjkFontMappings(preferredLanguages: ["ko-KR"]) - XCTAssertNotNil(mappings) - let fonts = Set(mappings!.map { $0.1 }) - XCTAssertTrue(fonts.contains("Apple SD Gothic Neo")) - let ranges = mappings!.map { $0.0 } - XCTAssertTrue(ranges.contains("U+AC00-U+D7AF")) + func testCJKFontMappingsReturnsNilForKoreanOnly() { + // Korean is not auto-mapped — Ghostty's native CTFontCreateForString + // fallback selects a better-matching font for Hangul. + XCTAssertNil(GhosttyApp.cjkFontMappings(preferredLanguages: ["ko-KR"])) } func testCJKFontMappingsReturnsPingFangForChinese() { @@ -2128,12 +2125,17 @@ final class GhosttyMouseFocusTests: XCTestCase { XCTAssertNil(GhosttyApp.cjkFontMappings(preferredLanguages: [])) } - func testCJKFontMappingsMultiLanguageIncludesKorean() { - let mappings = GhosttyApp.cjkFontMappings(preferredLanguages: ["ja", "ko-KR"]) - XCTAssertNotNil(mappings) - let fonts = Set(mappings!.map { $0.1 }) - XCTAssertTrue(fonts.contains("Hiragino Sans")) - XCTAssertTrue(fonts.contains("Apple SD Gothic Neo")) + func testCJKFontMappingsMultiLanguageSkipsKorean() { + // When both ja and ko are preferred, only Japanese mappings are generated. + // Korean is left to Ghostty's native CTFontCreateForString fallback. + let mappings = GhosttyApp.cjkFontMappings(preferredLanguages: ["ja-JP", "ko-KR"])! + + let hiraginoRanges = mappings.filter { $0.1 == "Hiragino Sans" }.map(\.0) + + XCTAssertTrue(hiraginoRanges.contains("U+3040-U+309F"), "Hiragana → Hiragino") + XCTAssertTrue(hiraginoRanges.contains("U+4E00-U+9FFF"), "Shared CJK → first lang font") + XCTAssertFalse(mappings.contains { $0.1 == "Apple SD Gothic Neo" }, "No Korean font mapping") + XCTAssertFalse(hiraginoRanges.contains("U+AC00-U+D7AF"), "Hangul NOT in Hiragino") } // MARK: userConfigContainsCJKCodepointMap From 10088a03d4083eb5457007085a9d905b82b1965e Mon Sep 17 00:00:00 2001 From: anthhub Date: Fri, 27 Mar 2026 19:58:54 +0800 Subject: [PATCH 5/5] Harden IME normalization: guard against empty strings and nil chars - Add !normalized.isEmpty guard so performKeyEquivalent falls back to the original event when KeyboardLayout returns an empty string - Also normalize when charactersIgnoringModifiers is nil/empty (e.g. synthetic browser key equivalents), not just when non-ASCII Co-Authored-By: Claude Opus 4.6 (1M context) --- Sources/AppDelegate.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index e9ab3371d52f..946c60fdbce5 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -12611,9 +12611,10 @@ private extension NSWindow { // non-ASCII characters (e.g. "ㅅ" instead of "t"), causing menu // shortcut matching to fail. var menuEvent = event - if let chars = event.charactersIgnoringModifiers, - !chars.allSatisfy({ $0.isASCII }), - let normalized = KeyboardLayout.character(forKeyCode: event.keyCode, modifierFlags: event.modifierFlags) { + let rawChars = event.charactersIgnoringModifiers ?? "" + if (rawChars.isEmpty || !rawChars.allSatisfy(\.isASCII)), + let normalized = KeyboardLayout.character(forKeyCode: event.keyCode, modifierFlags: event.modifierFlags), + !normalized.isEmpty { menuEvent = NSEvent.keyEvent( with: event.type, location: event.locationInWindow,