//go:build darwin package axinput /* #cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework AppKit -framework ApplicationServices #include #include #include #include #include static char* pvAXCopyError(NSString* message) { const char* utf8 = [message UTF8String]; if (!utf8) return strdup("unknown error"); return strdup(utf8); } static NSString* pvAXStringAttribute(AXUIElementRef element, CFStringRef attr) { CFTypeRef valueRef = NULL; AXError err = AXUIElementCopyAttributeValue(element, attr, &valueRef); if (err != kAXErrorSuccess || !valueRef) return nil; NSString* result = nil; if (CFGetTypeID(valueRef) == CFStringGetTypeID()) { result = [(__bridge NSString*)valueRef copy]; } CFRelease(valueRef); return [result autorelease]; } static NSString* pvAXElementSummary(AXUIElementRef element) { NSMutableArray* parts = [NSMutableArray array]; pid_t pid = 0; if (AXUIElementGetPid(element, &pid) == kAXErrorSuccess && pid > 0) { NSRunningApplication* app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid]; NSString* name = [app localizedName]; if (name.length > 0) { [parts addObject:[NSString stringWithFormat:@"app=%@", name]]; } [parts addObject:[NSString stringWithFormat:@"pid=%d", pid]]; } NSString* role = pvAXStringAttribute(element, kAXRoleAttribute); if (role.length > 0) { [parts addObject:[NSString stringWithFormat:@"role=%@", role]]; } NSString* subrole = pvAXStringAttribute(element, kAXSubroleAttribute); if (subrole.length > 0) { [parts addObject:[NSString stringWithFormat:@"subrole=%@", subrole]]; } if (parts.count == 0) return @"target=unknown"; return [parts componentsJoinedByString:@" "]; } static BOOL pvAXAttributeSettable(AXUIElementRef element, CFStringRef attr, Boolean* settable) { Boolean localSettable = false; AXError err = AXUIElementIsAttributeSettable(element, attr, &localSettable); if (settable) *settable = localSettable; return err == kAXErrorSuccess; } static char* pvAXCopyResult(int ok, NSString* method, NSString* target, NSString* message) { NSString* payload = [NSString stringWithFormat:@"%d\n%@\n%@\n%@", ok, method ?: @"ax_focused_element", target ?: @"target=unknown", message ?: @""]; return pvAXCopyError(payload); } static int pvAXTrusted(int prompt) { if (prompt) { NSDictionary* opts = @{(__bridge NSString*)kAXTrustedCheckOptionPrompt: @YES}; return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)opts) ? 1 : 0; } return AXIsProcessTrusted() ? 1 : 0; } static AXUIElementRef pvAXCopyFrontmostFocusedElement(AXError* outErr) { if (outErr) *outErr = kAXErrorFailure; NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; if (!app || app.processIdentifier <= 0) { return NULL; } AXUIElementRef appElement = AXUIElementCreateApplication(app.processIdentifier); if (!appElement) { return NULL; } AXUIElementSetMessagingTimeout(appElement, 1.0); AXUIElementRef focused = NULL; AXError err = AXUIElementCopyAttributeValue(appElement, kAXFocusedUIElementAttribute, (CFTypeRef*)&focused); CFRelease(appElement); if (outErr) *outErr = err; if (err != kAXErrorSuccess || !focused) { return NULL; } return focused; } static AXUIElementRef pvAXCopyBestFocusedElement(AXError* outSystemErr, AXError* outFocusedAppErr, AXError* outFocusedAppElementErr, AXError* outFrontmostErr) { if (outSystemErr) *outSystemErr = kAXErrorFailure; if (outFocusedAppErr) *outFocusedAppErr = kAXErrorFailure; if (outFocusedAppElementErr) *outFocusedAppElementErr = kAXErrorFailure; if (outFrontmostErr) *outFrontmostErr = kAXErrorFailure; AXUIElementRef system = AXUIElementCreateSystemWide(); if (!system) { return NULL; } AXUIElementSetMessagingTimeout(system, 1.0); AXUIElementRef focused = NULL; AXError systemErr = AXUIElementCopyAttributeValue(system, kAXFocusedUIElementAttribute, (CFTypeRef*)&focused); if (outSystemErr) *outSystemErr = systemErr; if (systemErr == kAXErrorSuccess && focused) { CFRelease(system); return focused; } AXUIElementRef focusedApp = NULL; AXError focusedAppErr = AXUIElementCopyAttributeValue(system, kAXFocusedApplicationAttribute, (CFTypeRef*)&focusedApp); if (outFocusedAppErr) *outFocusedAppErr = focusedAppErr; if (focusedAppErr == kAXErrorSuccess && focusedApp) { AXUIElementSetMessagingTimeout(focusedApp, 1.0); AXError focusedAppElementErr = AXUIElementCopyAttributeValue(focusedApp, kAXFocusedUIElementAttribute, (CFTypeRef*)&focused); if (outFocusedAppElementErr) *outFocusedAppElementErr = focusedAppElementErr; CFRelease(focusedApp); if (focusedAppElementErr == kAXErrorSuccess && focused) { CFRelease(system); return focused; } } CFRelease(system); return pvAXCopyFrontmostFocusedElement(outFrontmostErr); } static char* pvAXDirectInsert(const char* text) { if (!pvAXTrusted(0)) { return pvAXCopyResult(0, @"ax_focused_element", nil, @"accessibility permission is not granted"); } NSString* insert = [NSString stringWithUTF8String:text]; if (!insert) { return pvAXCopyResult(0, @"ax_focused_element", nil, @"input text is not valid UTF-8"); } AXUIElementRef focused = NULL; AXError systemErr = kAXErrorFailure; AXError focusedAppErr = kAXErrorFailure; AXError focusedAppElementErr = kAXErrorFailure; AXError frontmostErr = kAXErrorFailure; for (int attempt = 0; attempt < 3 && !focused; attempt++) { focused = pvAXCopyBestFocusedElement(&systemErr, &focusedAppErr, &focusedAppElementErr, &frontmostErr); if (!focused && attempt < 2) { usleep(50000); } } if (!focused) { return pvAXCopyResult(0, @"ax_focused_element", nil, [NSString stringWithFormat:@"focused element unavailable: system=%d focused_app=%d focused_app_element=%d frontmost=%d", systemErr, focusedAppErr, focusedAppElementErr, frontmostErr]); } NSString* target = pvAXElementSummary(focused); CFTypeRef valueRef = NULL; CFTypeRef rangeRef = NULL; AXError valueErr = AXUIElementCopyAttributeValue(focused, kAXValueAttribute, &valueRef); AXError rangeErr = AXUIElementCopyAttributeValue(focused, kAXSelectedTextRangeAttribute, &rangeRef); if (valueErr != kAXErrorSuccess || rangeErr != kAXErrorSuccess || !valueRef || !rangeRef || CFGetTypeID(valueRef) != CFStringGetTypeID() || CFGetTypeID(rangeRef) != AXValueGetTypeID()) { if (valueRef) CFRelease(valueRef); if (rangeRef) CFRelease(rangeRef); CFRelease(focused); return pvAXCopyResult(0, @"ax_focused_element", target, [NSString stringWithFormat:@"focused text state is not readable; value=%d range=%d", valueErr, rangeErr]); } CFRange selectedRange; if (!AXValueGetValue((AXValueRef)rangeRef, kAXValueCFRangeType, &selectedRange)) { CFRelease(valueRef); CFRelease(rangeRef); CFRelease(focused); return pvAXCopyResult(0, @"ax_value_replacement", target, @"focused element selected range is unreadable"); } NSString* value = (__bridge NSString*)valueRef; NSUInteger valueLength = [value length]; if (selectedRange.location < 0 || selectedRange.length < 0 || (NSUInteger)selectedRange.location > valueLength || (NSUInteger)(selectedRange.location + selectedRange.length) > valueLength) { CFRelease(valueRef); CFRelease(rangeRef); CFRelease(focused); return pvAXCopyResult(0, @"ax_value_replacement", target, @"focused element selected range is out of bounds"); } Boolean valueSettable = false; BOOL settableKnown = pvAXAttributeSettable(focused, kAXValueAttribute, &valueSettable); if (settableKnown && !valueSettable) { CFRelease(valueRef); CFRelease(rangeRef); CFRelease(focused); return pvAXCopyResult(0, @"ax_value_replacement", target, @"focused element value is not settable"); } NSRange nsRange = NSMakeRange((NSUInteger)selectedRange.location, (NSUInteger)selectedRange.length); NSString* updated = [value stringByReplacingCharactersInRange:nsRange withString:insert]; AXError setErr = AXUIElementSetAttributeValue(focused, kAXValueAttribute, (__bridge CFTypeRef)updated); if (setErr == kAXErrorSuccess) { CFRange newRange = CFRangeMake(selectedRange.location + [insert length], 0); AXValueRef newRangeValue = AXValueCreate(kAXValueCFRangeType, &newRange); if (newRangeValue) { AXUIElementSetAttributeValue(focused, kAXSelectedTextRangeAttribute, newRangeValue); CFRelease(newRangeValue); } } CFTypeRef verifyRef = NULL; AXError verifyErr = AXUIElementCopyAttributeValue(focused, kAXValueAttribute, &verifyRef); BOOL verified = false; if (verifyErr == kAXErrorSuccess && verifyRef && CFGetTypeID(verifyRef) == CFStringGetTypeID()) { NSString* verifiedValue = (__bridge NSString*)verifyRef; verified = [verifiedValue isEqualToString:updated]; } if (verifyRef) CFRelease(verifyRef); CFRelease(valueRef); CFRelease(rangeRef); CFRelease(focused); if (setErr != kAXErrorSuccess) { return pvAXCopyResult(0, @"ax_value_replacement", target, [NSString stringWithFormat:@"AX value replacement failed: %d", setErr]); } if (!verified) { return pvAXCopyResult(0, @"ax_value_replacement", target, [NSString stringWithFormat:@"AX value replacement was not observable after write; verify=%d", verifyErr]); } return pvAXCopyResult(1, @"ax_value_replacement", target, @"focused element value changed after write"); } static int pvSetClipboardText(const char* text) { NSPasteboard* pb = [NSPasteboard generalPasteboard]; if (!pb) return 0; NSString* str = [NSString stringWithUTF8String:text]; if (!str) return 0; [pb clearContents]; NSArray* types = @[NSPasteboardTypeString]; [pb declareTypes:types owner:nil]; BOOL wrote = [pb setString:str forType:NSPasteboardTypeString]; NSString* verified = [pb stringForType:NSPasteboardTypeString]; return wrote && verified && [verified isEqualToString:str] ? 1 : 0; } static int pvClearClipboard(void) { NSPasteboard* pb = [NSPasteboard generalPasteboard]; if (!pb) return 0; [pb clearContents]; return [pb stringForType:NSPasteboardTypeString] == nil ? 1 : 0; } static char* pvGetClipboardText(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 char* pvCopyFrontmostAppIdentity(void) { NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication]; if (!app || app.processIdentifier <= 0) return NULL; NSString* bundleID = [app bundleIdentifier] ?: @""; NSString* identity = [NSString stringWithFormat:@"bundle=%@ pid=%d", bundleID, app.processIdentifier]; const char* utf8 = [identity UTF8String]; if (!utf8) return NULL; return strdup(utf8); } static int pvPostCommandVIfFrontmostUnchanged(const char* expectedIdentity) { if (!expectedIdentity || strlen(expectedIdentity) == 0) return -1; char* currentIdentity = pvCopyFrontmostAppIdentity(); if (!currentIdentity) return -1; int same = strcmp(currentIdentity, expectedIdentity) == 0; free(currentIdentity); if (!same) return 0; CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState); if (!source) return -2; CGEventRef keyDown = CGEventCreateKeyboardEvent(source, (CGKeyCode)9, true); CGEventRef keyUp = CGEventCreateKeyboardEvent(source, (CGKeyCode)9, false); if (!keyDown || !keyUp) { if (keyDown) CFRelease(keyDown); if (keyUp) CFRelease(keyUp); CFRelease(source); return -2; } CGEventSetFlags(keyDown, kCGEventFlagMaskCommand); CGEventSetFlags(keyUp, kCGEventFlagMaskCommand); CGEventPost(kCGHIDEventTap, keyDown); // AX10DT_EXPERIMENT_ALLOW_CGEVENTPOST: one-shot Cmd+V only when explicitly enabled by config. CGEventPost(kCGHIDEventTap, keyUp); // AX10DT_EXPERIMENT_ALLOW_CGEVENTPOST: one-shot Cmd+V only when explicitly enabled by config. CFRelease(keyDown); CFRelease(keyUp); CFRelease(source); return 1; } */ import "C" import ( "strings" "unsafe" ) type darwinDriver struct { clipboard clipboardState } var ( readClipboardSnapshot = currentClipboardSnapshot writeClipboardText = writeClipboardTextDarwin clearClipboardText = clearClipboardTextDarwin ) func newPlatformDriver() Driver { return &darwinDriver{} } func (d *darwinDriver) AccessibilityTrusted(prompt bool) bool { if prompt { return C.pvAXTrusted(1) != 0 } return C.pvAXTrusted(0) != 0 } func (d *darwinDriver) DirectInsert(text string) DirectInsertResult { if text == "" { return DirectInsertResult{OK: false, Method: "ax_focused_element", Message: "text is empty"} } cText := C.CString(text) defer C.free(unsafe.Pointer(cText)) cResult := C.pvAXDirectInsert(cText) if cResult == nil { return DirectInsertResult{ OK: false, Inserted: false, NeedsPermission: !d.AccessibilityTrusted(false), Method: "ax_focused_element", Message: "AX direct insert returned no result", } } defer C.free(unsafe.Pointer(cResult)) ok, method, target, msg := parseAXDirectInsertPayload(C.GoString(cResult)) if !ok { return DirectInsertResult{ OK: false, Inserted: false, NeedsPermission: !d.AccessibilityTrusted(false), Method: method, Message: msg, Target: target, } } return DirectInsertResult{ OK: true, Inserted: true, Method: method, Message: msg, Target: target, } } func parseAXDirectInsertPayload(payload string) (bool, string, string, string) { parts := strings.SplitN(payload, "\n", 4) if len(parts) != 4 { return false, "ax_focused_element", "", payload } return parts[0] == "1", parts[1], parts[2], parts[3] } func (d *darwinDriver) CopyTextToClipboard(text string) ClipboardResult { if text == "" { return ClipboardResult{OK: false, Message: "text is empty"} } if !writeClipboardText(text) { return ClipboardResult{ OK: false, Message: "clipboard could not be written", } } return ClipboardResult{ OK: true, Prepared: true, Message: "clipboard updated; user must paste manually", } } func (d *darwinDriver) FrontmostAppIdentity() FrontmostAppIdentity { cIdentity := C.pvCopyFrontmostAppIdentity() if cIdentity == nil { return FrontmostAppIdentity{OK: false, Message: "frontmost app identity is unavailable"} } defer C.free(unsafe.Pointer(cIdentity)) identity := C.GoString(cIdentity) if strings.TrimSpace(identity) == "" { return FrontmostAppIdentity{OK: false, Message: "frontmost app identity is empty"} } return FrontmostAppIdentity{OK: true, Identity: identity} } func (d *darwinDriver) AutoPasteClipboardIfFrontmostUnchanged(expectedIdentity string) AutoPasteResult { if strings.TrimSpace(expectedIdentity) == "" { return AutoPasteResult{OK: false, Message: "frontmost app identity is unavailable"} } cExpected := C.CString(expectedIdentity) defer C.free(unsafe.Pointer(cExpected)) switch C.pvPostCommandVIfFrontmostUnchanged(cExpected) { case 1: return AutoPasteResult{OK: true, Pasted: true, FrontmostUnchanged: true, Message: "automatic paste event sent"} case 0: return AutoPasteResult{OK: false, Pasted: false, FrontmostUnchanged: false, Message: "frontmost app changed before automatic paste"} default: return AutoPasteResult{OK: false, Pasted: false, FrontmostUnchanged: false, Message: "automatic paste event could not be sent"} } } func (d *darwinDriver) PrepareClipboardFallback(text string) ClipboardResult { if text == "" { return ClipboardResult{OK: false, Message: "text is empty"} } restore := readClipboardSnapshot() if !writeClipboardText(text) { return ClipboardResult{ OK: false, Prepared: false, Message: "clipboard fallback could not be written", } } token := d.clipboard.remember(text, restore) return ClipboardResult{ OK: true, Prepared: true, Token: token, Message: "clipboard fallback prepared; user must paste manually", } } func (d *darwinDriver) RestoreClipboardFallback(token string) ClipboardResult { entry, ok := d.clipboard.take(token) if !ok { return ClipboardResult{OK: false, Message: "unknown or already restored fallback token"} } current := readClipboardSnapshot() if !shouldRestoreClipboard(current, entry.fallbackText) { return ClipboardResult{ OK: false, Restored: false, Message: "clipboard changed after fallback preparation; restore skipped", } } if entry.restore.hasText { if !writeClipboardText(entry.restore.text) { return ClipboardResult{OK: false, Restored: false, Message: "clipboard restore failed"} } return ClipboardResult{OK: true, Restored: true, Message: "clipboard restored"} } if !clearClipboardText() { return ClipboardResult{OK: false, Restored: false, Message: "clipboard restore failed"} } return ClipboardResult{OK: true, Restored: true, Message: "clipboard restored"} } func writeClipboardTextDarwin(text string) bool { cText := C.CString(text) defer C.free(unsafe.Pointer(cText)) return C.pvSetClipboardText(cText) != 0 } func clearClipboardTextDarwin() bool { return C.pvClearClipboard() != 0 } func currentClipboardSnapshot() clipboardSnapshot { cText := C.pvGetClipboardText() if cText == nil { return clipboardSnapshot{} } defer C.free(unsafe.Pointer(cText)) return clipboardSnapshot{text: C.GoString(cText), hasText: true} }