// Package clip writes a string to the host operating system's // clipboard via the platform's native clipboard tool. It is the // delivery channel behind `eeco go --copy`: a one-shot paste path for // an assistant that has no terminal and no filesystem access (a // chat-only LLM, a web-only Gemini session, an AI Studio prompt). // // The package shells out — macOS uses pbcopy, Windows uses clip.exe, // Linux uses wl-copy on Wayland and falls back to xclip then xsel on // X11. No third-party dependency is taken; the stdlib suffices. When // no clipboard tool is reachable on PATH the package returns // ErrNoClipboardTool so the caller can render a precise install hint // and exit cleanly under eeco's "blocked (required tool missing)" // contract. package clip import ( "errors" "fmt" "os" "os/exec" "runtime" "strings" ) // ErrNoClipboardTool is returned when no platform clipboard tool was // found on PATH. Callers should treat this as the workflow contract's // exit-2 ("blocked: required tool missing"). var ErrNoClipboardTool = errors.New("no clipboard tool found on PATH") // runner executes the chosen clipboard tool with the given args, // feeding text to its stdin. The default uses os/exec; tests // substitute a fake. var runner = execRunner // lookPath finds a tool on PATH. Indirected for testability. var lookPath = exec.LookPath // getenv reads an environment variable. Indirected for testability. var getenv = os.Getenv // goos reports the build target. Indirected for testability. var goos = func() string { return runtime.GOOS } // Copy writes text to the platform clipboard. It returns // ErrNoClipboardTool when no supported tool is on PATH, or a wrapped // error when the tool exits non-zero. func Copy(text string) error { name, args, ok := detect() if !ok { return ErrNoClipboardTool } if err := runner(name, args, text); err != nil { return fmt.Errorf("clip: %s: %w", name, err) } return nil } // detect picks a clipboard tool for the current OS and environment. // The returned (name, args) pair is ready to hand to runner. ok is // false when no candidate tool resolves on PATH. func detect() (string, []string, bool) { switch goos() { case "darwin": if _, err := lookPath("pbcopy"); err == nil { return "pbcopy", nil, true } case "windows": if _, err := lookPath("clip.exe"); err == nil { return "clip.exe", nil, true } if _, err := lookPath("clip"); err == nil { return "clip", nil, true } case "linux", "freebsd", "openbsd", "netbsd": if getenv("WAYLAND_DISPLAY") != "" { if _, err := lookPath("wl-copy"); err == nil { return "wl-copy", nil, true } } if _, err := lookPath("xclip"); err == nil { return "xclip", []string{"-selection", "clipboard"}, true } if _, err := lookPath("xsel"); err == nil { return "xsel", []string{"--clipboard", "--input"}, true } if _, err := lookPath("wl-copy"); err == nil { return "wl-copy", nil, true } } return "", nil, false } // InstallHint returns a one-line "install one of these" message tuned // to the current platform. Callers use it to render a helpful stderr // message when Copy returns ErrNoClipboardTool. func InstallHint() string { switch goos() { case "darwin": return "install pbcopy (ships with macOS — check that /usr/bin is on PATH)" case "windows": return "install clip.exe (ships with Windows — check that the System32 directory is on PATH)" case "linux", "freebsd", "openbsd", "netbsd": return "install one of: wl-copy (Wayland), xclip, or xsel" default: return "no clipboard tool is bundled for this platform" } } func execRunner(name string, args []string, stdin string) error { // #nosec G204 — name and args are chosen by detect() from a fixed // allow-list of platform clipboard tools; stdin carries the brief // text and is not interpolated into the command line. cmd := exec.Command(name, args...) cmd.Stdin = strings.NewReader(stdin) return cmd.Run() }