diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-24 20:59:48 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-24 20:59:48 +0300 |
| commit | c411883ec639c9893ed9c33021c890485e91727b (patch) | |
| tree | 797bb6efdb3a9425e3d8c31953f62452bd564cb1 /src/pane | |
| parent | 11a7ab4ae21ab9427bcb44b3be01f62a4ab58c47 (diff) | |
feat!: replace the custom graphics api with optimized native kitty rendering (#4561)
* feat!: replace the custom graphics api with optimized native kitty rendering
* fix: enable safe kitty optimizations automatically
---------
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/pane')
| -rw-r--r-- | src/pane/cursor.rs | 79 | ||||
| -rw-r--r-- | src/pane/osc.rs | 97 | ||||
| -rw-r--r-- | src/pane/xtgettcap.rs | 96 |
3 files changed, 264 insertions, 8 deletions
diff --git a/src/pane/cursor.rs b/src/pane/cursor.rs index 2ba94312..30137730 100644 --- a/src/pane/cursor.rs +++ b/src/pane/cursor.rs @@ -24,9 +24,16 @@ enum DecscusrParseState { } impl DecscusrTracker { - pub(crate) fn observe(&mut self, bytes: &[u8]) { - for &byte in bytes { - self.observe_byte(byte); + pub(crate) fn observe(&mut self, mut bytes: &[u8]) { + while !bytes.is_empty() { + if matches!(self.state, DecscusrParseState::Ground) { + let Some(escape) = bytes.iter().position(|&byte| byte == 0x1b) else { + return; + }; + bytes = &bytes[escape..]; + } + self.observe_byte(bytes[0]); + bytes = &bytes[1..]; } } @@ -256,6 +263,72 @@ fn is_jump(settled: TerminalCursorState, current: TerminalCursorState) -> bool { mod tests { use super::*; + fn assert_decscusr_matches_bytewise(chunks: &[&[u8]]) { + let mut optimized = DecscusrTracker::default(); + let mut bytewise = DecscusrTracker::default(); + for chunk in chunks { + optimized.observe(chunk); + for &byte in *chunk { + bytewise.observe_byte(byte); + } + assert_eq!( + optimized.cursor_shape_overridden(), + bytewise.cursor_shape_overridden(), + "chunks: {chunks:?}" + ); + // Compare the complete parser state, including incomplete CSI parameters. + assert_eq!( + format!("{:?}", optimized.state), + format!("{:?}", bytewise.state), + "chunks: {chunks:?}" + ); + } + } + + #[test] + fn decscusr_bulk_search_matches_bytewise_control_sequences_and_splits() { + let cases: &[&[u8]] = &[ + b"", + b"plain text\n\twithout escapes", + b"text\x1b[1 qmore\x1b[0 qend", + b"\x1b[ q\x1b[2 q\x1b[3 q\x1b[4 q\x1b[5 q\x1b[6 q\x1b[7 q", + b"\x1b\x1b[1 q\x1b[2;9 q\x1b[0:4 q", + b"\x1b[1q\x1b[? q\x1b[12$ q\x1b[1\x00 q\x1b[2\xff q", + b"\x1b[12\x1b[5 q\x1b]0;title\x07\x1bPdata\x1b\\", + b"\x1b[1 q\x1b", + b"\x1b[1 q\x1b[", + b"\x1b[1 q\x1b[0 ", + ]; + for &bytes in cases { + for split in 0..=bytes.len() { + assert_decscusr_matches_bytewise(&[&bytes[..split], &[], &bytes[split..]]); + } + let chunks: Vec<_> = bytes.chunks(1).collect(); + assert_decscusr_matches_bytewise(&chunks); + } + } + + #[test] + fn decscusr_bulk_search_matches_bytewise_all_byte_values() { + let all_bytes: Vec<u8> = (0..=255).collect(); + for split in 0..=all_bytes.len() { + assert_decscusr_matches_bytewise(&[&all_bytes[..split], &all_bytes[split..]]); + } + // Exercise every possible byte in ground, escape, and partial CSI states. + let prefixes: &[&[u8]] = &[b"", b"\x1b", b"\x1b[", b"\x1b[2", b"\x1b[0 ", b"\x1b[2;"]; + for &prefix in prefixes { + for byte in 0..=255u8 { + let mut bytes = b"\x1b[1 q".to_vec(); + bytes.extend_from_slice(prefix); + bytes.push(byte); + bytes.extend_from_slice(b" qtext\x1b[0 q\x1b[6 q"); + for split in 0..=bytes.len() { + assert_decscusr_matches_bytewise(&[&bytes[..split], &bytes[split..]]); + } + } + } + } + fn cursor(x: u16, y: u16, visible: bool, shape: u8) -> TerminalCursorState { TerminalCursorState { x, diff --git a/src/pane/osc.rs b/src/pane/osc.rs index 472fa7aa..85b8e780 100644 --- a/src/pane/osc.rs +++ b/src/pane/osc.rs @@ -63,8 +63,19 @@ fn is_ignored_string_intro(byte: u8) -> bool { impl DefaultColorOscTracker { pub(super) fn observe(&mut self, bytes: &[u8]) -> bool { let mut saw_default_color_set = false; - - for &byte in bytes { + let mut cursor = 0; + while cursor < bytes.len() { + if matches!( + self.state, + DefaultColorOscTrackerState::Ground | DefaultColorOscTrackerState::IgnoreString + ) { + let Some(offset) = bytes[cursor..].iter().position(|&byte| byte == 0x1b) else { + break; + }; + cursor += offset; + } + let byte = bytes[cursor]; + cursor += 1; match self.state { DefaultColorOscTrackerState::Ground => { if byte == 0x1b { @@ -157,7 +168,20 @@ pub(super) struct DefaultColorEventTracker { impl DefaultColorEventTracker { pub(super) fn observe(&mut self, bytes: &[u8]) { - for (index, &byte) in bytes.iter().enumerate() { + let mut cursor = 0; + while cursor < bytes.len() { + if matches!( + self.state, + DefaultColorOscTrackerState::Ground | DefaultColorOscTrackerState::IgnoreString + ) { + let Some(offset) = bytes[cursor..].iter().position(|&byte| byte == 0x1b) else { + break; + }; + cursor += offset; + } + let index = cursor; + let byte = bytes[cursor]; + cursor += 1; match self.state { DefaultColorOscTrackerState::Ground => { if byte == 0x1b { @@ -347,7 +371,19 @@ impl OscStreamCollector { const MAX_BODY_BYTES: usize = 4096; fn observe(&mut self, bytes: &[u8], mut receive: impl FnMut(&[u8])) { - for &byte in bytes { + let mut cursor = 0; + while cursor < bytes.len() { + if matches!( + self.state, + OscStreamState::Ground | OscStreamState::IgnoringString + ) { + let Some(offset) = bytes[cursor..].iter().position(|&byte| byte == 0x1b) else { + break; + }; + cursor += offset; + } + let byte = bytes[cursor]; + cursor += 1; match self.state { OscStreamState::Ground => { if byte == 0x1b { @@ -805,6 +841,59 @@ pub(super) fn restore_host_terminal_theme_if_needed( #[cfg(test)] mod tests { + #[test] + fn bulk_osc_scans_match_bytewise_state_and_response_offsets() { + use super::*; + let mut input = b"text\x1b_Gm=1;".to_vec(); + input.extend(std::iter::repeat_n(b'A', 8192)); + input.extend_from_slice(b"\x1b\\\x1b]10;?\x07\x1b]11;red\x1b\\\x1bPignored"); + input.extend(0u8..=255); + input.extend_from_slice(b"\x1b\\\x1b]12;"); + input.extend(std::iter::repeat_n(b'B', 4200)); + input.extend_from_slice(b"\x07\x1b]10;?\x1b\\\x1b]11;?\x07\x1b"); + for chunk_size in [1, 2, 3, 17, 4096, input.len()] { + let mut bulk = DefaultColorOscTracker::default(); + let mut scalar = DefaultColorOscTracker::default(); + let mut bulk_events = DefaultColorEventTracker::default(); + let mut scalar_events = DefaultColorEventTracker::default(); + let mut bulk_stream = OscStreamCollector::default(); + let mut scalar_stream = OscStreamCollector::default(); + for chunk in input.chunks(chunk_size) { + let changed = bulk.observe(chunk); + let mut scalar_changed = false; + let mut expected_events = Vec::new(); + let mut expected_bodies = Vec::new(); + for (offset, byte) in chunk.iter().enumerate() { + let byte = std::slice::from_ref(byte); + scalar_changed |= scalar.observe(byte); + scalar_events.observe(byte); + expected_events.extend(scalar_events.drain_pending().into_iter().map( + |mut event| { + event.end_offset += offset; + event + }, + )); + scalar_stream.observe(byte, |body| expected_bodies.push(body.to_vec())); + } + bulk_events.observe(chunk); + let mut bodies = Vec::new(); + bulk_stream.observe(chunk, |body| bodies.push(body.to_vec())); + assert_eq!(changed, scalar_changed); + assert_eq!((bulk.state, &bulk.body), (scalar.state, &scalar.body)); + assert_eq!(bulk_events.drain_pending(), expected_events); + assert_eq!( + (bulk_events.state, &bulk_events.body), + (scalar_events.state, &scalar_events.body) + ); + assert_eq!(bodies, expected_bodies); + assert_eq!( + (bulk_stream.state, &bulk_stream.body), + (scalar_stream.state, &scalar_stream.body) + ); + } + } + } + use tokio::sync::mpsc; use super::*; diff --git a/src/pane/xtgettcap.rs b/src/pane/xtgettcap.rs index 4d8fa572..ea6f3a09 100644 --- a/src/pane/xtgettcap.rs +++ b/src/pane/xtgettcap.rs @@ -38,7 +38,21 @@ enum C1XtgettcapTrackerState { impl C1XtgettcapQueryTracker { pub(super) fn observe(&mut self, bytes: &[u8]) { - for (index, &byte) in bytes.iter().enumerate() { + let mut index = 0; + while index < bytes.len() { + if self.state == C1XtgettcapTrackerState::IgnoreString { + // Payload cannot affect this state. CAN/SUB still need the + // global native-DCS bookkeeping when a dispatch is pending. + let next = bytes[index..].iter().position(|&byte| { + matches!(byte, 0x1b | 0x9c) + || (self.native_dcs_pending && matches!(byte, 0x18 | 0x1a)) + }); + match next { + Some(offset) => index += offset, + None => break, + } + } + let byte = bytes[index]; // With a 7-bit intro and raw ST, the native parser still holds the // DCS open. At its eventual unhook it can answer earlier keys in a // multi-key request again. Discard only that dispatch's XTGETTCAP @@ -164,6 +178,7 @@ impl C1XtgettcapQueryTracker { self.body.clear(); self.state = C1XtgettcapTrackerState::OversizedDcs; } + index += 1; } } @@ -239,3 +254,82 @@ fn append_upper_hex(bytes: &[u8], output: &mut Vec<u8>) { output.push(HEX[usize::from(byte & 0x0f)]); } } + +#[cfg(test)] +mod tests { + use super::*; + + fn observe_chunks(chunks: &[&[u8]]) -> (C1XtgettcapQueryTracker, Vec<C1XtgettcapResponse>) { + let mut tracker = C1XtgettcapQueryTracker::default(); + let mut pending = Vec::new(); + let mut offset = 0; + for chunk in chunks { + tracker.observe(chunk); + pending.extend(tracker.drain_pending().into_iter().map(|mut response| { + response.end_offset += offset; + response + })); + offset += chunk.len(); + } + (tracker, pending) + } + + fn assert_chunk_equivalence(bytes: &[u8]) { + let one_byte_chunks: Vec<_> = bytes.chunks(1).collect(); + let (expected, expected_pending) = observe_chunks(&one_byte_chunks); + // Includes a single bulk call and every split, especially ESC / ST. + for split in 0..=bytes.len() { + let (actual, actual_pending) = observe_chunks(&[&bytes[..split], &bytes[split..]]); + assert_eq!(actual_pending, expected_pending, "split {split}: {bytes:?}"); + assert_eq!(actual.state, expected.state); + assert_eq!(actual.raw_c1_intro, expected.raw_c1_intro); + assert_eq!(actual.native_dcs_pending, expected.native_dcs_pending); + assert_eq!(actual.body, expected.body); + assert_eq!(actual.pending, expected.pending); + } + } + + #[test] + fn ignored_string_bulk_matches_single_bytes() { + let mut all_bytes = b"\x1b_Gordinary kitty payload".to_vec(); + all_bytes.extend(0..=255u8); + all_bytes.extend_from_slice(b"\x1b\\\x90+q5463\x9c\x90+q5247"); + assert_chunk_equivalence(&all_bytes); + + // Exercise every byte while actually inside IgnoreString, both with + // and without a native dispatch waiting for ESC, CAN, or SUB. + for native_pending in [false, true] { + for intro in [b"\x1b_".as_slice(), b"\x98", b"\x9e", b"\x9f"] { + for byte in 0..=255u8 { + let mut bytes = Vec::new(); + if native_pending { + bytes.extend_from_slice(b"\x1bP+q5463;524742\x9c"); + } + bytes.extend_from_slice(intro); + bytes.extend_from_slice(b"Gordinary payload"); + bytes.push(byte); + assert_chunk_equivalence(&bytes); + bytes.extend_from_slice(b"more payload\x1b\\\x90+q5463\x9c\x90+q5247"); + assert_chunk_equivalence(&bytes); + } + } + } + } + + #[test] + fn ignored_string_preserves_native_dispatch_offsets() { + for cancel in [0x18, 0x1a, 0x1b] { + let mut bytes = b"\x1bP+q5463\x9c\x9fignored payload".to_vec(); + bytes.push(cancel); + let end_offset = bytes.len(); + bytes.extend_from_slice(b"\x1b\\\x90+q524742\x9c"); + let (_, pending) = observe_chunks(&[&bytes]); + assert_eq!(pending.len(), 3); + assert!(pending[1].suppress_native); + assert!(pending[1].bytes.is_empty()); + assert_eq!(pending[1].end_offset, end_offset); + assert_eq!(pending[2].end_offset, bytes.len()); + assert_chunk_equivalence(&bytes); + } + } +} |
