From 8b49d667635c5b9a229a76728b7c2ca41360f7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=88=98=EB=AF=BC?= Date: Fri, 20 Mar 2026 18:47:19 +0900 Subject: [PATCH] fix: support keyboard shortcuts with CJK input sources (Korean, Chinese, Japanese) (#1649) When a non-Latin input source is active, event.charactersIgnoringModifiers returns CJK characters that cannot match Latin shortcut keys. This adds ASCII-capable input source fallback in KeyboardLayout and updates the matchShortcut guard to skip early-return when event chars are non-ASCII. Co-authored-by: Claude Opus 4.6 (1M context) --- Sources/AppDelegate.swift | 10 +++++++++- Sources/KeyboardLayout.swift | 23 +++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index d3160cb7..968c08bc 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -10472,8 +10472,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // For command-based shortcuts, trust AppKit's layout-aware characters when present. // Keep this strict for letter shortcuts to avoid physical-key collisions across layouts, // while still allowing keyCode fallback for digit/punctuation shortcuts on non-US layouts. + // When a non-Latin input source is active (Korean, Chinese, Japanese, etc.), + // charactersIgnoringModifiers returns non-ASCII characters that can never match + // a Latin shortcut key — skip this guard and fall through to layout-based matching. let hasEventChars = !(eventCharsIgnoringModifiers?.isEmpty ?? true) + let eventCharsAreASCII = eventCharsIgnoringModifiers?.allSatisfy(\.isASCII) ?? true if hasEventChars, + eventCharsAreASCII, flags.contains(.command), !flags.contains(.control), shouldRequireCharacterMatchForCommandShortcut(shortcutKey: shortcutKey) { @@ -10496,12 +10501,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent // so keep ANSI keyCode fallback for control-modified shortcuts. Also allow fallback for // command punctuation shortcuts, since some non-US layouts report different characters // for the same physical key even when menu-equivalent semantics should still apply. + // When a non-Latin input source is active, treat non-ASCII event chars the same as + // absent chars — they carry no usable Latin key identity. + let hasUsableEventChars = hasEventChars && eventCharsAreASCII let allowANSIKeyCodeFallback = flags.contains(.control) || (flags.contains(.command) && !flags.contains(.control) && ( !shouldRequireCharacterMatchForCommandShortcut(shortcutKey: shortcutKey) - || (!hasEventChars && (layoutCharacter?.isEmpty ?? true)) + || (!hasUsableEventChars && (layoutCharacter?.isEmpty ?? true)) )) if allowANSIKeyCodeFallback, let expectedKeyCode = keyCodeForShortcutKey(shortcutKey) { return event.keyCode == expectedKeyCode diff --git a/Sources/KeyboardLayout.swift b/Sources/KeyboardLayout.swift index f7b7110a..2a04e981 100644 --- a/Sources/KeyboardLayout.swift +++ b/Sources/KeyboardLayout.swift @@ -15,12 +15,31 @@ class KeyboardLayout { /// Translate a physical keyCode to the character AppKit would use for shortcut matching, /// preserving command-aware layouts such as "Dvorak - QWERTY Command". + /// CJK input sources (Korean, Chinese, Japanese) lack kTISPropertyUnicodeKeyLayoutData, + /// so we fall back to TISCopyCurrentASCIICapableKeyboardInputSource() in that case. static func character( forKeyCode keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags = [] ) -> String? { - guard let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(), - let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { + if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(), + let result = characterFromInputSource(source, forKeyCode: keyCode, modifierFlags: modifierFlags) { + return result + } + // Current input source has no Unicode layout data (e.g. Korean, Chinese, Japanese IME). + // Fall back to the ASCII-capable source so shortcut matching still works. + if let asciiSource = TISCopyCurrentASCIICapableKeyboardInputSource()?.takeRetainedValue(), + let result = characterFromInputSource(asciiSource, forKeyCode: keyCode, modifierFlags: modifierFlags) { + return result + } + return nil + } + + private static func characterFromInputSource( + _ source: TISInputSource, + forKeyCode keyCode: UInt16, + modifierFlags: NSEvent.ModifierFlags + ) -> String? { + guard let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else { return nil }