//go:build darwin package platform /* #cgo darwin CFLAGS: -x objective-c -fobjc-arc #cgo darwin LDFLAGS: -framework Cocoa #include long cdropPasteboardChangeCount(void); char *cdropPasteboardReadText(int *sensitive); void cdropPasteboardWriteText(const char *text); */ import "C" import ( "context" "time" "unsafe" ) // pasteboardPollInterval is how often the watcher samples the change counter. // 400ms keeps copies feeling instant without busy-polling. const pasteboardPollInterval = 400 * time.Millisecond // darwinClipboard is the macOS Source: it polls NSPasteboard's changeCount for // local copies and writes plain text back. NSPasteboard read/write off the main // thread is the same approach the common clipboard libraries take. type darwinClipboard struct { poll time.Duration } // NewClipboardSource returns the platform clipboard Source (macOS). func NewClipboardSource() Source { return &darwinClipboard{poll: pasteboardPollInterval} } func (d *darwinClipboard) Watch(ctx context.Context) <-chan ClipboardEvent { ch := make(chan ClipboardEvent) go func() { defer close(ch) last := C.cdropPasteboardChangeCount() ticker := time.NewTicker(d.poll) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: cc := C.cdropPasteboardChangeCount() if cc == last { continue } last = cc var sensitive C.int cstr := C.cdropPasteboardReadText(&sensitive) if cstr == nil { continue // non-text payload (image / file) — ignore } text := C.GoString(cstr) C.free(unsafe.Pointer(cstr)) select { case ch <- ClipboardEvent{Text: text, Sensitive: sensitive != 0}: case <-ctx.Done(): return } } } }() return ch } func (d *darwinClipboard) Write(text string) error { c := C.CString(text) defer C.free(unsafe.Pointer(c)) C.cdropPasteboardWriteText(c) return nil }