//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 }