aboutsummaryrefslogtreecommitdiff
path: root/src/pane
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-24 20:59:48 +0300
committerGitHub <noreply@github.com>2026-09-24 20:59:48 +0300
commitc411883ec639c9893ed9c33021c890485e91727b (patch)
tree797bb6efdb3a9425e3d8c31953f62452bd564cb1 /src/pane
parent11a7ab4ae21ab9427bcb44b3be01f62a4ab58c47 (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.rs79
-rw-r--r--src/pane/osc.rs97
-rw-r--r--src/pane/xtgettcap.rs96
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);
+ }
+ }
+}