From fc586d313a041da7d709b42b0c088c0b43d53e4a Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 23 Mar 2026 08:32:55 +0000 Subject: [PATCH] Fix clipboard in WSL by using sidecar's atotto/clipboard td's built-in copyToClipboard only tries xclip/xsel on Linux, which fails in WSL. Set model.ClipboardFn to use atotto/clipboard which falls through to clip.exe on WSL. --- internal/plugins/tdmonitor/plugin.go | 6 ++++ internal/plugins/tdmonitor/plugin_test.go | 39 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/internal/plugins/tdmonitor/plugin.go b/internal/plugins/tdmonitor/plugin.go index 5d868a2c..b1aa0e87 100644 --- a/internal/plugins/tdmonitor/plugin.go +++ b/internal/plugins/tdmonitor/plugin.go @@ -5,6 +5,7 @@ import ( "os/exec" "time" + "github.com/atotto/clipboard" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/marcus/td/pkg/monitor" @@ -119,6 +120,11 @@ func (p *Plugin) Init(ctx *plugin.Context) error { p.model = model + // Use sidecar's clipboard (atotto/clipboard) instead of td's built-in one. + // td's copyToClipboard doesn't handle WSL (tries xclip/xsel only); + // atotto/clipboard falls through to clip.exe on WSL. + model.ClipboardFn = clipboard.WriteAll + // Register TD bindings with sidecar's keymap (single source of truth) if ctx.Keymap != nil && model.Keymap != nil { for _, b := range model.Keymap.ExportBindings() { diff --git a/internal/plugins/tdmonitor/plugin_test.go b/internal/plugins/tdmonitor/plugin_test.go index 9ab4d623..7d745ec3 100644 --- a/internal/plugins/tdmonitor/plugin_test.go +++ b/internal/plugins/tdmonitor/plugin_test.go @@ -3,6 +3,7 @@ package tdmonitor import ( "log/slog" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -158,6 +159,44 @@ func TestInitWithValidDatabase(t *testing.T) { p.Stop() } +func TestInitSetsClipboardFn(t *testing.T) { + // Create a temp directory with a td database so we don't depend on + // the repo having one. + tmpDir, err := os.MkdirTemp("", "tdmonitor-clipboard-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer func() { _ = os.RemoveAll(tmpDir) }() + + // Initialize td in the temp directory + cmd := exec.Command("td", "init") + cmd.Dir = tmpDir + if out, err := cmd.CombinedOutput(); err != nil { + t.Skipf("td init failed (td not installed?): %s: %v", out, err) + } + + p := New() + ctx := &plugin.Context{ + WorkDir: tmpDir, + Logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})), + } + + if err := p.Init(ctx); err != nil { + t.Fatalf("Init failed: %v", err) + } + defer p.Stop() + + if p.model == nil { + t.Fatal("model should be created when database exists") + } + + // ClipboardFn must be set so sidecar's clipboard (atotto/clipboard) is used + // instead of td's built-in one, which doesn't handle WSL. + if p.model.ClipboardFn == nil { + t.Error("model.ClipboardFn should be set to sidecar's clipboard implementation") + } +} + func TestDiagnosticsWithDatabase(t *testing.T) { projectRoot := findProjectRootWithDB(t)