diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 02:51:59 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-20 02:51:59 +0300 |
| commit | ed478be9835e984c42bc1e30fe84b5fb01a8c5d4 (patch) | |
| tree | 353b6ea5c7368a9f3b477bc8f1bbcd248e377577 /src/server/headless.rs | |
| parent | 2b97799b6715fd6c355db9c225029765971915e6 (diff) | |
fix: bridge remote clipboard image paste
refs #205
Diffstat (limited to 'src/server/headless.rs')
| -rw-r--r-- | src/server/headless.rs | 111 |
1 files changed, 99 insertions, 12 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index 56164cfa..ad7ea03a 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -64,6 +64,18 @@ enum LoopEvent { const MIN_COLS: u16 = 80; const MIN_ROWS: u16 = 24; +fn paste_payload_for_runtime(runtime: &crate::terminal::TerminalRuntime, text: &str) -> String { + if runtime + .input_state() + .map(|state| state.bracketed_paste) + .unwrap_or(false) + { + format!("\x1b[200~{text}\x1b[201~") + } else { + text.to_owned() + } +} + /// Legacy environment variable for overriding the client socket path. /// /// Contractual override behavior for auto-detect uses `HERDR_SOCKET_PATH`. @@ -237,6 +249,8 @@ struct ClientConnection { render_pending: bool, /// Last host mouse capture mode sent to this client. host_mouse_capture_active: Option<bool>, + /// Temporary files staged from this client's local clipboard image pastes. + staged_clipboard_files: Vec<PathBuf>, /// Channels for sending framed ServerMessage data to the client writer thread. writer: Option<ClientWriter>, } @@ -285,6 +299,7 @@ impl ClientConnection { graphics_cache: crate::kitty_graphics::HostGraphicsCache::default(), render_pending: false, host_mouse_capture_active: None, + staged_clipboard_files: Vec::new(), writer, } } @@ -653,17 +668,16 @@ impl HeadlessServer { let was_foreground = self.foreground_client_id == Some(client_id); self.send_client_graphics_cleanup(client_id); let removed = self.clients.remove(&client_id); - if let Some(ClientConnection { - mode: ClientConnectionMode::TerminalAttach { terminal_id }, - .. - }) = removed - { - self.terminal_attach_owners.remove(&terminal_id); - if let Some(terminal_id) = self.terminal_id_by_string(&terminal_id) { - self.app - .state - .direct_attach_resize_locks - .remove(&terminal_id); + if let Some(removed) = removed { + crate::server::clipboard_image::remove_files(removed.staged_clipboard_files); + if let ClientConnectionMode::TerminalAttach { terminal_id } = removed.mode { + self.terminal_attach_owners.remove(&terminal_id); + if let Some(terminal_id) = self.terminal_id_by_string(&terminal_id) { + self.app + .state + .direct_attach_resize_locks + .remove(&terminal_id); + } } } if was_foreground { @@ -834,6 +848,49 @@ impl HeadlessServer { self.app.state.terminal_runtimes.get(&terminal_id) } + fn write_client_clipboard_image( + &mut self, + client_id: u64, + extension: &str, + data: &[u8], + ) -> std::io::Result<String> { + let staged = crate::server::clipboard_image::stage(client_id, extension, data)?; + if let Some(client) = self.clients.get_mut(&client_id) { + client.staged_clipboard_files.push(staged.path); + } + info!(client_id, bytes = data.len(), path = %staged.paste_text, "staged client clipboard image"); + Ok(staged.paste_text) + } + + fn paste_client_clipboard_image_path(&mut self, client_id: u64, path: String) -> bool { + if let Some(ClientConnection { + mode: ClientConnectionMode::TerminalAttach { terminal_id }, + .. + }) = self.clients.get(&client_id) + { + if let Some(runtime) = self.runtime_for_terminal_id_string(terminal_id) { + let payload = paste_payload_for_runtime(runtime, &path); + if let Err(err) = runtime.try_send_bytes(Bytes::from(payload)) { + warn!(client_id, terminal_id = %terminal_id, err = %err, "terminal attach clipboard image paste failed"); + } + } + return true; + } + + let foreground_changed = self.promote_client_to_foreground(client_id); + if foreground_changed { + self.resize_shared_runtime_to_effective_size(); + } + if let Some(client) = self.clients.get_mut(&client_id) { + client.request_semantic_redraw_after_input(); + } + self.app.route_client_events( + vec![crate::raw_input::RawInputEvent::Paste(path)], + self.foreground_client_id == Some(client_id), + ); + true + } + fn pane_effective_state(&self, pane_id: crate::layout::PaneId) -> crate::detect::AgentState { self.app .state @@ -1451,6 +1508,25 @@ impl HeadlessServer { foreground_changed || theme_changed || interaction } } + ServerEvent::ClientClipboardImage { + client_id, + extension, + data, + } => { + debug!( + client_id, + len = data.len(), + extension = %extension, + "client clipboard image received" + ); + match self.write_client_clipboard_image(client_id, &extension, &data) { + Ok(path) => self.paste_client_clipboard_image_path(client_id, path), + Err(err) => { + warn!(client_id, err = %err, "failed to stage client clipboard image"); + true + } + } + } ServerEvent::ClientResize { client_id, cols, @@ -2101,7 +2177,12 @@ impl HeadlessServer { self.drain_api_requests_with_shutdown_check(); // Close all client connections. - self.clients.clear(); + let staged_files = self + .clients + .drain() + .flat_map(|(_, client)| client.staged_clipboard_files) + .collect::<Vec<_>>(); + crate::server::clipboard_image::remove_files(staged_files); // Remove socket files. self.cleanup_sockets()?; @@ -2126,6 +2207,12 @@ impl HeadlessServer { impl Drop for HeadlessServer { fn drop(&mut self) { + let staged_files = self + .clients + .drain() + .flat_map(|(_, client)| client.staged_clipboard_files) + .collect::<Vec<_>>(); + crate::server::clipboard_image::remove_files(staged_files); let _ = self.cleanup_sockets(); } } |
