From 303434aff3403431ad99037ab6c5281246c6139b Mon Sep 17 00:00:00 2001 From: HunterTom94 <26583005+HunterTom94@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:47:17 -0700 Subject: [PATCH 1/2] Fix ghost 'a' keypress from remote desktop modifier key events Some remote desktop software (e.g. Parsec, older Deskflow) sends flagsChanged events via IOHIDPostEvent without setting event.key.keyCode for modifier keys, causing it to default to 0 (kVK_ANSI_A). The osxKeycodeToRime function then maps keycode 0 to XK_a via additionalCodeMappings, producing a ghost 'a' keypress whenever Shift, CapsLock, or other modifier keys are pressed. This fix validates that event.keyCode corresponds to a known modifier virtual keycode for flagsChanged events. When an invalid keycode is detected (such as 0), the correct modifier keycode is inferred from the changed modifier flags instead. Fixes #825 Co-Authored-By: Claude Opus 4.6 (1M context) --- sources/SquirrelInputController.swift | 34 ++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index bad53d9f1..1a7c5a68d 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -62,9 +62,37 @@ final class SquirrelInputController: IMKInputController { } // print("[DEBUG] FLAGSCHANGED client: \(sender ?? "nil"), modifiers: \(modifiers)") var rimeModifiers: UInt32 = SquirrelKeycode.osxModifiersToRime(modifiers: modifiers) - // For flags-changed event, keyCode is available since macOS 10.15 - // (#715) - let rimeKeycode: UInt32 = SquirrelKeycode.osxKeycodeToRime(keycode: event.keyCode, keychar: nil, shift: false, caps: false) + // For flags-changed event, keyCode is available since macOS 10.15 (#715) + // Some remote desktop software (e.g. Parsec) sends flagsChanged events with + // keyCode defaulting to 0 (kVK_ANSI_A) instead of the actual modifier keycode, + // causing a ghost 'a' keypress. Validate and infer the correct keycode from + // the changed modifier flags when necessary. (#825) + let modifierKeycodes: Set = [ + UInt16(kVK_Shift), UInt16(kVK_RightShift), + UInt16(kVK_CapsLock), + UInt16(kVK_Control), UInt16(kVK_RightControl), + UInt16(kVK_Option), UInt16(kVK_RightOption), + UInt16(kVK_Command), UInt16(kVK_RightCommand), + UInt16(kVK_Function) + ] + var keyCode = event.keyCode + if !modifierKeycodes.contains(keyCode) { + if changes.contains(.capsLock) { + keyCode = UInt16(kVK_CapsLock) + } else if changes.contains(.shift) { + keyCode = UInt16(kVK_Shift) + } else if changes.contains(.control) { + keyCode = UInt16(kVK_Control) + } else if changes.contains(.option) { + keyCode = UInt16(kVK_Option) + } else if changes.contains(.command) { + keyCode = UInt16(kVK_Command) + } else { + handled = true + break + } + } + let rimeKeycode: UInt32 = SquirrelKeycode.osxKeycodeToRime(keycode: keyCode, keychar: nil, shift: false, caps: false) if changes.contains(.capsLock) { // NOTE: rime assumes XK_Caps_Lock to be sent before modifier changes, From 696bd3acfc02e96045102e9d4c0aa259e61b9925 Mon Sep 17 00:00:00 2001 From: HunterTom94 <26583005+HunterTom94@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:59:41 -0700 Subject: [PATCH 2/2] Address PR review: move modifier keycode logic to SquirrelKeycode - Move modifierKeycodes set and inference logic to MacOSKeyCodes.swift as static members, resolving Carbon import dependency and avoiding repeated Set allocation on every flagsChanged event - Update lastModifiers in fallback branch to prevent stale state Co-Authored-By: Claude Opus 4.6 (1M context) --- sources/MacOSKeyCodes.swift | 24 ++++++++++++++++++++++++ sources/SquirrelInputController.swift | 25 +++++-------------------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/sources/MacOSKeyCodes.swift b/sources/MacOSKeyCodes.swift index dd2ec3f7d..c9cc8cc04 100644 --- a/sources/MacOSKeyCodes.swift +++ b/sources/MacOSKeyCodes.swift @@ -65,6 +65,30 @@ struct SquirrelKeycode { return UInt32(XK_VoidSymbol) } + static let modifierKeycodes: Set = [ + UInt16(kVK_Shift), UInt16(kVK_RightShift), + UInt16(kVK_CapsLock), + UInt16(kVK_Control), UInt16(kVK_RightControl), + UInt16(kVK_Option), UInt16(kVK_RightOption), + UInt16(kVK_Command), UInt16(kVK_RightCommand), + UInt16(kVK_Function) + ] + + static func inferModifierKeycode(from changes: NSEvent.ModifierFlags) -> UInt16? { + if changes.contains(.capsLock) { + return UInt16(kVK_CapsLock) + } else if changes.contains(.shift) { + return UInt16(kVK_Shift) + } else if changes.contains(.control) { + return UInt16(kVK_Control) + } else if changes.contains(.option) { + return UInt16(kVK_Option) + } else if changes.contains(.command) { + return UInt16(kVK_Command) + } + return nil + } + private static let keycodeMappings: [Int: Int32] = [ // modifiers kVK_CapsLock: XK_Caps_Lock, diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index 1a7c5a68d..398a469af 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -67,30 +67,15 @@ final class SquirrelInputController: IMKInputController { // keyCode defaulting to 0 (kVK_ANSI_A) instead of the actual modifier keycode, // causing a ghost 'a' keypress. Validate and infer the correct keycode from // the changed modifier flags when necessary. (#825) - let modifierKeycodes: Set = [ - UInt16(kVK_Shift), UInt16(kVK_RightShift), - UInt16(kVK_CapsLock), - UInt16(kVK_Control), UInt16(kVK_RightControl), - UInt16(kVK_Option), UInt16(kVK_RightOption), - UInt16(kVK_Command), UInt16(kVK_RightCommand), - UInt16(kVK_Function) - ] var keyCode = event.keyCode - if !modifierKeycodes.contains(keyCode) { - if changes.contains(.capsLock) { - keyCode = UInt16(kVK_CapsLock) - } else if changes.contains(.shift) { - keyCode = UInt16(kVK_Shift) - } else if changes.contains(.control) { - keyCode = UInt16(kVK_Control) - } else if changes.contains(.option) { - keyCode = UInt16(kVK_Option) - } else if changes.contains(.command) { - keyCode = UInt16(kVK_Command) - } else { + if !SquirrelKeycode.modifierKeycodes.contains(keyCode) { + guard let inferred = SquirrelKeycode.inferModifierKeycode(from: changes) else { + lastModifiers = modifiers + rimeUpdate() handled = true break } + keyCode = inferred } let rimeKeycode: UInt32 = SquirrelKeycode.osxKeycodeToRime(keycode: keyCode, keychar: nil, shift: false, caps: false)