fix: defer clipboard init to avoid blocking TUI startup on X11-less hosts

On Linux, `arboard::Clipboard::new()` opens a blocking connect() to the
X11 Unix socket. When no X server is running (headless, WSL2 without
WSLg), the call hangs indefinitely. Because raw mode and the alternate
screen are already active at that point, Ctrl+C no longer generates
SIGINT and the event loop hasn't started yet — leaving the user with a
blank screen and no way to exit.

Move clipboard initialization from `ClipboardHandler::new()` (called
synchronously during App construction) to a lazy `ensure_clipboard()`
that runs on first read/write with a 500 ms timeout. If the X11
connection doesn't respond in time, the handler stays in fallback mode
and `write_text` falls through to the existing OSC 52 / pbcopy /
PowerShell paths.
This commit is contained in:
jayzhu
2026-05-18 19:44:21 +08:00
committed by Hunter Bown
parent eeccf7de6c
commit 5c452328ab
+36 -3
View File
@@ -55,26 +55,58 @@ pub enum ClipboardContent {
/// Clipboard reader/writer helper.
pub struct ClipboardHandler {
clipboard: Option<Clipboard>,
clipboard_init_attempted: bool,
#[cfg(test)]
written_text: Vec<String>,
}
impl ClipboardHandler {
/// Create a new clipboard handler, falling back to a no-op when unavailable.
/// Create a new clipboard handler without connecting.
///
/// The actual clipboard connection is deferred to first use
/// (`ensure_clipboard`) so that startup on hosts without an X11/Wayland
/// server (headless, WSL2) never blocks the TUI event loop.
pub fn new() -> Self {
let clipboard = Clipboard::new().ok();
Self {
clipboard,
clipboard: None,
clipboard_init_attempted: false,
#[cfg(test)]
written_text: Vec::new(),
}
}
/// Try to connect to the system clipboard, bounded by a short timeout.
///
/// On Linux, `arboard::Clipboard::new()` opens a blocking X11 connection.
/// When no X server is running (headless, WSL2 without WSLg), the connect
/// call can hang indefinitely. We spawn the connection attempt on a
/// temporary thread and give it 500 ms; if it doesn't return in time the
/// handler stays in fallback/no-op mode and `read`/`write_text` fall
/// through to their OSC52 and pbcopy/powershell fallbacks.
fn ensure_clipboard(&mut self) {
if self.clipboard_init_attempted {
return;
}
self.clipboard_init_attempted = true;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(Clipboard::new().ok());
});
// 500 ms is generous for a local Unix socket connect — the
// kernel either answers or doesn't.
self.clipboard = rx
.recv_timeout(std::time::Duration::from_millis(500))
.ok()
.flatten();
}
/// Read the clipboard and return the parsed content.
///
/// `workspace` is used as a fallback location when `~/.deepseek/` cannot
/// be resolved (e.g. running with a stripped HOME in CI sandboxes).
pub fn read(&mut self, workspace: &Path) -> Option<ClipboardContent> {
self.ensure_clipboard();
let clipboard = self.clipboard.as_mut()?;
if let Ok(text) = clipboard.get_text() {
return Some(ClipboardContent::Text(text));
@@ -99,6 +131,7 @@ impl ClipboardHandler {
#[cfg(not(test))]
{
self.ensure_clipboard();
if let Some(clipboard) = self.clipboard.as_mut()
&& clipboard.set_text(text.to_string()).is_ok()
{