From 765991ff72fa329d6c9a7324bb6df35772b09ad4 Mon Sep 17 00:00:00 2001 From: nchopra Date: Mon, 22 Dec 2025 16:27:12 +0530 Subject: [PATCH] fix: handle fn key while consuming shortcut keys --- .../Sources/SwiftHelper/ShortcutManager.swift | 30 ++++++++++++++++++- .../Sources/SwiftHelper/main.swift | 12 ++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/native-helpers/swift-helper/Sources/SwiftHelper/ShortcutManager.swift b/packages/native-helpers/swift-helper/Sources/SwiftHelper/ShortcutManager.swift index d6e9def..794c05a 100644 --- a/packages/native-helpers/swift-helper/Sources/SwiftHelper/ShortcutManager.swift +++ b/packages/native-helpers/swift-helper/Sources/SwiftHelper/ShortcutManager.swift @@ -17,6 +17,23 @@ class ShortcutManager { private var pushToTalkKeys: [String] = [] private var toggleRecordingKeys: [String] = [] + + // ============================================================================ + // IMPORTANT: Fn Key State Tracking + // ============================================================================ + // We track the Fn key state ourselves via flagsChanged events instead of + // trusting event.flags.contains(.maskSecondaryFn) on keyDown/keyUp events. + // + // WHY: macOS reports UNRELIABLE Fn flag on keyDown events, especially on + // MacBooks with the Globe/Fn key. The flag can be true even when Fn is NOT + // pressed, causing arrow keys and other keys to be incorrectly consumed. + // + // FIX: We update fnKeyDown only when we receive flagsChanged events (which + // are reliable for modifier state), and use this tracked state for shortcut + // matching in shouldConsumeKey(). + // ============================================================================ + private var fnKeyDown: Bool = false + private let lock = NSLock() private let dateFormatter: DateFormatter @@ -41,6 +58,15 @@ class ShortcutManager { logToStderr("[ShortcutManager] Shortcuts updated - PTT: \(pushToTalk), Toggle: \(toggleRecording)") } + /// Update the tracked Fn key state + /// Called from event tap callback when flagsChanged event is received + /// We track Fn separately because macOS can report unreliable Fn flag on keyDown events + func setFnKeyState(_ isDown: Bool) { + lock.lock() + defer { lock.unlock() } + fnKeyDown = isDown + } + /// Check if this key event should be consumed (prevent default behavior) /// Called from event tap callback for keyDown/keyUp events only func shouldConsumeKey(keyCode: Int, modifiers: ModifierState) -> Bool { @@ -53,8 +79,10 @@ class ShortcutManager { } // Build set of currently active keys (modifiers + this regular key) + // Note: We use tracked fnKeyDown instead of modifiers.fn because macOS + // can report unreliable Fn flag on keyDown events (especially on MacBooks) var activeKeys = Set() - if modifiers.fn { activeKeys.insert("Fn") } + if fnKeyDown { activeKeys.insert("Fn") } if modifiers.cmd { activeKeys.insert("Cmd") } if modifiers.ctrl { activeKeys.insert("Ctrl") } if modifiers.alt { activeKeys.insert("Alt") } diff --git a/packages/native-helpers/swift-helper/Sources/SwiftHelper/main.swift b/packages/native-helpers/swift-helper/Sources/SwiftHelper/main.swift index 3023c59..07c567b 100644 --- a/packages/native-helpers/swift-helper/Sources/SwiftHelper/main.swift +++ b/packages/native-helpers/swift-helper/Sources/SwiftHelper/main.swift @@ -81,6 +81,18 @@ func eventTapCallback( // Modifier-only events always pass through - they don't cause unwanted behavior on their own let keyCode = event.getIntegerValueField(.keyboardEventKeycode) + // ==================================================================== + // IMPORTANT: Track Fn key state via flagsChanged (NOT keyDown flags) + // ==================================================================== + // macOS reports UNRELIABLE .maskSecondaryFn on keyDown events, + // especially on MacBooks. The flag can be TRUE even when Fn is NOT + // pressed! flagsChanged events are reliable, so we track Fn state + // here and use it in ShortcutManager.shouldConsumeKey(). + // See ShortcutManager.swift for more details. + // ==================================================================== + let fnPressed = event.flags.contains(.maskSecondaryFn) + ShortcutManager.shared.setFnKeyState(fnPressed) + let payload = KeyEventPayload( key: nil, code: nil,