| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "os/exec" |
| 8 | "runtime" |
| 9 | "strings" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/atotto/clipboard" |
| 13 | |
| 14 | "reasonix/internal/proc" |
| 15 | ) |
| 16 | |
| 17 | // wslClipboardWriteScript reads stdin as UTF-8 and lets Windows PowerShell |
| 18 | // perform the UTF-16 clipboard write. Passing text through clip.exe instead |
| 19 | // makes the Windows program decode the raw UTF-8 bytes using the active OEM |
| 20 | // code page (CP936 on Chinese systems), producing mojibake such as 中文 -> 涓枃. |
| 21 | const wslClipboardWriteScript = `$stdin = [Console]::OpenStandardInput() |
| 22 | $reader = [IO.StreamReader]::new($stdin, [Text.UTF8Encoding]::new($false)) |
| 23 | Set-Clipboard -Value $reader.ReadToEnd()` |
| 24 | |
| 25 | // writeClipboardText uses the normal platform clipboard outside WSL. WSL is a |
| 26 | // Linux process: preserve its clipboard utilities, but replace the clip.exe |
| 27 | // fallback that would otherwise decode UTF-8 using a Windows code page. |
| 28 | func writeClipboardText(text string) error { |
| 29 | if isWSL() { |
| 30 | return writeWSLClipboardText(text) |
| 31 | } |
| 32 | return clipboard.WriteAll(text) |
| 33 | } |
| 34 | |
| 35 | func isWSL() bool { |
| 36 | return isWSLFor(runtime.GOOS, os.Getenv) |
| 37 | } |
| 38 | |
| 39 | func isWSLFor(goos string, getenv func(string) string) bool { |
| 40 | if goos != "linux" { |
| 41 | return false |
| 42 | } |
| 43 | return getenv("WSL_DISTRO_NAME") != "" || getenv("WSL_INTEROP") != "" |
| 44 | } |
| 45 | |
| 46 | func writeWSLClipboardText(text string) error { |
| 47 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| 48 | defer cancel() |
| 49 | cmd := newWSLClipboardCommand(ctx, text) |
| 50 | var stderr strings.Builder |
| 51 | cmd.Stderr = &stderr |
| 52 | if err := cmd.Run(); err != nil { |
| 53 | if ctx.Err() != nil { |
| 54 | return fmt.Errorf("write WSL clipboard: %w", ctx.Err()) |
| 55 | } |
| 56 | if detail := strings.TrimSpace(stderr.String()); detail != "" { |
| 57 | return fmt.Errorf("write WSL clipboard: %w: %s", err, detail) |
| 58 | } |
| 59 | return fmt.Errorf("write WSL clipboard: %w", err) |
| 60 | } |
| 61 | return nil |
| 62 | } |
| 63 | |
| 64 | func newWSLClipboardCommand(ctx context.Context, text string) *exec.Cmd { |
| 65 | args := []string{ |
| 66 | "powershell.exe", |
| 67 | "-NoProfile", |
| 68 | "-NonInteractive", |
| 69 | "-Command", |
| 70 | wslClipboardWriteScript, |
| 71 | } |
| 72 | // Keep native Linux clipboard support when Windows interop is unavailable. |
| 73 | switch { |
| 74 | case os.Getenv("WAYLAND_DISPLAY") != "" && clipboardCommandAvailable("wl-copy"): |
| 75 | args = []string{"wl-copy"} |
| 76 | case clipboardCommandAvailable("xclip"): |
| 77 | args = []string{"xclip", "-in", "-selection", "clipboard"} |
| 78 | case clipboardCommandAvailable("xsel"): |
| 79 | args = []string{"xsel", "--input", "--clipboard"} |
| 80 | } |
| 81 | cmd := proc.CommandContext(ctx, args[0], args[1:]...) |
| 82 | proc.SetCancelKillsTree(cmd) |
| 83 | // Bound pipe draining too, if an interop child inherits an output handle. |
| 84 | cmd.WaitDelay = time.Second |
| 85 | cmd.Stdin = strings.NewReader(text) |
| 86 | return cmd |
| 87 | } |
| 88 | |
| 89 | func clipboardCommandAvailable(name string) bool { |
| 90 | _, err := exec.LookPath(name) |
| 91 | return err == nil |
| 92 | } |
| 93 |