1// Package clipboard provides a clipboard adapter using wl-clipboard (Wayland) with X11 fallback.
2package clipboard
3
4import (
5 "context"
6 "fmt"
7 "os"
8 "os/exec"
9 "strings"
10
11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/logging"
13)
14
15// Adapter implements port.Clipboard using system clipboard tools.
16// Uses wl-clipboard for Wayland, falls back to xclip for X11.
17type Adapter struct {
18 copyCmd string
19 pasteCmd string
20}
21
22// New creates a new clipboard adapter.
23// Detects Wayland vs X11 and selects appropriate clipboard tool.
24func New() port.Clipboard {
25 a := &Adapter{}
26
27 // Check for Wayland first
28 if os.Getenv("WAYLAND_DISPLAY") != "" {
29 if path, err := exec.LookPath("wl-copy"); err == nil {
30 a.copyCmd = path
31 if pastePath, err := exec.LookPath("wl-paste"); err == nil {
32 a.pasteCmd = pastePath
33 }
34 }
35 }
36
37 // Fall back to X11 if Wayland tools not available
38 if a.copyCmd == "" && os.Getenv("DISPLAY") != "" {
39 if path, err := exec.LookPath("xclip"); err == nil {
40 a.copyCmd = path
41 a.pasteCmd = path
42 } else if path, err := exec.LookPath("xsel"); err == nil {
43 a.copyCmd = path
44 a.pasteCmd = path
45 }
46 }
47
48 return a
49}
50
51// WriteText copies text to the clipboard.
52func (a *Adapter) WriteText(ctx context.Context, text string) error {
53 log := logging.FromContext(ctx)
54
55 if a.copyCmd == "" {
56 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")
57 log.Error().Err(err).Msg("clipboard write failed")
58 return err
59 }
60
61 var cmd *exec.Cmd
62 if strings.Contains(a.copyCmd, "wl-copy") {
63 cmd = exec.CommandContext(ctx, a.copyCmd)
64 } else if strings.Contains(a.copyCmd, "xclip") {
65 cmd = exec.CommandContext(ctx, a.copyCmd, "-selection", "clipboard")
66 } else if strings.Contains(a.copyCmd, "xsel") {
67 cmd = exec.CommandContext(ctx, a.copyCmd, "--clipboard", "--input")
68 } else {
69 err := fmt.Errorf("unknown clipboard tool: %s", a.copyCmd)
70 log.Error().Err(err).Msg("clipboard write failed")
71 return err
72 }
73
74 cmd.Stdin = strings.NewReader(text)
75 if err := cmd.Run(); err != nil {
76 log.Error().Err(err).Str("tool", a.copyCmd).Msg("clipboard write failed")
77 return err
78 }
79
80 log.Debug().Str("tool", a.copyCmd).Int("len", len(text)).Msg("clipboard write success")
81 return nil
82}
83
84// ReadText reads text from the clipboard.
85func (a *Adapter) ReadText(ctx context.Context) (string, error) {
86 log := logging.FromContext(ctx)
87
88 if a.pasteCmd == "" {
89 err := fmt.Errorf("no clipboard tool available (install wl-clipboard or xclip)")
90 log.Error().Err(err).Msg("clipboard read failed")
91 return "", err
92 }
93
94 var cmd *exec.Cmd
95 if strings.Contains(a.pasteCmd, "wl-paste") {
96 cmd = exec.CommandContext(ctx, a.pasteCmd, "--no-newline")
97 } else if strings.Contains(a.pasteCmd, "xclip") {
98 cmd = exec.CommandContext(ctx, a.pasteCmd, "-selection", "clipboard", "-o")
99 } else if strings.Contains(a.pasteCmd, "xsel") {
100 cmd = exec.CommandContext(ctx, a.pasteCmd, "--clipboard", "--output")
101 } else {
102 err := fmt.Errorf("unknown clipboard tool: %s", a.pasteCmd)
103 log.Error().Err(err).Msg("clipboard read failed")
104 return "", err
105 }
106
107 out, err := cmd.Output()
108 if err != nil {
109 log.Debug().Err(err).Str("tool", a.pasteCmd).Msg("clipboard read failed (may be empty)")
110 return "", err
111 }
112
113 log.Debug().Str("tool", a.pasteCmd).Int("len", len(out)).Msg("clipboard read success")
114 return string(out), nil
115}
116
117// Clear clears the clipboard contents.
118func (a *Adapter) Clear(ctx context.Context) error {
119 return a.WriteText(ctx, "")
120}
121
122// HasText returns true if the clipboard contains text data.
123func (a *Adapter) HasText(ctx context.Context) (bool, error) {
124 text, err := a.ReadText(ctx)
125 if err != nil {
126 // Empty clipboard often returns error, treat as no text
127 return false, nil
128 }
129 return text != "", nil
130}