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 | |
| 61 | use 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 | |
| 78 | use oxedyne_fe2o3_core::{ |
| 79 | prelude::*, |
| 80 | test::test_it, |
| 81 | }; |
| 82 | |
| 83 | |
| 84 | #[test] |
| 85 | fn main() -> Outcome<()> { |
| 86 | |
| 87 | log_set_level!("debug"); |
| 88 | |
| 89 | let outcome = run_tests(); |
| 90 | |
| 91 | log_finish_wait!(); |
| 92 | |
| 93 | outcome |
| 94 | } |
| 95 | |
| 96 | fn 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. |
| 117 | struct 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. |
| 133 | fn 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. |
| 142 | fn 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. |
| 158 | fn 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. |
| 170 | fn 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. |
| 178 | fn 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. |
| 185 | struct 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. |
| 207 | fn 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. |
| 231 | struct 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. |
| 251 | fn 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. |
| 273 | fn 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. |
| 296 | static 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. |
| 694 | static 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. |
| 933 | static 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. |
| 993 | fn 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 | |
| 1090 | fn 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 | |
| 1244 | fn 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 | |
| 1384 | fn 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 | |
| 1439 | fn 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 |
| 1487 | z")); |
| 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 | |
| 1627 | fn 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 | |
| 1686 | fn 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. |
| 1794 | fn 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 | } |