1//go:build linux
2
3package tui
4
5import (
6 "context"
7 "io"
8 "os"
9 "os/exec"
10 "strings"
11)
12
13var supportedClipboardImageMIMEs = []string{
14 "image/png",
15 "image/jpeg",
16 "image/gif",
17 "image/webp",
18}
19
20type clipboardImageCommand struct {
21 name string
22 args []string
23}
24
25// ReadClipboardImage reads an image from the Wayland or X11 system clipboard.
26// Clipboard helper failures are intentionally treated as an empty clipboard so
27// that optional desktop integrations do not affect paste handling.
28func ReadClipboardImage(ctx context.Context) (ClipboardImage, bool, error) {
29 if ctx == nil {
30 ctx = context.Background()
31 }
32 ctx, cancel := context.WithTimeout(ctx, clipboardImageReadTimeout)
33 defer cancel()
34
35 if os.Getenv("WAYLAND_DISPLAY") != "" {
36 if image, ok := readClipboardImageWayland(ctx); ok {
37 return image, true, nil
38 }
39 }
40 if os.Getenv("DISPLAY") != "" {
41 if image, ok := readClipboardImageX11(ctx); ok {
42 return image, true, nil
43 }
44 }
45 return ClipboardImage{}, false, nil
46}
47
48// ReadClipboardImagePNG preserves the legacy Linux behavior. Linux image
49// reads retain their clipboard MIME type through ReadClipboardImage instead.
50func ReadClipboardImagePNG() (string, []byte, bool, error) {
51 return "", nil, false, nil
52}
53
54func readClipboardImageWayland(ctx context.Context) (ClipboardImage, bool) {
55 types, ok := runClipboardImageCommand(ctx, clipboardImageCommand{
56 name: "wl-paste",
57 args: []string{"--list-types"},
58 })
59 if !ok {
60 return ClipboardImage{}, false
61 }
62
63 available := make(map[string]bool)
64 for _, line := range strings.Split(string(types), "\n") {
65 mime := strings.ToLower(strings.TrimSpace(line))
66 if isSupportedClipboardImageMIME(mime) {
67 available[mime] = true
68 }
69 }
70 for _, mime := range supportedClipboardImageMIMEs {
71 if !available[mime] {
72 continue
73 }
74 data, ok := runClipboardImageCommand(ctx, clipboardImageCommand{
75 name: "wl-paste",
76 args: []string{"--type", mime},
77 })
78 if ok {
79 return ClipboardImage{MimeType: mime, Data: data}, true
80 }
81 }
82 return ClipboardImage{}, false
83}
84
85func readClipboardImageX11(ctx context.Context) (ClipboardImage, bool) {
86 for _, mime := range supportedClipboardImageMIMEs {
87 data, ok := runClipboardImageCommand(ctx, clipboardImageCommand{
88 name: "xclip",
89 args: []string{"-selection", "clipboard", "-out", "-target", mime},
90 })
91 if ok {
92 return ClipboardImage{MimeType: mime, Data: data}, true
93 }
94 }
95 return ClipboardImage{}, false
96}
97
98func isSupportedClipboardImageMIME(mime string) bool {
99 for _, supported := range supportedClipboardImageMIMEs {
100 if mime == supported {
101 return true
102 }
103 }
104 return false
105}
106
107func runClipboardImageCommand(ctx context.Context, command clipboardImageCommand) ([]byte, bool) {
108 path, err := exec.LookPath(command.name)
109 if err != nil {
110 return nil, false
111 }
112
113 cmd := exec.CommandContext(ctx, path, command.args...)
114 stdout, err := cmd.StdoutPipe()
115 if err != nil {
116 return nil, false
117 }
118 if err := cmd.Start(); err != nil {
119 return nil, false
120 }
121
122 data, readErr := io.ReadAll(io.LimitReader(stdout, int64(maxClipboardImageBytes)+1))
123 if readErr != nil || len(data) > maxClipboardImageBytes {
124 _ = cmd.Process.Kill()
125 _ = cmd.Wait()
126 return nil, false
127 }
128 if err := cmd.Wait(); err != nil || len(data) == 0 {
129 return nil, false
130 }
131 return data, true
132}