1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Mark {
15 PromptStart,
17 PromptEnd,
19 CommandStart,
21 CommandEnd(Option<i32>),
23}
24
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub struct Marks {
28 pub seen: bool,
31 pub running: bool,
33 pub last_exit: Option<i32>,
35 pub last: Option<Mark>,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41enum Scan {
42 Ground,
43 Escape,
44 Osc,
45 OscEscape,
46}
47
48const MAX_PAYLOAD: usize = 4096;
52
53#[derive(Debug)]
55pub struct Osc133 {
56 scan: Scan,
57 payload: Vec<u8>,
58 discard: bool,
59 marks: Marks,
60}
61
62impl Default for Osc133 {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl Osc133 {
69 pub fn new() -> Self {
70 Self { scan: Scan::Ground, payload: Vec::new(), discard: false, marks: Marks::default() }
71 }
72
73 pub fn marks(&self) -> Marks {
75 self.marks
76 }
77
78 pub fn feed(&mut self, bytes: &[u8]) {
80 for &byte in bytes {
81 self.step(byte);
82 }
83 }
84
85 fn step(&mut self, byte: u8) {
86 match self.scan {
87 Scan::Ground => {
88 if byte == 0x1b {
89 self.scan = Scan::Escape;
90 }
91 }
92 Scan::Escape => match byte {
93 b']' => {
94 self.scan = Scan::Osc;
95 self.payload.clear();
96 self.discard = false;
97 }
98 0x1b => {}
99 _ => self.scan = Scan::Ground,
100 },
101 Scan::Osc => match byte {
102 0x07 => {
103 self.finish();
104 self.scan = Scan::Ground;
105 }
106 0x1b => self.scan = Scan::OscEscape,
107 _ => self.push(byte),
108 },
109 Scan::OscEscape => match byte {
110 b'\\' => {
111 self.finish();
112 self.scan = Scan::Ground;
113 }
114 0x1b => {}
115 _ => {
116 self.finish();
119 self.scan = Scan::Ground;
120 self.step(byte);
121 }
122 },
123 }
124 }
125
126 fn push(&mut self, byte: u8) {
127 if self.discard {
128 return;
129 }
130 if self.payload.len() >= MAX_PAYLOAD {
131 self.discard = true;
132 self.payload.clear();
133 return;
134 }
135 self.payload.push(byte);
136 }
137
138 fn finish(&mut self) {
139 if self.discard {
140 self.discard = false;
141 self.payload.clear();
142 return;
143 }
144 let payload = std::mem::take(&mut self.payload);
145 let Some(rest) = payload.strip_prefix(b"133;") else { return };
146 let mark = match rest {
147 b"A" => Mark::PromptStart,
148 b"B" => Mark::PromptEnd,
149 b"C" => Mark::CommandStart,
150 b"D" => Mark::CommandEnd(None),
151 other => {
152 let Some(code) = other.strip_prefix(b"D;") else { return };
153 let Ok(text) = std::str::from_utf8(code) else { return };
154 let Ok(code) = text.parse::<i32>() else { return };
155 Mark::CommandEnd(Some(code))
156 }
157 };
158 self.record(mark);
159 }
160
161 fn record(&mut self, mark: Mark) {
162 self.marks.seen = true;
163 self.marks.last = Some(mark);
164 match mark {
165 Mark::CommandStart => self.marks.running = true,
166 Mark::CommandEnd(code) => {
167 self.marks.running = false;
168 self.marks.last_exit = code;
169 }
170 Mark::PromptStart | Mark::PromptEnd => {}
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn scan(bytes: &[u8]) -> Marks {
180 let mut scanner = Osc133::new();
181 scanner.feed(bytes);
182 scanner.marks()
183 }
184
185 #[test]
188 fn the_four_marks_are_recorded_in_order() {
189 let marks =
190 scan(b"\x1b]133;A\x07$ \x1b]133;B\x07ls\r\n\x1b]133;C\x07out\r\n\x1b]133;D;7\x07");
191 assert!(marks.seen, "the shell has integration");
192 assert!(!marks.running, "the command finished");
193 assert_eq!(marks.last_exit, Some(7), "the failing command's code");
194 assert_eq!(marks.last, Some(Mark::CommandEnd(Some(7))));
195 }
196
197 #[test]
200 fn a_command_start_with_no_end_is_running() {
201 let marks = scan(b"\x1b]133;A\x07\x1b]133;B\x07\x1b]133;C\x07");
202 assert!(marks.running);
203 assert_eq!(marks.last_exit, None);
204 }
205
206 #[test]
209 fn a_mark_split_across_reads_is_recorded() {
210 let mut scanner = Osc133::new();
211 scanner.feed(b"\x1b]13");
212 scanner.feed(b"3;D;130");
213 scanner.feed(b"\x07");
214 let marks = scanner.marks();
215 assert_eq!(marks.last_exit, Some(130), "Ctrl-C's code survives the split");
216 assert!(!marks.running);
217 }
218
219 #[test]
222 fn the_esc_backslash_terminator_ends_a_mark() {
223 let marks = scan(b"\x1b]133;D;0\x1b\\");
224 assert_eq!(marks.last_exit, Some(0));
225 }
226
227 #[test]
230 fn a_stream_with_no_marks_is_not_exact() {
231 let marks = scan(b"$ ls\r\nfile\r\n$ ");
232 assert!(!marks.seen);
233 assert!(!marks.running);
234 assert_eq!(marks.last_exit, None);
235 }
236
237 #[test]
240 fn other_osc_sequences_and_malformed_marks_are_ignored() {
241 let marks = scan(b"\x1b]0;title\x07\x1b]133;D;notanumber\x07\x1b]133;Z\x07");
242 assert!(!marks.seen, "none of those is a mark");
243 }
244
245 #[test]
248 fn an_oversized_payload_is_discarded_and_the_scanner_recovers() {
249 let mut scanner = Osc133::new();
250 scanner.feed(b"\x1b]52;c;");
251 scanner.feed(&vec![b'A'; MAX_PAYLOAD + 10]);
252 scanner.feed(b"\x07");
253 assert!(!scanner.marks().seen, "the oversized payload is not a mark");
254 scanner.feed(b"\x1b]133;D;3\x07");
255 assert_eq!(scanner.marks().last_exit, Some(3), "and the next mark is read");
256 }
257}