diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-28 04:11:56 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-28 04:12:53 +0300 |
| commit | bf9cc201244946496b4d950046bf7edcc4892994 (patch) | |
| tree | 920c866e67783cf31d2feff9642457204999c8eb /src/pane/kitty_keyboard.rs | |
| parent | 789738cada5afaa20bbd77a0d479fb689c455bfe (diff) | |
fix: preserve live handoff terminal state
Diffstat (limited to 'src/pane/kitty_keyboard.rs')
| -rw-r--r-- | src/pane/kitty_keyboard.rs | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/src/pane/kitty_keyboard.rs b/src/pane/kitty_keyboard.rs new file mode 100644 index 00000000..1b7d3274 --- /dev/null +++ b/src/pane/kitty_keyboard.rs @@ -0,0 +1,124 @@ +#[derive(Debug, Clone, Default)] +pub(crate) struct KittyKeyboardTracker { + pending: Vec<u8>, + stack: Vec<u16>, + flags: u16, +} + +impl KittyKeyboardTracker { + pub(crate) fn observe(&mut self, bytes: &[u8]) { + let combined; + let bytes = if self.pending.is_empty() { + bytes + } else { + combined = self + .pending + .iter() + .copied() + .chain(bytes.iter().copied()) + .collect::<Vec<_>>(); + self.pending.clear(); + &combined + }; + let mut index = 0; + while index < bytes.len() { + if bytes[index] != 0x1b { + index += 1; + continue; + } + if index + 1 >= bytes.len() { + self.store_pending(&bytes[index..]); + break; + } + if bytes[index + 1] != b'[' { + index += 1; + continue; + } + + let mut end = index + 2; + while end < bytes.len() && !(0x40..=0x7e).contains(&bytes[end]) { + end += 1; + } + if end >= bytes.len() { + self.store_pending(&bytes[index..]); + break; + } + + if bytes[end] == b'u' { + self.observe_csi_u(&bytes[index + 2..end]); + } + index = end + 1; + } + } + + fn store_pending(&mut self, bytes: &[u8]) { + self.pending.clear(); + if bytes.len() <= 64 { + self.pending.extend_from_slice(bytes); + } + } + + fn observe_csi_u(&mut self, params: &[u8]) { + let Some((&kind, rest)) = params.split_first() else { + return; + }; + match kind { + b'>' => { + let flags = parse_kitty_keyboard_flags(rest); + self.stack.push(self.flags); + self.flags = flags; + } + b'=' => { + self.flags = parse_kitty_keyboard_flags(rest); + } + b'<' => { + let count = parse_kitty_keyboard_flags(rest).max(1); + for _ in 0..count { + self.flags = self.stack.pop().unwrap_or(0); + } + } + _ => {} + } + } + + pub(crate) fn replay_ansi(&self) -> Option<String> { + if self.stack.is_empty() { + return (self.flags != 0).then(|| format!("\x1b[={}u", self.flags)); + } + + let mut ansi = String::new(); + let baseline = self.stack[0]; + if baseline != 0 { + ansi.push_str(&format!("\x1b[={baseline}u")); + } + for flags in self.stack.iter().skip(1).copied().chain([self.flags]) { + ansi.push_str(&format!("\x1b[>{flags}u")); + } + (!ansi.is_empty()).then_some(ansi) + } +} + +fn parse_kitty_keyboard_flags(bytes: &[u8]) -> u16 { + let first_param = bytes.split(|byte| *byte == b';').next().unwrap_or_default(); + std::str::from_utf8(first_param) + .ok() + .and_then(|value| value.parse::<u16>().ok()) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn buffers_split_csi_sequences() { + let mut tracker = KittyKeyboardTracker::default(); + + tracker.observe(b"\x1b[>1u\x1b[>5"); + tracker.observe(b"u\x1b[<"); + tracker.observe(b"u"); + + assert_eq!(tracker.flags, 1); + assert_eq!(tracker.stack, vec![0]); + } +} |
