1- import { $ } from "bun "
1+ import { spawn , execSync } from "node:child_process "
22import { platform , release } from "os"
33import clipboardy from "clipboardy"
44import { lazy } from "../../../../util/lazy.js"
55import { tmpdir } from "os"
66import path from "path"
77
8+ function runCommand ( cmd : string , args : string [ ] ) : Promise < Buffer | undefined > {
9+ return new Promise ( ( resolve ) => {
10+ const proc = spawn ( cmd , args , { stdio : [ "ignore" , "pipe" , "ignore" ] } )
11+ const chunks : Buffer [ ] = [ ]
12+ proc . stdout . on ( "data" , ( chunk ) => chunks . push ( chunk ) )
13+ proc . on ( "close" , ( code ) => {
14+ if ( code === 0 && chunks . length > 0 ) {
15+ resolve ( Buffer . concat ( chunks ) )
16+ } else {
17+ resolve ( undefined )
18+ }
19+ } )
20+ proc . on ( "error" , ( ) => resolve ( undefined ) )
21+ } )
22+ }
23+
24+ function runCommandText ( cmd : string , args : string [ ] ) : Promise < string | undefined > {
25+ return runCommand ( cmd , args ) . then ( ( buf ) => buf ?. toString ( "utf-8" ) )
26+ }
27+
28+ function writeToCommand ( cmd : string , args : string [ ] , data : string ) : Promise < void > {
29+ return new Promise ( ( resolve ) => {
30+ const proc = spawn ( cmd , args , { stdio : [ "pipe" , "ignore" , "ignore" ] } )
31+ proc . stdin . write ( data )
32+ proc . stdin . end ( )
33+ proc . on ( "close" , ( ) => resolve ( ) )
34+ proc . on ( "error" , ( ) => resolve ( ) )
35+ } )
36+ }
37+
38+ function execQuiet ( cmd : string ) : Promise < void > {
39+ return new Promise ( ( resolve ) => {
40+ const proc = spawn ( "sh" , [ "-c" , cmd ] , { stdio : "ignore" } )
41+ proc . on ( "close" , ( ) => resolve ( ) )
42+ proc . on ( "error" , ( ) => resolve ( ) )
43+ } )
44+ }
45+
46+ function which ( cmd : string ) : boolean {
47+ try {
48+ execSync ( `which ${ cmd } ` , { stdio : "ignore" } )
49+ return true
50+ } catch {
51+ return false
52+ }
53+ }
54+
855export namespace Clipboard {
956 export interface Content {
1057 data : string
@@ -17,38 +64,42 @@ export namespace Clipboard {
1764 if ( os === "darwin" ) {
1865 const tmpfile = path . join ( tmpdir ( ) , "opencode-clipboard.png" )
1966 try {
20- 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'`
21- . nothrow ( )
22- . quiet ( )
67+ await execQuiet (
68+ `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'` ,
69+ )
2370 const file = Bun . file ( tmpfile )
24- const buffer = await file . arrayBuffer ( )
25- return { data : Buffer . from ( buffer ) . toString ( "base64" ) , mime : "image/png" }
71+ if ( await file . exists ( ) ) {
72+ const buffer = await file . arrayBuffer ( )
73+ if ( buffer . byteLength > 0 ) {
74+ return { data : Buffer . from ( buffer ) . toString ( "base64" ) , mime : "image/png" }
75+ }
76+ }
2677 } catch {
2778 } finally {
28- await $ `rm -f "${ tmpfile } "` . nothrow ( ) . quiet ( )
79+ await execQuiet ( `rm -f "${ tmpfile } "` )
2980 }
3081 }
3182
3283 if ( os === "win32" || release ( ) . includes ( "WSL" ) ) {
3384 const script =
3485 "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()) }"
35- const base64 = await $ ` powershell.exe -NonInteractive -NoProfile -command " ${ script } "` . nothrow ( ) . text ( )
36- if ( base64 ) {
37- const imageBuffer = Buffer . from ( base64 . trim ( ) , "base64" )
86+ const result = await runCommandText ( " powershell.exe" , [ " -NonInteractive" , " -NoProfile" , " -command" , script ] )
87+ if ( result ) {
88+ const imageBuffer = Buffer . from ( result . trim ( ) , "base64" )
3889 if ( imageBuffer . length > 0 ) {
3990 return { data : imageBuffer . toString ( "base64" ) , mime : "image/png" }
4091 }
4192 }
4293 }
4394
4495 if ( os === "linux" ) {
45- const wayland = await $ ` wl-paste -t image/png` . nothrow ( ) . arrayBuffer ( )
96+ const wayland = await runCommand ( " wl-paste" , [ "-t" , " image/png" ] )
4697 if ( wayland && wayland . byteLength > 0 ) {
47- return { data : Buffer . from ( wayland ) . toString ( "base64" ) , mime : "image/png" }
98+ return { data : wayland . toString ( "base64" ) , mime : "image/png" }
4899 }
49- const x11 = await $ ` xclip -selection clipboard -t image/png -o` . nothrow ( ) . arrayBuffer ( )
100+ const x11 = await runCommand ( " xclip" , [ " -selection" , " clipboard" , "-t" , " image/png" , "-o" ] )
50101 if ( x11 && x11 . byteLength > 0 ) {
51- return { data : Buffer . from ( x11 ) . toString ( "base64" ) , mime : "image/png" }
102+ return { data : x11 . toString ( "base64" ) , mime : "image/png" }
52103 }
53104 }
54105
@@ -61,58 +112,40 @@ export namespace Clipboard {
61112 const getCopyMethod = lazy ( ( ) => {
62113 const os = platform ( )
63114
64- if ( os === "darwin" && Bun . which ( "osascript" ) ) {
115+ if ( os === "darwin" && which ( "osascript" ) ) {
65116 console . log ( "clipboard: using osascript" )
66117 return async ( text : string ) => {
67118 const escaped = text . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' )
68- await $ `osascript -e 'set the clipboard to "${ escaped } "'` . nothrow ( ) . quiet ( )
119+ await execQuiet ( `osascript -e 'set the clipboard to "${ escaped } "'` )
69120 }
70121 }
71122
72123 if ( os === "linux" ) {
73- if ( process . env [ "WAYLAND_DISPLAY" ] && Bun . which ( "wl-copy" ) ) {
124+ if ( process . env [ "WAYLAND_DISPLAY" ] && which ( "wl-copy" ) ) {
74125 console . log ( "clipboard: using wl-copy" )
75126 return async ( text : string ) => {
76- const proc = Bun . spawn ( [ "wl-copy" ] , { stdin : "pipe" , stdout : "ignore" , stderr : "ignore" } )
77- proc . stdin . write ( text )
78- proc . stdin . end ( )
79- await proc . exited . catch ( ( ) => { } )
127+ await writeToCommand ( "wl-copy" , [ ] , text )
80128 }
81129 }
82- if ( Bun . which ( "xclip" ) ) {
130+ if ( which ( "xclip" ) ) {
83131 console . log ( "clipboard: using xclip" )
84132 return async ( text : string ) => {
85- const proc = Bun . spawn ( [ "xclip" , "-selection" , "clipboard" ] , {
86- stdin : "pipe" ,
87- stdout : "ignore" ,
88- stderr : "ignore" ,
89- } )
90- proc . stdin . write ( text )
91- proc . stdin . end ( )
92- await proc . exited . catch ( ( ) => { } )
133+ await writeToCommand ( "xclip" , [ "-selection" , "clipboard" ] , text )
93134 }
94135 }
95- if ( Bun . which ( "xsel" ) ) {
136+ if ( which ( "xsel" ) ) {
96137 console . log ( "clipboard: using xsel" )
97138 return async ( text : string ) => {
98- const proc = Bun . spawn ( [ "xsel" , "--clipboard" , "--input" ] , {
99- stdin : "pipe" ,
100- stdout : "ignore" ,
101- stderr : "ignore" ,
102- } )
103- proc . stdin . write ( text )
104- proc . stdin . end ( )
105- await proc . exited . catch ( ( ) => { } )
139+ await writeToCommand ( "xsel" , [ "--clipboard" , "--input" ] , text )
106140 }
107141 }
108142 }
109143
110144 if ( os === "win32" ) {
111145 console . log ( "clipboard: using powershell" )
112146 return async ( text : string ) => {
113- // need to escape backticks because powershell uses them as escape code
114147 const escaped = text . replace ( / " / g, '""' ) . replace ( / ` / g, "``" )
115- await $ `powershell -NonInteractive -NoProfile -Command "Set-Clipboard -Value \"${ escaped } \""` . nothrow ( ) . quiet ( )
148+ await execQuiet ( `powershell -NonInteractive -NoProfile -Command "Set-Clipboard -Value \\ "${ escaped } \\ ""` )
116149 }
117150 }
118151
0 commit comments