//go:build darwin package hotkey /* #cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework Cocoa -framework ApplicationServices #import #include #include 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 volatile int g_monitorStopping = 0; static CFMachPortRef g_eventTap = NULL; static CFRunLoopSourceRef g_eventTapSource = NULL; static CFRunLoopRef g_eventTapRunLoop = 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_modifierEventSeq[4]; static volatile int g_modifierEventDown[4]; static volatile int g_modifierEventType[4]; static volatile unsigned long long g_modifierEventFlags[4]; static volatile double g_modifierEventAt[4]; static id g_activityToken = nil; static double nowSeconds(void) { return CFAbsoluteTimeGetCurrent(); } static void handleKeyEvent(unsigned short keyCode, BOOL isDown) { if (keyCode >= 128) return; g_keysDown[keyCode] = isDown ? 1 : 0; if (isDown) g_lastDownAt[keyCode] = nowSeconds(); } static int modifierDiagnosticIndexForKey(unsigned short keyCode) { if (keyCode == 58) return 0; // L-Option if (keyCode == 61) return 1; // R-Option if (keyCode == 55) return 2; // L-Command if (keyCode == 54) return 3; // R-Command return -1; } static void recordModifierEvent(unsigned short keyCode, int eventType, CGEventFlags flags) { int index = modifierDiagnosticIndexForKey(keyCode); if (index < 0) return; g_modifierEventSeq[index]++; g_modifierEventDown[index] = g_keysDown[keyCode] ? 1 : 0; g_modifierEventType[index] = eventType; g_modifierEventFlags[index] = flags; g_modifierEventAt[index] = nowSeconds(); } static unsigned long long modifierMaskForKey(unsigned short keyCode) { switch (keyCode) { case 59: // L-Ctrl case 62: // R-Ctrl return kCGEventFlagMaskControl; case 58: // L-Option case 61: // R-Option return kCGEventFlagMaskAlternate; case 56: // L-Shift case 60: // R-Shift return kCGEventFlagMaskShift; case 55: // L-Command case 54: // R-Command return kCGEventFlagMaskCommand; default: return 0; } } static void handleModifierEvent(unsigned short keyCode, CGEventFlags flags) { if (keyCode >= 128) return; unsigned long long mask = modifierMaskForKey(keyCode); if (mask == 0) return; BOOL physicalDown = CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, (CGKeyCode)keyCode); BOOL eventDown = (flags & mask) ? YES : NO; handleKeyEvent(keyCode, (physicalDown || eventDown) ? YES : NO); recordModifierEvent(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; } if (type != kCGEventKeyDown && type != kCGEventKeyUp && type != kCGEventFlagsChanged) { return event; } unsigned short keyCode = (unsigned short)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); if (type == kCGEventFlagsChanged) { handleModifierEvent(keyCode, CGEventGetFlags(event)); } else { handleKeyEvent(keyCode, type == kCGEventKeyDown); recordModifierEvent(keyCode, type == kCGEventKeyDown ? 1 : 2, CGEventGetFlags(event)); } return event; } static void startEventTapMonitor(void) { if (g_monitorStarted) { g_monitorLaunching = 0; return; } g_monitorStartAttempts++; CGEventMask eventMask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged); g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, eventMask, keyboardEventTapCallback, NULL); 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; g_eventTapRunLoop = CFRunLoopGetCurrent(); if (g_eventTapRunLoop != NULL) CFRetain(g_eventTapRunLoop); CFRunLoopRun(); if (g_eventTap != NULL) CGEventTapEnable(g_eventTap, false); if (g_eventTapSource != NULL) { CFRunLoopRemoveSource(CFRunLoopGetCurrent(), g_eventTapSource, kCFRunLoopCommonModes); CFRelease(g_eventTapSource); g_eventTapSource = NULL; } if (g_eventTap != NULL) { CFRelease(g_eventTap); g_eventTap = NULL; } if (g_eventTapRunLoop != NULL) { CFRelease(g_eventTapRunLoop); g_eventTapRunLoop = NULL; } g_monitorStarted = 0; g_monitorStopping = 0; } static int ensureAccessibility(void) { NSDictionary* opts = @{(__bridge NSString*)kAXTrustedCheckOptionPrompt: @YES}; return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0; } static void beginHotkeyActivity(void) { if (g_activityToken != nil) return; g_activityToken = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityUserInitiatedAllowingIdleSystemSleep reason:@"PrivateVoice Dictation global hotkey listener"]; } static void endHotkeyActivity(void) { if (g_activityToken == nil) return; [[NSProcessInfo processInfo] endActivity:g_activityToken]; g_activityToken = nil; } static void* eventTapThreadMain(void* arg) { (void)arg; @autoreleasepool { startEventTapMonitor(); } g_monitorLaunching = 0; return NULL; } static void startKeyMonitor(void) { if (g_monitorStarted || g_monitorLaunching || g_monitorStopping) 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); } static void stopKeyMonitor(void) { g_monitorStopping = 1; if (g_eventTap != NULL) CGEventTapEnable(g_eventTap, false); if (g_eventTapRunLoop != NULL) { CFRunLoopStop(g_eventTapRunLoop); } endHotkeyActivity(); } // isModifierDown polls the current system modifier flags via CGEventSource. // This works globally without needing event tap or NSEvent monitor callbacks. static int isModifierDown(unsigned long long flagMask) { CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState); return (flags & flagMask) ? 1 : 0; } static int isModifierDownCombined(unsigned long long flagMask) { CGEventFlags flags = CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState); return (flags & flagMask) ? 1 : 0; } static int isModifierDownAny(unsigned long long flagMask) { if (isModifierDown(flagMask)) return 1; return isModifierDownCombined(flagMask); } static int isCommandKey(int keyCode) { return keyCode == 54 || keyCode == 55; } static int isPhysicalKeyDownHID(int keyCode) { if (keyCode < 0 || keyCode >= 128) return 0; return CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, (CGKeyCode)keyCode) ? 1 : 0; } static int isPhysicalKeyDownCombined(int keyCode) { if (keyCode < 0 || keyCode >= 128) return 0; return CGEventSourceKeyState(kCGEventSourceStateCombinedSessionState, (CGKeyCode)keyCode) ? 1 : 0; } static int sideModifierDownSource(int keyCode) { if (keyCode >= 0 && keyCode < 128) { if (isPhysicalKeyDownHID(keyCode)) return 1; if (isPhysicalKeyDownCombined(keyCode)) return 2; unsigned long long mask = modifierMaskForKey((unsigned short)keyCode); int modifierDown = mask != 0 ? isModifierDownAny(mask) : 0; double age = g_lastDownAt[keyCode] > 0 ? nowSeconds() - g_lastDownAt[keyCode] : 999.0; if (g_keysDown[keyCode]) { if (modifierDown) return 3; if (age < 0.25) return 4; g_keysDown[keyCode] = 0; return 0; } if (age < 0.12) return 4; if (isCommandKey(keyCode) && modifierDown) return 5; } return 0; } static int isSideModifierDown(int keyCode) { int source = sideModifierDownSource(keyCode); return source != 0 && source != 5; } static int isKeyDown(int keyCode) { if (keyCode >= 0 && keyCode < 128) { if (isPhysicalKeyDownHID(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) { return isPhysicalKeyDownHID(keyCode); } 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) { if (keyCode < 0 || keyCode >= 128 || g_lastDownAt[keyCode] <= 0) return -1; return nowSeconds() - g_lastDownAt[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 modifierEventSeq(int index) { if (index < 0 || index > 3) return 0; return g_modifierEventSeq[index]; } static int modifierEventDown(int index) { if (index < 0 || index > 3) return 0; return g_modifierEventDown[index]; } static int modifierEventType(int index) { if (index < 0 || index > 3) return 0; return g_modifierEventType[index]; } static unsigned long long modifierEventFlags(int index) { if (index < 0 || index > 3) return 0; return g_modifierEventFlags[index]; } static double modifierEventAge(int index) { if (index < 0 || index > 3 || g_modifierEventAt[index] <= 0) return -1; return nowSeconds() - g_modifierEventAt[index]; } */ import "C" import ( "fmt" "time" "voicesnap/internal/logger" ) // macOS modifier flag masks for CGEventSourceFlagsState polling. const ( maskControl = 0x40000 // kCGEventFlagMaskControl maskShift = 0x20000 // kCGEventFlagMaskShift maskOption = 0x80000 // kCGEventFlagMaskAlternate maskCommand = 0x100000 // kCGEventFlagMaskCommand maskCapsLock = 0x10000 // kCGEventFlagMaskAlphaShift ) // Generic modifier VK codes use flag polling; left/right variants use physical // key polling because CGEvent modifier flags do not distinguish sides. var vkToFlagMask = map[int]C.ulonglong{ 0x11: maskControl, // Ctrl 0x12: maskOption, // Alt 0x10: maskShift, // Shift 0x14: maskCapsLock, // Caps Lock 0x5B: maskCommand, // Cmd 0x5C: maskCommand, // Cmd } var vkToPhysicalModifier = map[int]int{ 0xA2: 59, // L-Ctrl 0xA3: 62, // R-Ctrl 0xA4: 58, // L-Alt / Option 0xA5: 61, // R-Alt / Option 0xA0: 56, // L-Shift 0xA1: 60, // R-Shift 0x5B: 55, // L-Cmd 0x5C: 54, // R-Cmd } // macOS key code mapping for non-modifier keys. var vkToMac = map[int]int{ 0x20: 0x31, // Space 0x09: 0x30, // Tab 0x0D: 0x24, // Enter/Return 0x1B: 0x35, // Esc } type darwinListener struct { lastModifierSeq map[int]uint64 lastModifierState 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 PrivateVoice Dictation is removed and re-added in System Settings > Privacy & Security > Accessibility") } else { logger.Info("Accessibility permission granted") } C.startKeyMonitor() 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 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; attempts=%d failures=%d disabled=%d", uint64(C.monitorStartAttempts()), uint64(C.eventTapCreateFailures()), uint64(C.eventTapDisabledCount()), ) } return &darwinListener{ lastModifierSeq: map[int]uint64{}, lastModifierState: map[int]bool{}, lastMonitorRetry: time.Now(), } } func (l *darwinListener) IsKeyDown(vk int) bool { l.retryMonitorIfNeeded() if keyCode, ok := vkToPhysicalModifier[vk]; ok { source := int(C.sideModifierDownSource(C.int(keyCode))) down := sideModifierSourceAccepted(source) l.logModifierDiagnostics(vk, keyCode, down) return down } // Generic modifier keys: poll system flags directly. if mask, ok := vkToFlagMask[vk]; ok { return C.isModifierDown(mask) != 0 } // Regular keys: check NSEvent-tracked state macKey, ok := vkToMac[vk] if !ok { if vk >= 0x41 && vk <= 0x5A { macKey = vkToMacAlpha(vk) } else { return false } } 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) logModifierDiagnostics(vk, keyCode int, down bool) { index := modifierDiagnosticIndex(keyCode) if index < 0 { return } physicalHIDDown := C.isPhysicalKeyDown(C.int(keyCode)) != 0 physicalCombinedDown := C.isPhysicalKeyDownCombined(C.int(keyCode)) != 0 trackedDown := C.isTrackedKeyDown(C.int(keyCode)) != 0 recentDown := C.isRecentKeyDown(C.int(keyCode)) != 0 source := hotkeySourceName(int(C.sideModifierDownSource(C.int(keyCode)))) var aggregateFlagDown bool if mask := modifierFlagMaskForKeyCode(keyCode); mask != 0 { aggregateFlagDown = C.isModifierDownAny(mask) != 0 } seq := uint64(C.modifierEventSeq(C.int(index))) if seq != l.lastModifierSeq[vk] { l.lastModifierSeq[vk] = seq logger.Info( "Hotkey event tap: key=%s vk=0x%X keyCode=%d seq=%d event=%s eventDown=%t pollDown=%t source=%s physicalHID=%t physicalCombined=%t trackedDown=%t recentDown=%t aggregateFlagDown=%t flags=0x%X age=%.3fs tapStarted=%t launching=%t attempts=%d failures=%d disabled=%d", GetKeyName(vk), vk, keyCode, seq, modifierEventTypeName(int(C.modifierEventType(C.int(index)))), C.modifierEventDown(C.int(index)) != 0, down, source, physicalHIDDown, physicalCombinedDown, trackedDown, recentDown, aggregateFlagDown, uint64(C.modifierEventFlags(C.int(index))), float64(C.modifierEventAge(C.int(index))), C.isMonitorStarted() != 0, C.isMonitorLaunching() != 0, uint64(C.monitorStartAttempts()), uint64(C.eventTapCreateFailures()), uint64(C.eventTapDisabledCount()), ) } if previous, ok := l.lastModifierState[vk]; !ok || previous != down { l.lastModifierState[vk] = down logger.Info( "Hotkey poll: key=%s vk=0x%X keyCode=%d down=%t source=%s physicalHID=%t physicalCombined=%t trackedDown=%t recentDown=%t aggregateFlagDown=%t tapStarted=%t launching=%t attempts=%d failures=%d disabled=%d", GetKeyName(vk), vk, keyCode, down, source, physicalHIDDown, physicalCombinedDown, trackedDown, recentDown, aggregateFlagDown, C.isMonitorStarted() != 0, C.isMonitorLaunching() != 0, uint64(C.monitorStartAttempts()), uint64(C.eventTapCreateFailures()), uint64(C.eventTapDisabledCount()), ) } } func sideModifierSourceAccepted(source int) bool { return source > 0 && source != 5 } func modifierFlagMaskForKeyCode(keyCode int) C.ulonglong { switch keyCode { case 59, 62: return maskControl case 58, 61: return maskOption case 56, 60: return maskShift case 55, 54: return maskCommand default: return 0 } } func hotkeySourceName(source int) string { switch source { case 0: return "none" case 1: return "physicalHID" case 2: return "physicalCombined" case 3: return "eventTap" case 4: return "recent" case 5: return "aggregateCommandFlag" default: return "unknown" } } func modifierDiagnosticIndex(keyCode int) int { switch keyCode { case 58: return 0 case 61: return 1 case 55: return 2 case 54: return 3 default: return -1 } } func modifierEventTypeName(eventType int) string { switch eventType { case 1: return "keyDown" case 2: return "keyUp" case 3: return "flagsChanged" default: return fmt.Sprintf("unknown(%d)", eventType) } } func (l *darwinListener) IsAnyOtherKeyPressed(excludeVK int) bool { return l.IsAnyOtherKeyPressedSince(excludeVK, time.Time{}) } func (l *darwinListener) IsAnyOtherKeyPressedSince(excludeVK int, since time.Time) bool { excludeGroup := modifierGroup(excludeVK) modifiers := []int{0x11, 0xA2, 0xA3, 0x12, 0xA4, 0xA5, 0x10, 0xA0, 0xA1} for _, k := range modifiers { if k == excludeVK { continue } if excludeGroup != 0 && modifierGroup(k) == excludeGroup { continue } if l.wasPressedSince(k, since) { logger.Info("Hotkey combination source: key=%s vk=0x%X", GetKeyName(k), k) return true } } for vk := 0x41; vk <= 0x5A; vk++ { if vk == excludeVK { continue } if l.wasPressedSince(vk, since) { logger.Info("Hotkey combination source: key=%s vk=0x%X", GetKeyName(vk), vk) return true } } return false } func (l *darwinListener) Close() { C.stopKeyMonitor() } func (l *darwinListener) wasPressedSince(vk int, since time.Time) bool { if since.IsZero() { return l.IsKeyDown(vk) } keyCode, ok := macKeyCodeForVK(vk) if !ok { return false } age := float64(C.lastKeyDownAge(C.int(keyCode))) if age < 0 { return false } return time.Since(since).Seconds()-age >= -0.02 } func modifierGroup(vk int) int { switch vk { case 0x11, 0xA2, 0xA3: return 1 case 0x12, 0xA4, 0xA5: return 2 case 0x10, 0xA0, 0xA1: return 3 case 0x5B, 0x5C: return 4 default: return 0 } } func isGenericModifier(vk int) bool { return vk == 0x11 || vk == 0x12 || vk == 0x10 } func vkToMacAlpha(vk int) int { alphaMap := map[int]int{ 0x41: 0x00, 0x42: 0x0B, 0x43: 0x08, 0x44: 0x02, 0x45: 0x0E, 0x46: 0x03, 0x47: 0x05, 0x48: 0x04, 0x49: 0x22, 0x4A: 0x26, 0x4B: 0x28, 0x4C: 0x25, 0x4D: 0x2E, 0x4E: 0x2D, 0x4F: 0x1F, 0x50: 0x23, 0x51: 0x0C, 0x52: 0x0F, 0x53: 0x01, 0x54: 0x11, 0x55: 0x20, 0x56: 0x09, 0x57: 0x0D, 0x58: 0x07, 0x59: 0x10, 0x5A: 0x06, } if code, ok := alphaMap[vk]; ok { return code } return 0 } func macKeyCodeForVK(vk int) (int, bool) { if keyCode, ok := vkToPhysicalModifier[vk]; ok { return keyCode, true } if keyCode, ok := vkToMac[vk]; ok { return keyCode, true } if vk >= 0x41 && vk <= 0x5A { return vkToMacAlpha(vk), true } return 0, false }