diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-13 14:14:40 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-13 14:15:26 +0300 |
| commit | 4600309051e5d6bea1f64e72cec585b102117ce5 (patch) | |
| tree | 8ca78803bbef8453bdd2d9a0de312201cd0ee60d /src/server | |
| parent | fb9aff54087aa8a606e9260a689986d2bd76f5fa (diff) | |
feat: support kitty graphics in attached clients
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/client_transport.rs | 32 | ||||
| -rw-r--r-- | src/server/headless.rs | 147 | ||||
| -rw-r--r-- | src/server/protocol.rs | 61 | ||||
| -rw-r--r-- | src/server/render_stream.rs | 49 |
4 files changed, 252 insertions, 37 deletions
diff --git a/src/server/client_transport.rs b/src/server/client_transport.rs index c4c1acc0..a74bf7f6 100644 --- a/src/server/client_transport.rs +++ b/src/server/client_transport.rs @@ -51,6 +51,8 @@ pub(crate) enum ServerEvent { client_id: u64, cols: u16, rows: u16, + cell_width_px: u32, + cell_height_px: u32, render_encoding: RenderEncoding, writer: ClientWriter, }, @@ -61,6 +63,8 @@ pub(crate) enum ServerEvent { client_id: u64, cols: u16, rows: u16, + cell_width_px: u32, + cell_height_px: u32, }, /// A client detached gracefully. ClientDetach { client_id: u64 }, @@ -113,11 +117,13 @@ pub(crate) fn handle_client_handshake( } }; - let (client_cols, client_rows, render_encoding) = match hello { + let (client_cols, client_rows, cell_width_px, cell_height_px, render_encoding) = match hello { ClientMessage::Hello { version, cols, rows, + cell_width_px, + cell_height_px, requested_encoding, } => { // Version check. @@ -137,7 +143,13 @@ pub(crate) fn handle_client_handshake( // Clamp size. let (clamped_cols, clamped_rows) = clamp_terminal_size(cols, rows); - (clamped_cols, clamped_rows, requested_encoding) + ( + clamped_cols, + clamped_rows, + cell_width_px.max(1), + cell_height_px.max(1), + requested_encoding, + ) } _ => { // First message must be Hello. @@ -176,6 +188,8 @@ pub(crate) fn handle_client_handshake( client_id, cols: client_cols, rows: client_rows, + cell_width_px, + cell_height_px, render_encoding, writer, }); @@ -324,12 +338,19 @@ fn client_read_loop( ServerEvent::ClientInput { client_id, data } } } - ClientMessage::Resize { cols, rows } => { + ClientMessage::Resize { + cols, + rows, + cell_width_px, + cell_height_px, + } => { let (clamped_cols, clamped_rows) = clamp_terminal_size(cols, rows); ServerEvent::ClientResize { client_id, cols: clamped_cols, rows: clamped_rows, + cell_width_px: cell_width_px.max(1), + cell_height_px: cell_height_px.max(1), } } ClientMessage::Detach => ServerEvent::ClientDetach { client_id }, @@ -399,6 +420,8 @@ mod tests { version: PROTOCOL_VERSION, cols: 100, rows: 30, + cell_width_px: 8, + cell_height_px: 16, requested_encoding: RenderEncoding::TerminalAnsi, }, ) @@ -427,11 +450,14 @@ mod tests { client_id, cols, rows, + cell_width_px, + cell_height_px, render_encoding, writer, } => { assert_eq!(client_id, 42); assert_eq!((cols, rows), (100, 30)); + assert_eq!((cell_width_px, cell_height_px), (8, 16)); assert_eq!(render_encoding, RenderEncoding::TerminalAnsi); drop(writer); } diff --git a/src/server/headless.rs b/src/server/headless.rs index d9227404..3e978d5a 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -191,6 +191,8 @@ fn derive_client_socket_from_api_socket(api_socket_path: &Path) -> PathBuf { struct ClientConnection { /// The client's terminal size (after clamping). terminal_size: (u16, u16), + /// Pixel size of one client terminal cell. + cell_size: crate::kitty_graphics::HostCellSize, /// Last known host terminal default colors for this client. host_terminal_theme: crate::terminal_theme::TerminalTheme, /// Last reported focus state for this client's outer terminal. @@ -199,6 +201,8 @@ struct ClientConnection { last_activity: u64, /// Render baseline for the negotiated client encoding. render_state: ClientRenderState, + /// Client-local host Kitty graphics cache. + graphics_cache: crate::kitty_graphics::HostGraphicsCache, /// Whether a render was skipped because the render channel was full. render_pending: bool, /// Channels for sending framed ServerMessage data to the client writer thread. @@ -208,6 +212,7 @@ struct ClientConnection { impl ClientConnection { fn new( terminal_size: (u16, u16), + cell_size: crate::kitty_graphics::HostCellSize, host_terminal_theme: crate::terminal_theme::TerminalTheme, outer_terminal_focus: Option<bool>, last_activity: u64, @@ -216,10 +221,12 @@ impl ClientConnection { ) -> Self { Self { terminal_size, + cell_size, host_terminal_theme, outer_terminal_focus, last_activity, render_state: ClientRenderState::new(render_encoding), + graphics_cache: crate::kitty_graphics::HostGraphicsCache::default(), render_pending: false, writer, } @@ -490,9 +497,15 @@ impl HeadlessServer { if self.foreground_client_id.is_none() { return; } + let Some(client_id) = self.foreground_client_id else { + return; + }; + let Some(client) = self.clients.get(&client_id) else { + return; + }; let (cols, rows) = self.effective_size; let area = Rect::new(0, 0, cols, rows); - crate::ui::compute_view(&mut self.app.state, area); + crate::ui::compute_view_with_cell_size(&mut self.app.state, area, client.cell_size); // Shared runtime size changes affect pane wrapping and foreground-driven // rendering semantics. Force one fresh frame to every remaining client @@ -564,6 +577,7 @@ impl HeadlessServer { fn remove_client(&mut self, client_id: u64) -> bool { let was_foreground = self.foreground_client_id == Some(client_id); + self.send_client_graphics_cleanup(client_id); self.clients.remove(&client_id); if was_foreground { self.promote_latest_remaining_client() @@ -572,6 +586,33 @@ impl HeadlessServer { } } + fn send_client_graphics_cleanup(&mut self, client_id: u64) { + let (writer, bytes) = match self.clients.get_mut(&client_id) { + Some(client) => { + let bytes = client.graphics_cache.clear_bytes(); + (client.writer.as_ref().cloned(), bytes) + } + None => return, + }; + if bytes.is_empty() { + return; + } + let Some(writer) = writer else { + return; + }; + let Ok(serialized) = Self::frame_server_message(&ServerMessage::Graphics { bytes }) else { + return; + }; + let _ = writer.control.send(serialized); + } + + fn send_all_clients_graphics_cleanup(&mut self) { + let client_ids = self.clients.keys().copied().collect::<Vec<_>>(); + for client_id in client_ids { + self.send_client_graphics_cleanup(client_id); + } + } + fn update_client_host_theme_from_events( &mut self, client_id: u64, @@ -1047,15 +1088,29 @@ impl HeadlessServer { client_id, cols, rows, + cell_width_px, + cell_height_px, writer, render_encoding, } => { - info!(client_id, cols, rows, ?render_encoding, "client connected"); + info!( + client_id, + cols, + rows, + cell_width_px, + cell_height_px, + ?render_encoding, + "client connected" + ); let last_activity = self.allocate_activity_stamp(); self.clients.insert( client_id, ClientConnection::new( (cols, rows), + crate::kitty_graphics::HostCellSize { + width_px: cell_width_px, + height_px: cell_height_px, + }, crate::terminal_theme::TerminalTheme::default(), None, last_activity, @@ -1097,6 +1152,10 @@ impl HeadlessServer { self.app.state.detach_requested = false; info!(client_id, "client detach requested via keybind"); + // Clear client-local host graphics before sending ServerShutdown + // so the outer terminal does not retain stale images. + self.send_client_graphics_cleanup(client_id); + // Send a ServerShutdown with "detached" reason to this client // so it exits cleanly (not with a connection-lost error). // The client will close its connection after receiving this, @@ -1130,10 +1189,19 @@ impl HeadlessServer { client_id, cols, rows, + cell_width_px, + cell_height_px, } => { - info!(client_id, cols, rows, "client resize"); + info!( + client_id, + cols, rows, cell_width_px, cell_height_px, "client resize" + ); if let Some(client) = self.clients.get_mut(&client_id) { client.terminal_size = (cols, rows); + client.cell_size = crate::kitty_graphics::HostCellSize { + width_px: cell_width_px, + height_px: cell_height_px, + }; } self.promote_client_to_foreground(client_id); self.resize_shared_runtime_to_effective_size(); @@ -1355,20 +1423,21 @@ impl HeadlessServer { /// frames to all connected clients. fn render_and_stream(&mut self) { let foreground_client_id = self.foreground_client_id; - let mut render_targets: Vec<(u64, (u16, u16), bool)> = self - .clients - .iter() - .filter(|(_, client)| client.writer.is_some()) - .map(|(&client_id, client)| { - ( - client_id, - client.terminal_size, - foreground_client_id == Some(client_id), - ) - }) - .collect(); + let mut render_targets: Vec<(u64, (u16, u16), crate::kitty_graphics::HostCellSize, bool)> = + self.clients + .iter() + .filter(|(_, client)| client.writer.is_some()) + .map(|(&client_id, client)| { + ( + client_id, + client.terminal_size, + client.cell_size, + foreground_client_id == Some(client_id), + ) + }) + .collect(); - render_targets.sort_by_key(|(client_id, _, is_foreground)| (*is_foreground, *client_id)); + render_targets.sort_by_key(|(client_id, _, _, is_foreground)| (*is_foreground, *client_id)); if render_targets.is_empty() { let (cols, rows) = self.effective_size; @@ -1387,20 +1456,32 @@ impl HeadlessServer { } let mut broken_clients: Vec<u64> = Vec::new(); - for (client_id, (cols, rows), is_foreground) in render_targets { + for (client_id, (cols, rows), cell_size, is_foreground) in render_targets { let area = Rect::new(0, 0, cols, rows); - let (buffer, cursor) = crate::server::render_stream::render_virtual( + let (buffer, cursor) = crate::server::render_stream::render_virtual_with_cell_size( &mut self.app.state, area, is_foreground, + cell_size, ); let hyperlinks = crate::server::render_stream::visible_hyperlinks(&self.app.state); - let frame = + let mut frame = FrameData::from_ratatui_buffer_with_hyperlinks(&buffer, cursor, &hyperlinks); let Some(client) = self.clients.get_mut(&client_id) else { continue; }; + let mut next_graphics_cache = client.graphics_cache.clone(); + if self.app.state.kitty_graphics_enabled { + frame.graphics = crate::kitty_graphics::encode_local_pane_graphics( + &self.app.state, + cell_size, + &mut next_graphics_cache, + ); + } else { + frame.graphics = next_graphics_cache.clear_bytes(); + } + let Some(writer) = client.writer.as_ref().cloned() else { continue; }; @@ -1417,7 +1498,6 @@ impl HeadlessServer { client_id, claimed, max, "skipping oversized frame for client" ); - client.render_state.note_oversized_frame(frame); continue; } Err(err) => { @@ -1430,6 +1510,7 @@ impl HeadlessServer { match writer.render.try_send(serialized) { Ok(()) => { client.render_pending = false; + client.graphics_cache = next_graphics_cache; client.render_state.commit_sent_frame(frame, prepared); } Err(std::sync::mpsc::TrySendError::Full(_)) => { @@ -1534,7 +1615,8 @@ impl HeadlessServer { info!("server shutdown initiated"); self.shutting_down = true; - // Send ServerShutdown to all connected clients. + // Clear client-local host graphics, then send ServerShutdown to all connected clients. + self.send_all_clients_graphics_cleanup(); let shutdown_msg = ServerMessage::ServerShutdown { reason: Some("server is shutting down".to_owned()), }; @@ -1557,6 +1639,7 @@ impl HeadlessServer { // Send ServerShutdown to all remaining clients. if !self.clients.is_empty() { + self.send_all_clients_graphics_cleanup(); let shutdown_msg = ServerMessage::ServerShutdown { reason: Some("server is shutting down".to_owned()), }; @@ -2026,6 +2109,7 @@ mod tests { 1, ClientConnection::new( (160, 45), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme { foreground: Some(crate::terminal_theme::RgbColor { r: 0xaa, @@ -2048,6 +2132,7 @@ mod tests { 2, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme { foreground: Some(crate::terminal_theme::RgbColor { r: 0x10, @@ -2100,6 +2185,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2111,6 +2197,7 @@ mod tests { 2, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), Some(true), 2, @@ -2140,6 +2227,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2151,6 +2239,7 @@ mod tests { 2, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), Some(true), 2, @@ -2180,6 +2269,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), Some(true), 1, @@ -2215,6 +2305,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2226,6 +2317,7 @@ mod tests { 2, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 2, @@ -2270,6 +2362,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2303,6 +2396,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2348,6 +2442,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2406,6 +2501,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2449,6 +2545,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2499,6 +2596,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2530,6 +2628,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2562,6 +2661,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2573,6 +2673,7 @@ mod tests { 2, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 2, @@ -2614,6 +2715,7 @@ mod tests { 1, ClientConnection::new( (120, 40), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2625,6 +2727,7 @@ mod tests { 2, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 2, @@ -2668,6 +2771,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, @@ -2719,6 +2823,7 @@ mod tests { 1, ClientConnection::new( (80, 24), + crate::kitty_graphics::HostCellSize::default(), crate::terminal_theme::TerminalTheme::default(), None, 1, diff --git a/src/server/protocol.rs b/src/server/protocol.rs index 5eda0231..743817ac 100644 --- a/src/server/protocol.rs +++ b/src/server/protocol.rs @@ -13,11 +13,12 @@ use serde::{Deserialize, Serialize}; // --------------------------------------------------------------------------- /// Current protocol version. Bumped when wire format changes incompatibly. -pub const PROTOCOL_VERSION: u32 = 4; +pub const PROTOCOL_VERSION: u32 = 5; -/// Maximum allowed frame payload size (2 MB). Frames larger than this are -/// rejected to prevent denial-of-service via oversized length prefixes. -pub const MAX_FRAME_SIZE: usize = 2 * 1024 * 1024; +/// Maximum allowed frame payload size (32 MB). Frames larger than this are +/// rejected to prevent denial-of-service via oversized length prefixes while +/// leaving room for first-frame Kitty graphics image uploads. +pub const MAX_FRAME_SIZE: usize = 32 * 1024 * 1024; /// Length of the u32 little-endian length prefix in bytes. const LENGTH_PREFIX_BYTES: usize = 4; @@ -46,6 +47,10 @@ pub enum ClientMessage { cols: u16, /// Terminal height in rows. rows: u16, + /// Width of a terminal cell in physical pixels. + cell_width_px: u32, + /// Height of a terminal cell in physical pixels. + cell_height_px: u32, /// Render encoding requested by the client. requested_encoding: RenderEncoding, }, @@ -62,6 +67,10 @@ pub enum ClientMessage { cols: u16, /// New terminal height in rows. rows: u16, + /// Width of a terminal cell in physical pixels. + cell_width_px: u32, + /// Height of a terminal cell in physical pixels. + cell_height_px: u32, }, /// Graceful disconnect request. @@ -114,6 +123,8 @@ pub struct FrameData { pub cursor: Option<CursorState>, /// OSC 8 hyperlink URIs referenced by cells. pub hyperlinks: Vec<String>, + /// Kitty graphics protocol bytes to apply after the text frame. + pub graphics: Vec<u8>, } impl FrameData { @@ -178,6 +189,7 @@ impl FrameData { height, cursor, hyperlinks: hyperlink_uris, + graphics: Vec::new(), } } @@ -255,6 +267,12 @@ pub enum ServerMessage { /// Terminal bytes to write directly for a terminal-ANSI client. Terminal(TerminalFrame), + /// Client-local Kitty graphics bytes to write directly to the host terminal. + Graphics { + /// Raw Kitty graphics protocol bytes. + bytes: Vec<u8>, + }, + /// Server is shutting down. Clients should exit gracefully. ServerShutdown { /// Optional reason for the shutdown. @@ -546,6 +564,8 @@ mod tests { version: PROTOCOL_VERSION, cols: 80, rows: 24, + cell_width_px: 8, + cell_height_px: 16, requested_encoding: RenderEncoding::SemanticFrame, }; let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap(); @@ -584,7 +604,12 @@ mod tests { #[test] fn client_resize_roundtrip() { - let msg = ClientMessage::Resize { cols: 80, rows: 24 }; + let msg = ClientMessage::Resize { + cols: 80, + rows: 24, + cell_width_px: 8, + cell_height_px: 16, + }; let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap(); let (decoded, _): (ClientMessage, _) = bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap(); @@ -690,6 +715,7 @@ mod tests { visible: true, }), hyperlinks: vec!["https://example.com".to_owned()], + graphics: Vec::new(), }; let msg = ServerMessage::Frame(frame.clone()); let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap(); @@ -740,6 +766,17 @@ mod tests { } #[test] + fn server_graphics_roundtrip() { + let msg = ServerMessage::Graphics { + bytes: b"\x1b_Ga=d,d=A,q=2;\x1b\\".to_vec(), + }; + let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap(); + let (decoded, _): (ServerMessage, _) = + bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap(); + assert_eq!(msg, decoded); + } + + #[test] fn server_terminal_frame_roundtrip() { let msg = ServerMessage::Terminal(TerminalFrame { seq: 7, @@ -771,6 +808,8 @@ mod tests { version: PROTOCOL_VERSION, cols: 80, rows: 24, + cell_width_px: 8, + cell_height_px: 16, requested_encoding: RenderEncoding::SemanticFrame, }; let mut buf = Vec::new(); @@ -811,6 +850,7 @@ mod tests { visible: true, }), hyperlinks: Vec::new(), + graphics: Vec::new(), }; let msg = ServerMessage::Frame(frame); @@ -839,6 +879,8 @@ mod tests { version: PROTOCOL_VERSION, cols: (80 + (i % 40) as u16), rows: (24 + (i % 20) as u16), + cell_width_px: 8, + cell_height_px: 16, requested_encoding: RenderEncoding::SemanticFrame, }, 1 => ClientMessage::Input { @@ -847,6 +889,8 @@ mod tests { 2 => ClientMessage::Resize { cols: (100 + (i % 30) as u16), rows: (30 + (i % 10) as u16), + cell_width_px: 8, + cell_height_px: 16, }, 3 => ClientMessage::Detach, _ => unreachable!(), @@ -1142,6 +1186,7 @@ mod tests { height: 2, cursor: None, hyperlinks: Vec::new(), + graphics: Vec::new(), }; assert!(frame.to_ratatui_buffer().is_none()); } @@ -1263,6 +1308,8 @@ mod tests { version: PROTOCOL_VERSION, cols: 80, rows: 24, + cell_width_px: 8, + cell_height_px: 16, requested_encoding: RenderEncoding::SemanticFrame, }; let mut buf = Vec::new(); @@ -1294,6 +1341,8 @@ mod tests { version: PROTOCOL_VERSION, cols: 200, rows: 60, + cell_width_px: 8, + cell_height_px: 16, requested_encoding: RenderEncoding::SemanticFrame, }, ClientMessage::Input { @@ -1302,6 +1351,8 @@ mod tests { ClientMessage::Resize { cols: 100, rows: 30, + cell_width_px: 8, + cell_height_px: 16, }, ClientMessage::Detach, ]; diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs index 45542a7a..ca0ecddd 100644 --- a/src/server/render_stream.rs +++ b/src/server/render_stream.rs @@ -57,7 +57,8 @@ impl ClientRenderState { if blit_encoder.is_current(frame) { return None; } - let encoded = blit_encoder.encode(frame, false); + let mut encoded = blit_encoder.encode(frame, false); + insert_graphics_before_sync_end(&mut encoded.bytes, &frame.graphics); Some(PreparedRender { message: ServerMessage::Terminal(TerminalFrame { seq: *seq + 1, @@ -72,12 +73,6 @@ impl ClientRenderState { } } - pub(crate) fn note_oversized_frame(&mut self, frame: FrameData) { - if let Self::Semantic { last_frame } = self { - *last_frame = Some(frame); - } - } - pub(crate) fn commit_sent_frame(&mut self, frame: FrameData, prepared: PreparedRender) { match (self, prepared.encoded) { (Self::Semantic { last_frame }, None) => *last_frame = Some(frame), @@ -98,6 +93,30 @@ impl ClientRenderState { } } +const SYNC_OUTPUT_END: &[u8] = b"\x1b[?2026l"; + +fn insert_graphics_before_sync_end(encoded: &mut Vec<u8>, graphics: &[u8]) { + if graphics.is_empty() { + return; + } + + if let Some(sync_end) = rfind_subslice(encoded, SYNC_OUTPUT_END) { + encoded.splice(sync_end..sync_end, graphics.iter().copied()); + } else { + encoded.extend_from_slice(graphics); + } +} + +fn rfind_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> { + if needle.is_empty() || needle.len() > haystack.len() { + return None; + } + + haystack + .windows(needle.len()) + .rposition(|window| window == needle) +} + /// A prepared client render message plus any baseline state needed after send. pub(crate) struct PreparedRender { message: ServerMessage, @@ -202,8 +221,22 @@ pub(crate) fn render_virtual( area: Rect, resize_panes: bool, ) -> (ratatui::buffer::Buffer, Option<CursorState>) { + render_virtual_with_cell_size( + app_state, + area, + resize_panes, + crate::kitty_graphics::HostCellSize::default(), + ) +} + +pub(crate) fn render_virtual_with_cell_size( + app_state: &mut AppState, + area: Rect, + resize_panes: bool, + cell_size: crate::kitty_graphics::HostCellSize, +) -> (ratatui::buffer::Buffer, Option<CursorState>) { if resize_panes { - crate::ui::compute_view(app_state, area); + crate::ui::compute_view_with_cell_size(app_state, area, cell_size); } else { crate::ui::compute_view_without_resizing_panes(app_state, area); } |
