aboutsummaryrefslogtreecommitdiff
path: root/src/pane/kitty_keyboard.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-28 04:11:56 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-28 04:12:53 +0300
commitbf9cc201244946496b4d950046bf7edcc4892994 (patch)
tree920c866e67783cf31d2feff9642457204999c8eb /src/pane/kitty_keyboard.rs
parent789738cada5afaa20bbd77a0d479fb689c455bfe (diff)
fix: preserve live handoff terminal state
Diffstat (limited to 'src/pane/kitty_keyboard.rs')
-rw-r--r--src/pane/kitty_keyboard.rs124
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]);
+ }
+}