@@ -854,25 +854,71 @@ pub fn is_clipboard_available() -> bool {
854854///
855855/// Returns `true` if the text was successfully copied, `false` otherwise.
856856/// Failures are logged as warnings but don't cause errors.
857+ ///
858+ /// # Non-blocking behavior
859+ ///
860+ /// On Linux, clipboard operations can block indefinitely when no clipboard manager
861+ /// is available (e.g., SSH sessions without X11/Wayland forwarding). To prevent
862+ /// blocking the async event loop, the Linux implementation spawns a separate thread
863+ /// for the blocking `.wait()` call with a timeout of 2 seconds.
857864pub fn safe_clipboard_copy ( text : & str ) -> bool {
858- match arboard:: Clipboard :: new ( ) {
859- Ok ( mut clipboard) => {
860- #[ cfg( target_os = "linux" ) ]
861- {
862- use arboard:: SetExtLinux ;
863- // On Linux, use wait() to ensure the clipboard manager receives the data
864- // before the Clipboard object is dropped. This is critical because X11/Wayland
865- // clipboards require the source application to remain available.
866- match clipboard. set ( ) . wait ( ) . text ( text) {
867- Ok ( _) => true ,
868- Err ( e) => {
869- tracing:: warn!( "Clipboard copy failed: {}" , e) ;
870- false
865+ #[ cfg( target_os = "linux" ) ]
866+ {
867+ // On Linux, clipboard operations with wait() can block indefinitely if no
868+ // clipboard manager is available (e.g., SSH sessions without X11 forwarding).
869+ // To prevent blocking the async event loop, we spawn a separate thread with
870+ // a timeout.
871+ use std:: sync:: mpsc;
872+ use std:: time:: Duration ;
873+
874+ let text = text. to_string ( ) ;
875+ let ( tx, rx) = mpsc:: channel ( ) ;
876+
877+ std:: thread:: spawn ( move || {
878+ let result = match arboard:: Clipboard :: new ( ) {
879+ Ok ( mut clipboard) => {
880+ use arboard:: SetExtLinux ;
881+ // wait() is necessary on Linux to ensure the clipboard manager
882+ // receives the data before the Clipboard object is dropped
883+ match clipboard. set ( ) . wait ( ) . text ( & text) {
884+ Ok ( _) => true ,
885+ Err ( e) => {
886+ tracing:: warn!( "Clipboard copy failed: {}" , e) ;
887+ false
888+ }
871889 }
872890 }
891+ Err ( e) => {
892+ tracing:: debug!( "Clipboard unavailable: {}" , e) ;
893+ false
894+ }
895+ } ;
896+ // Ignore send error - the receiver may have timed out
897+ let _ = tx. send ( result) ;
898+ } ) ;
899+
900+ // Wait for the clipboard operation with a 2 second timeout
901+ // This prevents indefinite blocking when no clipboard manager is available
902+ match rx. recv_timeout ( Duration :: from_secs ( 2 ) ) {
903+ Ok ( result) => result,
904+ Err ( mpsc:: RecvTimeoutError :: Timeout ) => {
905+ tracing:: warn!(
906+ "Clipboard copy timed out - no clipboard manager available? \
907+ (X11/Wayland forwarding may not be configured)"
908+ ) ;
909+ false
910+ }
911+ Err ( mpsc:: RecvTimeoutError :: Disconnected ) => {
912+ tracing:: warn!( "Clipboard thread terminated unexpectedly" ) ;
913+ false
873914 }
874- #[ cfg( target_os = "windows" ) ]
875- {
915+ }
916+ }
917+
918+ #[ cfg( target_os = "windows" ) ]
919+ {
920+ match arboard:: Clipboard :: new ( ) {
921+ Ok ( mut clipboard) => {
876922 // On Windows, clipboard content persists after the Clipboard object is dropped,
877923 // but we need to ensure the set operation completes successfully.
878924 // Small delay helps ensure clipboard is fully populated before returning.
@@ -888,20 +934,27 @@ pub fn safe_clipboard_copy(text: &str) -> bool {
888934 }
889935 }
890936 }
891- #[ cfg( not( any( target_os = "linux" , target_os = "windows" ) ) ) ]
892- {
893- match clipboard. set_text ( text) {
894- Ok ( _) => true ,
895- Err ( e) => {
896- tracing:: warn!( "Clipboard copy failed: {}" , e) ;
897- false
898- }
899- }
937+ Err ( e) => {
938+ tracing:: debug!( "Clipboard unavailable: {}" , e) ;
939+ false
900940 }
901941 }
902- Err ( e) => {
903- tracing:: debug!( "Clipboard unavailable: {}" , e) ;
904- false
942+ }
943+
944+ #[ cfg( not( any( target_os = "linux" , target_os = "windows" ) ) ) ]
945+ {
946+ match arboard:: Clipboard :: new ( ) {
947+ Ok ( mut clipboard) => match clipboard. set_text ( text) {
948+ Ok ( _) => true ,
949+ Err ( e) => {
950+ tracing:: warn!( "Clipboard copy failed: {}" , e) ;
951+ false
952+ }
953+ } ,
954+ Err ( e) => {
955+ tracing:: debug!( "Clipboard unavailable: {}" , e) ;
956+ false
957+ }
905958 }
906959 }
907960}
0 commit comments