@@ -22,171 +22,169 @@ function writeOsc52(text: string): void {
2222 process . stdout . write ( sequence )
2323}
2424
25- export namespace Clipboard {
26- export interface Content {
27- data : string
28- mime : string
29- }
25+ export interface Content {
26+ data : string
27+ mime : string
28+ }
3029
31- // Checks clipboard for images first, then falls back to text.
32- //
33- // On Windows prompt/ can call this from multiple paste signals because
34- // terminals surface image paste differently:
35- // 1. A forwarded Ctrl+V keypress
36- // 2. An empty bracketed-paste hint for image-only clipboard in Windows
37- // Terminal <1.25
38- // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
39- export async function read ( ) : Promise < Content | undefined > {
40- const os = platform ( )
30+ // Checks clipboard for images first, then falls back to text.
31+ //
32+ // On Windows prompt/ can call this from multiple paste signals because
33+ // terminals surface image paste differently:
34+ // 1. A forwarded Ctrl+V keypress
35+ // 2. An empty bracketed-paste hint for image-only clipboard in Windows
36+ // Terminal <1.25
37+ // 3. A kitty Ctrl+V key-release fallback for Windows Terminal 1.25+
38+ export async function read ( ) : Promise < Content | undefined > {
39+ const os = platform ( )
4140
42- if ( os === "darwin" ) {
43- const tmpfile = path . join ( tmpdir ( ) , "opencode-clipboard.png" )
44- try {
45- await Process . run (
46- [
47- "osascript" ,
48- "-e" ,
49- 'set imageData to the clipboard as "PNGf"' ,
50- "-e" ,
51- `set fileRef to open for access POSIX file "${ tmpfile } " with write permission` ,
52- "-e" ,
53- "set eof fileRef to 0" ,
54- "-e" ,
55- "write imageData to fileRef" ,
56- "-e" ,
57- "close access fileRef" ,
58- ] ,
59- { nothrow : true } ,
60- )
61- const buffer = await Filesystem . readBytes ( tmpfile )
62- return { data : buffer . toString ( "base64" ) , mime : "image/png" }
63- } catch {
64- } finally {
65- await fs . rm ( tmpfile , { force : true } ) . catch ( ( ) => { } )
66- }
41+ if ( os === "darwin" ) {
42+ const tmpfile = path . join ( tmpdir ( ) , "opencode-clipboard.png" )
43+ try {
44+ await Process . run (
45+ [
46+ "osascript" ,
47+ "-e" ,
48+ 'set imageData to the clipboard as "PNGf"' ,
49+ "-e" ,
50+ `set fileRef to open for access POSIX file "${ tmpfile } " with write permission` ,
51+ "-e" ,
52+ "set eof fileRef to 0" ,
53+ "-e" ,
54+ "write imageData to fileRef" ,
55+ "-e" ,
56+ "close access fileRef" ,
57+ ] ,
58+ { nothrow : true } ,
59+ )
60+ const buffer = await Filesystem . readBytes ( tmpfile )
61+ return { data : buffer . toString ( "base64" ) , mime : "image/png" }
62+ } catch {
63+ } finally {
64+ await fs . rm ( tmpfile , { force : true } ) . catch ( ( ) => { } )
6765 }
66+ }
6867
69- // Windows/WSL: probe clipboard for images via PowerShell.
70- // Bracketed paste can't carry image data so we read it directly.
71- if ( os === "win32" || release ( ) . includes ( "WSL" ) ) {
72- const script =
73- "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()) }"
74- const base64 = await Process . text ( [ "powershell.exe" , "-NonInteractive" , "-NoProfile" , "-command" , script ] , {
75- nothrow : true ,
76- } )
77- if ( base64 . text ) {
78- const imageBuffer = Buffer . from ( base64 . text . trim ( ) , "base64" )
79- if ( imageBuffer . length > 0 ) {
80- return { data : imageBuffer . toString ( "base64" ) , mime : "image/png" }
81- }
68+ // Windows/WSL: probe clipboard for images via PowerShell.
69+ // Bracketed paste can't carry image data so we read it directly.
70+ if ( os === "win32" || release ( ) . includes ( "WSL" ) ) {
71+ const script =
72+ "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()) }"
73+ const base64 = await Process . text ( [ "powershell.exe" , "-NonInteractive" , "-NoProfile" , "-command" , script ] , {
74+ nothrow : true ,
75+ } )
76+ if ( base64 . text ) {
77+ const imageBuffer = Buffer . from ( base64 . text . trim ( ) , "base64" )
78+ if ( imageBuffer . length > 0 ) {
79+ return { data : imageBuffer . toString ( "base64" ) , mime : "image/png" }
8280 }
8381 }
82+ }
8483
85- if ( os === "linux" ) {
86- const wayland = await Process . run ( [ "wl-paste" , "-t" , "image/png" ] , { nothrow : true } )
87- if ( wayland . stdout . byteLength > 0 ) {
88- return { data : Buffer . from ( wayland . stdout ) . toString ( "base64" ) , mime : "image/png" }
89- }
90- const x11 = await Process . run ( [ "xclip" , "-selection" , "clipboard" , "-t" , "image/png" , "-o" ] , {
91- nothrow : true ,
92- } )
93- if ( x11 . stdout . byteLength > 0 ) {
94- return { data : Buffer . from ( x11 . stdout ) . toString ( "base64" ) , mime : "image/png" }
95- }
84+ if ( os === "linux" ) {
85+ const wayland = await Process . run ( [ "wl-paste" , "-t" , "image/png" ] , { nothrow : true } )
86+ if ( wayland . stdout . byteLength > 0 ) {
87+ return { data : Buffer . from ( wayland . stdout ) . toString ( "base64" ) , mime : "image/png" }
9688 }
97-
98- const text = await clipboardy . read ( ) . catch ( ( ) => { } )
99- if ( text ) {
100- return { data : text , mime : "text/plain" }
89+ const x11 = await Process . run ( [ "xclip" , "-selection" , "clipboard" , "-t" , "image/png" , "-o" ] , {
90+ nothrow : true ,
91+ } )
92+ if ( x11 . stdout . byteLength > 0 ) {
93+ return { data : Buffer . from ( x11 . stdout ) . toString ( "base64" ) , mime : "image/png" }
10194 }
10295 }
10396
104- const getCopyMethod = lazy ( ( ) => {
105- const os = platform ( )
97+ const text = await clipboardy . read ( ) . catch ( ( ) => { } )
98+ if ( text ) {
99+ return { data : text , mime : "text/plain" }
100+ }
101+ }
106102
107- if ( os === "darwin" && which ( "osascript" ) ) {
108- console . log ( "clipboard: using osascript" )
109- return async ( text : string ) => {
110- const escaped = text . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' )
111- await Process . run ( [ "osascript" , "-e" , `set the clipboard to "${ escaped } "` ] , { nothrow : true } )
112- }
103+ const getCopyMethod = lazy ( ( ) => {
104+ const os = platform ( )
105+
106+ if ( os === "darwin" && which ( "osascript" ) ) {
107+ console . log ( "clipboard: using osascript" )
108+ return async ( text : string ) => {
109+ const escaped = text . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' )
110+ await Process . run ( [ "osascript" , "-e" , `set the clipboard to "${ escaped } "` ] , { nothrow : true } )
113111 }
112+ }
114113
115- if ( os === "linux" ) {
116- if ( process . env [ "WAYLAND_DISPLAY" ] && which ( "wl-copy" ) ) {
117- console . log ( "clipboard: using wl-copy" )
118- return async ( text : string ) => {
119- const proc = Process . spawn ( [ "wl-copy" ] , { stdin : "pipe" , stdout : "ignore" , stderr : "ignore" } )
120- if ( ! proc . stdin ) return
121- proc . stdin . write ( text )
122- proc . stdin . end ( )
123- await proc . exited . catch ( ( ) => { } )
124- }
125- }
126- if ( which ( "xclip" ) ) {
127- console . log ( "clipboard: using xclip" )
128- return async ( text : string ) => {
129- const proc = Process . spawn ( [ "xclip" , "-selection" , "clipboard" ] , {
130- stdin : "pipe" ,
131- stdout : "ignore" ,
132- stderr : "ignore" ,
133- } )
134- if ( ! proc . stdin ) return
135- proc . stdin . write ( text )
136- proc . stdin . end ( )
137- await proc . exited . catch ( ( ) => { } )
138- }
114+ if ( os === "linux" ) {
115+ if ( process . env [ "WAYLAND_DISPLAY" ] && which ( "wl-copy" ) ) {
116+ console . log ( "clipboard: using wl-copy" )
117+ return async ( text : string ) => {
118+ const proc = Process . spawn ( [ "wl-copy" ] , { stdin : "pipe" , stdout : "ignore" , stderr : "ignore" } )
119+ if ( ! proc . stdin ) return
120+ proc . stdin . write ( text )
121+ proc . stdin . end ( )
122+ await proc . exited . catch ( ( ) => { } )
139123 }
140- if ( which ( "xsel" ) ) {
141- console . log ( "clipboard: using xsel" )
142- return async ( text : string ) => {
143- const proc = Process . spawn ( [ "xsel" , "--clipboard" , "--input" ] , {
144- stdin : "pipe" ,
145- stdout : "ignore " ,
146- stderr : "ignore" ,
147- } )
148- if ( ! proc . stdin ) return
149- proc . stdin . write ( text )
150- proc . stdin . end ( )
151- await proc . exited . catch ( ( ) => { } )
152- }
124+ }
125+ if ( which ( "xclip" ) ) {
126+ console . log ( "clipboard: using xclip" )
127+ return async ( text : string ) => {
128+ const proc = Process . spawn ( [ "xclip" , "-selection" , "clipboard" ] , {
129+ stdin : "pipe " ,
130+ stdout : "ignore" ,
131+ stderr : "ignore" ,
132+ } )
133+ if ( ! proc . stdin ) return
134+ proc . stdin . write ( text )
135+ proc . stdin . end ( )
136+ await proc . exited . catch ( ( ) => { } )
153137 }
154138 }
155-
156- if ( os === "win32" ) {
157- console . log ( "clipboard: using powershell" )
139+ if ( which ( "xsel" ) ) {
140+ console . log ( "clipboard: using xsel" )
158141 return async ( text : string ) => {
159- // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
160- const proc = Process . spawn (
161- [
162- "powershell.exe" ,
163- "-NonInteractive" ,
164- "-NoProfile" ,
165- "-Command" ,
166- "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())" ,
167- ] ,
168- {
169- stdin : "pipe" ,
170- stdout : "ignore" ,
171- stderr : "ignore" ,
172- } ,
173- )
174-
142+ const proc = Process . spawn ( [ "xsel" , "--clipboard" , "--input" ] , {
143+ stdin : "pipe" ,
144+ stdout : "ignore" ,
145+ stderr : "ignore" ,
146+ } )
175147 if ( ! proc . stdin ) return
176148 proc . stdin . write ( text )
177149 proc . stdin . end ( )
178150 await proc . exited . catch ( ( ) => { } )
179151 }
180152 }
153+ }
181154
182- console . log ( "clipboard: no native support" )
155+ if ( os === "win32" ) {
156+ console . log ( "clipboard: using powershell" )
183157 return async ( text : string ) => {
184- await clipboardy . write ( text ) . catch ( ( ) => { } )
158+ // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
159+ const proc = Process . spawn (
160+ [
161+ "powershell.exe" ,
162+ "-NonInteractive" ,
163+ "-NoProfile" ,
164+ "-Command" ,
165+ "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())" ,
166+ ] ,
167+ {
168+ stdin : "pipe" ,
169+ stdout : "ignore" ,
170+ stderr : "ignore" ,
171+ } ,
172+ )
173+
174+ if ( ! proc . stdin ) return
175+ proc . stdin . write ( text )
176+ proc . stdin . end ( )
177+ await proc . exited . catch ( ( ) => { } )
185178 }
186- } )
179+ }
187180
188- export async function copy ( text : string ) : Promise < void > {
189- writeOsc52 ( text )
190- await getCopyMethod ( ) ( text )
181+ console . log ( "clipboard: no native support" )
182+ return async ( text : string ) => {
183+ await clipboardy . write ( text ) . catch ( ( ) => { } )
191184 }
185+ } )
186+
187+ export async function copy ( text : string ) : Promise < void > {
188+ writeOsc52 ( text )
189+ await getCopyMethod ( ) ( text )
192190}
0 commit comments