on main help sign in
fix(clipboard): support platforms without clipboard access

The clipboard library has no Android implementation, so importing it unconditionally broke the Android release build. Route all clipboard access through a single build-tag-guarded package so unsupported platforms compile cleanly, and consolidate the previously duplicated read and write wrappers into one canonical home.

commit4bd011669bdc6a91a47702752319f481b7dbceb0 authorKieran Klukas <kieran@dunkirk.sh> dateFri 19 Jun 2026, 00:04 (4 months ago) parent98d79e9
10 files changed+95−52
internal/app/app.go+1−1 internal/clipboard/clipboard.go+44 internal/clipboard/clipboard_not_supported.go+13 internal/clipboard/clipboard_supported.go+30 internal/cmd/login.go+2−2 internal/ui/common/common.go+2−2 internal/ui/model/clipboard.go−15 internal/ui/model/clipboard_not_supported.go−7 internal/ui/model/clipboard_supported.go−23 internal/ui/model/ui.go+3−2
internal/app/app.go+1−1
@@ -21,6 +21,7 @@ import (
2121 "github.com/charmbracelet/crush/internal/agent"
2222 "github.com/charmbracelet/crush/internal/agent/notify"
2323 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
24+ "github.com/charmbracelet/crush/internal/clipboard"
2425 "github.com/charmbracelet/crush/internal/config"
2526 "github.com/charmbracelet/crush/internal/db"
2627 "github.com/charmbracelet/crush/internal/event"
@@ -42,7 +43,6 @@ import (
4243 "github.com/charmbracelet/x/ansi"
4344 "github.com/charmbracelet/x/exp/charmtone"
4445 "github.com/charmbracelet/x/term"
45− "golang.design/x/clipboard"
4646)
4747
4848// 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 (
99
1010 "charm.land/lipgloss/v2"
1111 "github.com/charmbracelet/crush/internal/client"
12+ "github.com/charmbracelet/crush/internal/clipboard"
1213 "github.com/charmbracelet/crush/internal/config"
1314 "github.com/charmbracelet/crush/internal/oauth"
1415 "github.com/charmbracelet/crush/internal/oauth/copilot"
@@ -16,7 +17,6 @@ import (
1617 "github.com/charmbracelet/x/ansi"
1718 "github.com/pkg/browser"
1819 "github.com/spf13/cobra"
19− "golang.design/x/clipboard"
2020)
2121
2222var loginCmd = &cobra.Command{
@@ -95,7 +95,7 @@ func loginHyper(c *client.Client, wsID string, force bool) error {
9595 return err
9696 }
9797
98− clipboard.Write(clipboard.FmtText, []byte(resp.UserCode))
98+ clipboard.WriteText(resp.UserCode)
9999 fmt.Println("The following code should be on clipboard already:")
100100
101101 fmt.Println()
internal/ui/common/common.go+2−2
@@ -6,12 +6,12 @@ import (
66 "os"
77
88 tea "charm.land/bubbletea/v2"
9+ "github.com/charmbracelet/crush/internal/clipboard"
910 "github.com/charmbracelet/crush/internal/config"
1011 "github.com/charmbracelet/crush/internal/ui/styles"
1112 "github.com/charmbracelet/crush/internal/ui/util"
1213 "github.com/charmbracelet/crush/internal/workspace"
1314 uv "github.com/charmbracelet/ultraviolet"
14− "golang.design/x/clipboard"
1515)
1616
1717// MaxAttachmentSize defines the maximum allowed size for file attachments (5 MB).
@@ -112,7 +112,7 @@ func CopyToClipboardWithCallback(text, successMessage string, callback tea.Cmd)
112112 return tea.Sequence(
113113 tea.SetClipboard(text),
114114 func() tea.Msg {
115− clipboard.Write(clipboard.FmtText, []byte(text))
115+ clipboard.WriteText(text)
116116 return nil
117117 },
118118 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 (
3131 agenttools "github.com/charmbracelet/crush/internal/agent/tools"
3232 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
3333 "github.com/charmbracelet/crush/internal/app"
34+ "github.com/charmbracelet/crush/internal/clipboard"
3435 "github.com/charmbracelet/crush/internal/commands"
3536 "github.com/charmbracelet/crush/internal/config"
3637 "github.com/charmbracelet/crush/internal/fsext"
@@ -3856,7 +3857,7 @@ func (m *UI) handleFilePathPaste(path string) tea.Cmd {
38563857// creates an attachment. If no image data is found, it falls back to
38573858// interpreting clipboard text as a file path.
38583859func (m *UI) pasteImageFromClipboard() tea.Msg {
3859− imageData, err := readClipboard(clipboardFormatImage)
3860+ imageData, err := clipboard.Read(clipboard.FormatImage)
38603861 if int64(len(imageData)) > common.MaxAttachmentSize {
38613862 return util.InfoMsg{
38623863 Type: util.InfoTypeError,
@@ -3873,7 +3874,7 @@ func (m *UI) pasteImageFromClipboard() tea.Msg {
38733874 }
38743875 }
38753876
3876− textData, textErr := readClipboard(clipboardFormatText)
3877+ textData, textErr := clipboard.Read(clipboard.FormatText)
38773878 if textErr != nil || len(textData) == 0 {
38783879 return nil // Clipboard is empty or does not contain an image
38793880 }