fix: use osascript for macos clipboard image paste
- commit
- 1f663cb867439ef84b2f5d9f81eaa8af38a0159e
- author
- mi-skam · · 2026-06-21 20:27 UTC
- committer
- mi-skam · · 2026-06-21 20:31 UTC
- parent
- 4a6d691
- signature
- SSH Unverified
SHA256:Yu1uEv3+xppM2hvm7BsWney9tUWafRiTe/kq8zX8X0wThe key is unknown
7 files · +229 -134
M README.md +1 -1
| @@ -576,7 +576,7 @@ Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jum | |||
| 576 | 576 | | `ctrl+c` | Clear the input and queue (while idle) or arm the exit hint (while busy). Press again within 2s to exit. Use `esc` to cancel a running turn. | | |
| 577 | 577 | | `ctrl+d` | Exit on empty input. | | |
| 578 | 578 | | `ctrl+l` | Redraw the screen. | | |
| 579 | - | | `ctrl+v` | Paste from the system clipboard through zot. Text is inserted normally; images are saved as temporary PNGs and attached to the next prompt. | | |
| 579 | + | | `ctrl+v` | Paste an image from the system clipboard through zot on macOS. Images are saved as temporary PNGs and attached to the next prompt. Other platforms currently use a no-op stub. Use your terminal/OS paste shortcut for text. | | |
| 580 | 580 | | `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash outputs over ~12 lines). | | |
| 581 | 581 | | `@` | Open the file picker. Browse files and directories in the working directory. | | |
| 582 | 582 | ||
| @@ -576,7 +576,7 @@ Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jum | |||||
| 576 | | `ctrl+c` | Clear the input and queue (while idle) or arm the exit hint (while busy). Press again within 2s to exit. Use `esc` to cancel a running turn. | | 576 | | `ctrl+c` | Clear the input and queue (while idle) or arm the exit hint (while busy). Press again within 2s to exit. Use `esc` to cancel a running turn. | | ||
| 577 | | `ctrl+d` | Exit on empty input. | | 577 | | `ctrl+d` | Exit on empty input. | | ||
| 578 | | `ctrl+l` | Redraw the screen. | | 578 | | `ctrl+l` | Redraw the screen. | | ||
| 579 | - | | `ctrl+v` | Paste from the system clipboard through zot. Text is inserted normally; images are saved as temporary PNGs and attached to the next prompt. | | 579 | + | | `ctrl+v` | Paste an image from the system clipboard through zot on macOS. Images are saved as temporary PNGs and attached to the next prompt. Other platforms currently use a no-op stub. Use your terminal/OS paste shortcut for text. | |
| 580 | | `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash outputs over ~12 lines). | | 580 | | `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash outputs over ~12 lines). | | ||
| 581 | | `@` | Open the file picker. Browse files and directories in the working directory. | | 581 | | `@` | Open the file picker. Browse files and directories in the working directory. | | ||
| 582 | 582 | ||||
A packages/agent/modes/clipboard_images.go +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | package modes | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | ||
| 6 | + | "github.com/patriceckhart/zot/packages/provider" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | type clipboardImageAttachment struct { | |
| 10 | + | Marker string | |
| 11 | + | Image provider.ImageBlock | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | func preparePromptWithClipboardImages(text string, pending []clipboardImageAttachment) (string, []provider.ImageBlock) { | |
| 15 | + | out := text | |
| 16 | + | images := make([]provider.ImageBlock, 0, len(pending)) | |
| 17 | + | for _, item := range pending { | |
| 18 | + | if item.Marker == "" || !strings.Contains(out, item.Marker) { | |
| 19 | + | continue | |
| 20 | + | } | |
| 21 | + | out = strings.ReplaceAll(out, item.Marker, " ") | |
| 22 | + | images = append(images, item.Image) | |
| 23 | + | } | |
| 24 | + | out = strings.Join(strings.Fields(out), " ") | |
| 25 | + | return out, images | |
| 26 | + | } | |
| @@ -0,0 +1,26 @@ | |||||
| 1 | + | package modes | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | ||||
| 6 | + | "github.com/patriceckhart/zot/packages/provider" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | type clipboardImageAttachment struct { | |||
| 10 | + | Marker string | |||
| 11 | + | Image provider.ImageBlock | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | func preparePromptWithClipboardImages(text string, pending []clipboardImageAttachment) (string, []provider.ImageBlock) { | |||
| 15 | + | out := text | |||
| 16 | + | images := make([]provider.ImageBlock, 0, len(pending)) | |||
| 17 | + | for _, item := range pending { | |||
| 18 | + | if item.Marker == "" || !strings.Contains(out, item.Marker) { | |||
| 19 | + | continue | |||
| 20 | + | } | |||
| 21 | + | out = strings.ReplaceAll(out, item.Marker, " ") | |||
| 22 | + | images = append(images, item.Image) | |||
| 23 | + | } | |||
| 24 | + | out = strings.Join(strings.Fields(out), " ") | |||
| 25 | + | return out, images | |||
| 26 | + | } | |||
A packages/agent/modes/clipboard_images_test.go +92 -0
| @@ -0,0 +1,92 @@ | |||
| 1 | + | package modes | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/patriceckhart/zot/packages/provider" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func testClipboardImage(marker string, data string) clipboardImageAttachment { | |
| 11 | + | return clipboardImageAttachment{ | |
| 12 | + | Marker: marker, | |
| 13 | + | Image: provider.ImageBlock{MimeType: "image/png", Data: []byte(data)}, | |
| 14 | + | } | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func TestPreparePromptWithClipboardImagesStripsPresentMarker(t *testing.T) { | |
| 18 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |
| 19 | + | ||
| 20 | + | text, images := preparePromptWithClipboardImages("describe this [clipboard image #1] please", pending) | |
| 21 | + | ||
| 22 | + | if text != "describe this please" { | |
| 23 | + | t.Fatalf("text = %q, want %q", text, "describe this please") | |
| 24 | + | } | |
| 25 | + | if len(images) != 1 { | |
| 26 | + | t.Fatalf("len(images) = %d, want 1", len(images)) | |
| 27 | + | } | |
| 28 | + | if string(images[0].Data) != "png-1" { | |
| 29 | + | t.Fatalf("image data = %q, want png-1", string(images[0].Data)) | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func TestPreparePromptWithClipboardImagesAllowsImageOnlyPrompt(t *testing.T) { | |
| 34 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |
| 35 | + | ||
| 36 | + | text, images := preparePromptWithClipboardImages("[clipboard image #1]", pending) | |
| 37 | + | ||
| 38 | + | if text != "" { | |
| 39 | + | t.Fatalf("text = %q, want empty", text) | |
| 40 | + | } | |
| 41 | + | if len(images) != 1 { | |
| 42 | + | t.Fatalf("len(images) = %d, want 1", len(images)) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | func TestPreparePromptWithClipboardImagesIgnoresDeletedMarker(t *testing.T) { | |
| 47 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |
| 48 | + | ||
| 49 | + | text, images := preparePromptWithClipboardImages("describe this from memory", pending) | |
| 50 | + | ||
| 51 | + | if text != "describe this from memory" { | |
| 52 | + | t.Fatalf("text = %q, want unchanged", text) | |
| 53 | + | } | |
| 54 | + | if len(images) != 0 { | |
| 55 | + | t.Fatalf("len(images) = %d, want 0", len(images)) | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func TestPreparePromptWithClipboardImagesHandlesMultipleImagesInPasteOrder(t *testing.T) { | |
| 60 | + | pending := []clipboardImageAttachment{ | |
| 61 | + | testClipboardImage("[clipboard image #1]", "png-1"), | |
| 62 | + | testClipboardImage("[clipboard image #2]", "png-2"), | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | text, images := preparePromptWithClipboardImages("compare [clipboard image #2] with [clipboard image #1]", pending) | |
| 66 | + | ||
| 67 | + | if text != "compare with" { | |
| 68 | + | t.Fatalf("text = %q, want %q", text, "compare with") | |
| 69 | + | } | |
| 70 | + | if len(images) != 2 { | |
| 71 | + | t.Fatalf("len(images) = %d, want 2", len(images)) | |
| 72 | + | } | |
| 73 | + | if string(images[0].Data) != "png-1" || string(images[1].Data) != "png-2" { | |
| 74 | + | t.Fatalf("images not attached in paste order: %q, %q", string(images[0].Data), string(images[1].Data)) | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 78 | + | func TestPreparePromptWithClipboardImagesDuplicateMarkerAttachesOnce(t *testing.T) { | |
| 79 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |
| 80 | + | ||
| 81 | + | text, images := preparePromptWithClipboardImages("[clipboard image #1] and again [clipboard image #1]", pending) | |
| 82 | + | ||
| 83 | + | if strings.Contains(text, "[clipboard image #1]") { | |
| 84 | + | t.Fatalf("text still contains marker: %q", text) | |
| 85 | + | } | |
| 86 | + | if text != "and again" { | |
| 87 | + | t.Fatalf("text = %q, want %q", text, "and again") | |
| 88 | + | } | |
| 89 | + | if len(images) != 1 { | |
| 90 | + | t.Fatalf("len(images) = %d, want 1", len(images)) | |
| 91 | + | } | |
| 92 | + | } | |
| @@ -0,0 +1,92 @@ | |||||
| 1 | + | package modes | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/patriceckhart/zot/packages/provider" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func testClipboardImage(marker string, data string) clipboardImageAttachment { | |||
| 11 | + | return clipboardImageAttachment{ | |||
| 12 | + | Marker: marker, | |||
| 13 | + | Image: provider.ImageBlock{MimeType: "image/png", Data: []byte(data)}, | |||
| 14 | + | } | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func TestPreparePromptWithClipboardImagesStripsPresentMarker(t *testing.T) { | |||
| 18 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |||
| 19 | + | ||||
| 20 | + | text, images := preparePromptWithClipboardImages("describe this [clipboard image #1] please", pending) | |||
| 21 | + | ||||
| 22 | + | if text != "describe this please" { | |||
| 23 | + | t.Fatalf("text = %q, want %q", text, "describe this please") | |||
| 24 | + | } | |||
| 25 | + | if len(images) != 1 { | |||
| 26 | + | t.Fatalf("len(images) = %d, want 1", len(images)) | |||
| 27 | + | } | |||
| 28 | + | if string(images[0].Data) != "png-1" { | |||
| 29 | + | t.Fatalf("image data = %q, want png-1", string(images[0].Data)) | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func TestPreparePromptWithClipboardImagesAllowsImageOnlyPrompt(t *testing.T) { | |||
| 34 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |||
| 35 | + | ||||
| 36 | + | text, images := preparePromptWithClipboardImages("[clipboard image #1]", pending) | |||
| 37 | + | ||||
| 38 | + | if text != "" { | |||
| 39 | + | t.Fatalf("text = %q, want empty", text) | |||
| 40 | + | } | |||
| 41 | + | if len(images) != 1 { | |||
| 42 | + | t.Fatalf("len(images) = %d, want 1", len(images)) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | func TestPreparePromptWithClipboardImagesIgnoresDeletedMarker(t *testing.T) { | |||
| 47 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |||
| 48 | + | ||||
| 49 | + | text, images := preparePromptWithClipboardImages("describe this from memory", pending) | |||
| 50 | + | ||||
| 51 | + | if text != "describe this from memory" { | |||
| 52 | + | t.Fatalf("text = %q, want unchanged", text) | |||
| 53 | + | } | |||
| 54 | + | if len(images) != 0 { | |||
| 55 | + | t.Fatalf("len(images) = %d, want 0", len(images)) | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func TestPreparePromptWithClipboardImagesHandlesMultipleImagesInPasteOrder(t *testing.T) { | |||
| 60 | + | pending := []clipboardImageAttachment{ | |||
| 61 | + | testClipboardImage("[clipboard image #1]", "png-1"), | |||
| 62 | + | testClipboardImage("[clipboard image #2]", "png-2"), | |||
| 63 | + | } | |||
| 64 | + | ||||
| 65 | + | text, images := preparePromptWithClipboardImages("compare [clipboard image #2] with [clipboard image #1]", pending) | |||
| 66 | + | ||||
| 67 | + | if text != "compare with" { | |||
| 68 | + | t.Fatalf("text = %q, want %q", text, "compare with") | |||
| 69 | + | } | |||
| 70 | + | if len(images) != 2 { | |||
| 71 | + | t.Fatalf("len(images) = %d, want 2", len(images)) | |||
| 72 | + | } | |||
| 73 | + | if string(images[0].Data) != "png-1" || string(images[1].Data) != "png-2" { | |||
| 74 | + | t.Fatalf("images not attached in paste order: %q, %q", string(images[0].Data), string(images[1].Data)) | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 78 | + | func TestPreparePromptWithClipboardImagesDuplicateMarkerAttachesOnce(t *testing.T) { | |||
| 79 | + | pending := []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")} | |||
| 80 | + | ||||
| 81 | + | text, images := preparePromptWithClipboardImages("[clipboard image #1] and again [clipboard image #1]", pending) | |||
| 82 | + | ||||
| 83 | + | if strings.Contains(text, "[clipboard image #1]") { | |||
| 84 | + | t.Fatalf("text still contains marker: %q", text) | |||
| 85 | + | } | |||
| 86 | + | if text != "and again" { | |||
| 87 | + | t.Fatalf("text = %q, want %q", text, "and again") | |||
| 88 | + | } | |||
| 89 | + | if len(images) != 1 { | |||
| 90 | + | t.Fatalf("len(images) = %d, want 1", len(images)) | |||
| 91 | + | } | |||
| 92 | + | } | |||
M packages/agent/modes/interactive.go +13 -21
| @@ -425,7 +425,7 @@ type Interactive struct { | |||
| 425 | 425 | pendingRescuePrompt string | |
| 426 | 426 | pendingRescueImages []provider.ImageBlock | |
| 427 | 427 | ||
| 428 | - | clipboardImages []provider.ImageBlock | |
| 428 | + | clipboardImages []clipboardImageAttachment | |
| 429 | 429 | } | |
| 430 | 430 | ||
| 431 | 431 | // welcomeVersionDuration is how long the welcome banner shows the | |
| @@ -2231,10 +2231,10 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||
| 2231 | 2231 | text := strings.TrimRight(i.ed.SubmitValue(), "\n") | |
| 2232 | 2232 | // Expand [file:name] and [dir:name/] chips to full paths. | |
| 2233 | 2233 | text = expandFileChips(text, i.cfg.CWD) | |
| 2234 | - | if text == "" { | |
| 2234 | + | text, images := preparePromptWithClipboardImages(text, i.clipboardImages) | |
| 2235 | + | if text == "" && len(images) == 0 { | |
| 2235 | 2236 | return false | |
| 2236 | 2237 | } | |
| 2237 | - | images := append([]provider.ImageBlock(nil), i.clipboardImages...) | |
| 2238 | 2238 | i.clipboardImages = nil | |
| 2239 | 2239 | i.ed.Clear() | |
| 2240 | 2240 | i.inputHistoryIndex = -1 | |
| @@ -2325,7 +2325,7 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||
| 2325 | 2325 | } | |
| 2326 | 2326 | ||
| 2327 | 2327 | func (i *Interactive) pasteClipboard() { | |
| 2328 | - | path, data, ok, err := tui.ReadClipboardImagePNG() | |
| 2328 | + | _, data, ok, err := tui.ReadClipboardImagePNG() | |
| 2329 | 2329 | if err != nil { | |
| 2330 | 2330 | i.mu.Lock() | |
| 2331 | 2331 | i.statusErr = "clipboard paste failed: " + err.Error() | |
| @@ -2335,29 +2335,21 @@ func (i *Interactive) pasteClipboard() { | |||
| 2335 | 2335 | } | |
| 2336 | 2336 | if ok { | |
| 2337 | 2337 | i.mu.Lock() | |
| 2338 | - | i.clipboardImages = append(i.clipboardImages, provider.ImageBlock{MimeType: "image/png", Data: data}) | |
| 2339 | - | i.ed.Insert("[image:" + path + "]") | |
| 2340 | - | i.statusOK = "pasted clipboard image to " + path | |
| 2338 | + | marker := fmt.Sprintf("[clipboard image #%d]", len(i.clipboardImages)+1) | |
| 2339 | + | i.clipboardImages = append(i.clipboardImages, clipboardImageAttachment{ | |
| 2340 | + | Marker: marker, | |
| 2341 | + | Image: provider.ImageBlock{MimeType: "image/png", Data: data}, | |
| 2342 | + | }) | |
| 2343 | + | i.ed.Insert(marker + " ") | |
| 2344 | + | i.statusOK = "" | |
| 2341 | 2345 | i.statusErr = "" | |
| 2342 | 2346 | i.mu.Unlock() | |
| 2343 | 2347 | return | |
| 2344 | 2348 | } | |
| 2345 | - | text, textOK, err := tui.ReadClipboardText() | |
| 2346 | 2349 | i.mu.Lock() | |
| 2347 | - | defer i.mu.Unlock() | |
| 2348 | - | if err != nil { | |
| 2349 | - | i.statusErr = "clipboard paste failed: " + err.Error() | |
| 2350 | - | i.statusOK = "" | |
| 2351 | - | return | |
| 2352 | - | } | |
| 2353 | - | if !textOK || text == "" { | |
| 2354 | - | i.statusErr = "clipboard does not contain text or an image" | |
| 2355 | - | i.statusOK = "" | |
| 2356 | - | return | |
| 2357 | - | } | |
| 2358 | - | i.ed.HandleKey(tui.Key{Kind: tui.KeyPaste, Paste: text}) | |
| 2350 | + | i.statusErr = "clipboard does not contain an image" | |
| 2359 | 2351 | i.statusOK = "" | |
| 2360 | - | i.statusErr = "" | |
| 2352 | + | i.mu.Unlock() | |
| 2361 | 2353 | } | |
| 2362 | 2354 | ||
| 2363 | 2355 | func (i *Interactive) handleInputHistoryKey(k tui.Key) bool { | |
| @@ -425,7 +425,7 @@ type Interactive struct { | |||||
| 425 | pendingRescuePrompt string | 425 | pendingRescuePrompt string | ||
| 426 | pendingRescueImages []provider.ImageBlock | 426 | pendingRescueImages []provider.ImageBlock | ||
| 427 | 427 | ||||
| 428 | - | clipboardImages []provider.ImageBlock | 428 | + | clipboardImages []clipboardImageAttachment |
| 429 | } | 429 | } | ||
| 430 | 430 | ||||
| 431 | // welcomeVersionDuration is how long the welcome banner shows the | 431 | // welcomeVersionDuration is how long the welcome banner shows the | ||
| @@ -2231,10 +2231,10 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||||
| 2231 | text := strings.TrimRight(i.ed.SubmitValue(), "\n") | 2231 | text := strings.TrimRight(i.ed.SubmitValue(), "\n") | ||
| 2232 | // Expand [file:name] and [dir:name/] chips to full paths. | 2232 | // Expand [file:name] and [dir:name/] chips to full paths. | ||
| 2233 | text = expandFileChips(text, i.cfg.CWD) | 2233 | text = expandFileChips(text, i.cfg.CWD) | ||
| 2234 | - | if text == "" { | 2234 | + | text, images := preparePromptWithClipboardImages(text, i.clipboardImages) |
| 2235 | + | if text == "" && len(images) == 0 { | |||
| 2235 | return false | 2236 | return false | ||
| 2236 | } | 2237 | } | ||
| 2237 | - | images := append([]provider.ImageBlock(nil), i.clipboardImages...) | |||
| 2238 | i.clipboardImages = nil | 2238 | i.clipboardImages = nil | ||
| 2239 | i.ed.Clear() | 2239 | i.ed.Clear() | ||
| 2240 | i.inputHistoryIndex = -1 | 2240 | i.inputHistoryIndex = -1 | ||
| @@ -2325,7 +2325,7 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { | |||||
| 2325 | } | 2325 | } | ||
| 2326 | 2326 | ||||
| 2327 | func (i *Interactive) pasteClipboard() { | 2327 | func (i *Interactive) pasteClipboard() { | ||
| 2328 | - | path, data, ok, err := tui.ReadClipboardImagePNG() | 2328 | + | _, data, ok, err := tui.ReadClipboardImagePNG() |
| 2329 | if err != nil { | 2329 | if err != nil { | ||
| 2330 | i.mu.Lock() | 2330 | i.mu.Lock() | ||
| 2331 | i.statusErr = "clipboard paste failed: " + err.Error() | 2331 | i.statusErr = "clipboard paste failed: " + err.Error() | ||
| @@ -2335,29 +2335,21 @@ func (i *Interactive) pasteClipboard() { | |||||
| 2335 | } | 2335 | } | ||
| 2336 | if ok { | 2336 | if ok { | ||
| 2337 | i.mu.Lock() | 2337 | i.mu.Lock() | ||
| 2338 | - | i.clipboardImages = append(i.clipboardImages, provider.ImageBlock{MimeType: "image/png", Data: data}) | 2338 | + | marker := fmt.Sprintf("[clipboard image #%d]", len(i.clipboardImages)+1) |
| 2339 | - | i.ed.Insert("[image:" + path + "]") | 2339 | + | i.clipboardImages = append(i.clipboardImages, clipboardImageAttachment{ |
| 2340 | - | i.statusOK = "pasted clipboard image to " + path | 2340 | + | Marker: marker, |
| 2341 | + | Image: provider.ImageBlock{MimeType: "image/png", Data: data}, | |||
| 2342 | + | }) | |||
| 2343 | + | i.ed.Insert(marker + " ") | |||
| 2344 | + | i.statusOK = "" | |||
| 2341 | i.statusErr = "" | 2345 | i.statusErr = "" | ||
| 2342 | i.mu.Unlock() | 2346 | i.mu.Unlock() | ||
| 2343 | return | 2347 | return | ||
| 2344 | } | 2348 | } | ||
| 2345 | - | text, textOK, err := tui.ReadClipboardText() | |||
| 2346 | i.mu.Lock() | 2349 | i.mu.Lock() | ||
| 2347 | - | defer i.mu.Unlock() | 2350 | + | i.statusErr = "clipboard does not contain an image" |
| 2348 | - | if err != nil { | |||
| 2349 | - | i.statusErr = "clipboard paste failed: " + err.Error() | |||
| 2350 | - | i.statusOK = "" | |||
| 2351 | - | return | |||
| 2352 | - | } | |||
| 2353 | - | if !textOK || text == "" { | |||
| 2354 | - | i.statusErr = "clipboard does not contain text or an image" | |||
| 2355 | - | i.statusOK = "" | |||
| 2356 | - | return | |||
| 2357 | - | } | |||
| 2358 | - | i.ed.HandleKey(tui.Key{Kind: tui.KeyPaste, Paste: text}) | |||
| 2359 | i.statusOK = "" | 2351 | i.statusOK = "" | ||
| 2360 | - | i.statusErr = "" | 2352 | + | i.mu.Unlock() |
| 2361 | } | 2353 | } | ||
| 2362 | 2354 | ||||
| 2363 | func (i *Interactive) handleInputHistoryKey(k tui.Key) bool { | 2355 | func (i *Interactive) handleInputHistoryKey(k tui.Key) bool { | ||
M packages/tui/clipboard_darwin.go +96 -71
| @@ -1,97 +1,82 @@ | |||
| 1 | - | //go:build darwin && cgo | |
| 1 | + | //go:build darwin | |
| 2 | 2 | ||
| 3 | 3 | package tui | |
| 4 | 4 | ||
| 5 | - | /* | |
| 6 | - | #cgo CFLAGS: -x objective-c | |
| 7 | - | #cgo LDFLAGS: -framework AppKit | |
| 8 | - | #import <AppKit/AppKit.h> | |
| 9 | - | #include <stdlib.h> | |
| 10 | - | #include <string.h> | |
| 11 | - | ||
| 12 | - | static char* zot_strdup(const char *s) { | |
| 13 | - | if (s == NULL) return NULL; | |
| 14 | - | char *out = malloc(strlen(s) + 1); | |
| 15 | - | if (out != NULL) strcpy(out, s); | |
| 16 | - | return out; | |
| 17 | - | } | |
| 18 | - | ||
| 19 | - | static int ZotWriteClipboardPNG(const char *path, char **err) { | |
| 20 | - | @autoreleasepool { | |
| 21 | - | NSPasteboard *pb = [NSPasteboard generalPasteboard]; | |
| 22 | - | NSArray *classes = @[[NSImage class]]; | |
| 23 | - | NSDictionary *options = @{}; | |
| 24 | - | NSArray *objects = [pb readObjectsForClasses:classes options:options]; | |
| 25 | - | NSImage *image = nil; | |
| 26 | - | if ([objects count] > 0 && [[objects objectAtIndex:0] isKindOfClass:[NSImage class]]) { | |
| 27 | - | image = (NSImage *)[objects objectAtIndex:0]; | |
| 28 | - | } | |
| 29 | - | if (image == nil) { | |
| 30 | - | NSData *tiff = [pb dataForType:NSPasteboardTypeTIFF]; | |
| 31 | - | if (tiff != nil) image = [[NSImage alloc] initWithData:tiff]; | |
| 32 | - | } | |
| 33 | - | if (image == nil) return 1; | |
| 34 | - | ||
| 35 | - | NSData *tiff = [image TIFFRepresentation]; | |
| 36 | - | if (tiff == nil) { | |
| 37 | - | if (err != NULL) *err = zot_strdup("clipboard image has no TIFF representation"); | |
| 38 | - | return 2; | |
| 39 | - | } | |
| 40 | - | NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:tiff]; | |
| 41 | - | if (rep == nil) { | |
| 42 | - | if (err != NULL) *err = zot_strdup("clipboard image could not be converted to a bitmap"); | |
| 43 | - | return 2; | |
| 44 | - | } | |
| 45 | - | NSData *png = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; | |
| 46 | - | if (png == nil) { | |
| 47 | - | if (err != NULL) *err = zot_strdup("clipboard image could not be encoded as PNG"); | |
| 48 | - | return 2; | |
| 49 | - | } | |
| 50 | - | NSString *dst = [NSString stringWithUTF8String:path]; | |
| 51 | - | NSError *writeErr = nil; | |
| 52 | - | if (![png writeToFile:dst options:NSDataWritingAtomic error:&writeErr]) { | |
| 53 | - | if (err != NULL) *err = zot_strdup([[writeErr localizedDescription] UTF8String]); | |
| 54 | - | return 2; | |
| 55 | - | } | |
| 56 | - | return 0; | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | */ | |
| 60 | - | import "C" | |
| 61 | - | ||
| 62 | 5 | import ( | |
| 63 | 6 | "crypto/rand" | |
| 64 | 7 | "encoding/hex" | |
| 65 | 8 | "fmt" | |
| 9 | + | "image/png" | |
| 66 | 10 | "os" | |
| 11 | + | "os/exec" | |
| 67 | 12 | "path/filepath" | |
| 13 | + | "strings" | |
| 68 | 14 | "time" | |
| 69 | - | "unsafe" | |
| 15 | + | ||
| 16 | + | "golang.org/x/image/tiff" | |
| 70 | 17 | ) | |
| 71 | 18 | ||
| 19 | + | const readClipboardImageScript = ` | |
| 20 | + | on run argv | |
| 21 | + | set outPath to item 1 of argv | |
| 22 | + | try | |
| 23 | + | set imgData to the clipboard as «class PNGf» | |
| 24 | + | set imgKind to "png" | |
| 25 | + | on error | |
| 26 | + | try | |
| 27 | + | set imgData to the clipboard as «class TIFF» | |
| 28 | + | set imgKind to "tiff" | |
| 29 | + | on error | |
| 30 | + | return "NO_IMAGE" | |
| 31 | + | end try | |
| 32 | + | end try | |
| 33 | + | ||
| 34 | + | set outFile to POSIX file outPath | |
| 35 | + | set fileRef to open for access outFile with write permission | |
| 36 | + | try | |
| 37 | + | set eof of fileRef to 0 | |
| 38 | + | write imgData to fileRef | |
| 39 | + | close access fileRef | |
| 40 | + | on error errMsg number errNum | |
| 41 | + | try | |
| 42 | + | close access fileRef | |
| 43 | + | end try | |
| 44 | + | error errMsg number errNum | |
| 45 | + | end try | |
| 46 | + | return imgKind | |
| 47 | + | end run | |
| 48 | + | ` | |
| 49 | + | ||
| 72 | 50 | func ReadClipboardImagePNG() (string, []byte, bool, error) { | |
| 73 | 51 | dir := clipboardImageDir() | |
| 74 | 52 | if err := os.MkdirAll(dir, 0o700); err != nil { | |
| 75 | 53 | return "", nil, false, err | |
| 76 | 54 | } | |
| 77 | 55 | path := filepath.Join(dir, "clipboard-"+time.Now().Format("20060102-150405")+"-"+randomHex(4)+".png") | |
| 78 | - | cpath := C.CString(path) | |
| 79 | - | defer C.free(unsafe.Pointer(cpath)) | |
| 80 | - | var cerr *C.char | |
| 81 | - | code := C.ZotWriteClipboardPNG(cpath, &cerr) | |
| 82 | - | if cerr != nil { | |
| 83 | - | defer C.free(unsafe.Pointer(cerr)) | |
| 56 | + | rawPath := path + ".raw" | |
| 57 | + | defer os.Remove(rawPath) | |
| 58 | + | ||
| 59 | + | kind, err := writeClipboardImageData(rawPath) | |
| 60 | + | if err != nil { | |
| 61 | + | return "", nil, false, err | |
| 84 | 62 | } | |
| 85 | - | if code == 1 { | |
| 63 | + | if kind == "" { | |
| 86 | 64 | return "", nil, false, nil | |
| 87 | 65 | } | |
| 88 | - | if code != 0 { | |
| 89 | - | msg := "could not read image from clipboard" | |
| 90 | - | if cerr != nil { | |
| 91 | - | msg = C.GoString(cerr) | |
| 66 | + | ||
| 67 | + | switch kind { | |
| 68 | + | case "png": | |
| 69 | + | if err := os.Rename(rawPath, path); err != nil { | |
| 70 | + | return "", nil, false, err | |
| 71 | + | } | |
| 72 | + | case "tiff": | |
| 73 | + | if err := convertTIFFFileToPNG(rawPath, path); err != nil { | |
| 74 | + | return "", nil, false, err | |
| 92 | 75 | } | |
| 93 | - | return "", nil, false, fmt.Errorf("%s", msg) | |
| 76 | + | default: | |
| 77 | + | return "", nil, false, fmt.Errorf("unexpected clipboard image kind %q", kind) | |
| 94 | 78 | } | |
| 79 | + | ||
| 95 | 80 | data, err := os.ReadFile(path) | |
| 96 | 81 | if err != nil { | |
| 97 | 82 | return "", nil, false, err | |
| @@ -99,6 +84,46 @@ func ReadClipboardImagePNG() (string, []byte, bool, error) { | |||
| 99 | 84 | return path, data, true, nil | |
| 100 | 85 | } | |
| 101 | 86 | ||
| 87 | + | func writeClipboardImageData(path string) (string, error) { | |
| 88 | + | cmd := exec.Command("/usr/bin/osascript", "-e", readClipboardImageScript, path) | |
| 89 | + | out, err := cmd.CombinedOutput() | |
| 90 | + | trimmed := strings.TrimSpace(string(out)) | |
| 91 | + | if err != nil { | |
| 92 | + | if strings.Contains(trimmed, "NO_IMAGE") || strings.Contains(trimmed, "Can’t make") || strings.Contains(trimmed, "Can't make") { | |
| 93 | + | return "", nil | |
| 94 | + | } | |
| 95 | + | if trimmed == "" { | |
| 96 | + | return "", fmt.Errorf("osascript failed: %w", err) | |
| 97 | + | } | |
| 98 | + | return "", fmt.Errorf("osascript failed: %s", trimmed) | |
| 99 | + | } | |
| 100 | + | if trimmed == "NO_IMAGE" { | |
| 101 | + | return "", nil | |
| 102 | + | } | |
| 103 | + | return trimmed, nil | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func convertTIFFFileToPNG(srcPath, dstPath string) error { | |
| 107 | + | in, err := os.Open(srcPath) | |
| 108 | + | if err != nil { | |
| 109 | + | return err | |
| 110 | + | } | |
| 111 | + | defer in.Close() | |
| 112 | + | ||
| 113 | + | img, err := tiff.Decode(in) | |
| 114 | + | if err != nil { | |
| 115 | + | return err | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | out, err := os.Create(dstPath) | |
| 119 | + | if err != nil { | |
| 120 | + | return err | |
| 121 | + | } | |
| 122 | + | defer out.Close() | |
| 123 | + | ||
| 124 | + | return png.Encode(out, img) | |
| 125 | + | } | |
| 126 | + | ||
| 102 | 127 | func clipboardImageDir() string { | |
| 103 | 128 | if info, err := os.Stat("/tmp"); err == nil && info.IsDir() { | |
| 104 | 129 | return filepath.Join("/tmp", "zot-clipboard-images") | |
| @@ -1,97 +1,82 @@ | |||||
| 1 | - | //go:build darwin && cgo | 1 | + | //go:build darwin |
| 2 | 2 | ||||
| 3 | package tui | 3 | package tui | ||
| 4 | 4 | ||||
| 5 | - | /* | |||
| 6 | - | #cgo CFLAGS: -x objective-c | |||
| 7 | - | #cgo LDFLAGS: -framework AppKit | |||
| 8 | - | #import <AppKit/AppKit.h> | |||
| 9 | - | #include <stdlib.h> | |||
| 10 | - | #include <string.h> | |||
| 11 | - | ||||
| 12 | - | static char* zot_strdup(const char *s) { | |||
| 13 | - | if (s == NULL) return NULL; | |||
| 14 | - | char *out = malloc(strlen(s) + 1); | |||
| 15 | - | if (out != NULL) strcpy(out, s); | |||
| 16 | - | return out; | |||
| 17 | - | } | |||
| 18 | - | ||||
| 19 | - | static int ZotWriteClipboardPNG(const char *path, char **err) { | |||
| 20 | - | @autoreleasepool { | |||
| 21 | - | NSPasteboard *pb = [NSPasteboard generalPasteboard]; | |||
| 22 | - | NSArray *classes = @[[NSImage class]]; | |||
| 23 | - | NSDictionary *options = @{}; | |||
| 24 | - | NSArray *objects = [pb readObjectsForClasses:classes options:options]; | |||
| 25 | - | NSImage *image = nil; | |||
| 26 | - | if ([objects count] > 0 && [[objects objectAtIndex:0] isKindOfClass:[NSImage class]]) { | |||
| 27 | - | image = (NSImage *)[objects objectAtIndex:0]; | |||
| 28 | - | } | |||
| 29 | - | if (image == nil) { | |||
| 30 | - | NSData *tiff = [pb dataForType:NSPasteboardTypeTIFF]; | |||
| 31 | - | if (tiff != nil) image = [[NSImage alloc] initWithData:tiff]; | |||
| 32 | - | } | |||
| 33 | - | if (image == nil) return 1; | |||
| 34 | - | ||||
| 35 | - | NSData *tiff = [image TIFFRepresentation]; | |||
| 36 | - | if (tiff == nil) { | |||
| 37 | - | if (err != NULL) *err = zot_strdup("clipboard image has no TIFF representation"); | |||
| 38 | - | return 2; | |||
| 39 | - | } | |||
| 40 | - | NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:tiff]; | |||
| 41 | - | if (rep == nil) { | |||
| 42 | - | if (err != NULL) *err = zot_strdup("clipboard image could not be converted to a bitmap"); | |||
| 43 | - | return 2; | |||
| 44 | - | } | |||
| 45 | - | NSData *png = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; | |||
| 46 | - | if (png == nil) { | |||
| 47 | - | if (err != NULL) *err = zot_strdup("clipboard image could not be encoded as PNG"); | |||
| 48 | - | return 2; | |||
| 49 | - | } | |||
| 50 | - | NSString *dst = [NSString stringWithUTF8String:path]; | |||
| 51 | - | NSError *writeErr = nil; | |||
| 52 | - | if (![png writeToFile:dst options:NSDataWritingAtomic error:&writeErr]) { | |||
| 53 | - | if (err != NULL) *err = zot_strdup([[writeErr localizedDescription] UTF8String]); | |||
| 54 | - | return 2; | |||
| 55 | - | } | |||
| 56 | - | return 0; | |||
| 57 | - | } | |||
| 58 | - | } | |||
| 59 | - | */ | |||
| 60 | - | import "C" | |||
| 61 | - | ||||
| 62 | import ( | 5 | import ( | ||
| 63 | "crypto/rand" | 6 | "crypto/rand" | ||
| 64 | "encoding/hex" | 7 | "encoding/hex" | ||
| 65 | "fmt" | 8 | "fmt" | ||
| 9 | + | "image/png" | |||
| 66 | "os" | 10 | "os" | ||
| 11 | + | "os/exec" | |||
| 67 | "path/filepath" | 12 | "path/filepath" | ||
| 13 | + | "strings" | |||
| 68 | "time" | 14 | "time" | ||
| 69 | - | "unsafe" | 15 | + | |
| 16 | + | "golang.org/x/image/tiff" | |||
| 70 | ) | 17 | ) | ||
| 71 | 18 | ||||
| 19 | + | const readClipboardImageScript = ` | |||
| 20 | + | on run argv | |||
| 21 | + | set outPath to item 1 of argv | |||
| 22 | + | try | |||
| 23 | + | set imgData to the clipboard as «class PNGf» | |||
| 24 | + | set imgKind to "png" | |||
| 25 | + | on error | |||
| 26 | + | try | |||
| 27 | + | set imgData to the clipboard as «class TIFF» | |||
| 28 | + | set imgKind to "tiff" | |||
| 29 | + | on error | |||
| 30 | + | return "NO_IMAGE" | |||
| 31 | + | end try | |||
| 32 | + | end try | |||
| 33 | + | ||||
| 34 | + | set outFile to POSIX file outPath | |||
| 35 | + | set fileRef to open for access outFile with write permission | |||
| 36 | + | try | |||
| 37 | + | set eof of fileRef to 0 | |||
| 38 | + | write imgData to fileRef | |||
| 39 | + | close access fileRef | |||
| 40 | + | on error errMsg number errNum | |||
| 41 | + | try | |||
| 42 | + | close access fileRef | |||
| 43 | + | end try | |||
| 44 | + | error errMsg number errNum | |||
| 45 | + | end try | |||
| 46 | + | return imgKind | |||
| 47 | + | end run | |||
| 48 | + | ` | |||
| 49 | + | ||||
| 72 | func ReadClipboardImagePNG() (string, []byte, bool, error) { | 50 | func ReadClipboardImagePNG() (string, []byte, bool, error) { | ||
| 73 | dir := clipboardImageDir() | 51 | dir := clipboardImageDir() | ||
| 74 | if err := os.MkdirAll(dir, 0o700); err != nil { | 52 | if err := os.MkdirAll(dir, 0o700); err != nil { | ||
| 75 | return "", nil, false, err | 53 | return "", nil, false, err | ||
| 76 | } | 54 | } | ||
| 77 | path := filepath.Join(dir, "clipboard-"+time.Now().Format("20060102-150405")+"-"+randomHex(4)+".png") | 55 | path := filepath.Join(dir, "clipboard-"+time.Now().Format("20060102-150405")+"-"+randomHex(4)+".png") | ||
| 78 | - | cpath := C.CString(path) | 56 | + | rawPath := path + ".raw" |
| 79 | - | defer C.free(unsafe.Pointer(cpath)) | 57 | + | defer os.Remove(rawPath) |
| 80 | - | var cerr *C.char | 58 | + | |
| 81 | - | code := C.ZotWriteClipboardPNG(cpath, &cerr) | 59 | + | kind, err := writeClipboardImageData(rawPath) |
| 82 | - | if cerr != nil { | 60 | + | if err != nil { |
| 83 | - | defer C.free(unsafe.Pointer(cerr)) | 61 | + | return "", nil, false, err |
| 84 | } | 62 | } | ||
| 85 | - | if code == 1 { | 63 | + | if kind == "" { |
| 86 | return "", nil, false, nil | 64 | return "", nil, false, nil | ||
| 87 | } | 65 | } | ||
| 88 | - | if code != 0 { | 66 | + | |
| 89 | - | msg := "could not read image from clipboard" | 67 | + | switch kind { |
| 90 | - | if cerr != nil { | 68 | + | case "png": |
| 91 | - | msg = C.GoString(cerr) | 69 | + | if err := os.Rename(rawPath, path); err != nil { |
| 70 | + | return "", nil, false, err | |||
| 71 | + | } | |||
| 72 | + | case "tiff": | |||
| 73 | + | if err := convertTIFFFileToPNG(rawPath, path); err != nil { | |||
| 74 | + | return "", nil, false, err | |||
| 92 | } | 75 | } | ||
| 93 | - | return "", nil, false, fmt.Errorf("%s", msg) | 76 | + | default: |
| 77 | + | return "", nil, false, fmt.Errorf("unexpected clipboard image kind %q", kind) | |||
| 94 | } | 78 | } | ||
| 79 | + | ||||
| 95 | data, err := os.ReadFile(path) | 80 | data, err := os.ReadFile(path) | ||
| 96 | if err != nil { | 81 | if err != nil { | ||
| 97 | return "", nil, false, err | 82 | return "", nil, false, err | ||
| @@ -99,6 +84,46 @@ func ReadClipboardImagePNG() (string, []byte, bool, error) { | |||||
| 99 | return path, data, true, nil | 84 | return path, data, true, nil | ||
| 100 | } | 85 | } | ||
| 101 | 86 | ||||
| 87 | + | func writeClipboardImageData(path string) (string, error) { | |||
| 88 | + | cmd := exec.Command("/usr/bin/osascript", "-e", readClipboardImageScript, path) | |||
| 89 | + | out, err := cmd.CombinedOutput() | |||
| 90 | + | trimmed := strings.TrimSpace(string(out)) | |||
| 91 | + | if err != nil { | |||
| 92 | + | if strings.Contains(trimmed, "NO_IMAGE") || strings.Contains(trimmed, "Can’t make") || strings.Contains(trimmed, "Can't make") { | |||
| 93 | + | return "", nil | |||
| 94 | + | } | |||
| 95 | + | if trimmed == "" { | |||
| 96 | + | return "", fmt.Errorf("osascript failed: %w", err) | |||
| 97 | + | } | |||
| 98 | + | return "", fmt.Errorf("osascript failed: %s", trimmed) | |||
| 99 | + | } | |||
| 100 | + | if trimmed == "NO_IMAGE" { | |||
| 101 | + | return "", nil | |||
| 102 | + | } | |||
| 103 | + | return trimmed, nil | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func convertTIFFFileToPNG(srcPath, dstPath string) error { | |||
| 107 | + | in, err := os.Open(srcPath) | |||
| 108 | + | if err != nil { | |||
| 109 | + | return err | |||
| 110 | + | } | |||
| 111 | + | defer in.Close() | |||
| 112 | + | ||||
| 113 | + | img, err := tiff.Decode(in) | |||
| 114 | + | if err != nil { | |||
| 115 | + | return err | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | out, err := os.Create(dstPath) | |||
| 119 | + | if err != nil { | |||
| 120 | + | return err | |||
| 121 | + | } | |||
| 122 | + | defer out.Close() | |||
| 123 | + | ||||
| 124 | + | return png.Encode(out, img) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 102 | func clipboardImageDir() string { | 127 | func clipboardImageDir() string { | ||
| 103 | if info, err := os.Stat("/tmp"); err == nil && info.IsDir() { | 128 | if info, err := os.Stat("/tmp"); err == nil && info.IsDir() { | ||
| 104 | return filepath.Join("/tmp", "zot-clipboard-images") | 129 | return filepath.Join("/tmp", "zot-clipboard-images") | ||
M packages/tui/clipboard_stub.go +1 -5
| @@ -1,11 +1,7 @@ | |||
| 1 | - | //go:build !darwin || !cgo | |
| 1 | + | //go:build !darwin | |
| 2 | 2 | ||
| 3 | 3 | package tui | |
| 4 | 4 | ||
| 5 | 5 | func ReadClipboardImagePNG() (string, []byte, bool, error) { | |
| 6 | 6 | return "", nil, false, nil | |
| 7 | 7 | } | |
| 8 | - | ||
| 9 | - | func ReadClipboardText() (string, bool, error) { | |
| 10 | - | return "", false, nil | |
| 11 | - | } | |
| @@ -1,11 +1,7 @@ | |||||
| 1 | - | //go:build !darwin || !cgo | 1 | + | //go:build !darwin |
| 2 | 2 | ||||
| 3 | package tui | 3 | package tui | ||
| 4 | 4 | ||||
| 5 | func ReadClipboardImagePNG() (string, []byte, bool, error) { | 5 | func ReadClipboardImagePNG() (string, []byte, bool, error) { | ||
| 6 | return "", nil, false, nil | 6 | return "", nil, false, nil | ||
| 7 | } | 7 | } | ||
| 8 | - | ||||
| 9 | - | func ReadClipboardText() (string, bool, error) { | |||
| 10 | - | return "", false, nil | |||
| 11 | - | } | |||
D packages/tui/clipboard_text_darwin.go +0 -36
| @@ -1,36 +0,0 @@ | |||
| 1 | - | //go:build darwin && cgo | |
| 2 | - | ||
| 3 | - | package tui | |
| 4 | - | ||
| 5 | - | /* | |
| 6 | - | #cgo CFLAGS: -x objective-c | |
| 7 | - | #cgo LDFLAGS: -framework AppKit | |
| 8 | - | #import <AppKit/AppKit.h> | |
| 9 | - | #include <stdlib.h> | |
| 10 | - | #include <string.h> | |
| 11 | - | ||
| 12 | - | static char* ZotReadClipboardString(void) { | |
| 13 | - | @autoreleasepool { | |
| 14 | - | NSPasteboard *pb = [NSPasteboard generalPasteboard]; | |
| 15 | - | NSString *str = [pb stringForType:NSPasteboardTypeString]; | |
| 16 | - | if (str == nil) return NULL; | |
| 17 | - | const char *utf8 = [str UTF8String]; | |
| 18 | - | if (utf8 == NULL) return NULL; | |
| 19 | - | char *out = malloc(strlen(utf8) + 1); | |
| 20 | - | if (out != NULL) strcpy(out, utf8); | |
| 21 | - | return out; | |
| 22 | - | } | |
| 23 | - | } | |
| 24 | - | */ | |
| 25 | - | import "C" | |
| 26 | - | ||
| 27 | - | import "unsafe" | |
| 28 | - | ||
| 29 | - | func ReadClipboardText() (string, bool, error) { | |
| 30 | - | cstr := C.ZotReadClipboardString() | |
| 31 | - | if cstr == nil { | |
| 32 | - | return "", false, nil | |
| 33 | - | } | |
| 34 | - | defer C.free(unsafe.Pointer(cstr)) | |
| 35 | - | return C.GoString(cstr), true, nil | |
| 36 | - | } | |
| @@ -1,36 +0,0 @@ | |||||
| 1 | - | //go:build darwin && cgo | |||
| 2 | - | ||||
| 3 | - | package tui | |||
| 4 | - | ||||
| 5 | - | /* | |||
| 6 | - | #cgo CFLAGS: -x objective-c | |||
| 7 | - | #cgo LDFLAGS: -framework AppKit | |||
| 8 | - | #import <AppKit/AppKit.h> | |||
| 9 | - | #include <stdlib.h> | |||
| 10 | - | #include <string.h> | |||
| 11 | - | ||||
| 12 | - | static char* ZotReadClipboardString(void) { | |||
| 13 | - | @autoreleasepool { | |||
| 14 | - | NSPasteboard *pb = [NSPasteboard generalPasteboard]; | |||
| 15 | - | NSString *str = [pb stringForType:NSPasteboardTypeString]; | |||
| 16 | - | if (str == nil) return NULL; | |||
| 17 | - | const char *utf8 = [str UTF8String]; | |||
| 18 | - | if (utf8 == NULL) return NULL; | |||
| 19 | - | char *out = malloc(strlen(utf8) + 1); | |||
| 20 | - | if (out != NULL) strcpy(out, utf8); | |||
| 21 | - | return out; | |||
| 22 | - | } | |||
| 23 | - | } | |||
| 24 | - | */ | |||
| 25 | - | import "C" | |||
| 26 | - | ||||
| 27 | - | import "unsafe" | |||
| 28 | - | ||||
| 29 | - | func ReadClipboardText() (string, bool, error) { | |||
| 30 | - | cstr := C.ZotReadClipboardString() | |||
| 31 | - | if cstr == nil { | |||
| 32 | - | return "", false, nil | |||
| 33 | - | } | |||
| 34 | - | defer C.free(unsafe.Pointer(cstr)) | |||
| 35 | - | return C.GoString(cstr), true, nil | |||
| 36 | - | } | |||