| | |
| | | #cgo LDFLAGS: -framework Cocoa -framework ApplicationServices |
| | | #import <Cocoa/Cocoa.h> |
| | | #include <ApplicationServices/ApplicationServices.h> |
| | | #include <pthread.h> |
| | | static volatile int g_keysDown[128]; |
| | | static volatile double g_lastDownAt[128]; |
| | | static volatile int g_monitorStarted = 0; |
| | | static volatile int g_monitorLaunching = 0; |
| | | static CFMachPortRef g_eventTap = NULL; |
| | | static CFRunLoopSourceRef g_eventTapSource = NULL; |
| | | static volatile unsigned long long g_monitorStartAttempts = 0; |
| | | static volatile unsigned long long g_eventTapCreateFailures = 0; |
| | | static volatile unsigned long long g_eventTapDisabledCount = 0; |
| | | static volatile unsigned long long g_altEventSeq[2]; |
| | | static volatile int g_altEventDown[2]; |
| | | static volatile int g_altEventType[2]; |
| | | static volatile unsigned long long g_altEventFlags[2]; |
| | | static volatile double g_altEventAt[2]; |
| | | static id g_activityToken = nil; |
| | | |
| | | static double nowSeconds(void) { |
| | | return CFAbsoluteTimeGetCurrent(); |
| | |
| | | if (mask == 0) return; |
| | | |
| | | BOOL physicalDown = CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, (CGKeyCode)keyCode); |
| | | if (physicalDown) { |
| | | handleKeyEvent(keyCode, YES); |
| | | recordAltEvent(keyCode, 3, flags); |
| | | return; |
| | | } |
| | | |
| | | BOOL groupDown = (flags & mask) ? YES : NO; |
| | | handleKeyEvent(keyCode, groupDown ? YES : NO); |
| | | handleKeyEvent(keyCode, physicalDown ? YES : NO); |
| | | recordAltEvent(keyCode, 3, flags); |
| | | } |
| | | |
| | | static CGEventRef keyboardEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { |
| | | if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) { |
| | | g_eventTapDisabledCount++; |
| | | if (g_eventTap != NULL) CGEventTapEnable(g_eventTap, true); |
| | | return event; |
| | | } |
| | |
| | | } |
| | | |
| | | static void startEventTapMonitor(void) { |
| | | if (g_monitorStarted) return; |
| | | if (g_monitorStarted) { |
| | | g_monitorLaunching = 0; |
| | | return; |
| | | } |
| | | g_monitorStartAttempts++; |
| | | |
| | | CGEventMask eventMask = CGEventMaskBit(kCGEventKeyDown) | |
| | | CGEventMaskBit(kCGEventKeyUp) | |
| | |
| | | eventMask, |
| | | keyboardEventTapCallback, |
| | | NULL); |
| | | if (g_eventTap == NULL) return; |
| | | if (g_eventTap == NULL) { |
| | | g_eventTapCreateFailures++; |
| | | g_monitorLaunching = 0; |
| | | return; |
| | | } |
| | | |
| | | g_eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0); |
| | | if (g_eventTapSource == NULL) { |
| | | g_eventTapCreateFailures++; |
| | | CFRelease(g_eventTap); |
| | | g_eventTap = NULL; |
| | | g_monitorLaunching = 0; |
| | | return; |
| | | } |
| | | |
| | | CFRunLoopAddSource(CFRunLoopGetCurrent(), g_eventTapSource, kCFRunLoopCommonModes); |
| | | CGEventTapEnable(g_eventTap, true); |
| | | g_monitorStarted = 1; |
| | | g_monitorLaunching = 0; |
| | | CFRunLoopRun(); |
| | | g_monitorStarted = 0; |
| | | } |
| | | |
| | | static int ensureAccessibility(void) { |
| | |
| | | return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0; |
| | | } |
| | | |
| | | static void startKeyMonitor(void) { |
| | | dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ |
| | | static void beginHotkeyActivity(void) { |
| | | if (g_activityToken != nil) return; |
| | | g_activityToken = [[NSProcessInfo processInfo] |
| | | beginActivityWithOptions:NSActivityUserInitiatedAllowingIdleSystemSleep |
| | | reason:@"VoiceSnap global hotkey listener"]; |
| | | } |
| | | |
| | | static void* eventTapThreadMain(void* arg) { |
| | | (void)arg; |
| | | @autoreleasepool { |
| | | startEventTapMonitor(); |
| | | }); |
| | | } |
| | | g_monitorLaunching = 0; |
| | | return NULL; |
| | | } |
| | | |
| | | static void startKeyMonitor(void) { |
| | | if (g_monitorStarted || g_monitorLaunching) return; |
| | | g_monitorLaunching = 1; |
| | | pthread_t thread; |
| | | int err = pthread_create(&thread, NULL, eventTapThreadMain, NULL); |
| | | if (err != 0) { |
| | | g_monitorLaunching = 0; |
| | | g_eventTapCreateFailures++; |
| | | return; |
| | | } |
| | | pthread_detach(thread); |
| | | } |
| | | |
| | | // isModifierDown polls the current system modifier flags via CGEventSource. |
| | |
| | | |
| | | static int isSideModifierDown(int keyCode) { |
| | | if (keyCode >= 0 && keyCode < 128) { |
| | | if (g_keysDown[keyCode]) return 1; |
| | | if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, (CGKeyCode)keyCode)) return 1; |
| | | if (g_keysDown[keyCode] && g_lastDownAt[keyCode] > 0 && nowSeconds() - g_lastDownAt[keyCode] < 0.25) return 1; |
| | | if (g_keysDown[keyCode]) g_keysDown[keyCode] = 0; |
| | | if (g_lastDownAt[keyCode] > 0 && nowSeconds() - g_lastDownAt[keyCode] < 0.12) return 1; |
| | | } |
| | | return 0; |
| | |
| | | |
| | | static int isKeyDown(int keyCode) { |
| | | if (keyCode >= 0 && keyCode < 128) { |
| | | if (g_keysDown[keyCode]) return 1; |
| | | if (CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, (CGKeyCode)keyCode)) return 1; |
| | | if (g_keysDown[keyCode] && g_lastDownAt[keyCode] > 0 && nowSeconds() - g_lastDownAt[keyCode] < 0.25) return 1; |
| | | if (g_keysDown[keyCode]) g_keysDown[keyCode] = 0; |
| | | if (g_lastDownAt[keyCode] > 0 && nowSeconds() - g_lastDownAt[keyCode] < 0.12) return 1; |
| | | } |
| | | return 0; |
| | | } |
| | | |
| | | static int isPhysicalKeyDown(int keyCode) { |
| | | if (keyCode < 0 || keyCode >= 128) return 0; |
| | | return CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, (CGKeyCode)keyCode) ? 1 : 0; |
| | | } |
| | | |
| | | static int isTrackedKeyDown(int keyCode) { |
| | | if (keyCode < 0 || keyCode >= 128) return 0; |
| | | return g_keysDown[keyCode] ? 1 : 0; |
| | | } |
| | | |
| | | static int isRecentKeyDown(int keyCode) { |
| | | if (keyCode < 0 || keyCode >= 128 || g_lastDownAt[keyCode] <= 0) return 0; |
| | | return nowSeconds() - g_lastDownAt[keyCode] < 0.12 ? 1 : 0; |
| | | } |
| | | |
| | | static double lastKeyDownAge(int keyCode) { |
| | |
| | | |
| | | static int isMonitorStarted(void) { |
| | | return g_monitorStarted; |
| | | } |
| | | |
| | | static int isMonitorLaunching(void) { |
| | | return g_monitorLaunching; |
| | | } |
| | | |
| | | static unsigned long long monitorStartAttempts(void) { |
| | | return g_monitorStartAttempts; |
| | | } |
| | | |
| | | static unsigned long long eventTapCreateFailures(void) { |
| | | return g_eventTapCreateFailures; |
| | | } |
| | | |
| | | static unsigned long long eventTapDisabledCount(void) { |
| | | return g_eventTapDisabledCount; |
| | | } |
| | | |
| | | static unsigned long long altEventSeq(int index) { |
| | |
| | | type darwinListener struct { |
| | | lastAltSeq map[int]uint64 |
| | | lastAltState map[int]bool |
| | | lastMonitorRetry time.Time |
| | | } |
| | | |
| | | func newPlatformListener() Listener { |
| | | C.beginHotkeyActivity() |
| | | trusted := C.ensureAccessibility() |
| | | if trusted == 0 { |
| | | logger.Info("Accessibility permission not granted for current app identity — hotkey won't work until VoiceSnap is removed and re-added in System Settings > Privacy & Security > Accessibility") |
| | |
| | | logger.Info("Accessibility permission granted") |
| | | } |
| | | C.startKeyMonitor() |
| | | time.Sleep(150 * time.Millisecond) |
| | | deadline := time.Now().Add(time.Second) |
| | | for C.isMonitorStarted() == 0 && C.isMonitorLaunching() != 0 && time.Now().Before(deadline) { |
| | | time.Sleep(50 * time.Millisecond) |
| | | } |
| | | if C.isMonitorStarted() == 0 { |
| | | logger.Error("Keyboard event tap failed to start; check Accessibility permission") |
| | | logger.Error( |
| | | "Keyboard event tap not running yet; attempts=%d failures=%d launching=%t. Physical hotkey polling fallback is enabled.", |
| | | uint64(C.monitorStartAttempts()), |
| | | uint64(C.eventTapCreateFailures()), |
| | | C.isMonitorLaunching() != 0, |
| | | ) |
| | | } else { |
| | | logger.Info("Keyboard event tap started") |
| | | logger.Info( |
| | | "Keyboard event tap started; attempts=%d failures=%d disabled=%d", |
| | | uint64(C.monitorStartAttempts()), |
| | | uint64(C.eventTapCreateFailures()), |
| | | uint64(C.eventTapDisabledCount()), |
| | | ) |
| | | } |
| | | return &darwinListener{ |
| | | lastAltSeq: map[int]uint64{}, |
| | | lastAltState: map[int]bool{}, |
| | | lastMonitorRetry: time.Now(), |
| | | } |
| | | } |
| | | |
| | | func (l *darwinListener) IsKeyDown(vk int) bool { |
| | | l.retryMonitorIfNeeded() |
| | | if keyCode, ok := vkToPhysicalModifier[vk]; ok { |
| | | down := C.isSideModifierDown(C.int(keyCode)) != 0 |
| | | l.logAltDiagnostics(vk, keyCode, down) |
| | |
| | | return C.isKeyDown(C.int(macKey)) != 0 |
| | | } |
| | | |
| | | func (l *darwinListener) retryMonitorIfNeeded() { |
| | | if C.isMonitorStarted() != 0 || C.isMonitorLaunching() != 0 { |
| | | return |
| | | } |
| | | if time.Since(l.lastMonitorRetry) < 10*time.Second { |
| | | return |
| | | } |
| | | l.lastMonitorRetry = time.Now() |
| | | logger.Info( |
| | | "Keyboard event tap retry requested; attempts=%d failures=%d disabled=%d", |
| | | uint64(C.monitorStartAttempts()), |
| | | uint64(C.eventTapCreateFailures()), |
| | | uint64(C.eventTapDisabledCount()), |
| | | ) |
| | | C.startKeyMonitor() |
| | | } |
| | | |
| | | func (l *darwinListener) logAltDiagnostics(vk, keyCode int, down bool) { |
| | | index := altDiagnosticIndex(keyCode) |
| | | if index < 0 { |
| | | return |
| | | } |
| | | |
| | | physicalDown := C.isPhysicalKeyDown(C.int(keyCode)) != 0 |
| | | trackedDown := C.isTrackedKeyDown(C.int(keyCode)) != 0 |
| | | recentDown := C.isRecentKeyDown(C.int(keyCode)) != 0 |
| | | |
| | | seq := uint64(C.altEventSeq(C.int(index))) |
| | | if seq != l.lastAltSeq[vk] { |
| | | l.lastAltSeq[vk] = seq |
| | | logger.Info( |
| | | "Hotkey event tap: key=%s vk=0x%X keyCode=%d seq=%d event=%s eventDown=%t pollDown=%t flags=0x%X age=%.3fs tapStarted=%t", |
| | | "Hotkey event tap: key=%s vk=0x%X keyCode=%d seq=%d event=%s eventDown=%t pollDown=%t physicalDown=%t trackedDown=%t recentDown=%t flags=0x%X age=%.3fs tapStarted=%t", |
| | | GetKeyName(vk), |
| | | vk, |
| | | keyCode, |
| | |
| | | altEventTypeName(int(C.altEventType(C.int(index)))), |
| | | C.altEventDown(C.int(index)) != 0, |
| | | down, |
| | | physicalDown, |
| | | trackedDown, |
| | | recentDown, |
| | | uint64(C.altEventFlags(C.int(index))), |
| | | float64(C.altEventAge(C.int(index))), |
| | | C.isMonitorStarted() != 0, |
| | |
| | | |
| | | if previous, ok := l.lastAltState[vk]; !ok || previous != down { |
| | | l.lastAltState[vk] = down |
| | | logger.Info("Hotkey poll: key=%s vk=0x%X keyCode=%d down=%t tapStarted=%t", GetKeyName(vk), vk, keyCode, down, C.isMonitorStarted() != 0) |
| | | logger.Info( |
| | | "Hotkey poll: key=%s vk=0x%X keyCode=%d down=%t source=%s physicalDown=%t trackedDown=%t recentDown=%t tapStarted=%t", |
| | | GetKeyName(vk), |
| | | vk, |
| | | keyCode, |
| | | down, |
| | | hotkeySource(down, physicalDown, trackedDown, recentDown), |
| | | physicalDown, |
| | | trackedDown, |
| | | recentDown, |
| | | C.isMonitorStarted() != 0, |
| | | ) |
| | | } |
| | | } |
| | | |
| | | func hotkeySource(down, physicalDown, trackedDown, recentDown bool) string { |
| | | switch { |
| | | case !down: |
| | | return "none" |
| | | case physicalDown: |
| | | return "physical" |
| | | case trackedDown: |
| | | return "eventTap" |
| | | case recentDown: |
| | | return "recent" |
| | | default: |
| | | return "unknown" |
| | | } |
| | | } |
| | | |