//go:build windows package clipmon import ( "bytes" "encoding/binary" "image" "image/color" "image/png" "strings" "syscall" "time" "unsafe" ) const ( cfBitmap = 2 cfDIB = 8 cfUnicodeText = 13 ) var ( user32 = syscall.NewLazyDLL("user32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") procOpenClipboard = user32.NewProc("OpenClipboard") procCloseClipboard = user32.NewProc("CloseClipboard") procGetClipboardData = user32.NewProc("GetClipboardData") procIsClipboardFormatAvailable = user32.NewProc("IsClipboardFormatAvailable") procGetClipboardSequenceNumber = user32.NewProc("GetClipboardSequenceNumber") procGlobalLock = kernel32.NewProc("GlobalLock") procGlobalUnlock = kernel32.NewProc("GlobalUnlock") procGlobalSize = kernel32.NewProc("GlobalSize") ) type bitmapInfoHeader struct { Size uint32 Width int32 Height int32 Planes uint16 BitCount uint16 Compression uint32 SizeImage uint32 XPelsPerMeter int32 YPelsPerMeter int32 ClrUsed uint32 ClrImportant uint32 } func startPlatform(w *Writer, events chan Event, stop <-chan struct{}, done chan struct{}) error { go func() { defer close(done) pollClipboard(w, stop) }() return nil } func pollClipboard(w *Writer, stop <-chan struct{}) { lastSeq := sequenceNumber() ticker := time.NewTicker(300 * time.Millisecond) defer ticker.Stop() for { select { case <-stop: return case <-ticker.C: seq := sequenceNumber() if seq == lastSeq { continue } lastSeq = seq captureClipboard(w) } } } func sequenceNumber() uint32 { n, _, _ := procGetClipboardSequenceNumber.Call() return uint32(n) } func captureClipboard(w *Writer) { ok, _, _ := procOpenClipboard.Call(0) if ok == 0 { return } defer procCloseClipboard.Call() if text, ok := readClipboardText(); ok && strings.TrimSpace(text) != "" { emit(Event{Kind: "text", Text: TrimText(text)}) } if pngData, ok := readClipboardImage(); ok { rel, err := w.SavePNG(pngData) if err != nil { return } emit(Event{Kind: "image", ImagePath: rel}) } } func readClipboardText() (string, bool) { ok, _, _ := procIsClipboardFormatAvailable.Call(cfUnicodeText) if ok == 0 { return "", false } handle, _, _ := procGetClipboardData.Call(cfUnicodeText) if handle == 0 { return "", false } ptr, _, _ := procGlobalLock.Call(handle) if ptr == 0 { return "", false } defer procGlobalUnlock.Call(handle) size, _, _ := procGlobalSize.Call(handle) if size == 0 { return "", false } data := unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), size/2) for i, unit := range data { if unit == 0 { return syscall.UTF16ToString(data[:i]), true } } return syscall.UTF16ToString(data), true } func readClipboardImage() ([]byte, bool) { if dib, ok := readClipboardDIB(); ok { return dib, true } return readClipboardBitmap() } func readClipboardDIB() ([]byte, bool) { ok, _, _ := procIsClipboardFormatAvailable.Call(cfDIB) if ok == 0 { return nil, false } handle, _, _ := procGetClipboardData.Call(cfDIB) if handle == 0 { return nil, false } ptr, _, _ := procGlobalLock.Call(handle) if ptr == 0 { return nil, false } defer procGlobalUnlock.Call(handle) size, _, _ := procGlobalSize.Call(handle) if size == 0 { return nil, false } raw := unsafe.Slice((*byte)(unsafe.Pointer(ptr)), size) return dibToPNG(append([]byte(nil), raw...)) } func readClipboardBitmap() ([]byte, bool) { ok, _, _ := procIsClipboardFormatAvailable.Call(cfBitmap) if ok == 0 { return nil, false } // ponytail: CF_BITMAP needs a DC to read pixels; most apps also publish CF_DIB return nil, false } func dibToPNG(raw []byte) ([]byte, bool) { if len(raw) < int(unsafe.Sizeof(bitmapInfoHeader{})) { return nil, false } var header bitmapInfoHeader if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &header); err != nil { return nil, false } if header.Size < uint32(unsafe.Sizeof(bitmapInfoHeader{})) || header.BitCount != 24 && header.BitCount != 32 { return nil, false } width := int(header.Width) height := int(header.Height) if width <= 0 || height <= 0 || width > 8192 || height > 8192 { return nil, false } topDown := false if height < 0 { topDown = true height = -height } headerSize := int(header.Size) colorTable := int(header.ClrUsed) * 4 if header.BitCount <= 8 && header.ClrUsed == 0 { colorTable = (1 << header.BitCount) * 4 } offset := headerSize + colorTable if offset > len(raw) { return nil, false } pixels := raw[offset:] stride := ((width*int(header.BitCount) + 31) / 32) * 4 if len(pixels) < stride*height { return nil, false } img := image.NewRGBA(image.Rect(0, 0, width, height)) bytesPerPixel := int(header.BitCount) / 8 for y := 0; y < height; y++ { srcY := y if !topDown { srcY = height - 1 - y } row := pixels[srcY*stride:] for x := 0; x < width; x++ { i := x * bytesPerPixel if i+2 >= len(row) { continue } b, g, r := row[i], row[i+1], row[i+2] a := uint8(255) if bytesPerPixel == 4 && i+3 < len(row) { a = row[i+3] } img.SetRGBA(x, y, color.RGBA{R: r, G: g, B: b, A: a}) } } var out bytes.Buffer if err := png.Encode(&out, img); err != nil { return nil, false } return out.Bytes(), true }