| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- 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<Content | undefined> {
- 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<void> {
- 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)
- }
- }
|