aboutsummaryrefslogtreecommitdiff
path: root/src/server/headless.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-20 02:51:59 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-20 02:51:59 +0300
commited478be9835e984c42bc1e30fe84b5fb01a8c5d4 (patch)
tree353b6ea5c7368a9f3b477bc8f1bbcd248e377577 /src/server/headless.rs
parent2b97799b6715fd6c355db9c225029765971915e6 (diff)
fix: bridge remote clipboard image paste
refs #205
Diffstat (limited to 'src/server/headless.rs')
-rw-r--r--src/server/headless.rs111
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();
}
}