Skip to main content

max / shop

Native shop-vt parser replaces vte + ApcScanner shop-vt: Paul Williams state machine written from spec (not vendored from vte). Same 14 states, same transitions, own Params representation (Vec<Vec<u16>> for the two-level ;/: separation), streaming UTF-8 across chunk boundaries. Adds apc_dispatch — the callback vte declines to expose and the whole reason we needed our own parser to unblock kitty graphics. shop-grid: switched from vte::{Params, Perform} to shop_vt::{Params, Perform}. API-compatible enough that no other changes were needed. shop binary: - vte::Parser -> shop_vt::Parser. - ApcScanner deleted (~80 LOC). APC now dispatches through a small ParseSink adapter (`impl shop_vt::Perform for ParseSink`) that forwards the mainline callbacks to Grid and pushes APC bodies onto a queue for the kitty-graphics parser. - vte crate removed from workspace deps entirely. Followups tracked in GO already: SIMD UTF-8 fast path (simdutf8 crate), BSU/ESU synchronized-update hooks, tests exercised against the reference DEC parser test vectors.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 00:31 UTC
Signed with PGP, not checked
Commit: fe9badd0591c5041f373a08e81bb0bd2365ee03a
Parent: bd989e4
8 files changed, +572 insertions, -91 deletions
M Cargo.lock +6 -12
@@ -920,11 +920,11 @@
920 920 "shop-grid",
921 921 "shop-pty",
922 922 "shop-render",
923 + "shop-vt",
923 924 "shop-wayland",
924 925 "smithay-client-toolkit",
925 926 "tracing",
926 927 "tracing-subscriber",
927 - "vte",
928 928 "wayland-client",
929 929 "wgpu",
930 930 ]
@@ -933,8 +933,8 @@
933 933 name = "shop-grid"
934 934 version = "0.0.0"
935 935 dependencies = [
936 + "shop-vt",
936 937 "tracing",
937 - "vte",
938 938 ]
939 939
940 940 [[package]]
@@ -958,6 +958,10 @@
958 958 "wgpu",
959 959 ]
960 960
961 + [[package]]
962 + name = "shop-vt"
963 + version = "0.0.0"
964 +
961 965 [[package]]
962 966 name = "shop-wayland"
963 967 version = "0.0.0"
@@ -1182,16 +1186,6 @@
1182 1186 source = "registry+https://github.com/rust-lang/crates.io-index"
1183 1187 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
1184 1188
1185 - [[package]]
1186 - name = "vte"
1187 - version = "0.15.0"
1188 - source = "registry+https://github.com/rust-lang/crates.io-index"
1189 - checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
1190 - dependencies = [
1191 - "arrayvec",
1192 - "memchr",
1193 - ]
1194 -
1195 1189 [[package]]
1196 1190 name = "wayland-backend"
1197 1191 version = "0.3.16"
M Cargo.toml +1 -1
@@ -7,6 +7,7 @@
7 7 "crates/shop-render",
8 8 "crates/shop-pty",
9 9 "crates/shop-grid",
10 + "crates/shop-vt",
10 11 ]
11 12
12 13 [workspace.dependencies]
@@ -24,7 +25,6 @@
24 25 guillotiere = "0.7"
25 26 bytemuck = { version = "1", features = ["derive"] }
26 27 nix = { version = "0.31", features = ["term", "process", "fs", "ioctl", "signal"] }
27 - vte = "0.15"
28 28 base64 = "0.23"
29 29 image = { version = "0.25", default-features = false, features = ["png"] }
30 30
@@ -13,5 +13,5 @@
13 13 workspace = true
14 14
15 15 [dependencies]
16 - vte.workspace = true
16 + shop-vt = { path = "../shop-vt" }
17 17 tracing.workspace = true
@@ -20,13 +20,13 @@
20 20 shop-render = { path = "../shop-render" }
21 21 shop-pty = { path = "../shop-pty" }
22 22 shop-grid = { path = "../shop-grid" }
23 + shop-vt = { path = "../shop-vt" }
23 24 kitty-graphics = { path = "../kitty-graphics" }
24 25 image = { workspace = true }
25 26 smithay-client-toolkit.workspace = true
26 27 wayland-client.workspace = true
27 28 calloop.workspace = true
28 29 calloop-wayland-source.workspace = true
29 - vte.workspace = true
30 30 wgpu.workspace = true
31 31 pollster.workspace = true
32 32 anyhow.workspace = true
@@ -7,8 +7,8 @@
7 7 //! Implements [`vte::Perform`], so the binary can pipe PTY bytes through a
8 8 //! `vte::Parser` straight into the grid.
9 9
10 + use shop_vt::{Params, Perform};
10 11 use tracing::trace;
11 - use vte::{Params, Perform};
12 12
13 13 /// A terminal color.
14 14 ///
@@ -156,7 +156,7 @@
156 156 let images = ImageRenderer::new(&device, format);
157 157
158 158 let grid = Grid::new(cols_initial, rows_initial);
159 - let parser = vte::Parser::new();
159 + let parser = shop_vt::Parser::new();
160 160 let mut app = App {
161 161 chrome,
162 162 surface,
@@ -169,7 +169,6 @@
169 169 pty,
170 170 grid,
171 171 parser,
172 - apc_scanner: ApcScanner::default(),
173 172 kitty: kgp::Parser::new(),
174 173 image_placement: None,
175 174 next_anon_image_id: 1,
@@ -212,9 +211,14 @@
212 211 break;
213 212 }
214 213 Ok(n) => {
215 - let mut for_vte = Vec::with_capacity(n);
216 - let apc_bodies = app.apc_scanner.scan(&buf[..n], &mut for_vte);
217 - app.parser.advance(&mut app.grid, &for_vte);
214 + let mut apc_bodies: Vec<Vec<u8>> = Vec::new();
215 + {
216 + let mut sink = ParseSink {
217 + grid: &mut app.grid,
218 + apc: &mut apc_bodies,
219 + };
220 + app.parser.advance(&mut sink, &buf[..n]);
221 + }
218 222 for body in apc_bodies {
219 223 if let Some(cmd) = app.kitty.feed(&body) {
220 224 handle_kitty(&mut *app, cmd);
@@ -283,71 +287,45 @@
283 287 Ok(())
284 288 }
285 289
286 - /// Stateful byte-stream scanner that separates kitty-graphics APC bodies
287 - /// (`\e_G<body>\e\` or `\e_G<body>\a`) from the rest of the vt stream.
288 - #[derive(Debug, Default)]
289 - struct ApcScanner {
290 - state: ApcScanState,
291 - body: Vec<u8>,
290 + /// Composite Perform: forwards the mainline callbacks to `Grid` and pushes
291 + /// APC bodies onto a separate queue. shop-vt exposes an `apc_dispatch` that
292 + /// `vte` off crates.io hides, which is what makes this crate hookup possible
293 + /// without a byte-stream pre-scanner.
294 + struct ParseSink<'a> {
295 + grid: &'a mut Grid,
296 + apc: &'a mut Vec<Vec<u8>>,
292 297 }
293 298
294 - #[derive(Debug, Default, Clone, Copy, PartialEq)]
295 - enum ApcScanState {
296 - #[default]
297 - Normal,
298 - SawEsc,
299 - InApc,
300 - InApcSawEsc,
301 - }
302 -
303 - impl ApcScanner {
304 - /// Consume `bytes`, appending non-APC bytes to `for_vte` and returning
305 - /// completed APC bodies (without the `\e_G` intro or the terminator).
306 - fn scan(&mut self, bytes: &[u8], for_vte: &mut Vec<u8>) -> Vec<Vec<u8>> {
307 - let mut bodies = Vec::new();
308 - for &b in bytes {
309 - match self.state {
310 - ApcScanState::Normal => {
311 - if b == 0x1b {
312 - self.state = ApcScanState::SawEsc;
313 - } else {
314 - for_vte.push(b);
315 - }
316 - }
317 - ApcScanState::SawEsc => {
318 - if b == b'_' {
319 - self.state = ApcScanState::InApc;
320 - self.body.clear();
321 - } else {
322 - // Not APC — replay the ESC and the follow byte.
323 - for_vte.push(0x1b);
324 - for_vte.push(b);
325 - self.state = ApcScanState::Normal;
326 - }
327 - }
328 - ApcScanState::InApc => {
329 - if b == 0x1b {
330 - self.state = ApcScanState::InApcSawEsc;
331 - } else if b == 0x07 {
332 - bodies.push(std::mem::take(&mut self.body));
333 - self.state = ApcScanState::Normal;
334 - } else {
335 - self.body.push(b);
336 - }
337 - }
338 - ApcScanState::InApcSawEsc => {
339 - if b == b'\\' {
340 - bodies.push(std::mem::take(&mut self.body));
341 - self.state = ApcScanState::Normal;
342 - } else {
343 - self.body.push(0x1b);
344 - self.body.push(b);
345 - self.state = ApcScanState::InApc;
346 - }
347 - }
348 - }
349 - }
350 - bodies
299 + impl shop_vt::Perform for ParseSink<'_> {
300 + fn print(&mut self, c: char) {
301 + <Grid as shop_vt::Perform>::print(self.grid, c);
302 + }
303 + fn execute(&mut self, byte: u8) {
304 + <Grid as shop_vt::Perform>::execute(self.grid, byte);
305 + }
306 + fn csi_dispatch(
307 + &mut self,
308 + params: &shop_vt::Params,
309 + intermediates: &[u8],
310 + ignore: bool,
311 + action: char,
312 + ) {
313 + <Grid as shop_vt::Perform>::csi_dispatch(
314 + self.grid,
315 + params,
316 + intermediates,
317 + ignore,
318 + action,
319 + );
320 + }
321 + fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
322 + <Grid as shop_vt::Perform>::esc_dispatch(self.grid, intermediates, ignore, byte);
323 + }
324 + fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
325 + <Grid as shop_vt::Perform>::osc_dispatch(self.grid, params, bell_terminated);
326 + }
327 + fn apc_dispatch(&mut self, data: &[u8]) {
328 + self.apc.push(data.to_vec());
351 329 }
352 330 }
353 331
@@ -463,13 +441,7 @@
463 441 images: ImageRenderer,
464 442 pty: Pty,
465 443 grid: Grid,
466 - parser: vte::Parser,
467 - /// Kitty-graphics uses APC escapes (`\e_G…\e\`) which `vte` recognizes but
468 - /// doesn't dispatch. The scanner peels them off before the byte stream
469 - /// reaches vte, hands the bodies to the kitty parser, and forwards the
470 - /// rest untouched. Followup: replace with a proper shop-vt that dispatches
471 - /// APC natively.
472 - apc_scanner: ApcScanner,
444 + parser: shop_vt::Parser,
473 445 kitty: kgp::Parser,
474 446 /// Single-image MVP: track the most recently displayed image + where.
475 447 /// Multi-image / z-order / delete-selectors are follow-ups.
@@ -1,0 +1,15 @@
1 + [package]
2 + name = "shop-vt"
3 + version = "0.0.0"
4 + description = "VT500-series parser with APC dispatch for shop"
5 + edition.workspace = true
6 + rust-version.workspace = true
7 + authors.workspace = true
8 + repository.workspace = true
9 + license.workspace = true
10 + publish = false
11 +
12 + [lints]
13 + workspace = true
14 +
15 + [dependencies]
@@ -1,0 +1,619 @@
1 + //! VT500-series escape-sequence parser.
2 + //!
3 + //! Implements the [Paul Williams state machine](https://vt100.net/emu/dec_ansi_parser)
4 + //! and adds an `apc_dispatch` callback that the `vte` crate on crates.io
5 + //! declines to expose. That callback is what makes the kitty graphics
6 + //! protocol land — its APC bodies (`\e_G…\e\`) are unreachable through
7 + //! stock vte.
8 + //!
9 + //! Deliberately narrow scope:
10 + //! - Written from the spec (not vendored from vte).
11 + //! - Params live in `Vec<Vec<u16>>` for clarity; not the tightest packing but
12 + //! trivial to iterate and terminals do not push millions of CSIs per sec.
13 + //! - No SIMD UTF-8 fast path yet — that's a `simdutf8` swap when we care.
14 + //! - No sync-update (BSU/ESU) hooks yet — added when someone starts using
15 + //! them and paint tearing shows up.
16 + //!
17 + //! State transitions are those of the reference DEC parser plus 8-bit C1
18 + //! shortcuts (0x9B for CSI etc.) accepted only in Ground; 0x80-0xFF in
19 + //! Ground is otherwise treated as UTF-8 continuation. That matches every
20 + //! modern UTF-8 terminal.
21 +
22 + #![deny(unsafe_op_in_unsafe_fn)]
23 +
24 + /// Handler for the parser's dispatch events.
25 + pub trait Perform {
26 + /// A printable UTF-8 character was received.
27 + fn print(&mut self, c: char) {
28 + let _ = c;
29 + }
30 +
31 + /// A C0 (0x00-0x1F) or C1 (0x80-0x9F) control code was received.
32 + fn execute(&mut self, byte: u8) {
33 + let _ = byte;
34 + }
35 +
36 + /// A CSI sequence completed (`\e[…<final>`).
37 + fn csi_dispatch(
38 + &mut self,
39 + params: &Params,
40 + intermediates: &[u8],
41 + ignore: bool,
42 + action: char,
43 + ) {
44 + let _ = (params, intermediates, ignore, action);
45 + }
46 +
47 + /// An ESC sequence completed (`\e<intermediates><final>`).
48 + fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
49 + let _ = (intermediates, ignore, byte);
50 + }
51 +
52 + /// An OSC (`\e]…\e\`) or (`\e]…\a`) completed. `params` are the
53 + /// semicolon-separated fields of the body.
54 + fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
55 + let _ = (params, bell_terminated);
56 + }
57 +
58 + /// A DCS sequence started; call [`Perform::put`] for each subsequent byte
59 + /// and [`Perform::unhook`] on termination.
60 + fn hook(
61 + &mut self,
62 + params: &Params,
63 + intermediates: &[u8],
64 + ignore: bool,
65 + action: char,
66 + ) {
67 + let _ = (params, intermediates, ignore, action);
68 + }
69 +
70 + fn put(&mut self, byte: u8) {
71 + let _ = byte;
72 + }
73 +
74 + fn unhook(&mut self) {}
75 +
76 + /// An APC / SOS / PM string completed. `data` is the string body without
77 + /// the leading `\e_` (etc.) or the trailing terminator. This is the hook
78 + /// the kitty graphics protocol needs.
79 + fn apc_dispatch(&mut self, data: &[u8]) {
80 + let _ = data;
81 + }
82 + }
83 +
84 + /// Iterable parameter list for CSI / DCS. Each iteration yields one
85 + /// parameter's subparameters (colon-separated, e.g. `38:2::R:G:B` gives one
86 + /// entry `[38, 2, 0, R, G, B]`; `38;2;R;G;B` gives five entries `[38] [2]
87 + /// [R] [G] [B]`).
88 + #[derive(Debug, Default, Clone)]
89 + pub struct Params {
90 + inner: Vec<Vec<u16>>,
91 + }
92 +
93 + impl Params {
94 + pub fn iter(&self) -> impl Iterator<Item = &[u16]> {
95 + self.inner.iter().map(Vec::as_slice)
96 + }
97 +
98 + pub fn is_empty(&self) -> bool {
99 + self.inner.is_empty()
100 + }
101 +
102 + pub fn len(&self) -> usize {
103 + self.inner.len()
104 + }
105 +
106 + fn clear(&mut self) {
107 + self.inner.clear();
108 + }
109 +
110 + fn push_new(&mut self) {
111 + self.inner.push(Vec::with_capacity(1));
112 + }
113 +
114 + fn ensure_open(&mut self) {
115 + if self.inner.is_empty() {
116 + self.push_new();
117 + }
118 + }
119 +
120 + fn append_digit(&mut self, digit: u16) {
121 + self.ensure_open();
122 + let group = self.inner.last_mut().unwrap();
123 + if group.is_empty() {
124 + group.push(digit);
125 + } else {
126 + let last = group.last_mut().unwrap();
127 + *last = last.saturating_mul(10).saturating_add(digit);
128 + }
129 + }
130 +
131 + fn new_subparam(&mut self) {
132 + self.ensure_open();
133 + self.inner.last_mut().unwrap().push(0);
134 + }
135 +
136 + fn new_param(&mut self) {
137 + self.push_new();
138 + }
139 + }
140 +
141 + #[derive(Copy, Clone, Debug, PartialEq, Eq)]
142 + enum State {
143 + Ground,
144 + Escape,
145 + EscapeIntermediate,
146 + CsiEntry,
147 + CsiParam,
148 + CsiIntermediate,
149 + CsiIgnore,
150 + OscString,
151 + DcsEntry,
152 + DcsParam,
153 + DcsIntermediate,
154 + DcsIgnore,
155 + DcsPassthrough,
156 + ApcString,
157 + Utf8,
158 + }
159 +
160 + /// Byte-stream parser. State persists across `advance` calls so partial
161 + /// sequences and UTF-8 continuations survive chunked reads.
162 + #[derive(Debug)]
163 + pub struct Parser {
164 + state: State,
165 + prev_string_state: State,
166 + intermediates: [u8; 2],
167 + intermediates_idx: usize,
168 + ignoring: bool,
169 + params: Params,
170 + osc_buf: Vec<u8>,
171 + osc_params: Vec<(usize, usize)>,
172 + apc_buf: Vec<u8>,
173 + utf8_buf: [u8; 4],
174 + utf8_idx: usize,
175 + utf8_expected: usize,
176 + }
177 +
178 + impl Default for Parser {
179 + fn default() -> Self {
180 + Self::new()
181 + }
182 + }
183 +
184 + impl Parser {
185 + pub fn new() -> Self {
186 + Self {
187 + state: State::Ground,
188 + prev_string_state: State::Ground,
189 + intermediates: [0; 2],
190 + intermediates_idx: 0,
191 + ignoring: false,
192 + params: Params::default(),
193 + osc_buf: Vec::with_capacity(2048),
194 + osc_params: Vec::with_capacity(8),
195 + apc_buf: Vec::with_capacity(2048),
196 + utf8_buf: [0; 4],
197 + utf8_idx: 0,
198 + utf8_expected: 0,
199 + }
200 + }
201 +
202 + pub fn advance<P: Perform>(&mut self, perform: &mut P, bytes: &[u8]) {
203 + for &b in bytes {
204 + self.step(perform, b);
205 + }
206 + }
207 +
208 + fn step<P: Perform>(&mut self, perform: &mut P, byte: u8) {
209 + match self.state {
210 + State::Ground => self.ground(perform, byte),
211 + State::Utf8 => self.utf8(perform, byte),
212 + State::Escape => self.escape(perform, byte),
213 + State::EscapeIntermediate => self.escape_intermediate(perform, byte),
214 + State::CsiEntry => self.csi_entry(perform, byte),
215 + State::CsiParam => self.csi_param(perform, byte),
216 + State::CsiIntermediate => self.csi_intermediate(perform, byte),
217 + State::CsiIgnore => self.csi_ignore(perform, byte),
218 + State::OscString => self.osc_string(perform, byte),
219 + State::DcsEntry => self.dcs_entry(perform, byte),
220 + State::DcsParam => self.dcs_param(perform, byte),
221 + State::DcsIntermediate => self.dcs_intermediate(perform, byte),
222 + State::DcsIgnore => self.dcs_ignore(perform, byte),
223 + State::DcsPassthrough => self.dcs_passthrough(perform, byte),
224 + State::ApcString => self.apc_string(perform, byte),
225 + }
226 + }
227 +
228 + // ---- state handlers ------------------------------------------------
229 +
230 + fn ground<P: Perform>(&mut self, perform: &mut P, b: u8) {
231 + match b {
232 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
233 + 0x18 | 0x1A => perform.execute(b),
234 + 0x1B => {
235 + self.clear();
236 + self.state = State::Escape;
237 + }
238 + 0x20..=0x7F => perform.print(b as char),
239 + 0x80..=0xBF => {} // Stray continuation byte — ignore.
240 + 0xC2..=0xDF => self.begin_utf8(b, 2),
241 + 0xE0..=0xEF => self.begin_utf8(b, 3),
242 + 0xF0..=0xF4 => self.begin_utf8(b, 4),
243 + _ => {} // 0xC0/0xC1/0xF5-0xFF invalid.
244 + }
245 + }
246 +
247 + fn begin_utf8(&mut self, first: u8, expected: usize) {
248 + self.utf8_buf[0] = first;
249 + self.utf8_idx = 1;
250 + self.utf8_expected = expected;
251 + self.state = State::Utf8;
252 + }
253 +
254 + fn utf8<P: Perform>(&mut self, perform: &mut P, b: u8) {
255 + // ESC in the middle of a UTF-8 sequence resets.
256 + if b == 0x1B {
257 + self.utf8_idx = 0;
258 + self.clear();
259 + self.state = State::Escape;
260 + return;
261 + }
262 + // Non-continuation byte breaks the sequence; return to Ground and
263 + // re-process the byte.
264 + if !(0x80..=0xBF).contains(&b) {
265 + self.utf8_idx = 0;
266 + self.state = State::Ground;
267 + self.ground(perform, b);
268 + return;
269 + }
270 + self.utf8_buf[self.utf8_idx] = b;
271 + self.utf8_idx += 1;
272 + if self.utf8_idx == self.utf8_expected {
273 + if let Ok(s) = std::str::from_utf8(&self.utf8_buf[..self.utf8_idx])
274 + && let Some(c) = s.chars().next()
275 + {
276 + perform.print(c);
277 + }
278 + self.utf8_idx = 0;
279 + self.state = State::Ground;
280 + }
281 + }
282 +
283 + fn escape<P: Perform>(&mut self, perform: &mut P, b: u8) {
284 + match b {
285 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
286 + 0x20..=0x2F => {
287 + self.collect(b);
288 + self.state = State::EscapeIntermediate;
289 + }
290 + 0x30..=0x4F | 0x51..=0x57 | 0x59 | 0x5A | 0x5C | 0x60..=0x7E => {
291 + perform.esc_dispatch(&self.intermediates[..self.intermediates_idx], self.ignoring, b);
292 + self.state = State::Ground;
293 + }
294 + 0x50 => {
295 + self.clear();
296 + self.state = State::DcsEntry;
297 + }
298 + 0x58 | 0x5E => {
299 + self.apc_buf.clear();
300 + self.state = State::ApcString;
301 + }
302 + 0x5B => {
303 + self.clear();
304 + self.state = State::CsiEntry;
305 + }
306 + 0x5D => {
307 + self.osc_start();
308 + self.state = State::OscString;
309 + }
310 + 0x5F => {
311 + self.apc_buf.clear();
312 + self.state = State::ApcString;
313 + }
314 + 0x7F => {} // Ignore.
315 + 0x18 | 0x1A => {
316 + perform.execute(b);
317 + self.state = State::Ground;
318 + }
319 + 0x1B => self.clear(),
320 + _ => {}
321 + }
322 + }
323 +
324 + fn escape_intermediate<P: Perform>(&mut self, perform: &mut P, b: u8) {
325 + match b {
326 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
327 + 0x20..=0x2F => self.collect(b),
328 + 0x30..=0x7E => {
329 + perform.esc_dispatch(&self.intermediates[..self.intermediates_idx], self.ignoring, b);
330 + self.state = State::Ground;
331 + }
332 + 0x7F => {}
333 + _ => {}
334 + }
335 + }
336 +
337 + fn csi_entry<P: Perform>(&mut self, perform: &mut P, b: u8) {
338 + match b {
339 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
340 + 0x20..=0x2F => {
341 + self.collect(b);
342 + self.state = State::CsiIntermediate;
343 + }
344 + 0x30..=0x39 => {
345 + self.params.append_digit((b - b'0') as u16);
346 + self.state = State::CsiParam;
347 + }
348 + 0x3A => {
349 + self.params.new_subparam();
350 + self.state = State::CsiParam;
351 + }
352 + 0x3B => {
353 + self.params.new_param();
354 + self.state = State::CsiParam;
355 + }
356 + 0x3C..=0x3F => {
357 + self.collect(b);
358 + self.state = State::CsiParam;
359 + }
360 + 0x40..=0x7E => {
361 + perform.csi_dispatch(
362 + &self.params,
363 + &self.intermediates[..self.intermediates_idx],
364 + self.ignoring,
365 + b as char,
366 + );
367 + self.state = State::Ground;
368 + }
369 + 0x7F => {}
370 + _ => {}
371 + }
372 + }
373 +
374 + fn csi_param<P: Perform>(&mut self, perform: &mut P, b: u8) {
375 + match b {
376 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
377 + 0x20..=0x2F => {
378 + self.collect(b);
379 + self.state = State::CsiIntermediate;
380 + }
381 + 0x30..=0x39 => self.params.append_digit((b - b'0') as u16),
382 + 0x3A => self.params.new_subparam(),
383 + 0x3B => self.params.new_param(),
384 + 0x3C..=0x3F => self.state = State::CsiIgnore,
385 + 0x40..=0x7E => {
386 + perform.csi_dispatch(
387 + &self.params,
388 + &self.intermediates[..self.intermediates_idx],
389 + self.ignoring,
390 + b as char,
391 + );
392 + self.state = State::Ground;
393 + }
394 + 0x7F => {}
395 + _ => {}
396 + }
397 + }
398 +
399 + fn csi_intermediate<P: Perform>(&mut self, perform: &mut P, b: u8) {
400 + match b {
401 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
402 + 0x20..=0x2F => self.collect(b),
403 + 0x30..=0x3F => self.state = State::CsiIgnore,
404 + 0x40..=0x7E => {
405 + perform.csi_dispatch(
406 + &self.params,
407 + &self.intermediates[..self.intermediates_idx],
408 + self.ignoring,
409 + b as char,
410 + );
411 + self.state = State::Ground;
412 + }
413 + 0x7F => {}
414 + _ => {}
415 + }
416 + }
417 +
418 + fn csi_ignore<P: Perform>(&mut self, perform: &mut P, b: u8) {
419 + match b {
420 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => perform.execute(b),
421 + 0x40..=0x7E => self.state = State::Ground,
422 + _ => {}
423 + }
424 + }
425 +
426 + fn osc_string<P: Perform>(&mut self, perform: &mut P, b: u8) {
427 + match b {
428 + 0x07 => {
429 + self.osc_end();
430 + self.dispatch_osc(perform, true);
431 + self.state = State::Ground;
432 + }
433 + 0x1B => {
434 + // Possible ST — end the OSC now, transition to Escape so a
435 + // following `\` gets consumed as a no-op esc_dispatch.
436 + self.osc_end();
437 + self.dispatch_osc(perform, false);
438 + self.prev_string_state = State::OscString;
439 + self.state = State::Escape;
440 + self.clear();
441 + }
442 + 0x3B => {
443 + // Parameter separator: close current field, open next.
444 + self.osc_params.push((self.osc_last_start(), self.osc_buf.len()));
445 + self.osc_mark_new();
446 + }
447 + _ => self.osc_buf.push(b),
448 + }
449 + }
450 +
451 + fn osc_start(&mut self) {
452 + self.osc_buf.clear();
453 + self.osc_params.clear();
454 + // Open the first parameter.
455 + self.osc_mark_new();
456 + }
457 +
458 + fn osc_mark_new(&mut self) {
459 + let start = self.osc_buf.len();
460 + // Sentinel start pair for the current (still-being-written) field.
461 + self.osc_params.push((start, start));
462 + }
463 +
464 + fn osc_last_start(&self) -> usize {
465 + self.osc_params.last().map(|p| p.0).unwrap_or(0)
466 + }
467 +
468 + fn osc_end(&mut self) {
469 + if let Some(last) = self.osc_params.last_mut() {
470 + last.1 = self.osc_buf.len();
471 + }
472 + }
473 +
474 + fn dispatch_osc<P: Perform>(&self, perform: &mut P, bell_terminated: bool) {
475 + let slices: Vec<&[u8]> = self
476 + .osc_params
477 + .iter()
478 + .map(|&(s, e)| &self.osc_buf[s..e])
479 + .collect();
480 + perform.osc_dispatch(&slices, bell_terminated);
481 + }
482 +
483 + fn dcs_entry<P: Perform>(&mut self, perform: &mut P, b: u8) {
484 + match b {
485 + 0x00..=0x17 | 0x19 | 0x1C..=0x1F => {}
486 + 0x20..=0x2F => {
487 + self.collect(b);
488 + self.state = State::DcsIntermediate;
489 + }
490 + 0x30..=0x39 => {
491 + self.params.append_digit((b - b'0') as u16);
492 + self.state = State::DcsParam;
493 + }
494 + 0x3A => self.state = State::DcsIgnore,
495 + 0x3B => {
496 + self.params.new_param();
497 + self.state = State::DcsParam;
498 + }
499 + 0x3C..=0x3F => {
500 + self.collect(b);
Lines truncated