diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 9ad696da..9604d4c2 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ A5001303 /* SurfaceSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001301 /* SurfaceSearchOverlay.swift */; }; A50012F1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F0 /* Backport.swift */; }; A50012F3 /* KeyboardShortcutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F2 /* KeyboardShortcutSettings.swift */; }; + A50012F5 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F4 /* KeyboardLayout.swift */; }; A5001521 /* PostHogAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001520 /* PostHogAnalytics.swift */; }; A5001201 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001211 /* UpdateController.swift */; }; A5001202 /* UpdateDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001212 /* UpdateDelegate.swift */; }; @@ -171,6 +172,7 @@ A5001301 /* SurfaceSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/SurfaceSearchOverlay.swift; sourceTree = ""; }; A50012F0 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A50012F2 /* KeyboardShortcutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSettings.swift; sourceTree = ""; }; + A50012F4 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; A5001211 /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateController.swift; sourceTree = ""; }; A5001212 /* UpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateDelegate.swift; sourceTree = ""; }; A5001213 /* UpdateDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateDriver.swift; sourceTree = ""; }; @@ -322,6 +324,7 @@ B9000017A1B2C3D4E5F60719 /* WindowDragHandleView.swift */, A50012F0 /* Backport.swift */, A50012F2 /* KeyboardShortcutSettings.swift */, + A50012F4 /* KeyboardLayout.swift */, A5001013 /* TabManager.swift */, A5001511 /* UITestRecorder.swift */, A5001520 /* PostHogAnalytics.swift */, @@ -559,6 +562,7 @@ B9000018A1B2C3D4E5F60719 /* WindowDragHandleView.swift in Sources */, A50012F1 /* Backport.swift in Sources */, A50012F3 /* KeyboardShortcutSettings.swift in Sources */, + A50012F5 /* KeyboardLayout.swift in Sources */, A5001003 /* TabManager.swift in Sources */, A5001501 /* UITestRecorder.swift in Sources */, A5001521 /* PostHogAnalytics.swift in Sources */, diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index cde12a35..37bd7ddd 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2918,7 +2918,10 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { "ign=\(cmuxScalarHex(event.charactersIgnoringModifiers)) mods=\(event.modifierFlags.rawValue)" ) #endif - return + // If Ghostty handled the key (action/encoding), we're done. + // If not (e.g. `ignore` keybind), fall through to interpretKeyEvents + // so the IME gets a chance to process this event. + if handled { return } } let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS @@ -2973,9 +2976,25 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { // so we can detect when composition ends. let markedTextBefore = markedText.length > 0 + // Capture the keyboard layout ID before interpretation so we can + // detect if an IME changed it (e.g. toggling input methods). + // We only check when not already in a preedit state. + let keyboardIdBefore: String? = if (!markedTextBefore) { + KeyboardLayout.id + } else { + nil + } + // Let the input system handle the event (for IME, dead keys, etc.) interpretKeyEvents([translationEvent]) + // If the keyboard layout changed, an input method grabbed the event. + // Sync preedit and return without sending the key to Ghostty. + if !markedTextBefore, let kbBefore = keyboardIdBefore, kbBefore != KeyboardLayout.id { + syncPreedit(clearIfNeeded: markedTextBefore) + return + } + // Sync the preedit state with Ghostty so it can render the IME // composition overlay (e.g. for Korean, Japanese, Chinese input). syncPreedit(clearIfNeeded: markedTextBefore) diff --git a/Sources/KeyboardLayout.swift b/Sources/KeyboardLayout.swift new file mode 100644 index 00000000..4407461f --- /dev/null +++ b/Sources/KeyboardLayout.swift @@ -0,0 +1,14 @@ +import Carbon + +class KeyboardLayout { + /// Return a string ID of the current keyboard input source. + static var id: String? { + if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(), + let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) { + let sourceId = Unmanaged.fromOpaque(sourceIdPointer).takeUnretainedValue() + return sourceId as String + } + + return nil + } +}