//go:build darwin
|
|
package input
|
|
/*
|
#cgo CFLAGS: -x objective-c
|
#cgo LDFLAGS: -framework AppKit -framework CoreGraphics -framework Carbon -framework ApplicationServices
|
#include <AppKit/AppKit.h>
|
#include <CoreGraphics/CoreGraphics.h>
|
#include <Carbon/Carbon.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
#include <stdlib.h>
|
#include <string.h>
|
|
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"
|
)
|
|
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 {
|
if C.ensureInputPermission() == 0 {
|
return fmt.Errorf("accessibility permission is required to paste")
|
}
|
|
state := &globalDarwinPasteState
|
state.mu.Lock()
|
state.generation++
|
generation := state.generation
|
|
originalClipboard := currentDarwinClipboardSnapshot()
|
if !keepClipboard {
|
originalClipboard = resolveDarwinRestoreTarget(
|
originalClipboard,
|
state.pendingRestore,
|
state.pendingText,
|
state.restoreTo,
|
)
|
}
|
|
cstr := C.CString(text)
|
defer C.free(unsafe.Pointer(cstr))
|
C.setClipboardText(cstr)
|
if !waitForDarwinClipboardText(text, darwinClipboardVerifyTimeout) {
|
state.pendingRestore = false
|
state.mu.Unlock()
|
return fmt.Errorf("clipboard verification failed before paste")
|
}
|
|
C.simulateCmdV()
|
|
if keepClipboard {
|
state.pendingRestore = false
|
state.mu.Unlock()
|
return nil
|
}
|
|
state.pendingRestore = true
|
state.pendingText = text
|
state.restoreTo = originalClipboard
|
state.mu.Unlock()
|
|
go restoreDarwinClipboardAfterPaste(generation, text, originalClipboard)
|
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)
|
}
|