Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_tui/tests/term.rs

65.0 KiB, 25 runs

created by r1870400018:20815, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Tests for the terminal model.
2//!
3//! The cases marked as coming from the oracle were not invented here. Each byte sequence was
4//! written to a file, that file was `cat`ed inside a detached `tmux` session of a known size, and
5//! the resulting screen was read back with `capture-pane -p` and the cursor with
6//! `display-message -p '#{cursor_x},#{cursor_y}'`. The expectations below are what tmux 3.6
7//! produced. To regenerate one, run:
8//!
9//! ```text
10//! printf '<the sequence>' > /tmp/seq
11//! tmux -L t -f /dev/null new-session -d -x <cols> -y <rows> -s o 'cat /tmp/seq; sleep 30'
12//! tmux -L t display-message -p -t o '#{cursor_x},#{cursor_y}'
13//! tmux -L t capture-pane -p -t o
14//! ```
15//!
16//! Two things tmux reports need translating. Its `cursor_x` is one past the final column when a
17//! wrap is pending, which is `Cursor::reported_col` here. Its `capture-pane` re-encodes a run of
18//! cells that a tab skipped over as a literal tab character, so the tab cases below compare the
19//! cursor against tmux and the text against what the cells must hold.
20//!
21//! Three of the tables below were built by different routes, because tmux cannot answer every
22//! question the same way.
23//!
24//! [`CASES`] is the hand written stream above. The character set cases in it needed one extra step:
25//! tmux does not translate the DEC special graphics set into Unicode, it flags the cells and wraps
26//! the run in `SO` and `SI` when `capture-pane -pe` is asked for. The flagged characters were
27//! translated with a table read out of tmux itself, by running tmux with its output going to a
28//! pseudoterminal and decoding the UTF-8 it drew there for every byte from 0x20 to 0x7E after an
29//! `ESC ( 0`.
30//!
31//! [`RESIZES`] came from tmux resizing its own window:
32//!
33//! ```text
34//! tmux -L t resize-window -t o -x <cols> -y <rows>
35//! tmux -L t capture-pane -p -S - -t o
36//! tmux -L t display-message -p -t o '#{history_size}'
37//! ```
38//!
39//! [`REFLOWS`] could not, because tmux 3.6 rewraps double width characters wrongly: it breaks
40//! `aa世世世` after four columns of six and after six of eight, and neither is where tmux itself
41//! puts the break when the same bytes are printed at that width. Printing is the definition a
42//! rewrap has to meet, so those cases compare against tmux printing the same stream at the width
43//! the resize goes to. That comparison was run over every width from two upward for eight strings
44//! of mixed wide and narrow characters, a hundred and sixteen in all, and agreed everywhere.
45//!
46//! The same comparison was also run over recordings of real programmes rather than hand written
47//! sequences. `script --log-out out.bin -c 'stty rows 24 cols 80; <programme>'` captures exactly
48//! what a programme writes to a pseudoterminal; feeding that recording to this model and to tmux
49//! and diffing the two screens covers far more of the grammar than any handwritten case can. Nine
50//! recordings were checked in this way, among them 225 kB of `ls --color`, `top -b`, `man`,
51//! `grep --color`, a carriage return progress bar, and `vim` and `less` stopped while their
52//! alternate screens were still up. All nine agreed with tmux on every cell and on the cursor,
53//! with the recording fed in chunks of varying size so that characters and sequences were cut at
54//! every kind of boundary. A tenth was added for the character sets: a curses programme drawing a
55//! box, hair lines, tees and the arithmetic symbols, recorded under `TERM=xterm-256color` and
56//! `LC_ALL=C` so that ncurses reached for the alternate character set rather than for UTF-8. Its
57//! sixteen rows agreed with tmux cell for cell, at chunk sizes of one, three, seven and sixty four
58//! bytes. `examples/term_dump.rs` is the tool that feeds a recording to this model, so that the
59//! comparison can be run again.
60
61use oxedyne_fe2o3_tui::lib_tui::term::{
62 charset::Charset,
63 cell::{
64 runs,
65 NamedColour,
66 TermColour,
67 Wide,
68 },
69 screen::Surface,
70 width::{
71 char_width,
72 str_width,
73 CharWidth,
74 },
75 Terminal,
76};
77
78use oxedyne_fe2o3_core::{
79 prelude::*,
80 test::test_it,
81};
82
83
84#[test]
85fn main() -> Outcome<()> {
86
87 log_set_level!("debug");
88
89 let outcome = run_tests();
90
91 log_finish_wait!();
92
93 outcome
94}
95
96fn run_tests() -> Outcome<()> {
97
98 let filter = "";
99
100 res!(test_oracle(filter));
101 res!(test_parser(filter));
102 res!(test_screen(filter));
103 res!(test_scrollback(filter));
104 res!(test_resize(filter));
105 res!(test_damage(filter));
106 res!(test_style(filter));
107 res!(test_charset(filter));
108
109 Ok(())
110}
111
112// ┌───────────────────────────────────────────────────────────────┐
113// │ HELPERS │
114// └───────────────────────────────────────────────────────────────┘
115
116/// One case whose expectation came from a real terminal.
117struct Case {
118 /// What the case is called.
119 name: &'static str,
120 /// Screen width.
121 cols: usize,
122 /// Screen height.
123 rows: usize,
124 /// The bytes fed to both terminals.
125 byts: &'static [u8],
126 /// The rows tmux showed, with trailing blanks removed.
127 lines: &'static [&'static str],
128 /// The cursor tmux reported, as column then row.
129 cur: (usize, usize),
130}
131
132/// Runs a case and compares the screen and the cursor with what the oracle gave.
133fn run_case(case: &Case) -> Outcome<()> {
134 let mut term = res!(Terminal::new(case.cols, case.rows));
135 res!(term.feed(case.byts));
136 res!(check_screen(case.name, &term, case.lines));
137 res!(check_cursor(case.name, &term, case.cur));
138 Ok(())
139}
140
141/// Compares every row of the screen with what was expected.
142fn check_screen(name: &str, term: &Terminal, lines: &[&str]) -> Outcome<()> {
143 let scr = term.screen();
144 req!(scr.rows(), lines.len(), "({}: row count)", name);
145 for r in 0..scr.rows() {
146 let got = scr.row_text(r);
147 if got != lines[r] {
148 return Err(err!(
149 "{}: row {} is {:?}, expected {:?}. Whole screen:\n{}",
150 name, r, got, lines[r], scr.text();
151 Test, Mismatch));
152 }
153 }
154 Ok(())
155}
156
157/// Compares the cursor with what was expected, in the form tmux reports it.
158fn check_cursor(name: &str, term: &Terminal, want: (usize, usize)) -> Outcome<()> {
159 let cur = term.screen().cursor();
160 let got = (cur.reported_col(), cur.row);
161 if got != want {
162 return Err(err!(
163 "{}: cursor is {:?}, expected {:?}.", name, got, want;
164 Test, Mismatch));
165 }
166 Ok(())
167}
168
169/// Turns an absent value into a test failure that names what was missing.
170fn need<T>(v: Option<T>, what: &str) -> Outcome<T> {
171 match v {
172 Some(v) => Ok(v),
173 None => Err(err!("{} was not there.", what; Test, Missing)),
174 }
175}
176
177/// Builds a terminal and feeds it once.
178fn fed(cols: usize, rows: usize, byts: &[u8]) -> Outcome<Terminal> {
179 let mut term = res!(Terminal::new(cols, rows));
180 res!(term.feed(byts));
181 Ok(term)
182}
183
184/// One resize case whose expectation came from tmux resizing its own window.
185struct Resize {
186 /// What the case is called.
187 name: &'static str,
188 /// Width before.
189 cols: usize,
190 /// Height before.
191 rows: usize,
192 /// Width after.
193 ncols: usize,
194 /// Height after.
195 nrows: usize,
196 /// The bytes fed before the resize.
197 byts: &'static [u8],
198 /// The scrollback and then the screen, as tmux showed them, with trailing blank lines removed.
199 lines: &'static [&'static str],
200 /// How many of those lines tmux held as scrollback.
201 hist: usize,
202 /// The cursor tmux reported, as column then row of the screen.
203 cur: (usize, usize),
204}
205
206/// Runs a resize case and compares the whole of the text and the cursor with the oracle.
207fn run_resize(case: &Resize) -> Outcome<()> {
208 let mut term = res!(Terminal::new(case.cols, case.rows));
209 res!(term.feed(case.byts));
210 res!(term.resize(case.ncols, case.nrows));
211 let got = whole_text(&term);
212 if got != case.lines {
213 return Err(err!(
214 "{}: the text is {:?}, expected {:?}.", case.name, got, case.lines;
215 Test, Mismatch));
216 }
217 req!(term.screen().scrollback_len(), case.hist, "({}: scrollback length)", case.name);
218 res!(check_cursor(case.name, &term, case.cur));
219 Ok(())
220}
221
222/// One resize case whose expectation came from tmux printing the same bytes at the width the
223/// resize goes to.
224///
225/// tmux is the oracle for what a rewrap must produce, but not through its own resize: tmux 3.6
226/// rewraps double width characters wrongly, splitting `aa世世世` after four columns of six and
227/// after six columns of eight, neither of which is where tmux itself puts the break when the same
228/// bytes are printed at that width. Printing is the definition a rewrap has to meet, so these
229/// cases compare against that instead, and the split between scrollback and screen is left out of
230/// the comparison because printing never fills one.
231struct Reflow {
232 /// What the case is called.
233 name: &'static str,
234 /// Width before.
235 cols: usize,
236 /// Height before.
237 rows: usize,
238 /// Width after.
239 ncols: usize,
240 /// Height after.
241 nrows: usize,
242 /// The bytes fed before the resize.
243 byts: &'static [u8],
244 /// The scrollback and then the screen, with trailing blank lines removed.
245 lines: &'static [&'static str],
246 /// The cursor, as column then row counted from the oldest line held.
247 cur: (usize, usize),
248}
249
250/// Runs a rewrap case and compares the whole of the text and the cursor with the oracle.
251fn run_reflow(case: &Reflow) -> Outcome<()> {
252 let mut term = res!(Terminal::new(case.cols, case.rows));
253 res!(term.feed(case.byts));
254 res!(term.resize(case.ncols, case.nrows));
255 let got = whole_text(&term);
256 if got != case.lines {
257 return Err(err!(
258 "{}: the text is {:?}, expected {:?}.", case.name, got, case.lines;
259 Test, Mismatch));
260 }
261 let scr = term.screen();
262 let cur = scr.cursor();
263 let want = (cur.reported_col(), scr.scrollback_len() + cur.row);
264 if want != case.cur {
265 return Err(err!(
266 "{}: the cursor is {:?}, expected {:?}.", case.name, want, case.cur;
267 Test, Mismatch));
268 }
269 Ok(())
270}
271
272/// The scrollback and then the screen, one string per line, with trailing blank lines removed.
273fn whole_text(term: &Terminal) -> Vec<String> {
274 let scr = term.screen();
275 let mut out = Vec::new();
276 for i in 0..scr.scrollback_len() {
277 match scr.scrollback_text(i) {
278 Some(s) => out.push(s),
279 None => {}
280 }
281 }
282 for r in 0..scr.rows() {
283 out.push(scr.row_text(r));
284 }
285 while out.last().map(|s| s.is_empty()) == Some(true) {
286 out.pop();
287 }
288 out
289}
290
291// ┌───────────────────────────────────────────────────────────────┐
292// │ THE ORACLE CASES │
293// └───────────────────────────────────────────────────────────────┘
294
295/// Cases whose expectations were taken from tmux 3.6.
296static CASES: &[Case] = &[
297 // ── Cursor movement ────────────────────────────────────────
298 Case {
299 name: "cursor absolute, back and down",
300 cols: 20, rows: 6,
301 byts: b"\x1b[3;5Habc\x1b[3Dxy\x1b[2Bz",
302 lines: &["", "", " xyc", "", " z", ""],
303 cur: (7, 4),
304 },
305 Case {
306 name: "cursor home and clamped",
307 cols: 20, rows: 6,
308 byts: b"ABCDE\x1b[Hz\x1b[10;99Hq",
309 lines: &["zBCDE", "", "", "", "", " q"],
310 cur: (20, 5),
311 },
312 // ── Erasing ────────────────────────────────────────────────
313 Case {
314 name: "erase line to end",
315 cols: 20, rows: 6,
316 byts: b"abcdefgh\x1b[1;4H\x1b[K",
317 lines: &["abc", "", "", "", "", ""],
318 cur: (3, 0),
319 },
320 Case {
321 name: "erase line to start",
322 cols: 20, rows: 6,
323 byts: b"abcdefgh\x1b[1;4H\x1b[1K",
324 lines: &[" efgh", "", "", "", "", ""],
325 cur: (3, 0),
326 },
327 Case {
328 name: "erase whole line",
329 cols: 20, rows: 6,
330 byts: b"abcdefgh\x1b[1;4H\x1b[2K",
331 lines: &["", "", "", "", "", ""],
332 cur: (3, 0),
333 },
334 Case {
335 name: "erase display to end",
336 cols: 20, rows: 6,
337 byts: b"one\r\ntwo\r\nthree\x1b[2;2H\x1b[J",
338 lines: &["one", "t", "", "", "", ""],
339 cur: (1, 1),
340 },
341 Case {
342 name: "erase display to start",
343 cols: 20, rows: 6,
344 byts: b"one\r\ntwo\r\nthree\x1b[2;2H\x1b[1J",
345 lines: &["", " o", "three", "", "", ""],
346 cur: (1, 1),
347 },
348 Case {
349 name: "erase whole display",
350 cols: 20, rows: 6,
351 byts: b"one\r\ntwo\r\nthree\x1b[2;2H\x1b[2J",
352 lines: &["", "", "", "", "", ""],
353 cur: (1, 1),
354 },
355 // ── Scrolling ──────────────────────────────────────────────
356 Case {
357 name: "line feed at the foot scrolls",
358 cols: 20, rows: 6,
359 byts: b"l1\r\nl2\r\nl3\r\nl4\r\nl5\r\nl6\r\nl7",
360 lines: &["l2", "l3", "l4", "l5", "l6", "l7"],
361 cur: (2, 5),
362 },
363 Case {
364 name: "scrolling region scrolls up",
365 cols: 20, rows: 6,
366 byts: b"a\r\nb\r\nc\r\nd\r\ne\r\nf\x1b[2;4r\x1b[4;1HX\r\nY",
367 lines: &["a", "c", "X", "Y", "e", "f"],
368 cur: (1, 3),
369 },
370 Case {
371 name: "reverse index scrolls the region down",
372 cols: 20, rows: 6,
373 byts: b"\x1b[2;4r\x1b[2;1HA\r\nB\r\nC\x1b[2;1H\x1bMZ",
374 lines: &["", "Z", "A", "B", "", ""],
375 cur: (1, 1),
376 },
377 Case {
378 name: "scroll up",
379 cols: 20, rows: 6,
380 byts: b"a\r\nb\r\nc\r\nd\x1b[2S",
381 lines: &["c", "d", "", "", "", ""],
382 cur: (1, 3),
383 },
384 Case {
385 name: "scroll down",
386 cols: 20, rows: 6,
387 byts: b"a\r\nb\r\nc\r\nd\x1b[1;1H\x1b[2T",
388 lines: &["", "", "a", "b", "c", "d"],
389 cur: (0, 0),
390 },
391 Case {
392 name: "setting the region homes the cursor",
393 cols: 10, rows: 4,
394 byts: b"aaa\r\nbbb\r\nccc\x1b[2;3rX",
395 lines: &["Xaa", "bbb", "ccc", ""],
396 cur: (1, 0),
397 },
398 // ── Insertion and deletion ─────────────────────────────────
399 Case {
400 name: "insert line",
401 cols: 20, rows: 6,
402 byts: b"a\r\nb\r\nc\r\nd\x1b[2;1H\x1b[L",
403 lines: &["a", "", "b", "c", "d", ""],
404 cur: (0, 1),
405 },
406 Case {
407 name: "delete line",
408 cols: 20, rows: 6,
409 byts: b"a\r\nb\r\nc\r\nd\x1b[2;1H\x1b[M",
410 lines: &["a", "c", "d", "", "", ""],
411 cur: (0, 1),
412 },
413 Case {
414 name: "insert characters",
415 cols: 20, rows: 6,
416 byts: b"abcdef\x1b[1;3H\x1b[2@\x1b[1;1H\x1b[8G\x1b[1;1Hzz",
417 lines: &["zz cdef", "", "", "", "", ""],
418 cur: (2, 0),
419 },
420 Case {
421 name: "delete characters",
422 cols: 20, rows: 6,
423 byts: b"abcdef\x1b[1;3H\x1b[2P",
424 lines: &["abef", "", "", "", "", ""],
425 cur: (2, 0),
426 },
427 Case {
428 name: "erase characters",
429 cols: 20, rows: 6,
430 byts: b"abcdef\x1b[1;3H\x1b[2X",
431 lines: &["ab ef", "", "", "", "", ""],
432 cur: (2, 0),
433 },
434 Case {
435 name: "insert mode pushes the line right",
436 cols: 10, rows: 4,
437 byts: b"abcdef\x1b[1;3H\x1b[4hXY",
438 lines: &["abXYcdef", "", "", ""],
439 cur: (4, 0),
440 },
441 // ── Wrapping and wide characters ───────────────────────────
442 Case {
443 name: "narrow wrap",
444 cols: 10, rows: 4,
445 byts: b"abcdefghijkl",
446 lines: &["abcdefghij", "kl", "", ""],
447 cur: (2, 1),
448 },
449 Case {
450 name: "wide characters mid line",
451 cols: 10, rows: 4,
452 byts: "ab\u{4e2d}\u{6587}cd".as_bytes(),
453 lines: &["ab\u{4e2d}\u{6587}cd", "", "", ""],
454 cur: (8, 0),
455 },
456 Case {
457 name: "wide character with one cell left wraps whole",
458 cols: 10, rows: 4,
459 byts: "abcdefghi\u{4e2d}\u{6587}".as_bytes(),
460 lines: &["abcdefghi", "\u{4e2d}\u{6587}", "", ""],
461 cur: (4, 1),
462 },
463 Case {
464 name: "wide character filling the final two cells",
465 cols: 10, rows: 4,
466 byts: "abcdefgh\u{4e2d}Z".as_bytes(),
467 lines: &["abcdefgh\u{4e2d}", "Z", "", ""],
468 cur: (1, 1),
469 },
470 Case {
471 name: "writing over a wide character clears its other half",
472 cols: 10, rows: 4,
473 byts: "ab\u{4e2d}\x08\x08Z".as_bytes(),
474 lines: &["abZ", "", "", ""],
475 cur: (3, 0),
476 },
477 Case {
478 name: "autowrap off pins the final column",
479 cols: 10, rows: 4,
480 byts: b"\x1b[?7labcdefghijklmn",
481 lines: &["abcdefghin", "", "", ""],
482 cur: (9, 0),
483 },
484 // ── Sequences that must be swallowed ───────────────────────
485 Case {
486 name: "unknown sequences are never printed",
487 cols: 40, rows: 4,
488 byts: b"A\x1b[?2026hB\x1b[>4;2mC\x1b[1;2;3;4;5;6;7;8wD",
489 lines: &["ABCD", "", "", ""],
490 cur: (4, 0),
491 },
492 Case {
493 name: "an over long parameter list is dropped",
494 cols: 10, rows: 4,
495 byts: b"A\x1b[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;\
496 1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;\
497 1;1;1;1;1;1;1;1;1;1;1;3;3HB",
498 lines: &["AB", "", "", ""],
499 cur: (2, 0),
500 },
501 Case {
502 name: "an over large parameter is dropped",
503 cols: 10, rows: 4,
504 byts: b"A\x1b[999999999999;1HB",
505 lines: &["AB", "", "", ""],
506 cur: (2, 0),
507 },
508 Case {
509 name: "operating system commands are swallowed",
510 cols: 40, rows: 4,
511 byts: b"A\x1b]2;My Title\x1b\\B\x1b]0;Second\x07C",
512 lines: &["ABC", "", "", ""],
513 cur: (3, 0),
514 },
515 // ── The rest ───────────────────────────────────────────────
516 Case {
517 name: "backward tabulation",
518 cols: 40, rows: 4,
519 byts: b"A\x1b[?9999hB\x1b[99ZC\x1b[1!pD",
520 lines: &["CD", "", "", ""],
521 cur: (2, 0),
522 },
523 Case {
524 name: "save and restore the cursor",
525 cols: 10, rows: 4,
526 byts: b"\x1b[2;3HA\x1b7\x1b[1;1HB\x1b8C",
527 lines: &["B", " AC", "", ""],
528 cur: (4, 1),
529 },
530 Case {
531 name: "the alternate screen is left behind",
532 cols: 10, rows: 4,
533 byts: b"main1\r\nmain2\x1b[?1049h\x1b[Halt\x1b[?1049l",
534 lines: &["main1", "main2", "", ""],
535 cur: (5, 1),
536 },
537 Case {
538 name: "the alternate screen starts blank",
539 cols: 10, rows: 4,
540 byts: b"main1\r\nmain2\x1b[?1049h\x1b[Halt",
541 lines: &["alt", "", "", ""],
542 cur: (3, 0),
543 },
544 Case {
545 name: "repeat the last character",
546 cols: 10, rows: 4,
547 byts: b"a\x1b[5b",
548 lines: &["aaaaaa", "", "", ""],
549 cur: (6, 0),
550 },
551 Case {
552 name: "the alignment pattern",
553 cols: 10, rows: 4,
554 byts: b"\x1b#8",
555 lines: &["EEEEEEEEEE", "EEEEEEEEEE", "EEEEEEEEEE", "EEEEEEEEEE"],
556 cur: (0, 0),
557 },
558 Case {
559 name: "erase the scrollback leaves the screen",
560 cols: 10, rows: 4,
561 byts: b"a\r\nb\r\nc\r\nd\r\ne\r\nf\x1b[3J",
562 lines: &["c", "d", "e", "f"],
563 cur: (1, 3),
564 },
565
566 // ── Character sets ─────────────────────────────────────────
567 // tmux does not translate the DEC special graphics set; it flags the cells and wraps the run
568 // in SO and SI when `capture-pane -pe` is asked for. The expectations below were decoded from
569 // that flagging with a table read out of tmux's own rendering: `ESC ( 0` followed by every
570 // byte from 0x20 to 0x7E was drawn by tmux into a pseudoterminal and the UTF-8 it wrote there
571 // was read back character by character. See `term::charset`.
572 Case {
573 name: "special graphics box",
574 cols: 12, rows: 5,
575 byts: b"\x1b(0lqqqk\x0d\x0ax x\x0d\x0amqqqj\x1b(BZ",
576 lines: &["┌───┐", "│ │", "└───┘Z", "", ""],
577 cur: (6, 2),
578 },
579 Case {
580 name: "special graphics repertoire",
581 cols: 40, rows: 3,
582 byts: b"\x1b(0`abcdefghijklmnopqrstuvwxyz{|}~\x1b(B",
583 lines: &["◆▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·", "", ""],
584 cur: (31, 0),
585 },
586 Case {
587 name: "special graphics punctuation",
588 cols: 12, rows: 3,
589 byts: b"\x1b(0+,-./0\x1b(B",
590 lines: &["→←↑↓/▮", "", ""],
591 cur: (6, 0),
592 },
593 Case {
594 name: "G1 designated and shifted in",
595 cols: 12, rows: 3,
596 byts: b"A\x1b)0\x0eqqq\x0fB",
597 lines: &["A───B", "", ""],
598 cur: (5, 0),
599 },
600 Case {
601 name: "G0 restored to ascii",
602 cols: 12, rows: 3,
603 byts: b"\x1b(0qqq\x1b(Bqqq",
604 lines: &["───qqq", "", ""],
605 cur: (6, 0),
606 },
607 Case {
608 name: "shift out with G1 undesignated",
609 cols: 12, rows: 3,
610 byts: b"\x1b(0q\x0eq\x0fq",
611 lines: &["─q─", "", ""],
612 cur: (3, 0),
613 },
614 Case {
615 name: "utf8 passes through graphics",
616 cols: 12, rows: 3,
617 byts: b"\x1b(0q\xc3\xa9q\xe4\xb8\x96q\x1b(B",
618 lines: &["─é─世─", "", ""],
619 cur: (6, 0),
620 },
621 Case {
622 name: "uk set is treated as ascii",
623 cols: 12, rows: 3,
624 byts: b"\x1b(A#[]\x1b(B",
625 lines: &["#[]", "", ""],
626 cur: (3, 0),
627 },
628 Case {
629 name: "single shifts are ignored",
630 cols: 12, rows: 3,
631 byts: b"\x1b*0A\x1bNqB\x1bOqC",
632 lines: &["AqBqC", "", ""],
633 cur: (5, 0),
634 },
635 Case {
636 name: "locking shifts two and three ignored",
637 cols: 12, rows: 3,
638 byts: b"\x1b*0\x1bnqqq\x1boA",
639 lines: &["qqqA", "", ""],
640 cur: (4, 0),
641 },
642 Case {
643 name: "graphics saved and restored",
644 cols: 12, rows: 3,
645 byts: b"\x1b(0q\x1b7\x1b(Bq\x1b8q",
646 lines: &["──", "", ""],
647 cur: (2, 0),
648 },
649 Case {
650 name: "graphics saved by CSI s",
651 cols: 12, rows: 3,
652 byts: b"\x1b(0q\x1b[s\x1b(Bqq\x1b[uq",
653 lines: &["──q", "", ""],
654 cur: (2, 0),
655 },
656 Case {
657 name: "reset clears the designation",
658 cols: 12, rows: 3,
659 byts: b"\x1b(0qqq\x1bcqqq",
660 lines: &["qqq", "", ""],
661 cur: (3, 0),
662 },
663 Case {
664 name: "graphics repeated by REP",
665 cols: 12, rows: 3,
666 byts: b"\x1b(0q\x1b[3b\x1b(B",
667 lines: &["────", "", ""],
668 cur: (4, 0),
669 },
670 Case {
671 name: "graphics wraps at the edge",
672 cols: 6, rows: 4,
673 byts: b"\x1b(0qqqqqqqqqqqqqq\x1b(B",
674 lines: &["──────", "──────", "──", ""],
675 cur: (2, 2),
676 },
677 Case {
678 name: "graphics through a scroll",
679 cols: 8, rows: 3,
680 byts: b"\x1b(0lqk\x0d\x0ax x\x0d\x0amqj\x0d\x0aabc\x0d\x0adef\x1b(B",
681 lines: &["└─┘", "▒␉␌", "␍␊°"],
682 cur: (3, 2),
683 },
684 Case {
685 name: "alignment pattern ignores graphics",
686 cols: 8, rows: 3,
687 byts: b"\x1b(0\x1b#8",
688 lines: &["EEEEEEEE", "EEEEEEEE", "EEEEEEEE"],
689 cur: (0, 0),
690 },
691];
692
693/// Resize cases whose expectations were taken from tmux 3.6 resizing its own window.
694static RESIZES: &[Resize] = &[
695 Resize {
696 name: "a hard newline does not join",
697 cols: 10, rows: 4,
698 ncols: 20, nrows: 4,
699 byts: b"AAA\x0d\x0aBBB",
700 lines: &["AAA", "BBB"],
701 hist: 0,
702 cur: (3, 1),
703 },
704 Resize {
705 name: "a soft wrap rejoins",
706 cols: 10, rows: 4,
707 ncols: 20, nrows: 4,
708 byts: b"AAAAAAAAAABBB",
709 lines: &["AAAAAAAAAABBB"],
710 hist: 0,
711 cur: (13, 0),
712 },
713 Resize {
714 name: "narrowing rewraps into the history",
715 cols: 26, rows: 3,
716 ncols: 10, nrows: 3,
717 byts: b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
718 lines: &["ABCDEFGHIJ", "KLMNOPQRST", "UVWXYZ"],
719 hist: 2,
720 cur: (6, 0),
721 },
722 Resize {
723 name: "a full row then a newline does not join",
724 cols: 10, rows: 4,
725 ncols: 20, nrows: 4,
726 byts: b"ABCDEFGHIJ\x0d\x0aK",
727 lines: &["ABCDEFGHIJ", "K"],
728 hist: 0,
729 cur: (1, 1),
730 },
731 Resize {
732 name: "a full row that ran on does join",
733 cols: 10, rows: 4,
734 ncols: 20, nrows: 4,
735 byts: b"ABCDEFGHIJK",
736 lines: &["ABCDEFGHIJK"],
737 hist: 0,
738 cur: (11, 0),
739 },
740 Resize {
741 name: "the cursor follows its character",
742 cols: 20, rows: 4,
743 ncols: 8, nrows: 4,
744 byts: b"ABCDEFGHIJKLMNO",
745 lines: &["ABCDEFGH", "IJKLMNO"],
746 hist: 1,
747 cur: (7, 0),
748 },
749 Resize {
750 name: "the cursor after a newline",
751 cols: 20, rows: 4,
752 ncols: 8, nrows: 4,
753 byts: b"ABC\x0d\x0a",
754 lines: &["ABC"],
755 hist: 0,
756 cur: (0, 1),
757 },
758 Resize {
759 name: "shorter with the cursor at the top",
760 cols: 10, rows: 6,
761 ncols: 10, nrows: 3,
762 byts: b"A\x1b[H",
763 lines: &["A"],
764 hist: 0,
765 cur: (0, 0),
766 },
767 Resize {
768 name: "shorter with the cursor at the foot",
769 cols: 10, rows: 6,
770 ncols: 10, nrows: 3,
771 byts: b"A\x0d\x0aB\x0d\x0aC\x0d\x0aD\x0d\x0aE\x0d\x0aF",
772 lines: &["A", "B", "C", "D", "E", "F"],
773 hist: 3,
774 cur: (1, 2),
775 },
776 Resize {
777 name: "shorter drops the rows below the cursor",
778 cols: 10, rows: 6,
779 ncols: 10, nrows: 3,
780 byts: b"A\x0d\x0aB\x0d\x0aC\x0d\x0aD\x1b[2;1H",
781 lines: &["A", "B", "C"],
782 hist: 0,
783 cur: (0, 1),
784 },
785 Resize {
786 name: "taller pulls the history back",
787 cols: 10, rows: 3,
788 ncols: 10, nrows: 5,
789 byts: b"A\x0d\x0aB\x0d\x0aC\x0d\x0aD\x0d\x0aE",
790 lines: &["A", "B", "C", "D", "E"],
791 hist: 0,
792 cur: (1, 4),
793 },
794 Resize {
795 name: "the history joins on widening",
796 cols: 10, rows: 2,
797 ncols: 30, nrows: 2,
798 byts: b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
799 lines: &["ABCDEFGHIJKLMNOPQRSTUVWXYZ"],
800 hist: 0,
801 cur: (26, 0),
802 },
803 Resize {
804 name: "a wrap whose tail filled the row",
805 cols: 5, rows: 4,
806 ncols: 20, nrows: 4,
807 byts: b"ABCDEFGHIJ",
808 lines: &["ABCDEFGHIJ"],
809 hist: 0,
810 cur: (10, 0),
811 },
812 Resize {
813 name: "narrower and taller at once",
814 cols: 26, rows: 2,
815 ncols: 10, nrows: 5,
816 byts: b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
817 lines: &["ABCDEFGHIJ", "KLMNOPQRST", "UVWXYZ"],
818 hist: 2,
819 cur: (6, 0),
820 },
821 Resize {
822 name: "the cursor on a later row",
823 cols: 20, rows: 4,
824 ncols: 8, nrows: 4,
825 byts: b"ABCDEFGHIJKLMNO\x0d\x0axy",
826 lines: &["ABCDEFGH", "IJKLMNO", "xy"],
827 hist: 1,
828 cur: (2, 1),
829 },
830 Resize {
831 name: "blank rows below the cursor",
832 cols: 20, rows: 6,
833 ncols: 8, nrows: 6,
834 byts: b"ABCDEFGHIJKLMNO\x0d\x0axy\x0d\x0a\x0d\x0a\x0d\x0a",
835 lines: &["ABCDEFGH", "IJKLMNO", "xy"],
836 hist: 1,
837 cur: (0, 4),
838 },
839 Resize {
840 name: "the alternate screen is not rewrapped",
841 cols: 10, rows: 4,
842 ncols: 20, nrows: 4,
843 byts: b"\x1b[?1049hABCDEFGHIJKLMNO",
844 lines: &["ABCDEFGHIJ", "KLMNO"],
845 hist: 0,
846 cur: (5, 1),
847 },
848 Resize {
849 name: "erasing a line ends it",
850 cols: 10, rows: 4,
851 ncols: 20, nrows: 4,
852 byts: b"ABCDEFGHIJK\x1b[H\x1b[2K",
853 lines: &["", "K"],
854 hist: 0,
855 cur: (0, 0),
856 },
857 Resize {
858 name: "overwriting a row leaves it running on",
859 cols: 10, rows: 4,
860 ncols: 20, nrows: 4,
861 byts: b"ABCDEFGHIJK\x1b[Hzz",
862 lines: &["zzCDEFGHIJK"],
863 hist: 0,
864 cur: (2, 0),
865 },
866 Resize {
867 name: "two wraps in succession",
868 cols: 10, rows: 4,
869 ncols: 30, nrows: 4,
870 byts: b"ABCDEFGHIJKLMNOPQRSTUV",
871 lines: &["ABCDEFGHIJKLMNOPQRSTUV"],
872 hist: 0,
873 cur: (22, 0),
874 },
875 Resize {
876 name: "a wrap that went to the history",
877 cols: 10, rows: 3,
878 ncols: 30, nrows: 3,
879 byts: b"ABCDEFGHIJK\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a",
880 lines: &["ABCDEFGHIJK"],
881 hist: 3,
882 cur: (0, 2),
883 },
884 Resize {
885 name: "a colour survives the rewrap",
886 cols: 10, rows: 4,
887 ncols: 20, nrows: 4,
888 byts: b"\x1b[31mABCDEFGHIJKLMNO\x1b[m",
889 lines: &["ABCDEFGHIJKLMNO"],
890 hist: 0,
891 cur: (15, 0),
892 },
893 Resize {
894 name: "narrowing to one column",
895 cols: 10, rows: 3,
896 ncols: 1, nrows: 3,
897 byts: b"ABCDE",
898 lines: &["A", "B", "C", "D", "E"],
899 hist: 4,
900 cur: (1, 0),
901 },
902 Resize {
903 name: "widening far past the text",
904 cols: 5, rows: 4,
905 ncols: 60, nrows: 4,
906 byts: b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
907 lines: &["ABCDEFGHIJKLMNOPQRSTUVWXYZ"],
908 hist: 0,
909 cur: (26, 0),
910 },
911 Resize {
912 name: "a long paragraph narrowed",
913 cols: 40, rows: 5,
914 ncols: 13, nrows: 5,
915 byts: b"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOP",
916 lines: &["ABCDEFGHIJKLM", "NOPQRSTUVWXYZ", "ABCDEFGHIJKLM", "NOPQRSTUVWXYZ", "ABCDEFGHIJKLM", "NOPQRSTUVWXYZ", "ABCDEFGHIJKLM", "NOPQRSTUVWXYZ", "ABCDEFGHIJKLM", "NOP"],
917 hist: 7,
918 cur: (3, 2),
919 },
920 Resize {
921 name: "a long paragraph widened again",
922 cols: 13, rows: 5,
923 ncols: 40, nrows: 5,
924 byts: b"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOP",
925 lines: &["ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMN", "OPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB", "CDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOP"],
926 hist: 0,
927 cur: (40, 2),
928 },
929];
930
931/// Rewrap cases whose expectations were taken from tmux 3.6 printing the same bytes at the width
932/// the resize goes to.
933static REFLOWS: &[Reflow] = &[
934 Reflow {
935 name: "a wide character straddles the new edge",
936 cols: 10, rows: 6,
937 ncols: 5, nrows: 6,
938 byts: b"aa\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96",
939 lines: &["aa世", "世世"],
940 cur: (4, 1),
941 },
942 Reflow {
943 name: "a wide character at an odd width",
944 cols: 10, rows: 6,
945 ncols: 4, nrows: 6,
946 byts: b"a\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96",
947 lines: &["a世", "世世", "世"],
948 cur: (2, 2),
949 },
950 Reflow {
951 name: "wide characters joined again",
952 cols: 4, rows: 6,
953 ncols: 12, nrows: 6,
954 byts: b"\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96",
955 lines: &["世世世世世"],
956 cur: (10, 0),
957 },
958 Reflow {
959 name: "wide and narrow mixed, narrowed",
960 cols: 20, rows: 8,
961 ncols: 7, nrows: 8,
962 byts: b"ab\xe4\xb8\x96cd\xe4\xb8\x96ef\xe4\xb8\x96gh",
963 lines: &["ab世cd", "世ef世g", "h"],
964 cur: (1, 2),
965 },
966 Reflow {
967 name: "wide and narrow mixed, widened",
968 cols: 7, rows: 8,
969 ncols: 20, nrows: 8,
970 byts: b"ab\xe4\xb8\x96cd\xe4\xb8\x96ef\xe4\xb8\x96gh",
971 lines: &["ab世cd世ef世gh"],
972 cur: (14, 0),
973 },
974 Reflow {
975 name: "every column a wide character",
976 cols: 20, rows: 8,
977 ncols: 6, nrows: 8,
978 byts: b"\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96",
979 lines: &["世世世", "世世世", "世世"],
980 cur: (4, 2),
981 },
982 Reflow {
983 name: "narrowed to two columns",
984 cols: 10, rows: 8,
985 ncols: 2, nrows: 8,
986 byts: b"aa\xe4\xb8\x96\xe4\xb8\x96\xe4\xb8\x96",
987 lines: &["aa", "世", "世", "世"],
988 cur: (2, 3),
989 },
990];
991
992/// Every case whose expectation came from tmux.
993fn test_oracle(filter: &'static str) -> Outcome<()> {
994
995 res!(test_it(filter, &["Oracle cases", "all", "term", "oracle"], || {
996 for case in CASES {
997 res!(run_case(case));
998 }
999 test!("{} cases agreed with tmux.", CASES.len());
1000 Ok(())
1001 }));
1002
1003 res!(test_it(filter, &["Oracle tab stops", "all", "term", "oracle", "tab"], || {
1004 // tmux re-encodes tab padding as a tab character in its capture, so only the cursor is
1005 // compared with it. The text is what the cells must hold given the stops it reported.
1006 let term = res!(fed(20, 6, b"a\tb\tc\td"));
1007 res!(check_cursor("default tab stops", &term, (20, 0)));
1008 req!(term.screen().row_text(0), fmt!("a b c d"));
1009
1010 let term = res!(fed(20, 6, b"\x1b[1;1H\x1b[3G\x1bH\x1b[1;1H\ty"));
1011 res!(check_cursor("a set tab stop", &term, (3, 0)));
1012 req!(term.screen().row_text(0), fmt!(" y"));
1013 Ok(())
1014 }));
1015
1016 res!(test_it(filter, &["Oracle combining mark", "all", "term", "oracle", "combining"], || {
1017 // tmux reports a base and a combining mark as occupying one cell, and puts the cursor
1018 // after it. This model composes the pair where a composed character exists.
1019 let term = res!(fed(10, 4, "e\u{0301}x".as_bytes()));
1020 res!(check_cursor("combining mark", &term, (2, 0)));
1021 req!(term.screen().row_text(0), fmt!("\u{e9}x"));
1022 Ok(())
1023 }));
1024
1025 res!(test_it(filter, &["Oracle scrollback", "all", "term", "oracle", "scrollback"], || {
1026 // tmux reported three lines of history for this flood, and none for the same flood on
1027 // the alternate screen.
1028 let term = res!(fed(10, 4, b"l1\r\nl2\r\nl3\r\nl4\r\nl5\r\nl6\r\nl7"));
1029 req!(term.screen().scrollback_len(), 3);
1030 req!(res!(need(term.screen().scrollback_text(0), "term.screen().scrollback_text(0)")), fmt!("l1"));
1031 req!(res!(need(term.screen().scrollback_text(2), "term.screen().scrollback_text(2)")), fmt!("l3"));
1032
1033 let term = res!(fed(10, 4, b"\x1b[?1049hA\r\nB\r\nC\r\nD\r\nE\r\nF"));
1034 req!(term.screen().scrollback_len(), 0);
1035 res!(check_screen("alternate screen flood", &term, &["C", "D", "E", "F"]));
1036
1037 // A region whose head is the head of the screen still fills the history.
1038 let term = res!(fed(10, 4, b"\x1b[1;3rl1\r\nl2\r\nl3\r\nl4"));
1039 req!(term.screen().scrollback_len(), 1);
1040 req!(res!(need(term.screen().scrollback_text(0), "term.screen().scrollback_text(0)")), fmt!("l1"));
1041 res!(check_screen("region at the head of the screen", &term, &["l2", "l3", "l4", ""]));
1042 res!(check_cursor("region at the head of the screen", &term, (2, 2)));
1043
1044 // Erasing the display with a parameter of three empties the history.
1045 let term = res!(fed(10, 4, b"a\r\nb\r\nc\r\nd\r\ne\r\nf\x1b[3J"));
1046 req!(term.screen().scrollback_len(), 0);
1047 Ok(())
1048 }));
1049
1050 res!(test_it(filter, &["Oracle cursor visibility", "all", "term", "oracle", "cursor"], || {
1051 let term = res!(fed(10, 4, b"\x1b[?25labc"));
1052 req!(term.screen().cursor().visible, false);
1053 res!(check_cursor("hidden cursor", &term, (3, 0)));
1054 let term = res!(fed(10, 4, b"\x1b[?25l\x1b[?25habc"));
1055 req!(term.screen().cursor().visible, true);
1056 Ok(())
1057 }));
1058
1059 res!(test_it(filter, &["Oracle colon colour", "all", "term", "oracle", "sgr"], || {
1060 // tmux rewrote `38:2::10:20:30` as `38;2;10;20;30`, so it read the colon form with an
1061 // empty colour space identifier as a direct colour.
1062 let term = res!(fed(10, 4, b"\x1b[38:2::10:20:30mX"));
1063 let cell = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1064 req!(cell.pen.fore, TermColour::Rgb(10, 20, 30));
1065
1066 let term = res!(fed(10, 4, b"\x1b[38;2;10;20;30mX"));
1067 let cell = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1068 req!(cell.pen.fore, TermColour::Rgb(10, 20, 30));
1069
1070 let term = res!(fed(10, 4, b"\x1b[38:5:196mX"));
1071 let cell = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1072 req!(cell.pen.fore, TermColour::Indexed(196));
1073 Ok(())
1074 }));
1075
1076 res!(test_it(filter, &["Oracle title", "all", "term", "oracle", "title"], || {
1077 let mut term = res!(fed(40, 4, b"A\x1b]2;My Title\x1b\\B\x1b]0;Second\x07C"));
1078 req!(res!(need(term.take_title(), "term.take_title()")), fmt!("Second"));
1079 req!(term.take_title(), Option::<String>::None);
1080 Ok(())
1081 }));
1082
1083 Ok(())
1084}
1085
1086// ┌───────────────────────────────────────────────────────────────┐
1087// │ THE PARSER │
1088// └───────────────────────────────────────────────────────────────┘
1089
1090fn test_parser(filter: &'static str) -> Outcome<()> {
1091
1092 res!(test_it(filter, &["Split character", "all", "term", "parse", "split"], || {
1093 // A three byte character cut after its first byte.
1094 let mut term = res!(Terminal::new(10, 2));
1095 res!(term.feed(b"ab\xe4"));
1096 req!(term.screen().row_text(0), fmt!("ab"));
1097 req!(term.is_partial(), true);
1098 res!(term.feed(b"\xb8\xadcd"));
1099 req!(term.screen().row_text(0), fmt!("ab\u{4e2d}cd"));
1100 req!(term.is_partial(), false);
1101
1102 // Cut between every pair of bytes in turn.
1103 let whole = "a\u{4e2d}b\u{1f600}c".as_bytes();
1104 for at in 0..whole.len() {
1105 let mut term = res!(Terminal::new(20, 2));
1106 res!(term.feed(&whole[..at]));
1107 res!(term.feed(&whole[at..]));
1108 req!(term.screen().row_text(0), fmt!("a\u{4e2d}b\u{1f600}c"),
1109 "(split at byte {})", at);
1110 }
1111 Ok(())
1112 }));
1113
1114 res!(test_it(filter, &["Malformed UTF-8", "all", "term", "parse", "utf8"], || {
1115 // A truncated character is one replacement, and the byte that truncated it still counts.
1116 let term = res!(fed(10, 2, b"a\xe4b"));
1117 req!(term.screen().row_text(0), fmt!("a\u{fffd}b"));
1118 // A stray continuation byte is one replacement.
1119 let term = res!(fed(10, 2, b"a\xb8b"));
1120 req!(term.screen().row_text(0), fmt!("a\u{fffd}b"));
1121 // An escape sequence arriving mid character is still obeyed.
1122 let term = res!(fed(10, 2, b"a\xe4\x1b[2Cb"));
1123 req!(term.screen().row_text(0), fmt!("a\u{fffd} b"));
1124 Ok(())
1125 }));
1126
1127 res!(test_it(filter, &["Split sequence", "all", "term", "parse", "split"], || {
1128 // A control sequence cut between every pair of bytes in turn.
1129 let whole = b"abc\x1b[2DX";
1130 for at in 0..whole.len() {
1131 let mut term = res!(Terminal::new(10, 2));
1132 res!(term.feed(&whole[..at]));
1133 res!(term.feed(&whole[at..]));
1134 req!(term.screen().row_text(0), fmt!("aXc"), "(split at byte {})", at);
1135 }
1136 // A sequence cut into single bytes.
1137 let mut term = res!(Terminal::new(10, 2));
1138 for b in b"\x1b[1;5H\x1b[31mZ" {
1139 res!(term.feed(&[*b]));
1140 }
1141 req!(term.screen().row_text(0), fmt!(" Z"));
1142 let cell = res!(need(term.screen().cell(4, 0), "term.screen().cell(4, 0)"));
1143 req!(cell.pen.fore, TermColour::Named(NamedColour::Red));
1144 Ok(())
1145 }));
1146
1147 res!(test_it(filter, &["Runaway sequences", "all", "term", "parse", "runaway"], || {
1148 // An operating system command far longer than any real one. Its payload is dropped, the
1149 // terminator is still found, and what follows prints.
1150 let mut byts = Vec::new();
1151 byts.extend_from_slice(b"A\x1b]0;");
1152 for _ in 0..100_000 {
1153 byts.push(b'x');
1154 }
1155 byts.extend_from_slice(b"\x07B");
1156 let mut term = res!(Terminal::new(10, 2));
1157 res!(term.feed(&byts));
1158 req!(term.screen().row_text(0), fmt!("AB"));
1159 req!(term.take_title(), Option::<String>::None);
1160
1161 // A control sequence with an absurd number of digits. Nothing is buffered and the
1162 // sequence is abandoned rather than acted on.
1163 let mut byts = Vec::new();
1164 byts.extend_from_slice(b"A\x1b[");
1165 for _ in 0..50_000 {
1166 byts.push(b'9');
1167 }
1168 byts.extend_from_slice(b"HB");
1169 let mut term = res!(Terminal::new(10, 2));
1170 res!(term.feed(&byts));
1171 req!(term.screen().row_text(0), fmt!("AB"));
1172
1173 // An operating system command whose terminator never arrives recovers at the next line
1174 // break instead of swallowing the rest of the stream.
1175 let term = res!(fed(10, 3, b"A\x1b]0;no end here\nB"));
1176 req!(term.screen().row_text(0), fmt!("A"));
1177 req!(term.screen().row_text(1), fmt!(" B"));
1178 Ok(())
1179 }));
1180
1181 res!(test_it(filter, &["Ignored sequences", "all", "term", "parse", "ignore"], || {
1182 // None of these may leave a mark on the screen.
1183 let noise: &[&[u8]] = &[
1184 b"\x1bP0;1|17/ab\x1b\\", // A device control string.
1185 b"\x1b_G a=T,f=100 \x1b\\", // An application programme command.
1186 b"\x1b^private\x1b\\", // A privacy message.
1187 b"\x1bX start of string \x1b\\", // A start of string.
1188 b"\x1b]52;c;aGVsbG8=\x07", // A clipboard command.
1189 b"\x1b[?1000;1002;1006h", // Mouse tracking.
1190 b"\x1b[>c", // A secondary device attributes query.
1191 b"\x1b[3;4;5;6;7$p", // A request with an intermediate.
1192 b"\x1b(B\x1b)0", // Character set designation.
1193 b"\x1b[?12;25h\x1b[?12l", // Cursor blink.
1194 b"\x1b[ q", // A cursor style.
1195 b"\x1b[8;24;80t", // A window operation.
1196 ];
1197 for byts in noise {
1198 let mut term = res!(Terminal::new(20, 3));
1199 res!(term.feed(b"["));
1200 res!(term.feed(byts));
1201 res!(term.feed(b"]"));
1202 let got = term.screen().row_text(0);
1203 if got != "[]" {
1204 return Err(err!(
1205 "The sequence {:?} left {:?} on the screen.", byts, got;
1206 Test, Mismatch));
1207 }
1208 }
1209 Ok(())
1210 }));
1211
1212 res!(test_it(filter, &["Reports", "all", "term", "parse", "report"], || {
1213 // The cursor position report counts from one.
1214 let mut term = res!(fed(20, 6, b"\x1b[3;5H\x1b[6n"));
1215 req!(term.take_replies(), b"\x1b[3;5R".to_vec());
1216 req!(term.take_replies(), Vec::<u8>::new());
1217
1218 // The status report and the device attributes.
1219 let mut term = res!(fed(20, 6, b"\x1b[5n\x1b[c"));
1220 req!(term.take_replies(), b"\x1b[0n\x1b[?1;2c".to_vec());
1221
1222 // A pending wrap is reported as the final column, not one past it.
1223 let mut term = res!(fed(10, 2, b"abcdefghij\x1b[6n"));
1224 req!(term.screen().cursor().reported_col(), 10);
1225 req!(term.take_replies(), b"\x1b[1;10R".to_vec());
1226 Ok(())
1227 }));
1228
1229 res!(test_it(filter, &["Bells", "all", "term", "parse", "bell"], || {
1230 let mut term = res!(fed(10, 2, b"a\x07b\x07\x07c"));
1231 req!(term.screen().row_text(0), fmt!("abc"));
1232 req!(term.take_bells(), 3);
1233 req!(term.take_bells(), 0);
1234 Ok(())
1235 }));
1236
1237 Ok(())
1238}
1239
1240// ┌───────────────────────────────────────────────────────────────┐
1241// │ THE SCREEN │
1242// └───────────────────────────────────────────────────────────────┘
1243
1244fn test_screen(filter: &'static str) -> Outcome<()> {
1245
1246 res!(test_it(filter, &["Deferred wrap", "all", "term", "screen", "wrap"], || {
1247 // A line exactly as wide as the screen must not scroll until something follows it.
1248 let term = res!(fed(5, 3, b"abcde"));
1249 req!(term.screen().cursor().row, 0);
1250 req!(term.screen().cursor().wrap_pending, true);
1251 req!(term.screen().row_text(0), fmt!("abcde"));
1252
1253 let term = res!(fed(5, 3, b"abcdef"));
1254 req!(term.screen().cursor().row, 1);
1255 req!(term.screen().row_text(1), fmt!("f"));
1256
1257 // A carriage return cancels the pending wrap.
1258 let term = res!(fed(5, 3, b"abcde\rZ"));
1259 req!(term.screen().row_text(0), fmt!("Zbcde"));
1260 req!(term.screen().row_text(1), fmt!(""));
1261
1262 // So does a backspace, which then leaves the cursor on the final column.
1263 let term = res!(fed(5, 3, b"abcde\x08Z"));
1264 req!(term.screen().row_text(0), fmt!("abcdZ"));
1265 Ok(())
1266 }));
1267
1268 res!(test_it(filter, &["Bare line feed", "all", "term", "screen", "linefeed"], || {
1269 // Without newline mode a line feed keeps the column; a pseudoterminal usually turns
1270 // this into a carriage return and line feed before the model ever sees it, so the model
1271 // must not do it a second time.
1272 let term = res!(fed(10, 3, b"abc\ndef"));
1273 req!(term.screen().row_text(0), fmt!("abc"));
1274 req!(term.screen().row_text(1), fmt!(" def"));
1275
1276 // With newline mode set it returns the carriage as well.
1277 let term = res!(fed(10, 3, b"\x1b[20habc\ndef"));
1278 req!(term.screen().row_text(1), fmt!("def"));
1279 Ok(())
1280 }));
1281
1282 res!(test_it(filter, &["Origin mode", "all", "term", "screen", "origin"], || {
1283 // With origin mode set, row one is the head of the region and the cursor cannot leave it.
1284 let term = res!(fed(10, 6, b"\x1b[3;5r\x1b[?6h\x1b[1;1HX\x1b[9;1HY"));
1285 req!(term.screen().row_text(2), fmt!("X"));
1286 req!(term.screen().row_text(4), fmt!("Y"));
1287 req!(term.screen().row_text(0), fmt!(""));
1288 Ok(())
1289 }));
1290
1291 res!(test_it(filter, &["Erase paints the background", "all", "term", "screen", "erase"], || {
1292 let term = res!(fed(10, 3, b"\x1b[41m\x1b[2J"));
1293 let cell = res!(need(term.screen().cell(3, 1), "term.screen().cell(3, 1)"));
1294 req!(cell.pen.back, TermColour::Named(NamedColour::Red));
1295 req!(cell.chr, ' ');
1296 // The foreground and the attributes are not carried into the erased cells.
1297 let term = res!(fed(10, 3, b"\x1b[1;31;42m\x1b[2J"));
1298 let cell = res!(need(term.screen().cell(3, 1), "term.screen().cell(3, 1)"));
1299 req!(cell.pen.back, TermColour::Named(NamedColour::Green));
1300 req!(cell.pen.fore, TermColour::Default);
1301 req!(cell.pen.attrs.bold(), false);
1302 Ok(())
1303 }));
1304
1305 res!(test_it(filter, &["Wide character cells", "all", "term", "screen", "wide"], || {
1306 let term = res!(fed(10, 2, "a\u{4e2d}b".as_bytes()));
1307 req!(res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)")).wide, Wide::No);
1308 req!(res!(need(term.screen().cell(1, 0), "term.screen().cell(1, 0)")).wide, Wide::Lead);
1309 req!(res!(need(term.screen().cell(1, 0), "term.screen().cell(1, 0)")).chr, '\u{4e2d}');
1310 req!(res!(need(term.screen().cell(2, 0), "term.screen().cell(2, 0)")).wide, Wide::Trail);
1311 req!(res!(need(term.screen().cell(3, 0), "term.screen().cell(3, 0)")).chr, 'b');
1312
1313 // Writing over the left half clears the right half.
1314 let term = res!(fed(10, 2, "a\u{4e2d}b\x1b[1;2HZ".as_bytes()));
1315 req!(term.screen().row_text(0), fmt!("aZ b"));
1316 req!(res!(need(term.screen().cell(2, 0), "term.screen().cell(2, 0)")).wide, Wide::No);
1317
1318 // Writing over the right half clears the left half.
1319 let term = res!(fed(10, 2, "a\u{4e2d}b\x1b[1;3HZ".as_bytes()));
1320 req!(term.screen().row_text(0), fmt!("a Zb"));
1321 req!(res!(need(term.screen().cell(1, 0), "term.screen().cell(1, 0)")).wide, Wide::No);
1322 Ok(())
1323 }));
1324
1325 res!(test_it(filter, &["Character widths", "all", "term", "screen", "width"], || {
1326 req!(char_width('a'), CharWidth::Narrow);
1327 req!(char_width('\u{4e2d}'), CharWidth::Wide);
1328 req!(char_width('\u{ff21}'), CharWidth::Wide);
1329 req!(char_width('\u{1f600}'), CharWidth::Wide);
1330 req!(char_width('\u{0301}'), CharWidth::Zero);
1331 req!(char_width('\u{200b}'), CharWidth::Zero);
1332 req!(char_width('\u{fe0f}'), CharWidth::Zero);
1333 req!(char_width('\n'), CharWidth::Zero);
1334 req!(str_width("ab\u{4e2d}"), 4);
1335 Ok(())
1336 }));
1337
1338 res!(test_it(filter, &["Cell size", "all", "term", "screen", "size"], || {
1339 // The argument for keeping combining marks out of the cell rests on this number. A
1340 // screen of two hundred columns with ten thousand lines of scrollback is two million
1341 // cells, so a cell that grew to hold a boxed string would cost tens of megabytes.
1342 let n = std::mem::size_of::<oxedyne_fe2o3_tui::lib_tui::term::Cell>();
1343 if n > 24 {
1344 return Err(err!("A cell has grown to {} bytes.", n; Test, Excessive));
1345 }
1346 test!("A cell is {} bytes.", n);
1347 Ok(())
1348 }));
1349
1350 res!(test_it(filter, &["Alternate screen", "all", "term", "screen", "alt"], || {
1351 let term = res!(fed(10, 3, b"keep\x1b[?1049halt\x1b[?1049l"));
1352 req!(term.screen().surface(), Surface::Primary);
1353 req!(term.screen().row_text(0), fmt!("keep"));
1354 req!(term.screen().scrollback_len(), 0);
1355
1356 // The alternate screen is blank when it comes forward a second time.
1357 let term = res!(fed(10, 3, b"\x1b[?1049halt\x1b[?1049l\x1b[?1049h"));
1358 req!(term.screen().surface(), Surface::Alternate);
1359 req!(term.screen().row_text(0), fmt!(""));
1360
1361 // The older form switches without saving the cursor.
1362 let term = res!(fed(10, 3, b"abc\x1b[?47hX\x1b[?47l"));
1363 req!(term.screen().row_text(0), fmt!("abc"));
1364 Ok(())
1365 }));
1366
1367 res!(test_it(filter, &["Reset", "all", "term", "screen", "reset"], || {
1368 let term = res!(fed(10, 3, b"\x1b[31mabc\x1b[2;4r\x1b[?7l\x1bcZ"));
1369 req!(term.screen().row_text(0), fmt!("Z"));
1370 req!(term.screen().region(), (0, 2));
1371 req!(term.screen().autowrap(), true);
1372 let cell = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1373 req!(cell.pen.fore, TermColour::Default);
1374 Ok(())
1375 }));
1376
1377 Ok(())
1378}
1379
1380// ┌───────────────────────────────────────────────────────────────┐
1381// │ SCROLLBACK │
1382// └───────────────────────────────────────────────────────────────┘
1383
1384fn test_scrollback(filter: &'static str) -> Outcome<()> {
1385
1386 res!(test_it(filter, &["Bounded scrollback", "all", "term", "scrollback", "bound"], || {
1387 // A thousand lines through a screen that keeps ten.
1388 let mut term = res!(Terminal::with_scrollback(20, 4, 10));
1389 for i in 1..=1000 {
1390 res!(term.feed(fmt!("l{}\r\n", i).as_bytes()));
1391 }
1392 req!(term.screen().scrollback_len(), 10);
1393 // The lines kept are the ten most recent to have left the screen.
1394 req!(res!(need(term.screen().scrollback_text(9), "term.screen().scrollback_text(9)")), fmt!("l997"));
1395 req!(res!(need(term.screen().scrollback_text(0), "term.screen().scrollback_text(0)")), fmt!("l988"));
1396 req!(term.screen().row_text(0), fmt!("l998"));
1397 req!(term.screen().row_text(2), fmt!("l1000"));
1398 Ok(())
1399 }));
1400
1401 res!(test_it(filter, &["No scrollback", "all", "term", "scrollback", "none"], || {
1402 let mut term = res!(Terminal::with_scrollback(20, 4, 0));
1403 for i in 1..=100 {
1404 res!(term.feed(fmt!("l{}\r\n", i).as_bytes()));
1405 }
1406 req!(term.screen().scrollback_len(), 0);
1407 req!(term.screen().row_text(2), fmt!("l100"));
1408 // The count of lines that left is still reported, so a renderer holding its own history
1409 // knows what it missed.
1410 req!(term.damage().scrolled(), 97);
1411 Ok(())
1412 }));
1413
1414 res!(test_it(filter, &["Scrollback trims blanks", "all", "term", "scrollback", "trim"], || {
1415 let mut term = res!(Terminal::with_scrollback(80, 2, 10));
1416 res!(term.feed(b"ab\r\ncd\r\nef"));
1417 let line = res!(need(term.screen().scrollback_line(0), "term.screen().scrollback_line(0)"));
1418 req!(line.len(), 2, "(a stored line keeps only what was written)");
1419 Ok(())
1420 }));
1421
1422 res!(test_it(filter, &["Region scrollback", "all", "term", "scrollback", "region"], || {
1423 // A region that does not start at the head of the screen keeps nothing, because a line
1424 // that vanishes from the middle of the screen has no place in a history that is read
1425 // from the top down. tmux does store it; see the note in the module documentation.
1426 let term = res!(fed(10, 4, b"\x1b[2;4rl1\r\nl2\r\nl3\r\nl4\r\nl5"));
1427 req!(term.screen().scrollback_len(), 0);
1428 res!(check_screen("mid screen region", &term, &["l1", "l3", "l4", "l5"]));
1429 Ok(())
1430 }));
1431
1432 Ok(())
1433}
1434
1435// ┌───────────────────────────────────────────────────────────────┐
1436// │ RESIZE │
1437// └───────────────────────────────────────────────────────────────┘
1438
1439fn test_resize(filter: &'static str) -> Outcome<()> {
1440
1441 res!(test_it(filter, &["Oracle resize cases", "all", "term", "resize", "oracle"], || {
1442 for case in RESIZES {
1443 res!(run_resize(case));
1444 }
1445 test!("{} resize cases agreed with tmux.", RESIZES.len());
1446 Ok(())
1447 }));
1448
1449 res!(test_it(filter, &["Oracle rewrap cases", "all", "term", "resize", "oracle", "wide"], || {
1450 for case in REFLOWS {
1451 res!(run_reflow(case));
1452 }
1453 test!("{} rewrap cases agreed with what tmux prints at the new width.", REFLOWS.len());
1454 Ok(())
1455 }));
1456
1457 res!(test_it(filter, &["Wider", "all", "term", "resize", "cols"], || {
1458 let mut term = res!(Terminal::new(5, 3));
1459 res!(term.feed(b"abc"));
1460 res!(term.resize(10, 3));
1461 req!(term.screen().cols(), 10);
1462 req!(term.screen().row_text(0), fmt!("abc"));
1463 Ok(())
1464 }));
1465
1466 res!(test_it(filter, &["Narrowing keeps every character", "all", "term", "resize", "cols"], || {
1467 // The point of rewrapping: what a reader could still read is still there. Narrowing and
1468 // widening again has to give back what was there to begin with.
1469 let mut term = res!(Terminal::new(40, 4));
1470 let text = b"the quick brown fox jumps over the lazy dog and then keeps running";
1471 res!(term.feed(text));
1472 let before = whole_text(&term);
1473 for w in [7usize, 13, 5, 31, 40] {
1474 res!(term.resize(w, 4));
1475 }
1476 let after = whole_text(&term);
1477 req!(after, before, "(L: after a round trip, R: before)");
1478 Ok(())
1479 }));
1480
1481 res!(test_it(filter, &["A wrapped line keeps its blanks", "all", "term", "resize", "cols"], || {
1482 // A line that ran on is stored at its full width even where its last cells are spaces,
1483 // because those spaces are columns of the text and a rewrap has to put them back.
1484 let mut term = res!(Terminal::with_scrollback(10, 2, 20));
1485 // Nine characters, a space in the tenth column, then a wrap onto the next line.
1486 res!(term.feed(b"abcdefghi jklm
1487z"));
1488 res!(term.resize(20, 2));
1489 req!(term.screen().row_text(0), fmt!("abcdefghi jklm"));
1490 Ok(())
1491 }));
1492
1493 res!(test_it(filter, &["Shorter", "all", "term", "resize", "rows"], || {
1494 let mut term = res!(Terminal::new(10, 3));
1495 res!(term.feed(b"a\r\nb\r\nc"));
1496 req!(term.screen().cursor().row, 2);
1497 res!(term.resize(10, 2));
1498 // The cursor is on the last row, so the row lost is the topmost, and it is kept.
1499 req!(term.screen().row_text(0), fmt!("b"));
1500 req!(term.screen().row_text(1), fmt!("c"));
1501 req!(term.screen().cursor().row, 1);
1502 req!(term.screen().scrollback_len(), 1);
1503 req!(res!(need(term.screen().scrollback_text(0), "term.screen().scrollback_text(0)")), fmt!("a"));
1504 Ok(())
1505 }));
1506
1507 res!(test_it(filter, &["Shorter below the cursor", "all", "term", "resize", "rows"], || {
1508 let mut term = res!(Terminal::new(10, 4));
1509 res!(term.feed(b"a\r\nb\r\nc\x1b[1;1H"));
1510 req!(term.screen().cursor().row, 0);
1511 res!(term.resize(10, 2));
1512 // There is room below the cursor, so nothing at the top is lost.
1513 req!(term.screen().row_text(0), fmt!("a"));
1514 req!(term.screen().row_text(1), fmt!("b"));
1515 req!(term.screen().scrollback_len(), 0);
1516 Ok(())
1517 }));
1518
1519 res!(test_it(filter, &["Taller", "all", "term", "resize", "rows"], || {
1520 let mut term = res!(Terminal::new(10, 2));
1521 res!(term.feed(b"a\r\nb\r\nc"));
1522 req!(term.screen().scrollback_len(), 1);
1523 res!(term.resize(10, 3));
1524 // The line that had left comes back rather than a blank row appearing.
1525 req!(term.screen().row_text(0), fmt!("a"));
1526 req!(term.screen().row_text(1), fmt!("b"));
1527 req!(term.screen().row_text(2), fmt!("c"));
1528 req!(term.screen().scrollback_len(), 0);
1529 req!(term.screen().cursor().row, 2);
1530 Ok(())
1531 }));
1532
1533 res!(test_it(filter, &["Taller with no history", "all", "term", "resize", "rows"], || {
1534 let mut term = res!(Terminal::new(10, 2));
1535 res!(term.feed(b"a\r\nb"));
1536 res!(term.resize(10, 4));
1537 req!(term.screen().row_text(0), fmt!("a"));
1538 req!(term.screen().row_text(1), fmt!("b"));
1539 req!(term.screen().row_text(3), fmt!(""));
1540 req!(term.screen().cursor().row, 1);
1541 Ok(())
1542 }));
1543
1544 res!(test_it(filter, &["Resize resets the region", "all", "term", "resize", "region"], || {
1545 let mut term = res!(Terminal::new(10, 6));
1546 res!(term.feed(b"\x1b[2;4r"));
1547 req!(term.screen().region(), (1, 3));
1548 res!(term.resize(10, 4));
1549 req!(term.screen().region(), (0, 3));
1550 req!(term.damage().is_all(), true);
1551 Ok(())
1552 }));
1553
1554 res!(test_it(filter, &["The viewport reads the scrollback", "all", "term", "resize", "view"], || {
1555 let mut term = res!(Terminal::with_scrollback(10, 3, 50));
1556 res!(term.feed(b"one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix"));
1557 req!(term.screen().scrollback_len(), 3);
1558 req!(term.screen().view_offset(), 0);
1559 req!(term.screen().view_row_text(0), fmt!("four"));
1560 term.screen_mut().set_view_offset(2);
1561 req!(term.screen().view_offset(), 2);
1562 req!(term.screen().view_row_text(0), fmt!("two"));
1563 req!(term.screen().view_row_text(1), fmt!("three"));
1564 req!(term.screen().view_row_text(2), fmt!("four"));
1565 // The offset cannot go further back than the scrollback reaches.
1566 term.screen_mut().set_view_offset(99);
1567 req!(term.screen().view_offset(), 3);
1568 req!(term.screen().view_row_text(0), fmt!("one"));
1569 Ok(())
1570 }));
1571
1572 res!(test_it(filter, &["Output leaves the viewport alone", "all", "term", "resize", "view"], || {
1573 // A user reading the history does not want the window sliding out from under them, so the
1574 // offset counts up as lines arrive and the same text stays on screen.
1575 let mut term = res!(Terminal::with_scrollback(10, 3, 50));
1576 res!(term.feed(b"one\r\ntwo\r\nthree\r\nfour"));
1577 term.screen_mut().set_view_offset(1);
1578 req!(term.screen().view_row_text(0), fmt!("one"));
1579 res!(term.feed(b"\r\nfive\r\nsix"));
1580 req!(term.screen().view_row_text(0), fmt!("one"));
1581 req!(term.screen().view_offset(), 3);
1582 Ok(())
1583 }));
1584
1585 res!(test_it(filter, &["A resize keeps the viewport on its text", "all", "term", "resize", "view"], || {
1586 // Narrowing moves every line, so an offset counted in lines would land somewhere else.
1587 // What the viewport is anchored to is the text at its top.
1588 let mut term = res!(Terminal::with_scrollback(20, 3, 50));
1589 res!(term.feed(b"alpha\r\nbravo\r\ncharlie\r\ndelta\r\necho\r\nfoxtrot"));
1590 term.screen_mut().set_view_offset(2);
1591 req!(term.screen().view_row_text(0), fmt!("bravo"));
1592 res!(term.resize(6, 3));
1593 req!(term.screen().view_row_text(0), fmt!("bravo"));
1594 res!(term.resize(40, 3));
1595 req!(term.screen().view_row_text(0), fmt!("bravo"));
1596 Ok(())
1597 }));
1598
1599 res!(test_it(filter, &["A rewrap moves the viewport with the line", "all", "term", "resize", "view"], || {
1600 // The anchor is a line of text, so when narrowing splits that line the viewport follows
1601 // the part of it that was at the top.
1602 let mut term = res!(Terminal::with_scrollback(12, 2, 50));
1603 res!(term.feed(b"aaaaaaaaaaaabbbbbbbbbbbbcccccccccccc\r\nz"));
1604 term.screen_mut().set_view_offset(2);
1605 req!(term.screen().view_row_text(0), fmt!("aaaaaaaaaaaa"));
1606 res!(term.resize(6, 2));
1607 req!(term.screen().view_row_text(0), fmt!("aaaaaa"));
1608 req!(term.screen().view_row_text(1), fmt!("aaaaaa"));
1609 Ok(())
1610 }));
1611
1612 res!(test_it(filter, &["Resize refuses nothing", "all", "term", "resize", "zero"], || {
1613 let mut term = res!(Terminal::new(10, 4));
1614 req!(term.resize(0, 4).is_err(), true);
1615 req!(term.resize(10, 0).is_err(), true);
1616 req!(Terminal::new(0, 0).is_err(), true);
1617 Ok(())
1618 }));
1619
1620 Ok(())
1621}
1622
1623// ┌───────────────────────────────────────────────────────────────┐
1624// │ DAMAGE │
1625// └───────────────────────────────────────────────────────────────┘
1626
1627fn test_damage(filter: &'static str) -> Outcome<()> {
1628
1629 res!(test_it(filter, &["Rows", "all", "term", "damage", "rows"], || {
1630 let mut term = res!(Terminal::new(10, 4));
1631 term.clear_damage();
1632 req!(term.damage().any(), false);
1633
1634 res!(term.feed(b"x"));
1635 req!(term.damage().dirty_rows(), vec![0]);
1636 req!(term.damage().cursor_moved(), true);
1637 term.clear_damage();
1638
1639 res!(term.feed(b"\x1b[3;1Hy"));
1640 req!(term.damage().dirty_rows(), vec![2]);
1641 term.clear_damage();
1642
1643 // Moving the cursor alone dirties no row.
1644 res!(term.feed(b"\x1b[1;1H"));
1645 req!(term.damage().dirty_rows(), Vec::<usize>::new());
1646 req!(term.damage().cursor_moved(), true);
1647 Ok(())
1648 }));
1649
1650 res!(test_it(filter, &["Whole surface", "all", "term", "damage", "all"], || {
1651 let mut term = res!(Terminal::new(10, 4));
1652 term.clear_damage();
1653 res!(term.feed(b"\x1b[2J"));
1654 req!(term.damage().is_all(), true);
1655 req!(term.damage().dirty_rows(), vec![0, 1, 2, 3]);
1656 term.clear_damage();
1657 req!(term.damage().is_all(), false);
1658
1659 // A scroll changes every row.
1660 res!(term.feed(b"a\r\nb\r\nc\r\nd\r\ne"));
1661 req!(term.damage().is_all(), true);
1662 req!(term.damage().scrolled(), 1);
1663 Ok(())
1664 }));
1665
1666 res!(test_it(filter, &["A flood costs one repaint", "all", "term", "damage", "flood"], || {
1667 // Ten thousand lines between two draws leaves the renderer one screen to paint, not ten
1668 // thousand.
1669 let mut term = res!(Terminal::new(80, 24));
1670 term.clear_damage();
1671 for i in 0..10_000 {
1672 res!(term.feed(fmt!("line {}\r\n", i).as_bytes()));
1673 }
1674 req!(term.damage().dirty_rows().len(), 24);
1675 req!(term.damage().scrolled(), 9977);
1676 Ok(())
1677 }));
1678
1679 Ok(())
1680}
1681
1682// ┌───────────────────────────────────────────────────────────────┐
1683// │ STYLE │
1684// └───────────────────────────────────────────────────────────────┘
1685
1686fn test_style(filter: &'static str) -> Outcome<()> {
1687
1688 res!(test_it(filter, &["Attributes", "all", "term", "style", "attrs"], || {
1689 let term = res!(fed(20, 2, b"\x1b[1;2;3;4;5;7;9mX\x1b[0mY"));
1690 let cell = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1691 req!(cell.pen.attrs.bold(), true);
1692 req!(cell.pen.attrs.dim(), true);
1693 req!(cell.pen.attrs.italic(), true);
1694 req!(cell.pen.attrs.underline(), true);
1695 req!(cell.pen.attrs.blink(), true);
1696 req!(cell.pen.attrs.reverse(), true);
1697 req!(cell.pen.attrs.strike(), true);
1698 let cell = res!(need(term.screen().cell(1, 0), "term.screen().cell(1, 0)"));
1699 req!(cell.pen.attrs.is_empty(), true);
1700
1701 // Each attribute has its own way off.
1702 let term = res!(fed(20, 2, b"\x1b[1;4;7m\x1b[22;24;27mX"));
1703 let cell = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1704 req!(cell.pen.attrs.is_empty(), true);
1705
1706 // The modern underline spelling, on and off.
1707 let term = res!(fed(20, 2, b"\x1b[4:3mX\x1b[4:0mY"));
1708 req!(res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)")).pen.attrs.underline(), true);
1709 req!(res!(need(term.screen().cell(1, 0), "term.screen().cell(1, 0)")).pen.attrs.underline(), false);
1710 Ok(())
1711 }));
1712
1713 res!(test_it(filter, &["Colours", "all", "term", "style", "colour"], || {
1714 let term = res!(fed(20, 2, b"\x1b[31;42mA\x1b[91;102mB\x1b[39;49mC"));
1715 let a = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1716 req!(a.pen.fore, TermColour::Named(NamedColour::Red));
1717 req!(a.pen.back, TermColour::Named(NamedColour::Green));
1718 let b = res!(need(term.screen().cell(1, 0), "term.screen().cell(1, 0)"));
1719 req!(b.pen.fore, TermColour::Named(NamedColour::BrightRed));
1720 req!(b.pen.back, TermColour::Named(NamedColour::BrightGreen));
1721 let c = res!(need(term.screen().cell(2, 0), "term.screen().cell(2, 0)"));
1722 req!(c.pen.fore, TermColour::Default);
1723 req!(c.pen.back, TermColour::Default);
1724
1725 // A palette entry and a direct colour, in one sequence with everything else.
1726 let term = res!(fed(20, 2, b"\x1b[1;38;5;196;48;2;1;2;3;4mZ"));
1727 let z = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1728 req!(z.pen.fore, TermColour::Indexed(196));
1729 req!(z.pen.back, TermColour::Rgb(1, 2, 3));
1730 req!(z.pen.attrs.bold(), true);
1731 req!(z.pen.attrs.underline(), true);
1732
1733 // Reverse video is resolved for the renderer.
1734 let term = res!(fed(20, 2, b"\x1b[31;7mR"));
1735 let r = res!(need(term.screen().cell(0, 0), "term.screen().cell(0, 0)"));
1736 let (fore, back) = r.pen.resolved();
1737 req!(fore, TermColour::Default);
1738 req!(back, TermColour::Named(NamedColour::Red));
1739 Ok(())
1740 }));
1741
1742 res!(test_it(filter, &["Runs", "all", "term", "style", "runs"], || {
1743 let term = res!(fed(20, 2, b"\x1b[31mab\x1b[0mcd"));
1744 let row = res!(need(term.screen().row(0), "term.screen().row(0)"));
1745 let list = res!(runs(row));
1746 req!(list.len(), 2);
1747 req!(list[0].col, 0);
1748 req!(list[0].text, fmt!("ab"));
1749 req!(list[0].pen.fore, TermColour::Named(NamedColour::Red));
1750 req!(list[1].col, 2);
1751 req!(list[1].text, fmt!("cd"));
1752 req!(list[1].pen.fore, TermColour::Default);
1753
1754 // A wide character counts two cells but one character.
1755 let term = res!(fed(20, 2, "a\u{4e2d}b".as_bytes()));
1756 let row = res!(need(term.screen().row(0), "term.screen().row(0)"));
1757 let list = res!(runs(row));
1758 req!(list.len(), 1);
1759 req!(list[0].text, fmt!("a\u{4e2d}b"));
1760 req!(list[0].cells, 4);
1761
1762 // A blank row in the default pen has nothing to draw.
1763 let term = res!(fed(20, 2, b""));
1764 let row = res!(need(term.screen().row(0), "term.screen().row(0)"));
1765 req!(res!(runs(row)).len(), 0);
1766 Ok(())
1767 }));
1768
1769 res!(test_it(filter, &["Modes", "all", "term", "style", "modes"], || {
1770 let term = res!(fed(20, 2, b"\x1b[?1h\x1b[?1000h\x1b[?1006h\x1b[?2004h\x1b="));
1771 req!(term.modes().app_cursor, true);
1772 req!(term.modes().mouse_button, true);
1773 req!(term.modes().mouse_sgr, true);
1774 req!(term.modes().bracketed_paste, true);
1775 req!(term.modes().app_keypad, true);
1776
1777 let term = res!(fed(20, 2, b"\x1b[?1h\x1b[?1l"));
1778 req!(term.modes().app_cursor, false);
1779 Ok(())
1780 }));
1781
1782 Ok(())
1783}
1784
1785// ┌───────────────────────────────────────────────────────────────┐
1786// │ CHARACTER SETS │
1787// └───────────────────────────────────────────────────────────────┘
1788
1789/// The parts of the character set machinery that tmux cannot answer for.
1790///
1791/// Everything a real terminal can be asked about is in the oracle table above. What is left here is
1792/// the state a caller can read back, and the one behaviour where tmux and the VT510 manual
1793/// disagree.
1794fn test_charset(filter: &'static str) -> Outcome<()> {
1795
1796 res!(test_it(filter, &["Charset state is readable", "all", "term", "charset"], || {
1797 let term = res!(fed(20, 3, b"\x1b(0\x1b)B"));
1798 req!(term.charsets().designated(0), Charset::DecSpecial);
1799 req!(term.charsets().designated(1), Charset::Ascii);
1800 req!(term.charsets().shift(), 0);
1801 req!(term.charsets().active(), Charset::DecSpecial);
1802
1803 let term = res!(fed(20, 3, b"\x1b)0\x0e"));
1804 req!(term.charsets().shift(), 1);
1805 req!(term.charsets().active(), Charset::DecSpecial);
1806 req!(term.charsets().designated(0), Charset::Ascii);
1807 Ok(())
1808 }));
1809
1810 res!(test_it(filter, &["Charset designators", "all", "term", "charset"], || {
1811 req!(Charset::from_designator(b'0'), Charset::DecSpecial);
1812 req!(Charset::from_designator(b'B'), Charset::Ascii);
1813 // A set this model does not know is ASCII rather than a guess.
1814 req!(Charset::from_designator(b'A'), Charset::Ascii);
1815 req!(Charset::from_designator(b'<'), Charset::Ascii);
1816 req!(Charset::Ascii.map('q'), 'q');
1817 req!(Charset::DecSpecial.map('q'), '\u{2500}');
1818 req!(Charset::DecSpecial.map('Q'), 'Q');
1819 req!(Charset::DecSpecial.map('\u{4e16}'), '\u{4e16}');
1820 req!(Charset::Ascii.is_ascii(), true);
1821 req!(Charset::DecSpecial.is_ascii(), false);
1822 Ok(())
1823 }));
1824
1825 res!(test_it(filter, &["Soft reset clears the sets", "all", "term", "charset", "reset"], || {
1826 // This one does not come from tmux. tmux 3.6 keeps a designated graphics set across
1827 // `DECSTR`; the VT510 manual lists the character sets among what a soft reset restores,
1828 // and xterm resets them. The manual is followed, because a terminal that keeps the line
1829 // drawing set through a reset shows `qqqq` to the next programme that prints plain text.
1830 let mut term = res!(Terminal::new(20, 3));
1831 res!(term.feed(b"\x1b(0\x1b)0\x0e"));
1832 req!(term.charsets().active(), Charset::DecSpecial);
1833 req!(term.charsets().shift(), 1);
1834 res!(term.feed(b"\x1b[!p"));
1835 req!(term.charsets().designated(0), Charset::Ascii);
1836 req!(term.charsets().designated(1), Charset::Ascii);
1837 req!(term.charsets().shift(), 0);
1838 res!(term.feed(b"qqq"));
1839 req!(term.screen().row_text(0), fmt!("qqq"));
1840 Ok(())
1841 }));
1842
1843 res!(test_it(filter, &["Graphics through the scrollback", "all", "term", "charset"], || {
1844 // What a line carries into the scrollback is the translated character, not the byte that
1845 // asked for it, so a renderer reading the history needs no charset state of its own.
1846 let mut term = res!(Terminal::with_scrollback(6, 2, 10));
1847 res!(term.feed(b"\x1b(0lqk\r\nmqj\r\nabc"));
1848 req!(res!(need(term.screen().scrollback_text(0), "scrollback line 0")),
1849 fmt!("\u{250C}\u{2500}\u{2510}"));
1850 req!(term.screen().row_text(0), fmt!("\u{2514}\u{2500}\u{2518}"));
1851 req!(term.screen().row_text(1), fmt!("\u{2592}\u{2409}\u{240C}"));
1852 Ok(())
1853 }));
1854
1855 Ok(())
1856}