Add cross-platform clipboard support

Implement CopyFile for darwin, linux, and windows and call it from
main to copy generated output to the clipboard. Enable CGO for builds,
update goreleaser and release workflow to use macOS, and add a
cross-platform test workflow that verifies the clipboard.
This commit is contained in:
2026-01-31 01:53:12 +01:00
parent fd5c8d72f5
commit af9e0a5372
7 changed files with 248 additions and 5 deletions
+43
View File
@@ -0,0 +1,43 @@
//go:build darwin
package clipboard
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa
#include <stdlib.h>
#import <Cocoa/Cocoa.h>
int copyFileToPasteboard(char* path) {
@autoreleasepool {
NSString *strPath = [NSString stringWithUTF8String:path];
if (!strPath) return 0;
NSPasteboard *pb = [NSPasteboard generalPasteboard];
[pb clearContents];
[pb declareTypes:@[NSFilenamesPboardType] owner:nil];
return [pb setPropertyList:@[strPath] forType:NSFilenamesPboardType] ? 1 : 0;
}
}
*/
import "C"
import (
"errors"
"path/filepath"
"unsafe"
)
func CopyFile(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
cPath := C.CString(absPath)
defer C.free(unsafe.Pointer(cPath))
if success := C.copyFileToPasteboard(cPath); success == 0 {
return errors.New("failed to write to macOS pasteboard")
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
//go:build linux
package clipboard
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
)
func CopyFile(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
uri := fmt.Sprintf("file://%s", absPath)
if isCommandAvailable("wl-copy") {
cmd := exec.Command("wl-copy", "--type", "text/uri-list")
cmd.Stdin = strings.NewReader(uri)
return cmd.Run()
}
if isCommandAvailable("xclip") {
cmd := exec.Command("xclip", "-selection", "clipboard", "-t", "text/uri-list")
cmd.Stdin = strings.NewReader(uri)
return cmd.Run()
}
return fmt.Errorf("install 'wl-copy' or 'xclip'")
}
func isCommandAvailable(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
+103
View File
@@ -0,0 +1,103 @@
//go:build windows
package clipboard
import (
"fmt"
"path/filepath"
"syscall"
"time"
"unsafe"
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
openClipboard = user32.NewProc("OpenClipboard")
closeClipboard = user32.NewProc("CloseClipboard")
emptyClipboard = user32.NewProc("EmptyClipboard")
setClipboardData = user32.NewProc("SetClipboardData")
globalAlloc = kernel32.NewProc("GlobalAlloc")
globalLock = kernel32.NewProc("GlobalLock")
globalUnlock = kernel32.NewProc("GlobalUnlock")
globalFree = kernel32.NewProc("GlobalFree")
)
const (
cfHDrop = 15
gHnd = 0x0042
)
type dropFiles struct {
pFiles uint32
pt struct{ x, y int32 }
fNC int32
fWide int32
}
func CopyFile(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
pathUTF16, err := syscall.UTF16FromString(absPath)
if err != nil {
return err
}
pathUTF16 = append(pathUTF16, 0)
dropSize := uint32(unsafe.Sizeof(dropFiles{}))
dataSize := uint32(len(pathUTF16) * 2)
totalSize := uintptr(dropSize + dataSize)
hMem, _, err := globalAlloc.Call(gHnd, totalSize)
if hMem == 0 {
return fmt.Errorf("GlobalAlloc failed: %v", err)
}
ptrVal, _, _ := globalLock.Call(hMem)
if ptrVal == 0 {
_, _, _ = globalFree.Call(hMem)
return fmt.Errorf("GlobalLock failed")
}
df := (*dropFiles)(unsafe.Pointer(ptrVal))
df.pFiles = dropSize
df.fWide = 1
targetPtr := unsafe.Pointer(ptrVal + uintptr(dropSize))
srcSlice := unsafe.Slice((*uint16)(unsafe.Pointer(&pathUTF16[0])), len(pathUTF16))
dstSlice := unsafe.Slice((*uint16)(targetPtr), len(pathUTF16))
copy(dstSlice, srcSlice)
_, _, _ = globalUnlock.Call(hMem)
var openSuccess bool
for range 10 {
ret, _, _ := openClipboard.Call(0)
if ret != 0 {
openSuccess = true
break
}
time.Sleep(20 * time.Millisecond)
}
if !openSuccess {
_, _, _ = globalFree.Call(hMem)
return fmt.Errorf("clipboard locked")
}
defer func() {
_, _, _ = closeClipboard.Call()
}()
_, _, _ = emptyClipboard.Call()
if ret, _, err := setClipboardData.Call(uintptr(cfHDrop), hMem); ret == 0 {
_, _, _ = globalFree.Call(hMem)
return fmt.Errorf("SetClipboardData failed: %v", err)
}
return nil
}