aboutsummaryrefslogtreecommitdiff
path: root/src/client/frame_output.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/client/frame_output.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (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.rs97
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)
+}