Go 117 lines
// 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()
}