|
|
@@ -22,171 +22,169 @@ function writeOsc52(text: string): void {
|
|
|
process.stdout.write(sequence)
|
|
|
}
|
|
|
|
|
|
-export namespace Clipboard {
|
|
|
- export interface Content {
|
|
|
- data: string
|
|
|
- mime: string
|
|
|
- }
|
|
|
+export interface Content {
|
|
|
+ data: string
|
|
|
+ mime: string
|
|
|
+}
|
|
|
|
|
|
- // Checks clipboard for images first, then falls back to text.
|
|
|
- //
|
|
|
- // On Windows prompt/ can call this from multiple paste signals because
|
|
|
- // terminals surface image paste differently:
|
|
|
- // 1. A forwarded Ctrl+V keypress
|
|
|
- // 2. An empty bracketed-paste hint for image-only clipboard in Windows
|
|
|
- // Terminal <1.25
|
|
|
- // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
|
|
|
- export async function read(): Promise<Content | undefined> {
|
|
|
- const os = platform()
|
|
|
+// Checks clipboard for images first, then falls back to text.
|
|
|
+//
|
|
|
+// On Windows prompt/ can call this from multiple paste signals because
|
|
|
+// terminals surface image paste differently:
|
|
|
+// 1. A forwarded Ctrl+V keypress
|
|
|
+// 2. An empty bracketed-paste hint for image-only clipboard in Windows
|
|
|
+// Terminal <1.25
|
|
|
+// 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
|
|
|
+export async function read(): Promise<Content | undefined> {
|
|
|
+ const os = platform()
|
|
|
|
|
|
- if (os === "darwin") {
|
|
|
- const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
|
|
|
- try {
|
|
|
- await Process.run(
|
|
|
- [
|
|
|
- "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: true },
|
|
|
- )
|
|
|
- const buffer = await Filesystem.readBytes(tmpfile)
|
|
|
- return { data: buffer.toString("base64"), mime: "image/png" }
|
|
|
- } catch {
|
|
|
- } finally {
|
|
|
- await fs.rm(tmpfile, { force: true }).catch(() => {})
|
|
|
- }
|
|
|
+ if (os === "darwin") {
|
|
|
+ const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
|
|
|
+ try {
|
|
|
+ await Process.run(
|
|
|
+ [
|
|
|
+ "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: true },
|
|
|
+ )
|
|
|
+ const buffer = await Filesystem.readBytes(tmpfile)
|
|
|
+ return { data: buffer.toString("base64"), mime: "image/png" }
|
|
|
+ } catch {
|
|
|
+ } finally {
|
|
|
+ await fs.rm(tmpfile, { force: true }).catch(() => {})
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // Windows/WSL: probe clipboard for images via PowerShell.
|
|
|
- // Bracketed paste can't carry image data so we read it directly.
|
|
|
- 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 Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
|
|
|
- nothrow: true,
|
|
|
- })
|
|
|
- if (base64.text) {
|
|
|
- const imageBuffer = Buffer.from(base64.text.trim(), "base64")
|
|
|
- if (imageBuffer.length > 0) {
|
|
|
- return { data: imageBuffer.toString("base64"), mime: "image/png" }
|
|
|
- }
|
|
|
+ // Windows/WSL: probe clipboard for images via PowerShell.
|
|
|
+ // Bracketed paste can't carry image data so we read it directly.
|
|
|
+ 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 Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
|
|
|
+ nothrow: true,
|
|
|
+ })
|
|
|
+ if (base64.text) {
|
|
|
+ const imageBuffer = Buffer.from(base64.text.trim(), "base64")
|
|
|
+ if (imageBuffer.length > 0) {
|
|
|
+ return { data: imageBuffer.toString("base64"), mime: "image/png" }
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (os === "linux") {
|
|
|
- const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
|
|
|
- if (wayland.stdout.byteLength > 0) {
|
|
|
- return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
|
|
|
- }
|
|
|
- const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
|
|
|
- nothrow: true,
|
|
|
- })
|
|
|
- if (x11.stdout.byteLength > 0) {
|
|
|
- return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
|
|
|
- }
|
|
|
+ if (os === "linux") {
|
|
|
+ const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true })
|
|
|
+ if (wayland.stdout.byteLength > 0) {
|
|
|
+ return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
|
|
|
}
|
|
|
-
|
|
|
- const text = await clipboardy.read().catch(() => {})
|
|
|
- if (text) {
|
|
|
- return { data: text, mime: "text/plain" }
|
|
|
+ const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
|
|
|
+ nothrow: true,
|
|
|
+ })
|
|
|
+ if (x11.stdout.byteLength > 0) {
|
|
|
+ return { data: Buffer.from(x11.stdout).toString("base64"), mime: "image/png" }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const getCopyMethod = lazy(() => {
|
|
|
- const os = platform()
|
|
|
+ const text = await clipboardy.read().catch(() => {})
|
|
|
+ if (text) {
|
|
|
+ return { data: text, mime: "text/plain" }
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- if (os === "darwin" && which("osascript")) {
|
|
|
- console.log("clipboard: using osascript")
|
|
|
- return async (text: string) => {
|
|
|
- const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
|
- await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
|
|
|
- }
|
|
|
+const getCopyMethod = lazy(() => {
|
|
|
+ const os = platform()
|
|
|
+
|
|
|
+ if (os === "darwin" && which("osascript")) {
|
|
|
+ console.log("clipboard: using osascript")
|
|
|
+ return async (text: string) => {
|
|
|
+ const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
|
+ await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (os === "linux") {
|
|
|
- if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
|
|
|
- console.log("clipboard: using wl-copy")
|
|
|
- return async (text: string) => {
|
|
|
- const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
|
|
- if (!proc.stdin) return
|
|
|
- proc.stdin.write(text)
|
|
|
- proc.stdin.end()
|
|
|
- await proc.exited.catch(() => {})
|
|
|
- }
|
|
|
- }
|
|
|
- if (which("xclip")) {
|
|
|
- console.log("clipboard: using xclip")
|
|
|
- return async (text: string) => {
|
|
|
- const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
|
|
|
- stdin: "pipe",
|
|
|
- stdout: "ignore",
|
|
|
- stderr: "ignore",
|
|
|
- })
|
|
|
- if (!proc.stdin) return
|
|
|
- proc.stdin.write(text)
|
|
|
- proc.stdin.end()
|
|
|
- await proc.exited.catch(() => {})
|
|
|
- }
|
|
|
+ if (os === "linux") {
|
|
|
+ if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
|
|
|
+ console.log("clipboard: using wl-copy")
|
|
|
+ return async (text: string) => {
|
|
|
+ const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
|
|
+ if (!proc.stdin) return
|
|
|
+ proc.stdin.write(text)
|
|
|
+ proc.stdin.end()
|
|
|
+ await proc.exited.catch(() => {})
|
|
|
}
|
|
|
- if (which("xsel")) {
|
|
|
- console.log("clipboard: using xsel")
|
|
|
- return async (text: string) => {
|
|
|
- const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
|
|
|
- stdin: "pipe",
|
|
|
- stdout: "ignore",
|
|
|
- stderr: "ignore",
|
|
|
- })
|
|
|
- if (!proc.stdin) return
|
|
|
- proc.stdin.write(text)
|
|
|
- proc.stdin.end()
|
|
|
- await proc.exited.catch(() => {})
|
|
|
- }
|
|
|
+ }
|
|
|
+ if (which("xclip")) {
|
|
|
+ console.log("clipboard: using xclip")
|
|
|
+ return async (text: string) => {
|
|
|
+ const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
|
|
|
+ stdin: "pipe",
|
|
|
+ stdout: "ignore",
|
|
|
+ stderr: "ignore",
|
|
|
+ })
|
|
|
+ if (!proc.stdin) return
|
|
|
+ proc.stdin.write(text)
|
|
|
+ proc.stdin.end()
|
|
|
+ await proc.exited.catch(() => {})
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- if (os === "win32") {
|
|
|
- console.log("clipboard: using powershell")
|
|
|
+ if (which("xsel")) {
|
|
|
+ console.log("clipboard: using xsel")
|
|
|
return async (text: string) => {
|
|
|
- // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
|
|
|
- const proc = Process.spawn(
|
|
|
- [
|
|
|
- "powershell.exe",
|
|
|
- "-NonInteractive",
|
|
|
- "-NoProfile",
|
|
|
- "-Command",
|
|
|
- "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
|
- ],
|
|
|
- {
|
|
|
- stdin: "pipe",
|
|
|
- stdout: "ignore",
|
|
|
- stderr: "ignore",
|
|
|
- },
|
|
|
- )
|
|
|
-
|
|
|
+ const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
|
|
|
+ stdin: "pipe",
|
|
|
+ stdout: "ignore",
|
|
|
+ stderr: "ignore",
|
|
|
+ })
|
|
|
if (!proc.stdin) return
|
|
|
proc.stdin.write(text)
|
|
|
proc.stdin.end()
|
|
|
await proc.exited.catch(() => {})
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- console.log("clipboard: no native support")
|
|
|
+ if (os === "win32") {
|
|
|
+ console.log("clipboard: using powershell")
|
|
|
return async (text: string) => {
|
|
|
- await clipboardy.write(text).catch(() => {})
|
|
|
+ // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
|
|
|
+ const proc = Process.spawn(
|
|
|
+ [
|
|
|
+ "powershell.exe",
|
|
|
+ "-NonInteractive",
|
|
|
+ "-NoProfile",
|
|
|
+ "-Command",
|
|
|
+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
|
+ ],
|
|
|
+ {
|
|
|
+ stdin: "pipe",
|
|
|
+ stdout: "ignore",
|
|
|
+ stderr: "ignore",
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ if (!proc.stdin) return
|
|
|
+ proc.stdin.write(text)
|
|
|
+ proc.stdin.end()
|
|
|
+ await proc.exited.catch(() => {})
|
|
|
}
|
|
|
- })
|
|
|
+ }
|
|
|
|
|
|
- export async function copy(text: string): Promise<void> {
|
|
|
- writeOsc52(text)
|
|
|
- await getCopyMethod()(text)
|
|
|
+ console.log("clipboard: no native support")
|
|
|
+ return async (text: string) => {
|
|
|
+ await clipboardy.write(text).catch(() => {})
|
|
|
}
|
|
|
+})
|
|
|
+
|
|
|
+export async function copy(text: string): Promise<void> {
|
|
|
+ writeOsc52(text)
|
|
|
+ await getCopyMethod()(text)
|
|
|
}
|