From b6c5e3fe9eddc8436cfbb15feb667e9acf271dff Mon Sep 17 00:00:00 2001 From: yasunogithub Date: Wed, 25 Feb 2026 00:30:17 +0900 Subject: [PATCH] Fix IME key events blocked by ctrl fast path and missing layout change detection The ctrl fast path unconditionally returned after calling ghostty_surface_key, even when it returned false (e.g. ignore keybindings), preventing IMEs from receiving Ctrl-modified key events. Now falls through to interpretKeyEvents when the key is not handled. Also adds keyboard layout change detection around interpretKeyEvents (matching Ghostty upstream) so that IME-triggered layout switches cause an early return instead of sending the key to Ghostty. Co-Authored-By: Claude Opus 4.6 --- GhosttyTabs.xcodeproj/project.pbxproj | 4 ++++ Sources/GhosttyTerminalView.swift | 21 ++++++++++++++++++++- Sources/KeyboardLayout.swift | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 Sources/KeyboardLayout.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index c3f2b4d9..eb90620f 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 266bc3b1..6aa39d92 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2916,7 +2916,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 @@ -2971,9 +2974,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..8e573f49 --- /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 = unsafeBitCast(sourceIdPointer, to: CFString.self) + return sourceId as String + } + + return nil + } +}