From 207be3c771d281baae6e5fa0fb74be9a056e97a2 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Tue, 1 Sep 2026 15:48:22 +0300 Subject: refactor: render the shell in the client (#3487) * feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations --- src/client/frame_output.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/client/frame_output.rs (limited to 'src/client/frame_output.rs') diff --git a/src/client/frame_output.rs b/src/client/frame_output.rs new file mode 100644 index 00000000..5d12c78c --- /dev/null +++ b/src/client/frame_output.rs @@ -0,0 +1,97 @@ +use std::collections::HashSet; +use std::io; +use std::sync::{Mutex, OnceLock}; + +use crate::protocol::render_ansi; + +static RECEIVED_KITTY_GRAPHICS_IDS: OnceLock>> = OnceLock::new(); + +pub(super) fn write_encoded_frame_with_graphics( + mut writer: impl io::Write, + encoded: &[u8], + graphics: &[u8], +) -> io::Result<()> { + if graphics.is_empty() { + return writer.write_all(encoded); + } + + let insertion = render_ansi::final_sync_output_end(encoded).unwrap_or(encoded.len()); + + writer.write_all(&encoded[..insertion])?; + record_received_kitty_graphics(graphics); + writer.write_all(b"\x1b7")?; + writer.write_all(graphics)?; + writer.write_all(b"\x1b8")?; + writer.write_all(&encoded[insertion..]) +} + +pub(super) fn contains_kitty_graphics_bytes(bytes: &[u8]) -> bool { + bytes.windows(3).any(|window| window == b"\x1b_G") +} + +pub(super) fn record_received_kitty_graphics(bytes: &[u8]) { + let ids = kitty_graphics_image_ids(bytes); + if ids.is_empty() { + return; + } + let set = RECEIVED_KITTY_GRAPHICS_IDS.get_or_init(|| Mutex::new(HashSet::new())); + if let Ok(mut set) = set.lock() { + set.extend(ids); + } +} + +pub(super) fn clear_received_kitty_graphics(mut writer: impl io::Write) -> io::Result<()> { + let Some(set) = RECEIVED_KITTY_GRAPHICS_IDS.get() else { + return Ok(()); + }; + let Ok(mut set) = set.lock() else { + return Ok(()); + }; + for id in set.drain() { + write!(writer, "\x1b_Ga=d,d=I,i={id},q=2;\x1b\\")?; + } + writer.flush() +} + +pub(super) fn kitty_graphics_image_ids(bytes: &[u8]) -> Vec { + let mut ids = Vec::new(); + let mut index = 0usize; + while let Some(start) = find_subslice(&bytes[index..], b"\x1b_G") { + let command_start = index + start + 3; + let Some(end) = find_subslice(&bytes[command_start..], b"\x1b\\") else { + break; + }; + let command = &bytes[command_start..command_start + end]; + if let Some(id) = kitty_graphics_command_image_id(command) { + ids.push(id); + } + index = command_start + end + 2; + } + ids +} + +fn kitty_graphics_command_image_id(command: &[u8]) -> Option { + let header_end = command + .iter() + .position(|byte| *byte == b';') + .unwrap_or(command.len()); + for part in command[..header_end].split(|byte| *byte == b',') { + let Some(value) = part.strip_prefix(b"i=") else { + continue; + }; + let text = std::str::from_utf8(value).ok()?; + if let Ok(id) = text.parse::() { + return Some(id); + } + } + None +} + +fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() || needle.len() > haystack.len() { + return None; + } + haystack + .windows(needle.len()) + .position(|window| window == needle) +} -- cgit v1.2.3