//go:build darwin package input /* #cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework AppKit -framework CoreGraphics -framework Carbon -framework ApplicationServices #include #include #include #include #include #include static int ensureInputPermission(void) { NSDictionary* opts = @{(__bridge NSString*)kAXTrustedCheckOptionPrompt: @YES}; return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0; } static void setClipboardText(const char* text) { NSPasteboard* pb = [NSPasteboard generalPasteboard]; [pb clearContents]; NSString* str = [NSString stringWithUTF8String:text]; [pb setString:str forType:NSPasteboardTypeString]; } static void clearClipboardText(void) { NSPasteboard* pb = [NSPasteboard generalPasteboard]; [pb clearContents]; } static char* getClipboardText(void) { NSPasteboard* pb = [NSPasteboard generalPasteboard]; NSString* str = [pb stringForType:NSPasteboardTypeString]; if (!str) return NULL; const char* utf8 = [str UTF8String]; if (!utf8) return NULL; return strdup(utf8); } static void simulateCmdV() { CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState); CGEventRef cmdDown = CGEventCreateKeyboardEvent(source, (CGKeyCode)kVK_Command, true); CGEventSetFlags(cmdDown, kCGEventFlagMaskCommand); CGEventPost(kCGHIDEventTap, cmdDown); CGEventRef vDown = CGEventCreateKeyboardEvent(source, (CGKeyCode)kVK_ANSI_V, true); CGEventSetFlags(vDown, kCGEventFlagMaskCommand); CGEventPost(kCGHIDEventTap, vDown); CGEventRef vUp = CGEventCreateKeyboardEvent(source, (CGKeyCode)kVK_ANSI_V, false); CGEventSetFlags(vUp, kCGEventFlagMaskCommand); CGEventPost(kCGHIDEventTap, vUp); CGEventRef cmdUp = CGEventCreateKeyboardEvent(source, (CGKeyCode)kVK_Command, false); CGEventSetFlags(cmdUp, 0); CGEventPost(kCGHIDEventTap, cmdUp); CFRelease(cmdDown); CFRelease(vDown); CFRelease(vUp); CFRelease(cmdUp); CFRelease(source); } static void typeUnicodeChar(unsigned int codepoint) { CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState); UniChar chars[2]; int len = 0; if (codepoint <= 0xFFFF) { chars[0] = (UniChar)codepoint; len = 1; } else { codepoint -= 0x10000; chars[0] = (UniChar)(0xD800 + (codepoint >> 10)); chars[1] = (UniChar)(0xDC00 + (codepoint & 0x3FF)); len = 2; } CGEventRef keyDown = CGEventCreateKeyboardEvent(source, 0, true); CGEventKeyboardSetUnicodeString(keyDown, len, chars); CGEventPost(kCGHIDEventTap, keyDown); CGEventRef keyUp = CGEventCreateKeyboardEvent(source, 0, false); CGEventKeyboardSetUnicodeString(keyUp, len, chars); CGEventPost(kCGHIDEventTap, keyUp); CFRelease(keyDown); CFRelease(keyUp); CFRelease(source); } */ import "C" import ( "fmt" "sync" "time" "unsafe" "voicesnap/internal/logger" ) const ( darwinClipboardVerifyTimeout = 500 * time.Millisecond darwinClipboardPollInterval = 15 * time.Millisecond darwinClipboardRestoreDelay = 900 * time.Millisecond ) type darwinPaster struct{} type darwinClipboardSnapshot struct { text string hasText bool } type darwinPasteState struct { mu sync.Mutex generation uint64 pendingRestore bool pendingText string restoreTo darwinClipboardSnapshot } var globalDarwinPasteState darwinPasteState func newPlatformPaster() Paster { return &darwinPaster{} } func (p *darwinPaster) Paste(text string, keepClipboard bool) error { pasteStart := time.Now() textRunes := len([]rune(text)) permissionStart := time.Now() if C.ensureInputPermission() == 0 { logger.Info( "PERF paste_clipboard_done result=permission_error permission_ms=%d text_runes=%d keep_clipboard=%t total_ms=%d", time.Since(permissionStart).Milliseconds(), textRunes, keepClipboard, time.Since(pasteStart).Milliseconds(), ) return fmt.Errorf("accessibility permission is required to paste") } permissionElapsed := time.Since(permissionStart) state := &globalDarwinPasteState stateLockStart := time.Now() state.mu.Lock() stateLockElapsed := time.Since(stateLockStart) state.generation++ generation := state.generation snapshotStart := time.Now() originalClipboard := currentDarwinClipboardSnapshot() if !keepClipboard { originalClipboard = resolveDarwinRestoreTarget( originalClipboard, state.pendingRestore, state.pendingText, state.restoreTo, ) } snapshotElapsed := time.Since(snapshotStart) cstr := C.CString(text) defer C.free(unsafe.Pointer(cstr)) setClipboardStart := time.Now() C.setClipboardText(cstr) setClipboardElapsed := time.Since(setClipboardStart) verifyStart := time.Now() if !waitForDarwinClipboardText(text, darwinClipboardVerifyTimeout) { verifyElapsed := time.Since(verifyStart) state.pendingRestore = false state.mu.Unlock() logger.Info( "PERF paste_clipboard_done result=verify_failed permission_ms=%d state_lock_ms=%d snapshot_ms=%d set_ms=%d verify_ms=%d text_runes=%d keep_clipboard=%t total_ms=%d", permissionElapsed.Milliseconds(), stateLockElapsed.Milliseconds(), snapshotElapsed.Milliseconds(), setClipboardElapsed.Milliseconds(), verifyElapsed.Milliseconds(), textRunes, keepClipboard, time.Since(pasteStart).Milliseconds(), ) return fmt.Errorf("clipboard verification failed before paste") } verifyElapsed := time.Since(verifyStart) cmdVStart := time.Now() C.simulateCmdV() cmdVElapsed := time.Since(cmdVStart) if keepClipboard { state.pendingRestore = false state.mu.Unlock() logger.Info( "PERF paste_clipboard_done result=success permission_ms=%d state_lock_ms=%d snapshot_ms=%d set_ms=%d verify_ms=%d cmdv_ms=%d text_runes=%d keep_clipboard=%t total_ms=%d", permissionElapsed.Milliseconds(), stateLockElapsed.Milliseconds(), snapshotElapsed.Milliseconds(), setClipboardElapsed.Milliseconds(), verifyElapsed.Milliseconds(), cmdVElapsed.Milliseconds(), textRunes, keepClipboard, time.Since(pasteStart).Milliseconds(), ) return nil } state.pendingRestore = true state.pendingText = text state.restoreTo = originalClipboard state.mu.Unlock() go restoreDarwinClipboardAfterPaste(generation, text, originalClipboard) logger.Info( "PERF paste_clipboard_done result=success permission_ms=%d state_lock_ms=%d snapshot_ms=%d set_ms=%d verify_ms=%d cmdv_ms=%d text_runes=%d keep_clipboard=%t total_ms=%d", permissionElapsed.Milliseconds(), stateLockElapsed.Milliseconds(), snapshotElapsed.Milliseconds(), setClipboardElapsed.Milliseconds(), verifyElapsed.Milliseconds(), cmdVElapsed.Milliseconds(), textRunes, keepClipboard, time.Since(pasteStart).Milliseconds(), ) return nil } func (p *darwinPaster) TypeText(text string) error { if C.ensureInputPermission() == 0 { return fmt.Errorf("accessibility permission is required to type") } for _, r := range text { C.typeUnicodeChar(C.uint(r)) } return nil } func currentDarwinClipboardSnapshot() darwinClipboardSnapshot { cText := C.getClipboardText() if cText == nil { return darwinClipboardSnapshot{} } defer C.free(unsafe.Pointer(cText)) return darwinClipboardSnapshot{ text: C.GoString(cText), hasText: true, } } func resolveDarwinRestoreTarget(current darwinClipboardSnapshot, pending bool, pendingText string, restoreTo darwinClipboardSnapshot) darwinClipboardSnapshot { if pending && current.hasText && current.text == pendingText { return restoreTo } return current } func waitForDarwinClipboardText(expected string, timeout time.Duration) bool { deadline := time.Now().Add(timeout) for { current := currentDarwinClipboardSnapshot() if current.hasText && current.text == expected { return true } if time.Now().After(deadline) { return false } time.Sleep(darwinClipboardPollInterval) } } func restoreDarwinClipboardAfterPaste(generation uint64, pastedText string, restoreTo darwinClipboardSnapshot) { time.Sleep(darwinClipboardRestoreDelay) state := &globalDarwinPasteState state.mu.Lock() defer state.mu.Unlock() if state.generation != generation || !state.pendingRestore || state.pendingText != pastedText { return } current := currentDarwinClipboardSnapshot() if !shouldRestoreDarwinClipboard( state.generation, generation, state.pendingRestore, state.pendingText, pastedText, current, ) { state.pendingRestore = false return } restoreDarwinClipboardSnapshot(restoreTo) state.pendingRestore = false } func shouldRestoreDarwinClipboard(currentGeneration, restoreGeneration uint64, pending bool, pendingText, pastedText string, current darwinClipboardSnapshot) bool { if currentGeneration != restoreGeneration || !pending || pendingText != pastedText { return false } return current.hasText && current.text == pastedText } func restoreDarwinClipboardSnapshot(snapshot darwinClipboardSnapshot) { if !snapshot.hasText { C.clearClipboardText() return } cText := C.CString(snapshot.text) defer C.free(unsafe.Pointer(cText)) C.setClipboardText(cText) }