@@ -194,24 +194,26 @@ export function TerminalView(props: Props) {
194194 //
195195 // In both cases: local tabs get the local /tmp path; SSH tabs have the
196196 // PNG uploaded to the remote /tmp first and get the remote path.
197- const pasteImage = async ( ) => {
198- if ( ! props . active ) return ;
197+ // Returns true if an image was found and pasted, false if clipboard had no image.
198+ const pasteImage = async ( ) : Promise < boolean > => {
199+ if ( ! props . active ) return false ;
199200 const conn = connections ( ) . find ( ( c ) => c . id === props . tab . connectionId ) ;
200- if ( ! conn ) return ;
201+ if ( ! conn ) return false ;
201202 const sid = props . tab . sessionId ;
202203 const localPath = await api . clipboardSaveImage ( ) ;
203- if ( ! localPath ) return ;
204+ if ( ! localPath ) return false ;
204205 if ( conn . kind === "local" ) {
205206 term ?. paste ( quoteShellPath ( localPath ) + " " ) ;
206207 term ?. focus ( ) ;
207- return ;
208+ return true ;
208209 }
209- if ( ! sid ) return ;
210+ if ( ! sid ) return false ;
210211 setUploading ( true ) ;
211212 try {
212213 const remotePath = await api . sshUploadFile ( sid , localPath ) ;
213214 term ?. paste ( quoteShellPath ( remotePath ) + " " ) ;
214215 term ?. focus ( ) ;
216+ return true ;
215217 } finally {
216218 setUploading ( false ) ;
217219 }
@@ -240,14 +242,23 @@ export function TerminalView(props: Props) {
240242 }
241243 }
242244
243- // Ctrl+Shift+V: explicit image paste shortcut, bypasses paste event .
244- const onCtrlShiftV = ( e : KeyboardEvent ) => {
245+ // Ctrl+Shift+V: paste image if clipboard has one, otherwise paste text .
246+ const onCtrlShiftV = async ( e : KeyboardEvent ) => {
245247 if ( ! e . ctrlKey || ! e . shiftKey || e . altKey || e . key !== "V" ) return ;
246248 if ( ! props . active ) return ;
247249 e . preventDefault ( ) ;
248- pasteImage ( ) . catch ( ( err ) =>
249- console . warn ( "clipboard image paste failed" , err ) ,
250- ) ;
250+ try {
251+ const didImage = await pasteImage ( ) ;
252+ if ( ! didImage ) {
253+ const text = await api . clipboardReadText ( ) ;
254+ if ( text ) {
255+ term ?. paste ( text ) ;
256+ term ?. focus ( ) ;
257+ }
258+ }
259+ } catch ( err ) {
260+ console . warn ( "clipboard paste failed" , err ) ;
261+ }
251262 } ;
252263 window . addEventListener ( "keydown" , onCtrlShiftV ) ;
253264 onCleanup ( ( ) => window . removeEventListener ( "keydown" , onCtrlShiftV ) ) ;
@@ -299,19 +310,35 @@ export function TerminalView(props: Props) {
299310 return out . join ( "\n" ) ;
300311 } ) ;
301312
302- // Auto-copy on selection: when the user finishes a mouse drag and there is
303- // a non-empty selection, push it to the OS clipboard.
304- const onMouseUp = ( ) => {
313+ // Auto-copy on selection: fires when the drag ends inside the terminal.
314+ const onMouseUp = ( e : MouseEvent ) => {
315+ if ( e . button !== 0 ) return ; // middle/right-click must not overwrite clipboard
305316 if ( ! term ?. hasSelection ( ) ) return ;
306317 const sel = term . getSelection ( ) ;
307318 if ( ! sel ) return ;
308- navigator . clipboard . writeText ( sel ) . catch ( ( e ) =>
319+ api . clipboardWriteText ( sel ) . catch ( ( e ) =>
309320 console . warn ( "clipboard write failed" , e ) ,
310321 ) ;
311322 } ;
312323 host . addEventListener ( "mouseup" , onMouseUp ) ;
313324 onCleanup ( ( ) => host . removeEventListener ( "mouseup" , onMouseUp ) ) ;
314325
326+ // Ctrl+Shift+C: explicit copy shortcut. Covers the case where the drag
327+ // ends outside the terminal (tab bar, etc.) and mouseup didn't fire on
328+ // host. Uses native arboard to avoid WebKitGTK clipboard API hangs.
329+ const onCopyKey = ( e : KeyboardEvent ) => {
330+ if ( ! props . active || ! e . ctrlKey || ! e . shiftKey || e . altKey || e . key !== "C" ) return ;
331+ if ( ! term ?. hasSelection ( ) ) return ;
332+ e . preventDefault ( ) ;
333+ const sel = term . getSelection ( ) ;
334+ if ( ! sel ) return ;
335+ api . clipboardWriteText ( sel ) . catch ( ( e ) =>
336+ console . warn ( "clipboard write failed" , e ) ,
337+ ) ;
338+ } ;
339+ window . addEventListener ( "keydown" , onCopyKey ) ;
340+ onCleanup ( ( ) => window . removeEventListener ( "keydown" , onCopyKey ) ) ;
341+
315342 // Middle-click: just suppress the browser's auto-scroll affordance.
316343 // WebKitGTK already does X11-style primary-selection paste into the
317344 // focused textarea, which xterm forwards via onData — doing our own
0 commit comments