diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /src/pane/xtgettcap.rs | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'src/pane/xtgettcap.rs')
| -rw-r--r-- | src/pane/xtgettcap.rs | 270 |
1 files changed, 84 insertions, 186 deletions
diff --git a/src/pane/xtgettcap.rs b/src/pane/xtgettcap.rs index 7d2dfc5d..4d8fa572 100644 --- a/src/pane/xtgettcap.rs +++ b/src/pane/xtgettcap.rs @@ -1,20 +1,26 @@ +// Compatibility for legacy raw-C1 XTGETTCAP only. Ordinary ESC P ... ESC \ +// requests are answered exclusively by libghostty. Keep bytes untouched: C1 +// normalization across the stream could corrupt UTF-8 or binary string payloads. use bytes::Bytes; #[derive(Debug, Default)] -pub(super) struct XtgettcapQueryTracker { - state: XtgettcapTrackerState, +pub(super) struct C1XtgettcapQueryTracker { + state: C1XtgettcapTrackerState, + raw_c1_intro: bool, + native_dcs_pending: bool, body: Vec<u8>, - pending: Vec<XtgettcapResponse>, + pending: Vec<C1XtgettcapResponse>, } #[derive(Debug, Clone, PartialEq, Eq)] -pub(super) struct XtgettcapResponse { +pub(super) struct C1XtgettcapResponse { pub(super) end_offset: usize, pub(super) bytes: Bytes, + pub(super) suppress_native: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -enum XtgettcapTrackerState { +enum C1XtgettcapTrackerState { #[default] Ground, Escape, @@ -30,114 +36,133 @@ enum XtgettcapTrackerState { OversizedDcsEscape, } -impl XtgettcapQueryTracker { +impl C1XtgettcapQueryTracker { pub(super) fn observe(&mut self, bytes: &[u8]) { for (index, &byte) in bytes.iter().enumerate() { + // 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 + // replies, not subsequent native requests or other response types. + if self.native_dcs_pending && matches!(byte, 0x1b | 0x18 | 0x1a) { + self.pending.push(C1XtgettcapResponse { + end_offset: index + 1, + bytes: Bytes::new(), + suppress_native: true, + }); + self.native_dcs_pending = false; + } match self.state { - XtgettcapTrackerState::Ground => { + C1XtgettcapTrackerState::Ground => { if byte == 0x1b { - self.state = XtgettcapTrackerState::Escape; + self.state = C1XtgettcapTrackerState::Escape; } else if byte == 0x90 { self.body.clear(); - self.state = XtgettcapTrackerState::DcsIntro; + self.raw_c1_intro = true; + self.state = C1XtgettcapTrackerState::DcsIntro; } else if byte == 0x9d { - self.state = XtgettcapTrackerState::IgnoreOsc; + self.state = C1XtgettcapTrackerState::IgnoreOsc; } else if matches!(byte, 0x98 | 0x9e | 0x9f) { - self.state = XtgettcapTrackerState::IgnoreString; + self.state = C1XtgettcapTrackerState::IgnoreString; } } - XtgettcapTrackerState::Escape => match byte { + C1XtgettcapTrackerState::Escape => match byte { b'P' => { + self.raw_c1_intro = false; self.body.clear(); - self.state = XtgettcapTrackerState::DcsIntro; + self.state = C1XtgettcapTrackerState::DcsIntro; } b']' => { self.body.clear(); - self.state = XtgettcapTrackerState::IgnoreOsc; + self.state = C1XtgettcapTrackerState::IgnoreOsc; } b'_' | b'^' | b'X' => { self.body.clear(); - self.state = XtgettcapTrackerState::IgnoreString; + self.state = C1XtgettcapTrackerState::IgnoreString; } - 0x1b => self.state = XtgettcapTrackerState::Escape, - _ => self.state = XtgettcapTrackerState::Ground, + 0x1b => self.state = C1XtgettcapTrackerState::Escape, + _ => self.state = C1XtgettcapTrackerState::Ground, }, - XtgettcapTrackerState::DcsIntro => match byte { - b'+' => self.state = XtgettcapTrackerState::DcsIntroPlus, - 0x1b => self.state = XtgettcapTrackerState::IgnoreStringEscape, - 0x9c => self.state = XtgettcapTrackerState::Ground, - _ => self.state = XtgettcapTrackerState::IgnoreString, + C1XtgettcapTrackerState::DcsIntro => match byte { + b'+' => self.state = C1XtgettcapTrackerState::DcsIntroPlus, + 0x1b => self.state = C1XtgettcapTrackerState::IgnoreStringEscape, + 0x9c => self.state = C1XtgettcapTrackerState::Ground, + _ => self.state = C1XtgettcapTrackerState::IgnoreString, }, - XtgettcapTrackerState::DcsIntroPlus => match byte { - b'q' => self.state = XtgettcapTrackerState::DcsBody, - 0x1b => self.state = XtgettcapTrackerState::IgnoreStringEscape, - 0x9c => self.state = XtgettcapTrackerState::Ground, - _ => self.state = XtgettcapTrackerState::IgnoreString, + C1XtgettcapTrackerState::DcsIntroPlus => match byte { + b'q' => self.state = C1XtgettcapTrackerState::DcsBody, + 0x1b => self.state = C1XtgettcapTrackerState::IgnoreStringEscape, + 0x9c => self.state = C1XtgettcapTrackerState::Ground, + _ => self.state = C1XtgettcapTrackerState::IgnoreString, }, - XtgettcapTrackerState::DcsBody => match byte { - 0x1b => self.state = XtgettcapTrackerState::DcsEscape, + C1XtgettcapTrackerState::DcsBody => match byte { + 0x1b => self.state = C1XtgettcapTrackerState::DcsEscape, 0x9c => { + self.native_dcs_pending |= !self.raw_c1_intro; self.finalize(index + 1); - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } _ => self.body.push(byte), }, - XtgettcapTrackerState::DcsEscape => { + C1XtgettcapTrackerState::DcsEscape => { if byte == b'\\' { - self.finalize(index + 1); - self.state = XtgettcapTrackerState::Ground; + if self.raw_c1_intro { + self.finalize(index + 1); + } else { + self.body.clear(); + } + self.state = C1XtgettcapTrackerState::Ground; } else if byte != 0x1b { self.body.clear(); - self.state = XtgettcapTrackerState::IgnoreString; + self.state = C1XtgettcapTrackerState::IgnoreString; } } - XtgettcapTrackerState::IgnoreOsc => { + C1XtgettcapTrackerState::IgnoreOsc => { if byte == 0x1b { - self.state = XtgettcapTrackerState::IgnoreOscEscape; + self.state = C1XtgettcapTrackerState::IgnoreOscEscape; } else if matches!(byte, 0x07 | 0x9c) { - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } } - XtgettcapTrackerState::IgnoreOscEscape => { + C1XtgettcapTrackerState::IgnoreOscEscape => { if byte == b'\\' { - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } else if byte != 0x1b { - self.state = XtgettcapTrackerState::IgnoreOsc; + self.state = C1XtgettcapTrackerState::IgnoreOsc; } } - XtgettcapTrackerState::IgnoreString => { + C1XtgettcapTrackerState::IgnoreString => { if byte == 0x1b { - self.state = XtgettcapTrackerState::IgnoreStringEscape; + self.state = C1XtgettcapTrackerState::IgnoreStringEscape; } else if byte == 0x9c { - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } } - XtgettcapTrackerState::IgnoreStringEscape => { + C1XtgettcapTrackerState::IgnoreStringEscape => { if byte == b'\\' { - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } else if byte != 0x1b { - self.state = XtgettcapTrackerState::IgnoreString; + self.state = C1XtgettcapTrackerState::IgnoreString; } } - XtgettcapTrackerState::OversizedDcs => { + C1XtgettcapTrackerState::OversizedDcs => { if byte == 0x1b { - self.state = XtgettcapTrackerState::OversizedDcsEscape; + self.state = C1XtgettcapTrackerState::OversizedDcsEscape; } else if byte == 0x9c { - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } } - XtgettcapTrackerState::OversizedDcsEscape => { + C1XtgettcapTrackerState::OversizedDcsEscape => { if byte == b'\\' { - self.state = XtgettcapTrackerState::Ground; + self.state = C1XtgettcapTrackerState::Ground; } else if byte != 0x1b { - self.state = XtgettcapTrackerState::OversizedDcs; + self.state = C1XtgettcapTrackerState::OversizedDcs; } } } if self.body.len() > 1024 { self.body.clear(); - self.state = XtgettcapTrackerState::OversizedDcs; + self.state = C1XtgettcapTrackerState::OversizedDcs; } } } @@ -145,13 +170,17 @@ impl XtgettcapQueryTracker { fn finalize(&mut self, end_offset: usize) { for cap_hex in self.body.split(|byte| *byte == b';') { if let Some(bytes) = xtgettcap_response(cap_hex) { - self.pending.push(XtgettcapResponse { end_offset, bytes }); + self.pending.push(C1XtgettcapResponse { + end_offset, + bytes, + suppress_native: false, + }); } } self.body.clear(); } - pub(super) fn drain_pending(&mut self) -> Vec<XtgettcapResponse> { + pub(super) fn drain_pending(&mut self) -> Vec<C1XtgettcapResponse> { std::mem::take(&mut self.pending) } } @@ -210,134 +239,3 @@ fn append_upper_hex(bytes: &[u8], output: &mut Vec<u8>) { output.push(HEX[usize::from(byte & 0x0f)]); } } - -#[cfg(test)] -mod tests { - use super::*; - - fn response_bytes(responses: Vec<XtgettcapResponse>) -> Vec<Bytes> { - responses - .into_iter() - .map(|response| response.bytes) - .collect() - } - - #[test] - fn tracker_returns_multiple_capabilities_in_order() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x1bP+q5463;524742\x1b\\"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![ - Bytes::from_static(b"\x1bP1+r5463\x1b\\"), - Bytes::from_static(b"\x1bP1+r524742=38\x1b\\"), - ] - ); - } - - #[test] - fn tracker_normalizes_mixed_case_query_keys() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x1bP+q4d73\x1b\\"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![Bytes::from_static( - b"\x1bP1+r4D73=5C455D35323B25703125733B25703225735C303037\x1b\\" - )] - ); - } - - #[test] - fn tracker_ignores_unsupported_capabilities() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x1bP+q6E6F7065\x1b\\"); - - assert!(response_bytes(tracker.drain_pending()).is_empty()); - } - - #[test] - fn tracker_returns_underline_style_capability() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x1bP+q536D756C78\x1b\\"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![Bytes::from_static( - b"\x1bP1+r536D756C78=5C455B343A25703125646D\x1b\\" - )] - ); - } - - #[test] - fn tracker_keeps_split_query_until_string_terminator() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x1bP+q537"); - assert!(response_bytes(tracker.drain_pending()).is_empty()); - tracker.observe(b"5\x1b"); - assert!(response_bytes(tracker.drain_pending()).is_empty()); - tracker.observe(b"\\"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![Bytes::from_static(b"\x1bP1+r5375\x1b\\")] - ); - } - - #[test] - fn tracker_resumes_after_ignored_osc_bel_terminator() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x1b]0;title\x07\x1bP+q5463\x1b\\"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![Bytes::from_static(b"\x1bP1+r5463\x1b\\")] - ); - } - - #[test] - fn tracker_accepts_eight_bit_dcs_and_string_terminator() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x90+q5463\x9c"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![Bytes::from_static(b"\x1bP1+r5463\x1b\\")] - ); - } - - #[test] - fn tracker_ignores_xtgettcap_bytes_inside_eight_bit_osc() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"\x9dtitle\x1bP+q5463\x9c\x1bP+q5463\x1b\\"); - - assert_eq!( - response_bytes(tracker.drain_pending()), - vec![Bytes::from_static(b"\x1bP1+r5463\x1b\\")] - ); - } - - #[test] - fn tracker_reports_response_end_offsets() { - let mut tracker = XtgettcapQueryTracker::default(); - - tracker.observe(b"before\x1bP+q5463\x1b\\after"); - - assert_eq!( - tracker.drain_pending(), - vec![XtgettcapResponse { - end_offset: 16, - bytes: Bytes::from_static(b"\x1bP1+r5463\x1b\\"), - }] - ); - } -} |
