From 7d2eb19a0f05762b3f2a5267d17b0b6258217634 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Thu, 12 Mar 2026 21:13:23 +0530 Subject: [PATCH] suppress fn hotkey events in terminal apps --- speaktype/App/AppDelegate.swift | 90 ++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/speaktype/App/AppDelegate.swift b/speaktype/App/AppDelegate.swift index 092e965..b30cda6 100644 --- a/speaktype/App/AppDelegate.swift +++ b/speaktype/App/AppDelegate.swift @@ -4,6 +4,10 @@ import SwiftUI class AppDelegate: NSObject, NSApplicationDelegate { private var miniRecorderController: MiniRecorderWindowController? + private var globalFlagsMonitor: Any? + private var localFlagsMonitor: Any? + private var hotkeyEventTap: CFMachPort? + private var hotkeyEventTapSource: CFRunLoopSource? var isHotkeyPressed = false private var cancellables = Set() private var lastHandledHotkeyTimestamp: TimeInterval = 0 @@ -54,25 +58,98 @@ class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Hotkey Monitoring private func setupHotkeyMonitoring() { + setupSuppressingHotkeyEventTap() + // Add global monitor for hotkey events - NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in + globalFlagsMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in self?.handleHotkeyEvent(event) } // Add local monitor for hotkey events (same logic) - NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in + localFlagsMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in self?.handleHotkeyEvent(event) return event } } + private func setupSuppressingHotkeyEventTap() { + guard hotkeyEventTap == nil else { return } + + let eventMask = (1 << CGEventType.flagsChanged.rawValue) + let callback: CGEventTapCallBack = { _, type, event, refcon in + guard let refcon else { + return Unmanaged.passUnretained(event) + } + + let appDelegate = Unmanaged.fromOpaque(refcon).takeUnretainedValue() + return appDelegate.handleHotkeyEventTap(type: type, event: event) + } + + guard + let eventTap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .defaultTap, + eventsOfInterest: CGEventMask(eventMask), + callback: callback, + userInfo: Unmanaged.passUnretained(self).toOpaque() + ) + else { + print("Failed to create suppressing hotkey event tap") + return + } + + let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) + CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) + CGEvent.tapEnable(tap: eventTap, enable: true) + + hotkeyEventTap = eventTap + hotkeyEventTapSource = runLoopSource + } + + private func handleHotkeyEventTap(type: CGEventType, event: CGEvent) -> Unmanaged? { + if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput { + if let hotkeyEventTap { + CGEvent.tapEnable(tap: hotkeyEventTap, enable: true) + } + return Unmanaged.passUnretained(event) + } + + guard type == .flagsChanged else { + return Unmanaged.passUnretained(event) + } + + let currentHotkey = getSelectedHotkey() + guard currentHotkey == .fn else { + return Unmanaged.passUnretained(event) + } + + let keyCode = UInt16(event.getIntegerValueField(.keyboardEventKeycode)) + guard keyCode == currentHotkey.keyCode else { + return Unmanaged.passUnretained(event) + } + + let isPressed = event.flags.contains(.maskSecondaryFn) + DispatchQueue.main.async { [weak self] in + self?.handleHotkeyStateChange(isPressed: isPressed) + } + + // Suppress the Fn flagsChanged event so terminal apps do not receive raw CSI sequences. + return nil + } + private func handleHotkeyEvent(_ event: NSEvent) { let currentHotkey = getSelectedHotkey() guard event.keyCode == currentHotkey.keyCode else { return } let isPressed = event.modifierFlags.contains(currentHotkey.modifierFlag) - guard !isDuplicateHotkeyEvent(event, isPressed: isPressed) else { return } + handleHotkeyStateChange(isPressed: isPressed) + } + + private func handleHotkeyStateChange(isPressed: Bool) { + guard !isDuplicateHotkeyEvent(isPressed: isPressed) else { return } + let currentHotkey = getSelectedHotkey() if isPressed && !isHotkeyPressed { isHotkeyPressed = true @@ -100,12 +177,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - private func isDuplicateHotkeyEvent(_ event: NSEvent, isPressed: Bool) -> Bool { + private func isDuplicateHotkeyEvent(isPressed: Bool) -> Bool { + let now = ProcessInfo.processInfo.systemUptime let isDuplicate = - abs(event.timestamp - lastHandledHotkeyTimestamp) < 0.05 + abs(now - lastHandledHotkeyTimestamp) < 0.05 && lastHandledHotkeyPressedState == isPressed - lastHandledHotkeyTimestamp = event.timestamp + lastHandledHotkeyTimestamp = now lastHandledHotkeyPressedState = isPressed return isDuplicate }