220 lines
6.2 KiB
Go
220 lines
6.2 KiB
Go
//go:build windows
|
|
|
|
package clipboard
|
|
|
|
import (
|
|
"fmt"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
const cfUnicodeText = 13
|
|
|
|
var (
|
|
user32 = syscall.NewLazyDLL("user32.dll")
|
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
|
|
procOpenClipboard = user32.NewProc("OpenClipboard")
|
|
procCloseClipboard = user32.NewProc("CloseClipboard")
|
|
procEmptyClipboard = user32.NewProc("EmptyClipboard")
|
|
procGetClipboardData = user32.NewProc("GetClipboardData")
|
|
procSetClipboardData = user32.NewProc("SetClipboardData")
|
|
|
|
procGlobalAlloc = kernel32.NewProc("GlobalAlloc")
|
|
procGlobalLock = kernel32.NewProc("GlobalLock")
|
|
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
|
|
procGlobalFree = kernel32.NewProc("GlobalFree")
|
|
procGlobalSize = kernel32.NewProc("GlobalSize")
|
|
)
|
|
|
|
type windowsClipboard struct{}
|
|
|
|
// New creates a Windows clipboard using Win32 API (syscall, no CGo).
|
|
func New() (Clipboard, error) {
|
|
return newWindowsClipboard()
|
|
}
|
|
|
|
func newWindowsClipboard() (*windowsClipboard, error) {
|
|
return &windowsClipboard{}, nil
|
|
}
|
|
|
|
func (c *windowsClipboard) Read() (string, error) {
|
|
r1, _, err := procOpenClipboard.Call(0)
|
|
if r1 == 0 {
|
|
return "", fmt.Errorf("clipboard read: OpenClipboard: %w", err)
|
|
}
|
|
defer func() { _, _, _ = procCloseClipboard.Call() }()
|
|
|
|
h, _, _ := procGetClipboardData.Call(uintptr(cfUnicodeText))
|
|
if h == 0 {
|
|
// No text in clipboard — not an error, just empty.
|
|
return "", nil
|
|
}
|
|
|
|
defer func() { _, _, _ = procGlobalUnlock.Call(h) }()
|
|
|
|
// Query the actual size of the global memory block so we never read past it.
|
|
size, _, _ := procGlobalSize.Call(h)
|
|
if size == 0 {
|
|
return "", nil
|
|
}
|
|
units := size / 2 // bytes → UTF-16 code units
|
|
|
|
// Lock and read the global memory block.
|
|
// Use a helper to keep the uintptr→unsafe.Pointer conversion close to the syscall.
|
|
mem := lockAndRead(h, units)
|
|
if mem == nil {
|
|
return "", fmt.Errorf("clipboard read: GlobalLock failed")
|
|
}
|
|
return syscall.UTF16ToString(mem), nil
|
|
}
|
|
|
|
func (c *windowsClipboard) Write(text string) error {
|
|
r1, _, err := procOpenClipboard.Call(0)
|
|
if r1 == 0 {
|
|
return fmt.Errorf("clipboard write: OpenClipboard: %w", err)
|
|
}
|
|
defer func() { _, _, _ = procCloseClipboard.Call() }()
|
|
|
|
r1, _, err = procEmptyClipboard.Call()
|
|
if r1 == 0 {
|
|
return fmt.Errorf("clipboard write: EmptyClipboard: %w", err)
|
|
}
|
|
|
|
utf16, err := syscall.UTF16FromString(text)
|
|
if err != nil {
|
|
return fmt.Errorf("clipboard write: UTF16FromString: %w", err)
|
|
}
|
|
|
|
// Allocate global memory: size in bytes (2 bytes per UTF-16 unit, including null terminator)
|
|
byteSize := uintptr(len(utf16) * 2)
|
|
h, _, err := procGlobalAlloc.Call(0, byteSize)
|
|
if h == 0 {
|
|
return fmt.Errorf("clipboard write: GlobalAlloc: %w", err)
|
|
}
|
|
|
|
// Lock and write data using a helper (keeps uintptr→unsafe.Pointer close to syscall).
|
|
if !lockAndWrite(h, utf16) {
|
|
_, _, _ = procGlobalFree.Call(h)
|
|
return fmt.Errorf("clipboard write: GlobalLock failed")
|
|
}
|
|
|
|
_, _, _ = procGlobalUnlock.Call(h)
|
|
|
|
r1, _, err = procSetClipboardData.Call(uintptr(cfUnicodeText), h)
|
|
if r1 == 0 {
|
|
_, _, _ = procGlobalFree.Call(h)
|
|
return fmt.Errorf("clipboard write: SetClipboardData: %w", err)
|
|
}
|
|
|
|
// Windows now owns the handle — do not free it.
|
|
return nil
|
|
}
|
|
|
|
// lockAndRead locks a global memory handle and returns its UTF-16 data of the
|
|
// given length (in code units). Returns nil on failure. Caller must call
|
|
// GlobalUnlock after use.
|
|
func lockAndRead(h uintptr, units uintptr) []uint16 {
|
|
p, _, _ := procGlobalLock.Call(h)
|
|
if p == 0 {
|
|
return nil
|
|
}
|
|
// Use unsafe.Add to derive a pointer from nil; the uintptr came from
|
|
// GlobalLock (GMEM_FIXED) and is a valid virtual address, not a Go pointer.
|
|
ptr := unsafe.Add(unsafe.Pointer(nil), p)
|
|
return unsafe.Slice((*uint16)(ptr), units)
|
|
}
|
|
|
|
// lockAndWrite locks a global memory handle and copies UTF-16 data into it.
|
|
// Returns false on failure. Caller must call GlobalUnlock after use.
|
|
func lockAndWrite(h uintptr, data []uint16) bool {
|
|
p, _, _ := procGlobalLock.Call(h)
|
|
if p == 0 {
|
|
return false
|
|
}
|
|
ptr := unsafe.Add(unsafe.Pointer(nil), p)
|
|
dst := unsafe.Slice((*uint16)(ptr), len(data))
|
|
copy(dst, data)
|
|
return true
|
|
}
|
|
|
|
// ReadImage returns the clipboard image (CF_DIB) encoded as PNG.
|
|
func (c *windowsClipboard) ReadImage() (string, []byte, error) {
|
|
r1, _, _ := procOpenClipboard.Call(0)
|
|
if r1 == 0 {
|
|
return "", nil, ErrNoImage
|
|
}
|
|
defer func() { _, _, _ = procCloseClipboard.Call() }()
|
|
|
|
h, _, _ := procGetClipboardData.Call(uintptr(cfDIB))
|
|
if h == 0 {
|
|
return "", nil, ErrNoImage
|
|
}
|
|
|
|
size, _, _ := procGlobalSize.Call(h)
|
|
if size == 0 {
|
|
return "", nil, ErrNoImage
|
|
}
|
|
|
|
p, _, _ := procGlobalLock.Call(h)
|
|
if p == 0 {
|
|
return "", nil, ErrNoImage
|
|
}
|
|
defer func() { _, _, _ = procGlobalUnlock.Call(h) }()
|
|
|
|
// Copy the DIB bytes out of the global block before parsing.
|
|
ptr := unsafe.Add(unsafe.Pointer(nil), p)
|
|
raw := unsafe.Slice((*byte)(ptr), size)
|
|
dib := make([]byte, size)
|
|
copy(dib, raw)
|
|
|
|
pngBytes, err := dibToPNG(dib)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
return "image/png", pngBytes, nil
|
|
}
|
|
|
|
// WriteImage writes a PNG image to the clipboard as a CF_DIB.
|
|
func (c *windowsClipboard) WriteImage(mime string, data []byte) error {
|
|
if mime != "" && mime != "image/png" {
|
|
return fmt.Errorf("clipboard: unsupported image mime %q (only PNG)", mime)
|
|
}
|
|
dib, err := pngToDIB(data)
|
|
if err != nil {
|
|
return fmt.Errorf("clipboard: decode image: %w", err)
|
|
}
|
|
|
|
r1, _, err2 := procOpenClipboard.Call(0)
|
|
if r1 == 0 {
|
|
return fmt.Errorf("clipboard write image: OpenClipboard: %w", err2)
|
|
}
|
|
defer func() { _, _, _ = procCloseClipboard.Call() }()
|
|
|
|
if r1, _, err2 = procEmptyClipboard.Call(); r1 == 0 {
|
|
return fmt.Errorf("clipboard write image: EmptyClipboard: %w", err2)
|
|
}
|
|
|
|
h, _, err2 := procGlobalAlloc.Call(0, uintptr(len(dib)))
|
|
if h == 0 {
|
|
return fmt.Errorf("clipboard write image: GlobalAlloc: %w", err2)
|
|
}
|
|
|
|
p, _, _ := procGlobalLock.Call(h)
|
|
if p == 0 {
|
|
_, _, _ = procGlobalFree.Call(h)
|
|
return fmt.Errorf("clipboard write image: GlobalLock failed")
|
|
}
|
|
ptr := unsafe.Add(unsafe.Pointer(nil), p)
|
|
dst := unsafe.Slice((*byte)(ptr), len(dib))
|
|
copy(dst, dib)
|
|
_, _, _ = procGlobalUnlock.Call(h)
|
|
|
|
if r1, _, err2 = procSetClipboardData.Call(uintptr(cfDIB), h); r1 == 0 {
|
|
_, _, _ = procGlobalFree.Call(h)
|
|
return fmt.Errorf("clipboard write image: SetClipboardData: %w", err2)
|
|
}
|
|
// Windows now owns the handle.
|
|
return nil
|
|
}
|