Skip to content

Commit cf3143d

Browse files
committed
fix(tui): prevent clipboard copy from blocking async event loop on Linux
The safe_clipboard_copy() function was using arboard's blocking .wait() call on Linux which could hang indefinitely when no clipboard manager is available (e.g., SSH sessions without X11/Wayland forwarding). This caused the TUI to freeze when pressing 'c' on the login screen. Fix: Spawn the blocking clipboard operation in a separate thread with a 2-second timeout. If the operation times out or fails, return false gracefully with a warning log message. This ensures: - Clipboard copy works correctly on systems with clipboard support - Clipboard copy fails gracefully (returns false) on systems without clipboard support, without blocking the UI - The async event loop remains responsive
1 parent e6658f8 commit cf3143d

File tree

1 file changed

+80
-27
lines changed

1 file changed

+80
-27
lines changed

src/cortex-tui/src/runner/terminal.rs

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
857864
pub 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

Comments
 (0)