From 699ca9aa53bb716800012ad47f73403b88c643a9 Mon Sep 17 00:00:00 2001 From: OfficialKris <37947442+officialkris@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:40:50 -0700 Subject: [PATCH 1/3] port to purego --- clipboard_darwin.go | 131 +++++++++++++++++++++++++++++++------------- clipboard_darwin.m | 62 --------------------- clipboard_nocgo.go | 2 +- go.mod | 1 + go.sum | 2 + 5 files changed, 96 insertions(+), 102 deletions(-) delete mode 100644 clipboard_darwin.m diff --git a/clipboard_darwin.go b/clipboard_darwin.go index bcda127..507dc00 100644 --- a/clipboard_darwin.go +++ b/clipboard_darwin.go @@ -8,82 +8,93 @@ package clipboard -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework Cocoa -#import -#import - -unsigned int clipboard_read_string(void **out); -unsigned int clipboard_read_image(void **out); -int clipboard_write_string(const void *bytes, NSInteger n); -int clipboard_write_image(const void *bytes, NSInteger n); -NSInteger clipboard_change_count(); -*/ -import "C" import ( "context" "time" "unsafe" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/objc" ) +var ( + appkit = must(purego.Dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", purego.RTLD_GLOBAL|purego.RTLD_NOW)) + + _NSPasteboardTypeString = must2(purego.Dlsym(appkit, "NSPasteboardTypeString")) + _NSPasteboardTypePNG = must2(purego.Dlsym(appkit, "NSPasteboardTypePNG")) + + class_NSPasteboard = objc.GetClass("NSPasteboard") + class_NSData = objc.GetClass("NSData") + + sel_generalPasteboard = objc.RegisterName("generalPasteboard") + sel_length = objc.RegisterName("length") + sel_getBytesLength = objc.RegisterName("getBytes:length:") + sel_dataForType = objc.RegisterName("dataForType:") + sel_clearContents = objc.RegisterName("clearContents") + sel_setDataForType = objc.RegisterName("setData:forType:") + sel_dataWithBytesLength = objc.RegisterName("dataWithBytes:length:") + sel_changeCount = objc.RegisterName("changeCount") +) + +func must(sym uintptr, err error) uintptr { + if err != nil { + panic(err) + } + return sym +} + +func must2(sym uintptr, err error) uintptr { + if err != nil { + panic(err) + } + // dlsym returns a pointer to the object so dereference like this to avoid possible misuse of 'unsafe.Pointer' warning + return **(**uintptr)(unsafe.Pointer(&sym)) +} + func initialize() error { return nil } func read(t Format) (buf []byte, err error) { - var ( - data unsafe.Pointer - n C.uint - ) switch t { case FmtText: - n = C.clipboard_read_string(&data) + return clipboard_read_string(), nil case FmtImage: - n = C.clipboard_read_image(&data) - } - if data == nil { - return nil, errUnavailable + return clipboard_read_image(), nil } - defer C.free(unsafe.Pointer(data)) - if n == 0 { - return nil, nil - } - return C.GoBytes(data, C.int(n)), nil + return nil, errUnavailable } // write writes the given data to clipboard and // returns true if success or false if failed. func write(t Format, buf []byte) (<-chan struct{}, error) { - var ok C.int + var ok bool switch t { case FmtText: if len(buf) == 0 { - ok = C.clipboard_write_string(unsafe.Pointer(nil), 0) + ok = clipboard_write_string(nil) } else { - ok = C.clipboard_write_string(unsafe.Pointer(&buf[0]), - C.NSInteger(len(buf))) + ok = clipboard_write_string(buf) } case FmtImage: if len(buf) == 0 { - ok = C.clipboard_write_image(unsafe.Pointer(nil), 0) + ok = clipboard_write_image(nil) } else { - ok = C.clipboard_write_image(unsafe.Pointer(&buf[0]), - C.NSInteger(len(buf))) + ok = clipboard_write_image(buf) } default: return nil, errUnsupported } - if ok != 0 { + if !ok { return nil, errUnavailable } // use unbuffered data to prevent goroutine leak changed := make(chan struct{}, 1) - cnt := C.long(C.clipboard_change_count()) + cnt := clipboard_change_count() go func() { for { // not sure if we are too slow or the user too fast :) time.Sleep(time.Second) - cur := C.long(C.clipboard_change_count()) + cur := clipboard_change_count() if cnt != cur { changed <- struct{}{} close(changed) @@ -98,7 +109,7 @@ func watch(ctx context.Context, t Format) <-chan []byte { recv := make(chan []byte, 1) // not sure if we are too slow or the user too fast :) ti := time.NewTicker(time.Second) - lastCount := C.long(C.clipboard_change_count()) + lastCount := clipboard_change_count() go func() { for { select { @@ -106,7 +117,7 @@ func watch(ctx context.Context, t Format) <-chan []byte { close(recv) return case <-ti.C: - this := C.long(C.clipboard_change_count()) + this := clipboard_change_count() if lastCount != this { b := Read(t) if b == nil { @@ -120,3 +131,45 @@ func watch(ctx context.Context, t Format) <-chan []byte { }() return recv } + +func clipboard_read_string() []byte { + var pasteboard = objc.ID(class_NSPasteboard).Send(sel_generalPasteboard) + var data = pasteboard.Send(sel_dataForType, _NSPasteboardTypeString) + if data == 0 { + return nil + } + var size = uint(data.Send(sel_length)) + out := make([]byte, size) + data.Send(sel_getBytesLength, unsafe.SliceData(out), size) + return out +} + +func clipboard_read_image() []byte { + var pasteboard = objc.ID(class_NSPasteboard).Send(sel_generalPasteboard) + data := pasteboard.Send(sel_dataForType, _NSPasteboardTypePNG) + if data == 0 { + return nil + } + size := data.Send(sel_length) + out := make([]byte, size) + data.Send(sel_getBytesLength, unsafe.SliceData(out), size) + return out +} + +func clipboard_write_image(bytes []byte) bool { + pasteboard := objc.ID(class_NSPasteboard).Send(sel_generalPasteboard) + data := objc.ID(class_NSData).Send(sel_dataWithBytesLength, unsafe.SliceData(bytes), len(bytes)) + pasteboard.Send(sel_clearContents) + return pasteboard.Send(sel_setDataForType, data, _NSPasteboardTypePNG) != 0 +} + +func clipboard_write_string(bytes []byte) bool { + pasteboard := objc.ID(class_NSPasteboard).Send(sel_generalPasteboard) + data := objc.ID(class_NSData).Send(sel_dataWithBytesLength, unsafe.SliceData(bytes), len(bytes)) + pasteboard.Send(sel_clearContents) + return pasteboard.Send(sel_setDataForType, data, _NSPasteboardTypeString) != 0 +} + +func clipboard_change_count() int { + return int(objc.ID(class_NSPasteboard).Send(sel_generalPasteboard).Send(sel_changeCount)) +} diff --git a/clipboard_darwin.m b/clipboard_darwin.m deleted file mode 100644 index 177e771..0000000 --- a/clipboard_darwin.m +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2021 The golang.design Initiative Authors. -// All rights reserved. Use of this source code is governed -// by a MIT license that can be found in the LICENSE file. -// -// Written by Changkun Ou - -//go:build darwin && !ios - -// Interact with NSPasteboard using Objective-C -// https://developer.apple.com/documentation/appkit/nspasteboard?language=objc - -#import -#import - -unsigned int clipboard_read_string(void **out) { - NSPasteboard * pasteboard = [NSPasteboard generalPasteboard]; - NSData *data = [pasteboard dataForType:NSPasteboardTypeString]; - if (data == nil) { - return 0; - } - NSUInteger siz = [data length]; - *out = malloc(siz); - [data getBytes: *out length: siz]; - return siz; -} - -unsigned int clipboard_read_image(void **out) { - NSPasteboard * pasteboard = [NSPasteboard generalPasteboard]; - NSData *data = [pasteboard dataForType:NSPasteboardTypePNG]; - if (data == nil) { - return 0; - } - NSUInteger siz = [data length]; - *out = malloc(siz); - [data getBytes: *out length: siz]; - return siz; -} - -int clipboard_write_string(const void *bytes, NSInteger n) { - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - NSData *data = [NSData dataWithBytes: bytes length: n]; - [pasteboard clearContents]; - BOOL ok = [pasteboard setData: data forType:NSPasteboardTypeString]; - if (!ok) { - return -1; - } - return 0; -} -int clipboard_write_image(const void *bytes, NSInteger n) { - NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; - NSData *data = [NSData dataWithBytes: bytes length: n]; - [pasteboard clearContents]; - BOOL ok = [pasteboard setData: data forType:NSPasteboardTypePNG]; - if (!ok) { - return -1; - } - return 0; -} - -NSInteger clipboard_change_count() { - return [[NSPasteboard generalPasteboard] changeCount]; -} diff --git a/clipboard_nocgo.go b/clipboard_nocgo.go index 16dc865..c94c579 100644 --- a/clipboard_nocgo.go +++ b/clipboard_nocgo.go @@ -1,4 +1,4 @@ -//go:build !windows && !cgo +//go:build !darwin && !windows && !cgo package clipboard diff --git a/go.mod b/go.mod index ba0dd55..52ed776 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module golang.design/x/clipboard go 1.24 require ( + github.com/ebitengine/purego v0.8.4 golang.org/x/image v0.28.0 golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f ) diff --git a/go.sum b/go.sum index a68e266..5ee6074 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE= golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= From 51b53cb0fee3ec378e17abbcdd907b4ac53cbd3a Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Thu, 26 Jun 2025 07:55:08 -0400 Subject: [PATCH 2/3] if size is zero return nil --- clipboard_darwin.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clipboard_darwin.go b/clipboard_darwin.go index 507dc00..9855103 100644 --- a/clipboard_darwin.go +++ b/clipboard_darwin.go @@ -141,6 +141,9 @@ func clipboard_read_string() []byte { var size = uint(data.Send(sel_length)) out := make([]byte, size) data.Send(sel_getBytesLength, unsafe.SliceData(out), size) + if size == 0 { + return nil + } return out } From a9fa549c19604519d5669e840a80073344a0d666 Mon Sep 17 00:00:00 2001 From: TotallyGamerJet Date: Thu, 26 Jun 2025 09:15:20 -0400 Subject: [PATCH 3/3] clean up tests and documentation --- README.md | 2 +- clipboard_darwin.go | 4 ++-- clipboard_test.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d712007..c362e51 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ accessing system clipboards, but here are a few details you might need to know. ### Dependency -- macOS: require Cgo, no dependency +- macOS: no Cgo, no build dependency - Linux: require X11 dev package. For instance, install `libx11-dev` or `xorg-dev` or `libX11-devel` to access X window system. Wayland sessions are currently unsupported; running under Wayland typically requires an XWayland bridge and `DISPLAY` to be set. diff --git a/clipboard_darwin.go b/clipboard_darwin.go index 9855103..a22f872 100644 --- a/clipboard_darwin.go +++ b/clipboard_darwin.go @@ -139,11 +139,11 @@ func clipboard_read_string() []byte { return nil } var size = uint(data.Send(sel_length)) - out := make([]byte, size) - data.Send(sel_getBytesLength, unsafe.SliceData(out), size) if size == 0 { return nil } + out := make([]byte, size) + data.Send(sel_getBytesLength, unsafe.SliceData(out), size) return out } diff --git a/clipboard_test.go b/clipboard_test.go index 9dce284..627c5c0 100644 --- a/clipboard_test.go +++ b/clipboard_test.go @@ -30,8 +30,8 @@ func TestClipboardInit(t *testing.T) { if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" { t.Skip("CGO_ENABLED is set to 1") } - if runtime.GOOS == "windows" { - t.Skip("Windows does not need to check for cgo") + if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + t.Skip("Windows and macOS does not need to check for cgo") } if err := clipboard.Init(); !errors.Is(err, clipboard.ErrCgoDisabled) { @@ -297,8 +297,8 @@ func TestClipboardNoCgo(t *testing.T) { if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" { t.Skip("CGO_ENABLED is set to 1") } - if runtime.GOOS == "windows" { - t.Skip("Windows should always be tested") + if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + t.Skip("Windows and macOS should always be tested") } t.Run("Read", func(t *testing.T) {