Skip to main content

max / shop

28.7 KB · 720 lines History Blame Raw
1 //! The parser callbacks: printing, C0 execution, CSI, ESC and OSC dispatch.
2 //!
3 //! This is the only place a wire byte turns into a call on the grid. The work
4 //! each arm does lives in the subsystem modules; what is here is the mapping
5 //! from the escape sequence to it, plus the replies the query arms send back.
6
7 use crate::{CursorShape, Grid, MouseEncoding, MouseTracking};
8 use shop_vt::{Params, Perform};
9 use tracing::trace;
10
11 /// One channel as OSC 10/11 want it: four hex digits, the 8-bit value
12 /// doubled. `0x25` becomes `2525`, which is the 16-bit reading of the same
13 /// intensity and what every terminal sends.
14 fn osc_channel(v: u8) -> String {
15 format!("{v:02x}{v:02x}")
16 }
17
18 fn param1(params: &Params, default: u16) -> u16 {
19 let first = params
20 .iter()
21 .next()
22 .and_then(|p| p.first().copied())
23 .unwrap_or(0);
24 if first == 0 { default } else { first }
25 }
26
27 fn param2(params: &Params, defaults: (u16, u16)) -> (u16, u16) {
28 let mut it = params.iter();
29 let a = it.next().and_then(|p| p.first().copied()).unwrap_or(0);
30 let b = it.next().and_then(|p| p.first().copied()).unwrap_or(0);
31 let a = if a == 0 { defaults.0 } else { a };
32 let b = if b == 0 { defaults.1 } else { b };
33 (a, b)
34 }
35
36 impl Perform for Grid {
37 fn print(&mut self, c: char) {
38 self.place_char(c);
39 }
40
41 fn execute(&mut self, byte: u8) {
42 // Any C0 that isn't NUL/BEL moves the cursor or scrolls; invalidate
43 // the fast-path row cache up front so we don't have to sprinkle it
44 // across every arm.
45 self.invalidate_cur_row();
46 match byte {
47 0x08 => {
48 // BS
49 if self.cursor.col > 0 {
50 self.cursor.col -= 1;
51 }
52 self.cursor.wrap_next = false;
53 }
54 0x09 => {
55 // HT — advance to next multiple of 8, clamped.
56 let next = ((self.cursor.col / 8) + 1) * 8;
57 self.cursor.col = next.min(self.cols - 1);
58 self.cursor.wrap_next = false;
59 }
60 0x0A..=0x0C => {
61 // LF / VT / FF
62 self.newline();
63 }
64 0x0D => {
65 // CR
66 self.cursor.col = 0;
67 self.cursor.wrap_next = false;
68 }
69 0x07 => {} // BEL — ignore for now
70 other => trace!("unhandled C0 {other:#x}"),
71 }
72 }
73
74 fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {
75 // Nearly every CSI mutates cursor, scroll region, or screen; a couple
76 // (cursor visibility, SGR) don't but the invalidation is a single
77 // store — cheaper than branching on which arm we're taking.
78 self.invalidate_cur_row();
79 let private = intermediates.first().copied() == Some(b'?');
80 match (action, private) {
81 ('H' | 'f', false) => {
82 let (row, col) = param2(params, (1, 1));
83 self.set_cursor(row, col);
84 }
85 ('A', false) => {
86 let n = param1(params, 1) as i32;
87 self.move_by(-n, 0);
88 }
89 ('B', false) => {
90 let n = param1(params, 1) as i32;
91 self.move_by(n, 0);
92 }
93 ('C', false) => {
94 let n = param1(params, 1) as i32;
95 self.move_by(0, n);
96 }
97 ('D', false) => {
98 let n = param1(params, 1) as i32;
99 self.move_by(0, -n);
100 }
101 ('E', false) => {
102 let n = param1(params, 1) as i32;
103 self.move_by(n, 0);
104 self.cursor.col = 0;
105 }
106 ('F', false) => {
107 let n = param1(params, 1) as i32;
108 self.move_by(-n, 0);
109 self.cursor.col = 0;
110 }
111 ('G', false) => {
112 let col = param1(params, 1);
113 self.cursor.col = col.saturating_sub(1).min(self.cols - 1);
114 self.cursor.wrap_next = false;
115 }
116 ('d', false) => {
117 let row = param1(params, 1);
118 self.cursor.row = row.saturating_sub(1).min(self.rows - 1);
119 self.cursor.wrap_next = false;
120 }
121 ('J', false) => {
122 self.erase_display(param1(params, 0));
123 }
124 ('K', false) => {
125 self.erase_line(param1(params, 0));
126 }
127 ('L', false) => self.insert_lines(param1(params, 1)),
128 ('M', false) => self.delete_lines(param1(params, 1)),
129 ('@', false) => self.insert_chars(param1(params, 1)),
130 ('P', false) => self.delete_chars(param1(params, 1)),
131 ('X', false) => self.erase_chars(param1(params, 1)),
132 // DECSC/DECRC in their CSI spelling, the same pair as `ESC 7` and
133 // `ESC 8`. `CSI s` is DECSLRM under DECLRMM, which shop does not
134 // implement and no program can have turned on, so there is nothing
135 // for it to be mistaken for here.
136 ('s', false) if intermediates.is_empty() => self.save_cursor(),
137 ('u', false) if intermediates.is_empty() => self.restore_cursor(),
138 // DA1, "what are you". Guarded on empty intermediates because
139 // `CSI > c` is DA2, a different question, and the private-flag
140 // check above only screens for `?`.
141 //
142 // 62 is VT220, which is about what the VT side implements; 22 is
143 // ANSI colour. Sixel is 4 and is deliberately absent: shop has no
144 // sixel, and claiming it means a client picks sixel over kitty
145 // graphics and draws nothing.
146 ('c', false) if intermediates.is_empty() => self.reply(b"\x1b[?62;22c"),
147 // DSR. Two questions share the final byte: 5 is "are you well"
148 // and 6 is "where is the cursor" (CPR).
149 //
150 // CPR is not an optional courtesy. A line editor that draws a
151 // prompt has to know which row it starts on, and reedline asks
152 // this before it draws anything at all: nushell under a terminal
153 // that never answers sits on a blank screen with a live cursor,
154 // taking no input, because the shell is still waiting for us.
155 // That is what shop did in Alloy, where nu is the login shell,
156 // while bash — which asks nothing — hid it in daily use.
157 //
158 // Rows and columns are 1-based on the wire and 0-based here.
159 // There is no origin mode to subtract: the cursor is absolute
160 // even inside a scrolling region.
161 ('n', false) if intermediates.is_empty() => match param1(params, 0) {
162 5 => self.reply(b"\x1b[0n"),
163 6 => {
164 let (row, col) = (self.cursor.row + 1, self.cursor.col + 1);
165 self.reply(format!("\x1b[{row};{col}R").as_bytes());
166 }
167 other => trace!("unhandled DSR {other}"),
168 },
169 // DECXCPR, the private form of the same question. The reply keeps
170 // the `?` and carries a third parameter, the page, which is always
171 // 1 here because shop has no page memory.
172 ('n', true) if param1(params, 0) == 6 => {
173 let (row, col) = (self.cursor.row + 1, self.cursor.col + 1);
174 self.reply(format!("\x1b[?{row};{col};1R").as_bytes());
175 }
176 // XTVERSION. `DCS > | name(version) ST`, the form kitty and foot
177 // both answer in, which is what makes it parseable by the clients
178 // that ask.
179 ('q', false) if intermediates.first().copied() == Some(b'>') => {
180 let reply = format!(
181 "\x1bP>|{}({})\x1b\\",
182 self.identity.name, self.identity.version
183 );
184 self.reply(reply.as_bytes());
185 }
186 // XTWINOPS reports. Only the three read-only ones: the rest of
187 // this sequence moves and resizes windows, which is the
188 // compositor's business and not something a program on a PTY gets
189 // to do here.
190 //
191 // Sizes are physical pixels. Programs that place images need cell
192 // size in particular, and the ioctl that also carries it
193 // (TIOCSWINSZ) is not what all of them read.
194 ('t', false) if intermediates.is_empty() => {
195 let (cw, ch) = self.identity.cell_px;
196 match param1(params, 0) {
197 // Text area, in pixels.
198 14 => {
199 let (w, h) = (self.cols * cw, self.rows * ch);
200 self.reply(format!("\x1b[4;{h};{w}t").as_bytes());
201 }
202 // One cell, in pixels. Height first, as the report orders it.
203 16 => self.reply(format!("\x1b[6;{ch};{cw}t").as_bytes()),
204 // Text area, in cells.
205 18 => {
206 let (rows, cols) = (self.rows, self.cols);
207 self.reply(format!("\x1b[8;{rows};{cols}t").as_bytes());
208 }
209 other => trace!("unhandled XTWINOPS {other}"),
210 }
211 }
212 ('S', false) => {
213 self.scroll_up_in_region(param1(params, 1));
214 }
215 ('T', false) => {
216 self.scroll_down_in_region(param1(params, 1));
217 }
218 ('r', false) => {
219 // DECSTBM: transition-safe. If we're currently in a partial
220 // region with a rotated region_origin, unroll it back to
221 // logical order first. If the new region is partial, unroll
222 // the fullscreen ring so outside-region rows sit at their
223 // logical physical positions. Unroll preserves logical
224 // contents so the renderer's per-row cache stays valid — no
225 // need to mark rows dirty here.
226 let (top, bot) = param2(params, (1, self.rows));
227 let new_top = top.saturating_sub(1).min(self.rows - 1);
228 let new_bottom = bot.saturating_sub(1).min(self.rows - 1);
229 // A region needs at least two rows, and its top has to be
230 // above its bottom. DEC and xterm both drop the whole request
231 // when it does not, cursor move included, and so does this:
232 // `region_size` is computed as `bottom - top + 1` in the scroll
233 // paths, so an inverted pair underflows there — a panic in
234 // debug and a region of ~65,000 rows in release, which is a
235 // row index off the end of the ring feeding the unchecked
236 // store in `place_char`. Found by the soak oracle on
237 // `ESC [ 20 ; 3 r`, 2026-08-29.
238 if new_top >= new_bottom {
239 return;
240 }
241 self.unroll_region();
242 self.scroll_top = new_top;
243 self.scroll_bottom = new_bottom;
244 if self.is_partial_region() {
245 self.unroll_active_ring();
246 }
247 self.cursor.row = 0;
248 self.cursor.col = 0;
249 }
250 ('m', false) => self.apply_sgr(params),
251 ('q', false) if intermediates.first().copied() == Some(b' ') => {
252 let shape = param1(params, 1);
253 self.cursor_shape = match shape {
254 0..=2 => CursorShape::Block,
255 3 | 4 => CursorShape::Underline,
256 5 | 6 => CursorShape::Bar,
257 _ => self.cursor_shape,
258 };
259 }
260 ('h' | 'l', true) => {
261 for p in params.iter() {
262 if let Some(&code) = p.first() {
263 match code {
264 25 => self.cursor.visible = action == 'h',
265 1049 | 47 | 1047 => self.swap_alt(action == 'h'),
266 // DECSET/DECRST 2026: synchronized update. `h`
267 // begins a batch (renderer should hold frames
268 // until `l` or the caller's timeout); `l` ends
269 // it. Grid just tracks the state — the binary
270 // is what actually defers the redraw.
271 1 => self.cursor_keys_application = action == 'h',
272 1007 => self.alternate_scroll = action == 'h',
273 2004 => self.bracketed_paste = action == 'h',
274 2026 => self.sync_update = action == 'h',
275 // Mouse tracking. Clearing any level turns the
276 // pointer back over to the user rather than
277 // dropping to the next level down: a program
278 // clearing 1002 is done with the mouse, not asking
279 // for 1000, and it clears only what it set.
280 9 | 1000 | 1002 | 1003 => {
281 let level = match code {
282 9 => MouseTracking::Press,
283 1000 => MouseTracking::Click,
284 1002 => MouseTracking::Drag,
285 _ => MouseTracking::Motion,
286 };
287 if action == 'h' {
288 self.mouse_tracking = level;
289 } else if self.mouse_tracking == level {
290 self.mouse_tracking = MouseTracking::Off;
291 }
292 }
293 1006 => {
294 self.mouse_encoding = if action == 'h' {
295 MouseEncoding::Sgr
296 } else {
297 MouseEncoding::X10
298 }
299 }
300 // 1005 (utf-8 coordinates) and 1015 (urxvt) are
301 // the two other answers to X10's coordinate
302 // ceiling, and both are worse than 1006: 1005
303 // makes a report ambiguous with UTF-8 text, and
304 // 1015 is ambiguous with a DSR reply. Declined
305 // rather than unimplemented, and a program that
306 // asks keeps whatever it had — every one of them
307 // asks for 1006 first.
308 1005 | 1015 => trace!("declined mouse encoding {code}"),
309 _ => {}
310 }
311 }
312 }
313 }
314 _ => trace!("unhandled CSI {action} private={private}"),
315 }
316 }
317
318 fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, byte: u8) {
319 self.invalidate_cur_row();
320 match byte {
321 b'7' => self.save_cursor(),
322 b'8' => self.restore_cursor(),
323 // DECKPAM / DECKPNM. Application keypad is an ESC pair rather
324 // than a DECSET, for no reason beyond how DEC numbered things.
325 b'=' => self.keypad_application = true,
326 b'>' => self.keypad_application = false,
327 b'M' => {
328 // RI — reverse index
329 if self.cursor.row == self.scroll_top {
330 self.scroll_down_in_region(1);
331 } else {
332 self.cursor.row = self.cursor.row.saturating_sub(1);
333 }
334 }
335 _ => trace!("unhandled ESC {}", byte as char),
336 }
337 }
338
339 fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
340 let Some(id) = params.first().and_then(|p| std::str::from_utf8(p).ok()) else {
341 return;
342 };
343 match id {
344 // OSC 0 = icon + title, OSC 2 = title only. OSC 1 = icon-only,
345 // treat as no-op (Wayland has no separate icon-name concept).
346 "0" | "2" => {
347 if let Some(payload) = params.get(1)
348 && let Ok(s) = std::str::from_utf8(payload)
349 {
350 self.pending_title = Some(s.to_string());
351 }
352 }
353 // OSC 10 and 11, default foreground and background. Only the `?`
354 // query form: setting them is a separate feature, and answering a
355 // set request would be worse than ignoring it.
356 //
357 // Programs ask in order to tell light from dark, so this decides
358 // whether anything that adapts to the terminal's polarity adapts
359 // the right way. shop's theme knows the answer; nothing else does.
360 "10" | "11" if params.get(1) == Some(&b"?".as_slice()) => {
361 let c = if id == "10" {
362 self.identity.fg
363 } else {
364 self.identity.bg
365 };
366 let colour = format!(
367 "rgb:{}/{}/{}",
368 osc_channel(c[0]),
369 osc_channel(c[1]),
370 osc_channel(c[2])
371 );
372 // Terminated the way the question was. A client that asked
373 // with BEL may well be parsing for one.
374 let end: &str = if bell_terminated { "\x07" } else { "\x1b\\" };
375 self.reply(format!("\x1b]{id};{colour}{end}").as_bytes());
376 }
377 _ => {}
378 }
379 }
380 }
381
382 #[cfg(test)]
383 mod tests {
384 use crate::testutil::{assert_cursor, feed, identified, reply_to, row_str};
385 use crate::*;
386
387 // ---- cursor shape --------------------------------------------------
388
389 #[test]
390 fn decscusr_sets_shape() {
391 let mut g = Grid::new(10, 2);
392 assert_eq!(g.cursor_shape(), CursorShape::Block);
393 feed(&mut g, b"\x1b[3 q");
394 assert_eq!(g.cursor_shape(), CursorShape::Underline);
395 feed(&mut g, b"\x1b[6 q");
396 assert_eq!(g.cursor_shape(), CursorShape::Bar);
397 feed(&mut g, b"\x1b[1 q");
398 assert_eq!(g.cursor_shape(), CursorShape::Block);
399 }
400
401 // ---- synchronized update (DECSET 2026) -----------------------------
402
403 #[test]
404 fn sync_update_toggles_on_2026() {
405 let mut g = Grid::new(10, 1);
406 assert!(!g.sync_update());
407 feed(&mut g, b"\x1b[?2026h");
408 assert!(g.sync_update());
409 feed(&mut g, b"\x1b[?2026l");
410 assert!(!g.sync_update());
411 }
412
413 // ---- alt screen ----------------------------------------------------
414
415 #[test]
416 fn alt_screen_swaps_and_restores() {
417 let mut g = Grid::new(6, 2);
418 feed(&mut g, b"MAIN");
419 assert_eq!(row_str(&g, 0), "MAIN");
420 feed(&mut g, b"\x1b[?1049h"); // enter alt
421 // Alt starts blank.
422 assert_eq!(row_str(&g, 0), "");
423 feed(&mut g, b"ALT");
424 assert_eq!(row_str(&g, 0), "ALT");
425 feed(&mut g, b"\x1b[?1049l"); // exit
426 assert_eq!(row_str(&g, 0), "MAIN");
427 }
428
429 // ---- OSC title -----------------------------------------------------
430
431 #[test]
432 fn osc_title_sets_pending() {
433 let mut g = Grid::new(10, 2);
434 feed(&mut g, b"\x1b]0;hello\x07");
435 assert_eq!(g.take_pending_title().as_deref(), Some("hello"));
436 // Draining clears it.
437 assert!(g.take_pending_title().is_none());
438 }
439
440 #[test]
441 fn osc_two_also_sets_title() {
442 let mut g = Grid::new(10, 2);
443 feed(&mut g, b"\x1b]2;from OSC 2\x07");
444 assert_eq!(g.take_pending_title().as_deref(), Some("from OSC 2"));
445 }
446
447 // ---- device attributes ---------------------------------------------
448
449 #[test]
450 fn da1_is_answered() {
451 let mut g = Grid::new(10, 3);
452 assert!(g.take_pending_replies().is_empty(), "nothing owed yet");
453 feed(&mut g, b"\x1b[c");
454 assert_eq!(g.take_pending_replies(), b"\x1b[?62;22c".to_vec());
455 }
456
457 #[test]
458 fn da1_does_not_claim_sixel() {
459 // Attribute 4 is sixel. Claiming it makes a client prefer sixel over
460 // kitty graphics, and shop would then draw nothing at all.
461 let mut g = Grid::new(10, 3);
462 feed(&mut g, b"\x1b[c");
463 let reply = String::from_utf8(g.take_pending_replies()).unwrap();
464 let attrs: Vec<&str> = reply
465 .trim_start_matches("\x1b[?")
466 .trim_end_matches('c')
467 .split(';')
468 .collect();
469 assert!(!attrs.contains(&"4"), "claimed sixel in {reply:?}");
470 }
471
472 #[test]
473 fn da1_with_an_explicit_zero_is_the_same_question() {
474 let mut g = Grid::new(10, 3);
475 feed(&mut g, b"\x1b[0c");
476 assert_eq!(g.take_pending_replies(), b"\x1b[?62;22c".to_vec());
477 }
478
479 #[test]
480 fn da2_is_not_answered_with_da1() {
481 // `CSI > c` is a different question. The private-flag check only
482 // screens for `?`, so without the intermediates guard this arm would
483 // answer it, and answer it wrongly.
484 let mut g = Grid::new(10, 3);
485 feed(&mut g, b"\x1b[>c");
486 assert!(g.take_pending_replies().is_empty());
487 }
488
489 #[test]
490 fn replies_are_drained_not_repeated() {
491 let mut g = Grid::new(10, 3);
492 feed(&mut g, b"\x1b[c");
493 assert!(!g.take_pending_replies().is_empty());
494 assert!(g.take_pending_replies().is_empty(), "drained once only");
495 }
496
497 #[test]
498 fn two_queries_in_one_parse_both_get_answers() {
499 let mut g = Grid::new(10, 3);
500 feed(&mut g, b"\x1b[c\x1b[c");
501 assert_eq!(
502 g.take_pending_replies(),
503 b"\x1b[?62;22c\x1b[?62;22c".to_vec()
504 );
505 }
506
507 #[test]
508 fn a_query_does_not_disturb_the_screen() {
509 let mut g = Grid::new(10, 3);
510 feed(&mut g, b"hi\x1b[c");
511 let _ = g.take_pending_replies();
512 assert_eq!(row_str(&g, 0), "hi");
513 assert_cursor(&g, 0, 2);
514 }
515
516 // ---- identity queries ----------------------------------------------
517
518 #[test]
519 fn cpr_answers_where_the_cursor_is() {
520 // The reply a line editor is blocked on. 1-based, row first.
521 let mut g = Grid::new(20, 5);
522 feed(&mut g, b"\x1b[3;7H");
523 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[3;7R");
524 }
525
526 #[test]
527 fn cpr_from_the_home_position_is_one_one() {
528 // The startup case, and the one an off-by-one would hide in: a fresh
529 // grid is at 0,0 internally and 1,1 on the wire.
530 let mut g = Grid::new(20, 5);
531 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[1;1R");
532 }
533
534 #[test]
535 fn cpr_reports_the_absolute_row_inside_a_scrolling_region() {
536 // No origin mode here, so a region does not renumber anything.
537 let mut g = Grid::new(20, 10);
538 feed(&mut g, b"\x1b[3;8r\x1b[5;2H");
539 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[5;2R");
540 }
541
542 #[test]
543 fn dsr_five_reports_good_health() {
544 let mut g = Grid::new(20, 5);
545 assert_eq!(reply_to(&mut g, b"\x1b[5n"), "\x1b[0n");
546 }
547
548 #[test]
549 fn decxcpr_keeps_the_private_marker_and_names_the_page() {
550 let mut g = Grid::new(20, 5);
551 feed(&mut g, b"\x1b[2;4H");
552 assert_eq!(reply_to(&mut g, b"\x1b[?6n"), "\x1b[?2;4;1R");
553 }
554
555 #[test]
556 fn an_unknown_dsr_is_answered_with_silence() {
557 // Answering a question we did not understand is worse than not
558 // answering: the reply lands in the program's input as text.
559 let mut g = Grid::new(20, 5);
560 assert_eq!(reply_to(&mut g, b"\x1b[99n"), "");
561 }
562
563 #[test]
564 fn xtversion_names_the_terminal() {
565 let mut g = identified();
566 assert_eq!(reply_to(&mut g, b"\x1b[>q"), "\x1bP>|shop(1.2.3)\x1b\\");
567 }
568
569 #[test]
570 fn cell_size_is_reported_height_first() {
571 // `CSI 6 ; height ; width t`. Getting the order wrong puts every
572 // image preview at the wrong aspect, which is the sort of bug that
573 // reads as a rendering problem rather than a reply problem.
574 let mut g = identified();
575 assert_eq!(reply_to(&mut g, b"\x1b[16t"), "\x1b[6;20;9t");
576 }
577
578 #[test]
579 fn text_area_is_reported_in_both_units() {
580 let mut g = identified();
581 // 80x24 cells of 9x20 px.
582 assert_eq!(reply_to(&mut g, b"\x1b[14t"), "\x1b[4;480;720t");
583 assert_eq!(reply_to(&mut g, b"\x1b[18t"), "\x1b[8;24;80t");
584 }
585
586 #[test]
587 fn cell_size_follows_the_identity() {
588 let mut g = identified();
589 g.set_identity(Identity {
590 cell_px: (18, 40),
591 ..Identity::default()
592 });
593 assert_eq!(reply_to(&mut g, b"\x1b[16t"), "\x1b[6;40;18t");
594 }
595
596 #[test]
597 fn window_manipulation_is_not_obeyed() {
598 // The same sequence resizes and moves windows. Those are the
599 // compositor's, and a program on the PTY does not get to ask.
600 let mut g = identified();
601 assert_eq!(reply_to(&mut g, b"\x1b[3;0;0t"), "", "move window");
602 assert_eq!(reply_to(&mut g, b"\x1b[8;50;100t"), "", "resize window");
603 assert_eq!(g.cols(), 80);
604 assert_eq!(g.rows(), 24);
605 }
606
607 #[test]
608 fn the_default_colours_are_answered_as_sixteen_bit() {
609 let mut g = identified();
610 assert_eq!(
611 reply_to(&mut g, b"\x1b]11;?\x1b\\"),
612 "\x1b]11;rgb:2525/2323/1f1f\x1b\\"
613 );
614 assert_eq!(
615 reply_to(&mut g, b"\x1b]10;?\x1b\\"),
616 "\x1b]10;rgb:e6e6/dede/d3d3\x1b\\"
617 );
618 }
619
620 #[test]
621 fn a_colour_query_is_terminated_the_way_it_was_asked() {
622 let mut g = identified();
623 let bel = reply_to(&mut g, b"\x1b]11;?\x07");
624 assert!(bel.ends_with('\x07'), "got {bel:?}");
625 let st = reply_to(&mut g, b"\x1b]11;?\x1b\\");
626 assert!(st.ends_with("\x1b\\"), "got {st:?}");
627 }
628
629 #[test]
630 fn setting_a_colour_is_not_mistaken_for_asking() {
631 // OSC 11 with a value is a set request. shop does not implement it,
632 // and answering it with the current colour would be a lie about
633 // having done something.
634 let mut g = identified();
635 assert_eq!(reply_to(&mut g, b"\x1b]11;#ff0000\x1b\\"), "");
636 }
637
638 #[test]
639 fn identity_queries_leave_the_screen_alone() {
640 let mut g = identified();
641 feed(&mut g, b"hi");
642 let _ = reply_to(&mut g, b"\x1b[>q\x1b[16t\x1b]11;?\x1b\\");
643 assert_eq!(row_str(&g, 0), "hi");
644 assert_cursor(&g, 0, 2);
645 }
646
647 #[test]
648 fn decckm_toggles_cursor_key_mode() {
649 let mut g = Grid::new(10, 3);
650 assert!(!g.cursor_keys_application());
651 feed(&mut g, b"\x1b[?1h");
652 assert!(g.cursor_keys_application());
653 feed(&mut g, b"\x1b[?1l");
654 assert!(!g.cursor_keys_application());
655 }
656
657 #[test]
658 fn deckpam_and_deckpnm_toggle_the_keypad() {
659 // An ESC pair rather than a DECSET, unlike every other mode here.
660 let mut g = Grid::new(10, 3);
661 assert!(!g.keypad_application());
662 feed(&mut g, b"\x1b=");
663 assert!(g.keypad_application());
664 feed(&mut g, b"\x1b>");
665 assert!(!g.keypad_application());
666 }
667
668 #[test]
669 fn bracketed_paste_toggles_on_2004() {
670 let mut g = Grid::new(10, 3);
671 assert!(!g.bracketed_paste(), "off until a program asks");
672 feed(&mut g, b"\x1b[?2004h");
673 assert!(g.bracketed_paste());
674 feed(&mut g, b"\x1b[?2004l");
675 assert!(!g.bracketed_paste());
676 }
677
678 #[test]
679 fn bracketed_paste_survives_an_alt_screen_round_trip() {
680 // vim sets it, and leaving the alt screen is not the shell revoking
681 // it — the shell set its own before vim ever started.
682 let mut g = Grid::new(10, 3);
683 feed(&mut g, b"\x1b[?2004h");
684 feed(&mut g, b"\x1b[?1049h");
685 feed(&mut g, b"\x1b[?1049l");
686 assert!(g.bracketed_paste());
687 }
688
689 #[test]
690 fn alternate_scroll_toggles_on_1007() {
691 let mut g = Grid::new(10, 3);
692 assert!(g.alternate_scroll(), "on until a program says otherwise");
693 feed(&mut g, b"\x1b[?1007l");
694 assert!(!g.alternate_scroll());
695 feed(&mut g, b"\x1b[?1007h");
696 assert!(g.alternate_scroll());
697 }
698
699 #[test]
700 fn alternate_scroll_survives_an_alt_screen_round_trip() {
701 // The mode is the user's answer to what the wheel means, not the alt
702 // screen's, so entering and leaving one does not restore the default.
703 let mut g = Grid::new(10, 3);
704 feed(&mut g, b"\x1b[?1007l");
705 feed(&mut g, b"\x1b[?1049h");
706 feed(&mut g, b"\x1b[?1049l");
707 assert!(!g.alternate_scroll());
708 }
709
710 #[test]
711 fn on_alt_follows_1049() {
712 let mut g = Grid::new(10, 3);
713 assert!(!g.on_alt());
714 feed(&mut g, b"\x1b[?1049h");
715 assert!(g.on_alt());
716 feed(&mut g, b"\x1b[?1049l");
717 assert!(!g.on_alt());
718 }
719 }
720