Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/ui/model/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ const (
var (
errClipboardPlatformUnsupported = errors.New("clipboard operations are not supported on this platform")
errClipboardUnknownFormat = errors.New("unknown clipboard format")
errClipboardImageUnavailable = errors.New("clipboard does not contain a supported image format")
)
112 changes: 112 additions & 0 deletions internal/ui/model/clipboard_image_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//go:build darwin && !ios

package model

import (
"bytes"
"image/png"
"runtime"
"unsafe"

"github.com/ebitengine/purego"
"github.com/ebitengine/purego/objc"
"golang.org/x/image/tiff"
)

var (
nsPasteboardClass objc.Class
nsDataClass objc.Class

selGeneralPasteboard objc.SEL
selDataForType objc.SEL
selBytes objc.SEL
selLength objc.SEL

nsPasteboardTypeTIFF objc.ID

clipboardImageDarwinInitialized bool
clipboardImageDarwinInitError error
)

func initClipboardImageDarwin() {
if clipboardImageDarwinInitialized {
return
}
clipboardImageDarwinInitialized = true

appkit, err := purego.Dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
clipboardImageDarwinInitError = err
return
}

nsPasteboardClass = objc.GetClass("NSPasteboard")
nsDataClass = objc.GetClass("NSData")

selGeneralPasteboard = objc.RegisterName("generalPasteboard")
selDataForType = objc.RegisterName("dataForType:")
selBytes = objc.RegisterName("bytes")
selLength = objc.RegisterName("length")

typeTIFFPtr, err := purego.Dlsym(appkit, "NSPasteboardTypeTIFF")
if err != nil {
clipboardImageDarwinInitError = err
return
}
nsPasteboardTypeTIFF = objc.ID(*(*uintptr)(unsafe.Pointer(typeTIFFPtr)))
}

// readClipboardImageFallback attempts to read image data from the macOS
// clipboard in formats other than PNG (e.g., TIFF from WeChat screenshots).
// It returns the image encoded as PNG.
func readClipboardImageFallback() ([]byte, error) {
initClipboardImageDarwin()
if clipboardImageDarwinInitError != nil {
return nil, clipboardImageDarwinInitError
}

runtime.LockOSThread()
defer runtime.UnlockOSThread()

pasteboard := objc.ID(nsPasteboardClass).Send(selGeneralPasteboard)
if pasteboard == 0 {
return nil, errClipboardImageUnavailable
}

data := pasteboard.Send(selDataForType, nsPasteboardTypeTIFF)
if data == 0 {
return nil, errClipboardImageUnavailable
}

length := objc.Send[uint64](data, selLength)
if length == 0 {
return nil, errClipboardImageUnavailable
}

bytesPtr := data.Send(selBytes)
if bytesPtr == 0 {
return nil, errClipboardImageUnavailable
}

buf := make([]byte, length)
copyBytes(buf, uintptr(bytesPtr), int(length))

img, err := tiff.Decode(bytes.NewReader(buf))
if err != nil {
return nil, err
}

var out bytes.Buffer
if err := png.Encode(&out, img); err != nil {
return nil, err
}
return out.Bytes(), nil
}

// copyBytes copies n bytes from src to dst. It is defined locally to avoid
// depending on the implementation details of go-nativeclipboard.
func copyBytes(dst []byte, src uintptr, n int) {
for i := range n {
dst[i] = *(*byte)(unsafe.Pointer(src + uintptr(i)))
}
}
10 changes: 10 additions & 0 deletions internal/ui/model/clipboard_image_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !(darwin && !ios)

package model

// readClipboardImageFallback is a no-op on non-macOS platforms. The
// go-nativeclipboard library already handles the common image formats on
// Linux and Windows.
func readClipboardImageFallback() ([]byte, error) {
return nil, errClipboardImageUnavailable
}
6 changes: 6 additions & 0 deletions internal/ui/model/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3730,6 +3730,12 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd {
// interpreting clipboard text as a file path.
func (m *UI) pasteImageFromClipboard() tea.Msg {
imageData, err := readClipboard(clipboardFormatImage)
if err != nil {
// The nativeclipboard library may not support every clipboard image
// format on all platforms (e.g., WeChat screenshots on macOS are
// copied as TIFF). Try a platform-specific fallback before giving up.
imageData, err = readClipboardImageFallback()
}
if int64(len(imageData)) > common.MaxAttachmentSize {
return util.InfoMsg{
Type: util.InfoTypeError,
Expand Down
Loading