aboutsummaryrefslogtreecommitdiff
path: root/src/pane/xtgettcap.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /src/pane/xtgettcap.rs
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'src/pane/xtgettcap.rs')
-rw-r--r--src/pane/xtgettcap.rs270
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\\"),
- }]
- );
- }
-}