aboutsummaryrefslogtreecommitdiff
path: root/src/client
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/client
parent2b97799b6715fd6c355db9c225029765971915e6 (diff)
fix: bridge remote clipboard image paste
refs #205
Diffstat (limited to 'src/client')
-rw-r--r--src/client/mod.rs58
1 files changed, 56 insertions, 2 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 101e168c..ff5c144f 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -32,8 +32,8 @@ use tracing::{debug, info, warn};
use crate::server::headless::client_socket_path;
use crate::server::protocol::{
- self, ClientMessage, NotifyKind, RenderEncoding, ServerMessage, MAX_FRAME_SIZE,
- MAX_GRAPHICS_FRAME_SIZE, PROTOCOL_VERSION,
+ self, ClientMessage, NotifyKind, RenderEncoding, ServerMessage, MAX_CLIPBOARD_IMAGE_PAYLOAD,
+ MAX_FRAME_SIZE, MAX_GRAPHICS_FRAME_SIZE, PROTOCOL_VERSION,
};
static RECEIVED_KITTY_GRAPHICS_IDS: OnceLock<Mutex<HashSet<u32>>> = OnceLock::new();
@@ -640,6 +640,34 @@ async fn run_client_loop(
}
data
};
+ if should_bridge_clipboard_image_paste(&data) {
+ if let Some(image) = crate::platform::read_clipboard_image() {
+ if image.bytes.len() > MAX_CLIPBOARD_IMAGE_PAYLOAD {
+ warn!(
+ bytes = image.bytes.len(),
+ max = MAX_CLIPBOARD_IMAGE_PAYLOAD,
+ "local clipboard image is too large to bridge"
+ );
+ continue;
+ }
+ info!(
+ bytes = image.bytes.len(),
+ extension = image.extension,
+ "bridging local clipboard image paste to remote server"
+ );
+ let msg = ClientMessage::ClipboardImage {
+ extension: image.extension.to_owned(),
+ data: image.bytes,
+ };
+ if let Err(e) = write_to_server(&mut write_stream, &msg) {
+ return Err(ClientError::ConnectionLost(e));
+ }
+ continue;
+ }
+ info!(
+ "clipboard image paste trigger received, but local clipboard has no image"
+ );
+ }
let msg = ClientMessage::Input { data };
if let Err(e) = write_to_server(&mut write_stream, &msg) {
return Err(ClientError::ConnectionLost(e));
@@ -875,6 +903,21 @@ fn sound_from_notify_message(message: &str) -> Option<crate::sound::Sound> {
}
}
+fn should_bridge_clipboard_image_paste(data: &[u8]) -> bool {
+ if data == b"\x1b[200~\x1b[201~" {
+ return true;
+ }
+
+ let events = crate::raw_input::parse_raw_input_bytes_sync(data);
+ matches!(
+ events.as_slice(),
+ [crate::raw_input::RawInputEvent::Key(key)]
+ if key.kind == crossterm::event::KeyEventKind::Press
+ && key.modifiers == crossterm::event::KeyModifiers::CONTROL
+ && matches!(key.code, crossterm::event::KeyCode::Char('v' | 'V'))
+ )
+}
+
// ---------------------------------------------------------------------------
// Clipboard forwarding
// ---------------------------------------------------------------------------
@@ -1069,6 +1112,17 @@ mod tests {
use super::*;
#[test]
+ fn clipboard_image_paste_bridge_triggers_on_ctrl_v_and_empty_paste() {
+ assert!(should_bridge_clipboard_image_paste(&[0x16]));
+ assert!(should_bridge_clipboard_image_paste(b"\x1b[118;5u"));
+ assert!(should_bridge_clipboard_image_paste(b"\x1b[200~\x1b[201~"));
+ assert!(!should_bridge_clipboard_image_paste(
+ b"\x1b[200~text\x1b[201~"
+ ));
+ assert!(!should_bridge_clipboard_image_paste(b"v"));
+ }
+
+ #[test]
fn graphics_bytes_are_written_after_blit_with_saved_cursor() {
let mut output = Vec::new();
write_encoded_frame_with_graphics(