import { $ } from "bun" import type { CliRenderer } from "@opentui/core" import { platform, release } from "os" import clipboardy from "clipboardy" import { lazy } from "../../../../util/lazy.js" import { tmpdir } from "os" import path from "path" const rendererRef = { current: undefined as CliRenderer | undefined } export namespace Clipboard { export interface Content { data: string mime: string } export function setRenderer(renderer: CliRenderer | undefined): void { rendererRef.current = renderer } export async function read(): Promise { const os = platform() if (os === "darwin") { const tmpfile = path.join(tmpdir(), "opencode-clipboard.png") try { await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'` .nothrow() .quiet() const file = Bun.file(tmpfile) const buffer = await file.arrayBuffer() return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" } } catch { } finally { await $`rm -f "${tmpfile}"`.nothrow().quiet() } } if (os === "win32" || release().includes("WSL")) { const script = "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }" const base64 = await $`powershell.exe -NonInteractive -NoProfile -command "${script}"`.nothrow().text() if (base64) { const imageBuffer = Buffer.from(base64.trim(), "base64") if (imageBuffer.length > 0) { return { data: imageBuffer.toString("base64"), mime: "image/png" } } } } if (os === "linux") { const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer() if (wayland && wayland.byteLength > 0) { return { data: Buffer.from(wayland).toString("base64"), mime: "image/png" } } const x11 = await $`xclip -selection clipboard -t image/png -o`.nothrow().arrayBuffer() if (x11 && x11.byteLength > 0) { return { data: Buffer.from(x11).toString("base64"), mime: "image/png" } } } const text = await clipboardy.read().catch(() => {}) if (text) { return { data: text, mime: "text/plain" } } } const getCopyMethod = lazy(() => { const os = platform() if (os === "darwin" && Bun.which("osascript")) { console.log("clipboard: using osascript") return async (text: string) => { const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet() } } if (os === "linux") { if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) { console.log("clipboard: using wl-copy") return async (text: string) => { const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) } } if (Bun.which("xclip")) { console.log("clipboard: using xclip") return async (text: string) => { const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe", stdout: "ignore", stderr: "ignore", }) proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) } } if (Bun.which("xsel")) { console.log("clipboard: using xsel") return async (text: string) => { const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe", stdout: "ignore", stderr: "ignore", }) proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) } } } if (os === "win32") { console.log("clipboard: using powershell") return async (text: string) => { // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) const proc = Bun.spawn( [ "powershell.exe", "-NonInteractive", "-NoProfile", "-Command", "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())", ], { stdin: "pipe", stdout: "ignore", stderr: "ignore", }, ) proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) } } console.log("clipboard: no native support") return async (text: string) => { await clipboardy.write(text).catch(() => {}) } }) export async function copy(text: string): Promise { const renderer = rendererRef.current if (renderer) { // Try OSC52 but don't early return - always fall back to native method // OSC52 may report success but not actually work in all terminals renderer.copyToClipboardOSC52(text) } await getCopyMethod()(text) } }