1//go:build linux
2
3package tui
4
5import (
6 "errors"
7 "fmt"
8 "os"
9 "strings"
10)
11
12type linuxImageClipboardBackend struct {
13 name string
14 listArgs []string
15 readArgs func(string) []string
16}
17
18// ReadClipboardImagePNG reads and normalizes an image from a Wayland or X11
19// clipboard. Small command-line clients preserve zot's CGO-free build.
20func ReadClipboardImagePNG() (string, []byte, bool, error) {
21 backends := linuxImageClipboardBackends()
22 var lastErr error
23 found := false
24 for _, backend := range backends {
25 data, ok, err := readLinuxClipboardImage(backend)
26 if errors.Is(err, errClipboardImageCommandUnavailable) {
27 continue
28 }
29 found = true
30 if err != nil {
31 lastErr = err
32 continue
33 }
34 if !ok {
35 return "", nil, false, nil
36 }
37 pngData, err := normalizeClipboardImagePNG(data)
38 if err != nil {
39 return "", nil, false, fmt.Errorf("invalid clipboard image: %w", err)
40 }
41 return "", pngData, true, nil
42 }
43 if found && lastErr != nil {
44 return "", nil, false, fmt.Errorf("read image clipboard: %w", lastErr)
45 }
46 return "", nil, false, nil
47}
48
49func linuxImageClipboardBackends() []linuxImageClipboardBackend {
50 wayland := linuxImageClipboardBackend{
51 name: "wl-paste",
52 listArgs: []string{"--list-types"},
53 readArgs: func(mime string) []string { return []string{"--no-newline", "--type", mime} },
54 }
55 xclip := linuxImageClipboardBackend{
56 name: "xclip",
57 listArgs: []string{"-selection", "clipboard", "-target", "TARGETS", "-out"},
58 readArgs: func(mime string) []string { return []string{"-selection", "clipboard", "-target", mime, "-out"} },
59 }
60
61 var backends []linuxImageClipboardBackend
62 if os.Getenv("WAYLAND_DISPLAY") != "" {
63 backends = append(backends, wayland)
64 }
65 if os.Getenv("DISPLAY") != "" {
66 backends = append(backends, xclip)
67 }
68 if len(backends) == 0 {
69 backends = []linuxImageClipboardBackend{wayland, xclip}
70 }
71 return backends
72}
73
74func readLinuxClipboardImage(backend linuxImageClipboardBackend) ([]byte, bool, error) {
75 types, err := runClipboardImageCommand(backend.name, maxClipboardMetadataBytes, backend.listArgs...)
76 if err != nil {
77 return nil, false, err
78 }
79 mime, ok := preferredClipboardImageMIME(string(types))
80 if !ok {
81 return nil, false, nil
82 }
83 data, err := runClipboardImageCommand(backend.name, maxClipboardImageBytes, backend.readArgs(mime)...)
84 if err != nil {
85 return nil, false, err
86 }
87 if len(data) == 0 {
88 return nil, false, fmt.Errorf("%s returned empty data for %s", backend.name, mime)
89 }
90 return data, true, nil
91}
92
93func preferredClipboardImageMIME(types string) (string, bool) {
94 available := make(map[string]string)
95 for _, field := range strings.Fields(types) {
96 mime := strings.TrimSpace(field)
97 available[strings.ToLower(mime)] = mime
98 }
99 for _, mime := range []string{
100 "image/png",
101 "image/tiff",
102 "image/x-tiff",
103 "image/jpeg",
104 "image/jpg",
105 "image/webp",
106 "image/gif",
107 } {
108 if original, ok := available[mime]; ok {
109 return original, true
110 }
111 }
112 return "", false
113}