| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- // Copyright 2021 The golang.design Initiative Authors.
- // All rights reserved. Use of this source code is governed
- // by a MIT license that can be found in the LICENSE file.
- //
- // Written by Changkun Ou <changkun.de>
- //go:build linux
- package clipboard
- import (
- "bytes"
- "context"
- "fmt"
- "log/slog"
- "os"
- "os/exec"
- "strings"
- "sync"
- "time"
- )
- var (
- // Clipboard tools in order of preference
- clipboardTools = []struct {
- name string
- readCmd []string
- writeCmd []string
- readImg []string
- writeImg []string
- available bool
- }{
- {
- name: "xclip",
- readCmd: []string{"xclip", "-selection", "clipboard", "-o"},
- writeCmd: []string{"xclip", "-selection", "clipboard"},
- readImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png", "-o"},
- writeImg: []string{"xclip", "-selection", "clipboard", "-t", "image/png"},
- },
- {
- name: "xsel",
- readCmd: []string{"xsel", "--clipboard", "--output"},
- writeCmd: []string{"xsel", "--clipboard", "--input"},
- readImg: []string{"xsel", "--clipboard", "--output"},
- writeImg: []string{"xsel", "--clipboard", "--input"},
- },
- {
- name: "wl-copy",
- readCmd: []string{"wl-paste", "-n"},
- writeCmd: []string{"wl-copy"},
- readImg: []string{"wl-paste", "-t", "image/png", "-n"},
- writeImg: []string{"wl-copy", "-t", "image/png"},
- },
- }
- selectedTool int = -1
- toolMutex sync.Mutex
- lastChangeTime time.Time
- changeTimeMu sync.Mutex
- )
- func initialize() error {
- toolMutex.Lock()
- defer toolMutex.Unlock()
- if selectedTool >= 0 {
- return nil // Already initialized
- }
- order := []string{"xclip", "xsel", "wl-copy"}
- if os.Getenv("WAYLAND_DISPLAY") != "" {
- order = []string{"wl-copy", "xclip", "xsel"}
- }
- for _, name := range order {
- for i, tool := range clipboardTools {
- if tool.name == name {
- cmd := exec.Command("which", tool.name)
- if err := cmd.Run(); err == nil {
- clipboardTools[i].available = true
- if selectedTool < 0 {
- selectedTool = i
- slog.Debug("Clipboard tool found", "tool", tool.name)
- }
- }
- break
- }
- }
- }
- if selectedTool < 0 {
- slog.Warn(
- "No clipboard utility found on system. Copy/paste functionality will be disabled. See https://opencode.ai/docs/troubleshooting/ for more information.",
- )
- return fmt.Errorf(`%w: No clipboard utility found. Install one of the following:
- For X11 systems:
- apt install -y xclip
- # or
- apt install -y xsel
- For Wayland systems:
- apt install -y wl-clipboard
- If running in a headless environment, you may also need:
- apt install -y xvfb
- # and run:
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- export DISPLAY=:99.0`, errUnavailable)
- }
- return nil
- }
- func read(t Format) (buf []byte, err error) {
- // Ensure clipboard is initialized before attempting to read
- if err := initialize(); err != nil {
- slog.Debug("Clipboard read failed: not initialized", "error", err)
- return nil, err
- }
- toolMutex.Lock()
- tool := clipboardTools[selectedTool]
- toolMutex.Unlock()
- switch t {
- case FmtText:
- return readText(tool)
- case FmtImage:
- return readImage(tool)
- default:
- return nil, errUnsupported
- }
- }
- func readText(tool struct {
- name string
- readCmd []string
- writeCmd []string
- readImg []string
- writeImg []string
- available bool
- }) ([]byte, error) {
- // First check if clipboard contains text
- cmd := exec.Command(tool.readCmd[0], tool.readCmd[1:]...)
- out, err := cmd.Output()
- if err != nil {
- // Check if it's because clipboard contains non-text data
- if tool.name == "xclip" {
- // xclip returns error when clipboard doesn't contain requested type
- checkCmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
- targets, _ := checkCmd.Output()
- if bytes.Contains(targets, []byte("image/png")) &&
- !bytes.Contains(targets, []byte("UTF8_STRING")) {
- return nil, errUnavailable
- }
- }
- return nil, errUnavailable
- }
- return out, nil
- }
- func readImage(tool struct {
- name string
- readCmd []string
- writeCmd []string
- readImg []string
- writeImg []string
- available bool
- }) ([]byte, error) {
- if tool.name == "xsel" {
- // xsel doesn't support image types well, return error
- return nil, errUnavailable
- }
- cmd := exec.Command(tool.readImg[0], tool.readImg[1:]...)
- out, err := cmd.Output()
- if err != nil {
- return nil, errUnavailable
- }
- // Verify it's PNG data
- if len(out) < 8 ||
- !bytes.Equal(out[:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
- return nil, errUnavailable
- }
- return out, nil
- }
- func write(t Format, buf []byte) (<-chan struct{}, error) {
- // Ensure clipboard is initialized before attempting to write
- if err := initialize(); err != nil {
- return nil, err
- }
- toolMutex.Lock()
- tool := clipboardTools[selectedTool]
- toolMutex.Unlock()
- var cmd *exec.Cmd
- switch t {
- case FmtText:
- if len(buf) == 0 {
- // Write empty string
- cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
- cmd.Stdin = bytes.NewReader([]byte{})
- } else {
- cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
- cmd.Stdin = bytes.NewReader(buf)
- }
- case FmtImage:
- if tool.name == "xsel" {
- // xsel doesn't support image types well
- return nil, errUnavailable
- }
- if len(buf) == 0 {
- // Clear clipboard
- cmd = exec.Command(tool.writeCmd[0], tool.writeCmd[1:]...)
- cmd.Stdin = bytes.NewReader([]byte{})
- } else {
- cmd = exec.Command(tool.writeImg[0], tool.writeImg[1:]...)
- cmd.Stdin = bytes.NewReader(buf)
- }
- default:
- return nil, errUnsupported
- }
- if err := cmd.Run(); err != nil {
- return nil, errUnavailable
- }
- // Update change time
- changeTimeMu.Lock()
- lastChangeTime = time.Now()
- currentTime := lastChangeTime
- changeTimeMu.Unlock()
- // Create change notification channel
- changed := make(chan struct{}, 1)
- go func() {
- for {
- time.Sleep(time.Second)
- changeTimeMu.Lock()
- if !lastChangeTime.Equal(currentTime) {
- changeTimeMu.Unlock()
- changed <- struct{}{}
- close(changed)
- return
- }
- changeTimeMu.Unlock()
- }
- }()
- return changed, nil
- }
- func watch(ctx context.Context, t Format) <-chan []byte {
- recv := make(chan []byte, 1)
- // Ensure clipboard is initialized before starting watch
- if err := initialize(); err != nil {
- close(recv)
- return recv
- }
- ti := time.NewTicker(time.Second)
- // Get initial clipboard content
- var lastContent []byte
- if b := Read(t); b != nil {
- lastContent = make([]byte, len(b))
- copy(lastContent, b)
- }
- go func() {
- defer close(recv)
- defer ti.Stop()
- for {
- select {
- case <-ctx.Done():
- return
- case <-ti.C:
- b := Read(t)
- if b == nil {
- continue
- }
- // Check if content changed
- if !bytes.Equal(lastContent, b) {
- recv <- b
- lastContent = make([]byte, len(b))
- copy(lastContent, b)
- }
- }
- }
- }()
- return recv
- }
- // Helper function to check clipboard content type for xclip
- func getClipboardTargets() []string {
- cmd := exec.Command("xclip", "-selection", "clipboard", "-t", "TARGETS", "-o")
- out, err := cmd.Output()
- if err != nil {
- return nil
- }
- return strings.Split(string(out), "\n")
- }
|