diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/client/frame_output.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff) | |
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
Diffstat (limited to 'src/client/frame_output.rs')
| -rw-r--r-- | src/client/frame_output.rs | 97 |
1 files changed, 97 insertions, 0 deletions
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<Mutex<HashSet<u32>>> = 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<u32> { + 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<u32> { + 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::<u32>() { + return Some(id); + } + } + None +} + +fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> { + if needle.is_empty() || needle.len() > haystack.len() { + return None; + } + haystack + .windows(needle.len()) + .position(|window| window == needle) +} |
