|
|
@ -148,13 +148,29 @@ public final class HotkeyManager { |
|
|
// 2. Global Event Tap for high-priority interception (Microphone Key, Option, CapsLock, Fn/Globe) |
|
|
// 2. Global Event Tap for high-priority interception (Microphone Key, Option, CapsLock, Fn/Globe) |
|
|
setupEventTap() |
|
|
setupEventTap() |
|
|
|
|
|
|
|
|
// 3. Fallback NSEvent monitor |
|
|
|
|
|
|
|
|
// 3. Dual-layer fallback NSEvent monitor (catches flagsChanged/Option even after wake/sleep) |
|
|
setupGlobalMonitor() |
|
|
setupGlobalMonitor() |
|
|
|
|
|
|
|
|
|
|
|
// 4. Watch for system and display sleep/wake to ensure event tap is always alive |
|
|
|
|
|
NotificationCenter.default.removeObserver(self, name: NSWorkspace.didWakeNotification, object: nil) |
|
|
|
|
|
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.didWakeNotification, object: nil, queue: .main) { [weak self] _ in |
|
|
|
|
|
print("HotkeyManager: System woke from sleep. Re-registering hotkeys...") |
|
|
|
|
|
self?.registerHotkeys() |
|
|
|
|
|
} |
|
|
|
|
|
NotificationCenter.default.removeObserver(self, name: NSWorkspace.screensDidWakeNotification, object: nil) |
|
|
|
|
|
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.screensDidWakeNotification, object: nil, queue: .main) { [weak self] _ in |
|
|
|
|
|
print("HotkeyManager: Screens woke. Verifying hotkey event tap...") |
|
|
|
|
|
if let tap = self?.eventTapPort { |
|
|
|
|
|
CGEvent.tapEnable(tap: tap, enable: true) |
|
|
|
|
|
} else { |
|
|
|
|
|
self?.registerHotkeys() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private func triggerAction() { |
|
|
private func triggerAction() { |
|
|
let now = Date().timeIntervalSince1970 |
|
|
let now = Date().timeIntervalSince1970 |
|
|
guard (now - lastTriggerTime) > 0.25 else { return } |
|
|
|
|
|
|
|
|
guard (now - lastTriggerTime) > 0.10 else { return } |
|
|
lastTriggerTime = now |
|
|
lastTriggerTime = now |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
self?.onHotkeyPressed?() |
|
|
self?.onHotkeyPressed?() |
|
|
@ -169,6 +185,15 @@ public final class HotkeyManager { |
|
|
guard let refcon = refcon else { return Unmanaged.passRetained(event) } |
|
|
guard let refcon = refcon else { return Unmanaged.passRetained(event) } |
|
|
let manager = Unmanaged<HotkeyManager>.fromOpaque(refcon).takeUnretainedValue() |
|
|
let manager = Unmanaged<HotkeyManager>.fromOpaque(refcon).takeUnretainedValue() |
|
|
|
|
|
|
|
|
|
|
|
// Auto-recover if macOS disabled tap on timeout or user input |
|
|
|
|
|
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput { |
|
|
|
|
|
print("HotkeyManager: EventTap was disabled by OS (\(type.rawValue)). Re-enabling now...") |
|
|
|
|
|
if let tap = manager.eventTapPort { |
|
|
|
|
|
CGEvent.tapEnable(tap: tap, enable: true) |
|
|
|
|
|
} |
|
|
|
|
|
return Unmanaged.passRetained(event) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
let flags = event.flags.rawValue |
|
|
let flags = event.flags.rawValue |
|
|
let keyCode = event.getIntegerValueField(.keyboardEventKeycode) |
|
|
let keyCode = event.getIntegerValueField(.keyboardEventKeycode) |
|
|
|
|
|
|
|
|
@ -294,12 +319,49 @@ public final class HotkeyManager { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private func setupGlobalMonitor() { |
|
|
private func setupGlobalMonitor() { |
|
|
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in |
|
|
|
|
|
guard let self = self, self.eventTapPort == nil else { return } |
|
|
|
|
|
|
|
|
if let mon = globalMonitor { |
|
|
|
|
|
NSEvent.removeMonitor(mon) |
|
|
|
|
|
self.globalMonitor = nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if self.currentPreset == .controlSpace { |
|
|
|
|
|
if event.keyCode == 49 && event.modifierFlags.contains(.control) { |
|
|
|
|
|
|
|
|
// Active backup monitor that never dies on sleep/wake |
|
|
|
|
|
globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged, .keyDown, .keyUp]) { [weak self] event in |
|
|
|
|
|
guard let self = self else { return } |
|
|
|
|
|
|
|
|
|
|
|
let flags = event.modifierFlags |
|
|
|
|
|
let keyCode = event.keyCode |
|
|
|
|
|
|
|
|
|
|
|
if self.currentPreset == .option { |
|
|
|
|
|
if keyCode == 58 || keyCode == 61 { // Left or Right Option |
|
|
|
|
|
let isAlt = flags.contains(.option) |
|
|
|
|
|
if isAlt != self.isOptionPhysicallyDown { |
|
|
|
|
|
self.isOptionPhysicallyDown = isAlt |
|
|
|
|
|
if isAlt { |
|
|
self.triggerAction() |
|
|
self.triggerAction() |
|
|
|
|
|
} else if self.currentMode == .pushToTalk { |
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
self.onHotkeyReleased?() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} else if self.currentPreset == .controlSpace { |
|
|
|
|
|
if keyCode == 49 && flags.contains(.control) { |
|
|
|
|
|
self.triggerAction() |
|
|
|
|
|
} |
|
|
|
|
|
} else if self.currentPreset == .capsLock { |
|
|
|
|
|
if keyCode == 57 { |
|
|
|
|
|
let isCaps = flags.contains(.capsLock) |
|
|
|
|
|
if isCaps != self.isCapsLockPhysicallyDown { |
|
|
|
|
|
self.isCapsLockPhysicallyDown = isCaps |
|
|
|
|
|
if isCaps { |
|
|
|
|
|
self.triggerAction() |
|
|
|
|
|
} else if self.currentMode == .pushToTalk { |
|
|
|
|
|
DispatchQueue.main.async { |
|
|
|
|
|
self.onHotkeyReleased?() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|