on main help sign in
fix(clipboard): support platforms without clipboard access
4bd0116Kieran Klukas
internal/app/app.go+1−1
@@ -21,6 +21,7 @@ import (
21 "github.com/charmbracelet/crush/internal/agent"
21 "github.com/charmbracelet/crush/internal/agent"
22 "github.com/charmbracelet/crush/internal/agent/notify"
22 "github.com/charmbracelet/crush/internal/agent/notify"
23 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
23 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
24+ "github.com/charmbracelet/crush/internal/clipboard"
24 "github.com/charmbracelet/crush/internal/config"
25 "github.com/charmbracelet/crush/internal/config"
25 "github.com/charmbracelet/crush/internal/db"
26 "github.com/charmbracelet/crush/internal/db"
26 "github.com/charmbracelet/crush/internal/event"
27 "github.com/charmbracelet/crush/internal/event"
@@ -42,7 +43,6 @@ import (
42 "github.com/charmbracelet/x/ansi"
43 "github.com/charmbracelet/x/ansi"
43 "github.com/charmbracelet/x/exp/charmtone"
44 "github.com/charmbracelet/x/exp/charmtone"
44 "github.com/charmbracelet/x/term"
45 "github.com/charmbracelet/x/term"
45− "golang.design/x/clipboard"
46)
46)
47
47
48// UpdateAvailableMsg is sent when a new version is available.
48// UpdateAvailableMsg is sent when a new version is available.
internal/clipboard/clipboard.gonew file+44
@@ -0,0 +1,44 @@
1+// Package clipboard provides cross-platform clipboard access behind build-tag
2+// guards so that unsupported platforms (e.g., Android, iOS) compile without
3+// requiring CGO or platform-specific dependencies.
4+package clipboard
5+
6+import "errors"
7+
8+// Format identifies the kind of data to read from the clipboard.
9+type Format int
10+
11+const (
12+ // FormatText is plain UTF-8 text.
13+ FormatText Format = iota
14+ // FormatImage is binary image data (PNG).
15+ FormatImage
16+)
17+
18+var (
19+ // ErrUnsupported is returned when clipboard access is not available on
20+ // the current platform.
21+ ErrUnsupported = errors.New("clipboard operations are not supported on this platform")
22+ // ErrEmpty is returned when the clipboard holds no data for the
23+ // requested format.
24+ ErrEmpty = errors.New("clipboard is empty or holds an unsupported format")
25+)
26+
27+// Init initializes the clipboard subsystem. On unsupported platforms it
28+// returns ErrUnsupported but is otherwise safe to call.
29+func Init() error {
30+ return initClipboard()
31+}
32+
33+// WriteText writes plain text to the system clipboard. On unsupported
34+// platforms it is a no-op.
35+func WriteText(text string) {
36+ writeText(text)
37+}
38+
39+// Read returns the clipboard contents for the given format. It returns
40+// ErrEmpty when no matching data is present and ErrUnsupported on platforms
41+// without clipboard support.
42+func Read(f Format) ([]byte, error) {
43+ return read(f)
44+}
internal/clipboard/clipboard_not_supported.gonew file+13
@@ -0,0 +1,13 @@
1+//go:build !((darwin || linux || windows || freebsd || openbsd || netbsd) && !ios && !android)
2+
3+package clipboard
4+
5+func initClipboard() error {
6+ return ErrUnsupported
7+}
8+
9+func writeText(string) {}
10+
11+func read(Format) ([]byte, error) {
12+ return nil, ErrUnsupported
13+}
internal/clipboard/clipboard_supported.gonew file+30
@@ -0,0 +1,30 @@
1+//go:build (darwin || linux || windows || freebsd || openbsd || netbsd) && !ios && !android
2+
3+package clipboard
4+
5+import "golang.design/x/clipboard"
6+
7+func initClipboard() error {
8+ return clipboard.Init()
9+}
10+
11+func writeText(text string) {
12+ clipboard.Write(clipboard.FmtText, []byte(text))
13+}
14+
15+func read(f Format) ([]byte, error) {
16+ var format clipboard.Format
17+ switch f {
18+ case FormatText:
19+ format = clipboard.FmtText
20+ case FormatImage:
21+ format = clipboard.FmtImage
22+ default:
23+ return nil, ErrEmpty
24+ }
25+ data := clipboard.Read(format)
26+ if data == nil {
27+ return nil, ErrEmpty
28+ }
29+ return data, nil
30+}
internal/cmd/login.go+2−2
@@ -9,6 +9,7 @@ import (
9
9
10 "charm.land/lipgloss/v2"
10 "charm.land/lipgloss/v2"
11 "github.com/charmbracelet/crush/internal/client"
11 "github.com/charmbracelet/crush/internal/client"
12+ "github.com/charmbracelet/crush/internal/clipboard"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/oauth"
14 "github.com/charmbracelet/crush/internal/oauth"
14 "github.com/charmbracelet/crush/internal/oauth/copilot"
15 "github.com/charmbracelet/crush/internal/oauth/copilot"
@@ -16,7 +17,6 @@ import (
16 "github.com/charmbracelet/x/ansi"
17 "github.com/charmbracelet/x/ansi"
17 "github.com/pkg/browser"
18 "github.com/pkg/browser"
18 "github.com/spf13/cobra"
19 "github.com/spf13/cobra"
19− "golang.design/x/clipboard"
20)
20)
21
21
22var loginCmd = &cobra.Command{
22var loginCmd = &cobra.Command{
@@ -95,7 +95,7 @@ func loginHyper(c *client.Client, wsID string, force bool) error {
95 return err
95 return err
96 }
96 }
97
97
98− clipboard.Write(clipboard.FmtText, []byte(resp.UserCode))
98+ clipboard.WriteText(resp.UserCode)
99 fmt.Println("The following code should be on clipboard already:")
99 fmt.Println("The following code should be on clipboard already:")
100
100
101 fmt.Println()
101 fmt.Println()
internal/ui/common/common.go+2−2
@@ -6,12 +6,12 @@ import (
6 "os"
6 "os"
7
7
8 tea "charm.land/bubbletea/v2"
8 tea "charm.land/bubbletea/v2"
9+ "github.com/charmbracelet/crush/internal/clipboard"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/ui/styles"
11 "github.com/charmbracelet/crush/internal/ui/styles"
11 "github.com/charmbracelet/crush/internal/ui/util"
12 "github.com/charmbracelet/crush/internal/ui/util"
12 "github.com/charmbracelet/crush/internal/workspace"
13 "github.com/charmbracelet/crush/internal/workspace"
13 uv "github.com/charmbracelet/ultraviolet"
14 uv "github.com/charmbracelet/ultraviolet"
14− "golang.design/x/clipboard"
15)
15)
16
16
17// MaxAttachmentSize defines the maximum allowed size for file attachments (5 MB).
17// MaxAttachmentSize defines the maximum allowed size for file attachments (5 MB).
@@ -112,7 +112,7 @@ func CopyToClipboardWithCallback(text, successMessage string, callback tea.Cmd)
112 return tea.Sequence(
112 return tea.Sequence(
113 tea.SetClipboard(text),
113 tea.SetClipboard(text),
114 func() tea.Msg {
114 func() tea.Msg {
115− clipboard.Write(clipboard.FmtText, []byte(text))
115+ clipboard.WriteText(text)
116 return nil
116 return nil
117 },
117 },
118 callback,
118 callback,
internal/ui/model/clipboard.godeleted−15
@@ -1,15 +0,0 @@
1−package model
2−
3−import "errors"
4−
5−type clipboardFormat int
6−
7−const (
8− clipboardFormatText clipboardFormat = iota
9− clipboardFormatImage
10−)
11−
12−var (
13− errClipboardPlatformUnsupported = errors.New("clipboard operations are not supported on this platform")
14− errClipboardUnknownFormat = errors.New("unknown clipboard format")
15−)
internal/ui/model/clipboard_not_supported.godeleted−7
@@ -1,7 +0,0 @@
1−//go:build !((darwin || linux || windows || freebsd || openbsd || netbsd) && !ios && !android)
2−
3−package model
4−
5−func readClipboard(clipboardFormat) ([]byte, error) {
6− return nil, errClipboardPlatformUnsupported
7−}
internal/ui/model/clipboard_supported.godeleted−23
@@ -1,23 +0,0 @@
1−//go:build (darwin || linux || windows || freebsd || openbsd || netbsd) && !ios && !android
2−
3−package model
4−
5−import "golang.design/x/clipboard"
6−
7−func readClipboard(f clipboardFormat) ([]byte, error) {
8− switch f {
9− case clipboardFormatText:
10− data := clipboard.Read(clipboard.FmtText)
11− if data == nil {
12− return nil, errClipboardUnknownFormat
13− }
14− return data, nil
15− case clipboardFormatImage:
16− data := clipboard.Read(clipboard.FmtImage)
17− if data == nil {
18− return nil, errClipboardUnknownFormat
19− }
20− return data, nil
21− }
22− return nil, errClipboardUnknownFormat
23−}
internal/ui/model/ui.go+3−2
@@ -31,6 +31,7 @@ import (
31 agenttools "github.com/charmbracelet/crush/internal/agent/tools"
31 agenttools "github.com/charmbracelet/crush/internal/agent/tools"
32 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
32 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
33 "github.com/charmbracelet/crush/internal/app"
33 "github.com/charmbracelet/crush/internal/app"
34+ "github.com/charmbracelet/crush/internal/clipboard"
34 "github.com/charmbracelet/crush/internal/commands"
35 "github.com/charmbracelet/crush/internal/commands"
35 "github.com/charmbracelet/crush/internal/config"
36 "github.com/charmbracelet/crush/internal/config"
36 "github.com/charmbracelet/crush/internal/fsext"
37 "github.com/charmbracelet/crush/internal/fsext"
@@ -3856,7 +3857,7 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd {
3856// creates an attachment. If no image data is found, it falls back to
3857// creates an attachment. If no image data is found, it falls back to
3857// interpreting clipboard text as a file path.
3858// interpreting clipboard text as a file path.
3858func (m *UI) pasteImageFromClipboard() tea.Msg {
3859func (m *UI) pasteImageFromClipboard() tea.Msg {
3859− imageData, err := readClipboard(clipboardFormatImage)
3860+ imageData, err := clipboard.Read(clipboard.FormatImage)
3860 if int64(len(imageData)) > common.MaxAttachmentSize {
3861 if int64(len(imageData)) > common.MaxAttachmentSize {
3861 return util.InfoMsg{
3862 return util.InfoMsg{
3862 Type: util.InfoTypeError,
3863 Type: util.InfoTypeError,
@@ -3873,7 +3874,7 @@ func (m *UI) pasteImageFromClipboard() tea.Msg {
3873 }
3874 }
3874 }
3875 }
3875
3876
3876− textData, textErr := readClipboard(clipboardFormatText)
3877+ textData, textErr := clipboard.Read(clipboard.FormatText)
3877 if textErr != nil || len(textData) == 0 {
3878 if textErr != nil || len(textData) == 0 {
3878 return nil // Clipboard is empty or does not contain an image
3879 return nil // Clipboard is empty or does not contain an image
3879 }
3880 }