-
Notifications
You must be signed in to change notification settings - Fork 368
feat(tui): paste images from clipboard via cmd+v #2887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| //go:build darwin | ||
|
|
||
| package editor | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "time" | ||
| ) | ||
|
|
||
| // readClipboardImage attempts to read an image from the macOS clipboard using | ||
| // osascript. If the clipboard contains an image, it is written to a temp file | ||
| // and the path is returned. If the clipboard has no image (or osascript fails), | ||
| // ("", nil) is returned so the caller can fall through to text-paste behaviour. | ||
| func readClipboardImage() (string, error) { | ||
| tmpPath := fmt.Sprintf("%s/clipboard-image-%d.png", os.TempDir(), time.Now().UnixNano()) | ||
|
|
||
| script := fmt.Sprintf(` | ||
| set imgData to the clipboard as «class PNGf» | ||
| set tmpFile to open for access POSIX file %q with write permission | ||
| write imgData to tmpFile | ||
| close access tmpFile | ||
| `, tmpPath) | ||
|
|
||
| cmd := exec.Command("osascript", "-e", script) | ||
| if err := cmd.Run(); err != nil { | ||
| // No image in clipboard (or osascript unavailable) — silent fallback. | ||
| _ = os.Remove(tmpPath) // clean up any partial file | ||
| return "", nil | ||
| } | ||
|
|
||
| return tmpPath, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| //go:build !darwin | ||
|
|
||
| package editor | ||
|
|
||
| // readClipboardImage is a no-op stub for non-Darwin platforms. | ||
| // Image-from-clipboard paste is only supported on macOS. | ||
| func readClipboardImage() (string, error) { | ||
| return "", nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -880,6 +880,13 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) { | |
| } | ||
|
|
||
| func (e *editor) handleClipboardPaste() (layout.Model, tea.Cmd) { | ||
| if imgPath, err := readClipboardImage(); err == nil && imgPath != "" { | ||
| if attachErr := e.AttachFile(imgPath); attachErr == nil && len(e.attachments) > 0 { | ||
| e.attachments[len(e.attachments)-1].isTemp = true | ||
| } | ||
| return e, textarea.Blink | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM] Temp clipboard image file leaked when When if imgPath, err := readClipboardImage(); err == nil && imgPath != "" {
if attachErr := e.AttachFile(imgPath); attachErr == nil && len(e.attachments) > 0 {
e.attachments[len(e.attachments)-1].isTemp = true
}
return e, textarea.Blink // temp file leaked if AttachFile failed
}Fix: add cleanup on the error path: if attachErr := e.AttachFile(imgPath); attachErr == nil && len(e.attachments) > 0 {
e.attachments[len(e.attachments)-1].isTemp = true
} else if attachErr != nil {
_ = os.Remove(imgPath)
}There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [MEDIUM] Silent failure: The if imgPath, err := readClipboardImage(); err == nil && imgPath != "" {
if attachErr := e.AttachFile(imgPath); attachErr == nil && len(e.attachments) > 0 {
e.attachments[len(e.attachments)-1].isTemp = true
}
return e, textarea.Blink // ← exits even when AttachFile failed
}
// text paste — unreachable when clipboard has an imageFix: Only return early on successful attachment; fall through to text paste (or show an error) on if imgPath, err := readClipboardImage(); err == nil && imgPath != "" {
if attachErr := e.AttachFile(imgPath); attachErr == nil && len(e.attachments) > 0 {
e.attachments[len(e.attachments)-1].isTemp = true
return e, textarea.Blink
}
// AttachFile failed — clean up and fall through to text paste
_ = os.Remove(imgPath)
} |
||
| } | ||
|
|
||
| content, err := clipboard.ReadAll() | ||
| if err != nil { | ||
| slog.Warn("failed to read clipboard", "error", err) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[MEDIUM] Binary PNG data routed through a text-only attachment path — will corrupt image on send
When
AttachFilesucceeds andisTemp = trueis set, the image attachment is handled by the pre-existingcollectAttachments()code which reads the temp file and stores it as:collectAttachmentswas designed for text-paste temp files. For a clipboard PNG,datawill be raw binary bytes. Go'sstring(data)preserves the bytes in memory, but Go'sencoding/jsonencoder replaces invalid UTF-8 sequences with the Unicode replacement character (U+FFFD), corrupting the image before it reaches the model. File-reference attachments use theFilePathfield which avoids this issue entirely — clipboard images should follow the same path, or PNG bytes should be base64-encoded intoContent.