//go:build darwin package hotkey /* #cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework Cocoa -framework ApplicationServices #import #include static volatile int g_keysDown[128]; static volatile double g_lastDownAt[128]; static volatile int g_monitorStarted = 0; static CFMachPortRef g_eventTap = NULL; static CFRunLoopSourceRef g_eventTapSource = NULL; 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 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 altIndexForKey(unsigned short keyCode) { if (keyCode == 58) return 0; // L-Option if (keyCode == 61) return 1; // R-Option return -1; } static void recordAltEvent(unsigned short keyCode, int eventType, CGEventFlags flags) { int index = altIndexForKey(keyCode); if (index < 0) return; g_altEventSeq[index]++; g_altEventDown[index] = g_keysDown[keyCode] ? 1 : 0; g_altEventType[index] = eventType; g_altEventFlags[index] = flags; g_altEventAt[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); if (physicalDown) { handleKeyEvent(keyCode, YES); recordAltEvent(keyCode, 3, flags); return; } BOOL groupDown = (flags & mask) ? YES : NO; handleKeyEvent(keyCode, groupDown ? YES : NO); recordAltEvent(keyCode, 3, flags); } static CGEventRef keyboardEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) { 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); recordAltEvent(keyCode, type == kCGEventKeyDown ? 1 : 2, CGEventGetFlags(event)); } return event; } static void startEventTapMonitor(void) { if (g_monitorStarted) return; CGEventMask eventMask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | CGEventMaskBit(kCGEventFlagsChanged); g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, eventMask, keyboardEventTapCallback, NULL); if (g_eventTap == NULL) return; g_eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0); if (g_eventTapSource == NULL) { CFRelease(g_eventTap); g_eventTap = NULL; return; } CFRunLoopAddSource(CFRunLoopGetCurrent(), g_eventTapSource, kCFRunLoopCommonModes); CGEventTapEnable(g_eventTap, true); g_monitorStarted = 1; CFRunLoopRun(); } static int ensureAccessibility(void) { NSDictionary* opts = @{(__bridge NSString*)kAXTrustedCheckOptionPrompt: @YES}; return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0; } static void startKeyMonitor(void) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ startEventTapMonitor(); }); } // 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 isSideModifierDown(int keyCode) { if (keyCode >= 0 && keyCode < 128) { if (g_keysDown[keyCode]) return 1; 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 (g_lastDownAt[keyCode] > 0 && nowSeconds() - g_lastDownAt[keyCode] < 0.12) return 1; } return 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 unsigned long long altEventSeq(int index) { if (index < 0 || index > 1) return 0; return g_altEventSeq[index]; } static int altEventDown(int index) { if (index < 0 || index > 1) return 0; return g_altEventDown[index]; } static int altEventType(int index) { if (index < 0 || index > 1) return 0; return g_altEventType[index]; } static unsigned long long altEventFlags(int index) { if (index < 0 || index > 1) return 0; return g_altEventFlags[index]; } static double altEventAge(int index) { if (index < 0 || index > 1 || g_altEventAt[index] <= 0) return -1; return nowSeconds() - g_altEventAt[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 { lastAltSeq map[int]uint64 lastAltState map[int]bool } func newPlatformListener() Listener { 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") } else { logger.Info("Accessibility permission granted") } C.startKeyMonitor() time.Sleep(150 * time.Millisecond) if C.isMonitorStarted() == 0 { logger.Error("Keyboard event tap failed to start; check Accessibility permission") } else { logger.Info("Keyboard event tap started") } return &darwinListener{ lastAltSeq: map[int]uint64{}, lastAltState: map[int]bool{}, } } func (l *darwinListener) IsKeyDown(vk int) bool { if keyCode, ok := vkToPhysicalModifier[vk]; ok { down := C.isSideModifierDown(C.int(keyCode)) != 0 l.logAltDiagnostics(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) logAltDiagnostics(vk, keyCode int, down bool) { index := altDiagnosticIndex(keyCode) if index < 0 { return } 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", GetKeyName(vk), vk, keyCode, seq, altEventTypeName(int(C.altEventType(C.int(index)))), C.altEventDown(C.int(index)) != 0, down, 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) } } func altDiagnosticIndex(keyCode int) int { switch keyCode { case 58: return 0 case 61: return 1 default: return -1 } } func altEventTypeName(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) 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 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 }