clipboard_linux.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. // Copyright 2021 The golang.design Initiative Authors.
  2. // All rights reserved. Use of this source code is governed
  3. // by a MIT license that can be found in the LICENSE file.
  4. //
  5. // Written by Changkun Ou <changkun.de>
  6. //go:build linux
  7. package clipboard
  8. import (
  9. "bytes"
  10. "context"
  11. "fmt"
  12. "log/slog"
  13. "os/exec"
  14. "strings"
  15. "sync"
  16. "time"
  17. )
  18. var (
  19. // Clipboard tools in order of preference
  20. clipboardTools = []struct {
  21. name string
  22. readCmd []string
  23. writeCmd []string
  24. readImg []string
  25. writeImg []string
  26. available bool
  27. }{
  28. {
  29. name: "xclip",
  30. readCmd: []string{"xclip", "-selection", "clipboard", "-o"},
  31. writeCmd: []string{"xclip", "-selection", "clipboard"},
  32. readImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png", "-o"},
  33. writeImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png"},
  34. },
  35. {
  36. name: "xsel",
  37. readCmd: []string{"xsel", "--clipboard", "--output"},
  38. writeCmd: []string{"xsel", "--clipboard", "--input"},
  39. readImg: []string{"xsel", "--clipboard", "--output"},
  40. writeImg: []string{"xsel", "--clipboard", "--input"},
  41. },
  42. {
  43. name: "wl-clipboard",
  44. readCmd: []string{"wl-paste", "-n"},
  45. writeCmd: []string{"wl-copy"},
  46. readImg: []string{"wl-paste", "-t", "image/png", "-n"},
  47. writeImg: []string{"wl-copy", "-t", "image/png"},
  48. },
  49. }
  50. selectedTool int = -1
  51. toolMutex sync.Mutex
  52. lastChangeTime time.Time
  53. changeTimeMu sync.Mutex
  54. )
  55. func initialize() error {
  56. toolMutex.Lock()
  57. defer toolMutex.Unlock()
  58. if selectedTool >= 0 {
  59. return nil // Already initialized
  60. }
  61. // Check which clipboard tool is available
  62. for i, tool := range clipboardTools {
  63. cmd := exec.Command("which", tool.name)
  64. if err := cmd.Run(); err == nil {
  65. clipboardTools[i].available = true
  66. if selectedTool < 0 {
  67. selectedTool = i
  68. slog.Debug("Clipboard tool found", "tool", tool.name)
  69. }
  70. }
  71. }
  72. if selectedTool < 0 {
  73. slog.Warn("No clipboard utility found on system. Copy/paste functionality will be disabled.")
  74. return fmt.Errorf(`%w: No clipboard utility found. Install one of the following:
  75. For X11 systems:
  76. apt install -y xclip
  77. # or
  78. apt install -y xsel
  79. For Wayland systems:
  80. apt install -y wl-clipboard
  81. If running in a headless environment, you may also need:
  82. apt install -y xvfb
  83. # and run:
  84. Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
  85. export DISPLAY=:99.0`, errUnavailable)
  86. }
  87. return nil
  88. }
  89. func read(t Format) (buf []byte, err error) {
  90. // Ensure clipboard is initialized before attempting to read
  91. if err := initialize(); err != nil {
  92. slog.Debug("Clipboard read failed: not initialized", "error", err)
  93. return nil, err
  94. }
  95. toolMutex.Lock()
  96. tool := clipboardTools[selectedTool]
  97. toolMutex.Unlock()
  98. switch t {
  99. case FmtText:
  100. return readText(tool)
  101. case FmtImage:
  102. return readImage(tool)
  103. default:
  104. return nil, errUnsupported
  105. }
  106. }
  107. func readText(tool struct {
  108. name string
  109. readCmd []string
  110. writeCmd []string
  111. readImg []string
  112. writeImg []string
  113. available bool
  114. }) ([]byte, error) {
  115. // First check if clipboard contains text
  116. cmd := exec.Command(tool.readCmd[0], tool.readCmd[1:]...)
  117. out, err := cmd.Output()
  118. if err != nil {
  119. // Check if it's because clipboard contains non-text data
  120. if tool.name == "xclip" {
  121. // xclip returns error when clipboard doesn't contain requested type
  122. checkCmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
  123. targets, _ := checkCmd.Output()
  124. if bytes.Contains(targets, []byte("image/png")) && !bytes.Contains(targets, []byte("UTF8_STRING")) {
  125. return nil, errUnavailable
  126. }
  127. }
  128. return nil, errUnavailable
  129. }
  130. return out, nil
  131. }
  132. func readImage(tool struct {
  133. name string
  134. readCmd []string
  135. writeCmd []string
  136. readImg []string
  137. writeImg []string
  138. available bool
  139. }) ([]byte, error) {
  140. if tool.name == "xsel" {
  141. // xsel doesn't support image types well, return error
  142. return nil, errUnavailable
  143. }
  144. cmd := exec.Command(tool.readImg[0], tool.readImg[1:]...)
  145. out, err := cmd.Output()
  146. if err != nil {
  147. return nil, errUnavailable
  148. }
  149. // Verify it's PNG data
  150. if len(out) < 8 || !bytes.Equal(out[:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
  151. return nil, errUnavailable
  152. }
  153. return out, nil
  154. }
  155. func write(t Format, buf []byte) (<-chan struct{}, error) {
  156. // Ensure clipboard is initialized before attempting to write
  157. if err := initialize(); err != nil {
  158. return nil, err
  159. }
  160. toolMutex.Lock()
  161. tool := clipboardTools[selectedTool]
  162. toolMutex.Unlock()
  163. var cmd *exec.Cmd
  164. switch t {
  165. case FmtText:
  166. if len(buf) == 0 {
  167. // Write empty string
  168. cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
  169. cmd.Stdin = bytes.NewReader([]byte{})
  170. } else {
  171. cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
  172. cmd.Stdin = bytes.NewReader(buf)
  173. }
  174. case FmtImage:
  175. if tool.name == "xsel" {
  176. // xsel doesn't support image types well
  177. return nil, errUnavailable
  178. }
  179. if len(buf) == 0 {
  180. // Clear clipboard
  181. cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
  182. cmd.Stdin = bytes.NewReader([]byte{})
  183. } else {
  184. cmd = exec.Command(tool.writeImg[0], tool.writeImg[1:]...)
  185. cmd.Stdin = bytes.NewReader(buf)
  186. }
  187. default:
  188. return nil, errUnsupported
  189. }
  190. if err := cmd.Run(); err != nil {
  191. return nil, errUnavailable
  192. }
  193. // Update change time
  194. changeTimeMu.Lock()
  195. lastChangeTime = time.Now()
  196. currentTime := lastChangeTime
  197. changeTimeMu.Unlock()
  198. // Create change notification channel
  199. changed := make(chan struct{}, 1)
  200. go func() {
  201. for {
  202. time.Sleep(time.Second)
  203. changeTimeMu.Lock()
  204. if !lastChangeTime.Equal(currentTime) {
  205. changeTimeMu.Unlock()
  206. changed <- struct{}{}
  207. close(changed)
  208. return
  209. }
  210. changeTimeMu.Unlock()
  211. }
  212. }()
  213. return changed, nil
  214. }
  215. func watch(ctx context.Context, t Format) <-chan []byte {
  216. recv := make(chan []byte, 1)
  217. // Ensure clipboard is initialized before starting watch
  218. if err := initialize(); err != nil {
  219. close(recv)
  220. return recv
  221. }
  222. ti := time.NewTicker(time.Second)
  223. // Get initial clipboard content
  224. var lastContent []byte
  225. if b := Read(t); b != nil {
  226. lastContent = make([]byte, len(b))
  227. copy(lastContent, b)
  228. }
  229. go func() {
  230. defer close(recv)
  231. defer ti.Stop()
  232. for {
  233. select {
  234. case <-ctx.Done():
  235. return
  236. case <-ti.C:
  237. b := Read(t)
  238. if b == nil {
  239. continue
  240. }
  241. // Check if content changed
  242. if !bytes.Equal(lastContent, b) {
  243. recv <- b
  244. lastContent = make([]byte, len(b))
  245. copy(lastContent, b)
  246. }
  247. }
  248. }
  249. }()
  250. return recv
  251. }
  252. // Helper function to check clipboard content type for xclip
  253. func getClipboardTargets() []string {
  254. cmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
  255. out, err := cmd.Output()
  256. if err != nil {
  257. return nil
  258. }
  259. return strings.Split(string(out), "\n")
  260. }