max / shop
- Co-Authored-By
- Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 file changed,
+426 insertions,
-56 deletions
| @@ -209,53 +209,146 @@ | |||
| 209 | 209 | } | |
| 210 | 210 | ||
| 211 | 211 | fn apply_sgr(&mut self, params: &Params) { | |
| 212 | - | // Flatten all subparameter groups into one stream so both the | |
| 213 | - | // xterm-style `38;2;R;G;B` and the ITU-T `38:2::R:G:B` forms work. | |
| 214 | - | let flat: Vec<u16> = params.iter().flat_map(|p| p.iter().copied()).collect(); | |
| 212 | + | // Bare `\e[m` is the same as `\e[0m` — full reset. | |
| 213 | + | if params.is_empty() { | |
| 214 | + | self.pending_fg = Color::Default; | |
| 215 | + | self.pending_bg = Color::Default; | |
| 216 | + | self.pending_attrs = Attrs::default(); | |
| 217 | + | return; | |
| 218 | + | } | |
| 219 | + | ||
| 220 | + | // Collect param-group references so we can distinguish colon-form | |
| 221 | + | // (subparams grouped) from semicolon-form (separate groups). | |
| 222 | + | let groups: Vec<&[u16]> = params.iter().collect(); | |
| 215 | 223 | let mut i = 0; | |
| 216 | - | while i < flat.len() { | |
| 217 | - | let p = flat[i]; | |
| 218 | - | match p { | |
| 219 | - | 0 => { | |
| 220 | - | self.pending_fg = Color::Default; | |
| 221 | - | self.pending_bg = Color::Default; | |
| 222 | - | self.pending_attrs = Attrs::default(); | |
| 223 | - | } | |
| 224 | - | 1 => self.pending_attrs.bold = true, | |
| 225 | - | 2 => self.pending_attrs.dim = true, | |
| 226 | - | 3 => self.pending_attrs.italic = true, | |
| 227 | - | 4 => self.pending_attrs.underline = true, | |
| 228 | - | 7 => self.pending_attrs.reverse = true, | |
| 229 | - | 9 => self.pending_attrs.strikethrough = true, | |
| 230 | - | 22 => { | |
| 231 | - | self.pending_attrs.bold = false; | |
| 232 | - | self.pending_attrs.dim = false; | |
| 233 | - | } | |
| 234 | - | 23 => self.pending_attrs.italic = false, | |
| 235 | - | 24 => self.pending_attrs.underline = false, | |
| 236 | - | 27 => self.pending_attrs.reverse = false, | |
| 237 | - | 29 => self.pending_attrs.strikethrough = false, | |
| 238 | - | 30..=37 => self.pending_fg = Color::Named((p - 30) as u8), | |
| 239 | - | 38 => { | |
| 240 | - | if let Some((c, consumed)) = parse_extended_color(&flat[i + 1..]) { | |
| 241 | - | self.pending_fg = c; | |
| 242 | - | i += consumed; | |
| 243 | - | } | |
| 244 | - | } | |
| 245 | - | 39 => self.pending_fg = Color::Default, | |
| 246 | - | 40..=47 => self.pending_bg = Color::Named((p - 40) as u8), | |
| 247 | - | 48 => { | |
| 248 | - | if let Some((c, consumed)) = parse_extended_color(&flat[i + 1..]) { | |
| 249 | - | self.pending_bg = c; | |
| 250 | - | i += consumed; | |
| 251 | - | } | |
| 252 | - | } | |
| 253 | - | 49 => self.pending_bg = Color::Default, | |
| 254 | - | 90..=97 => self.pending_fg = Color::Named(((p - 90) + 8) as u8), | |
| 255 | - | 100..=107 => self.pending_bg = Color::Named(((p - 100) + 8) as u8), | |
| 256 | - | _ => {} | |
| 224 | + | while i < groups.len() { | |
| 225 | + | let group = groups[i]; | |
| 226 | + | if group.len() > 1 { | |
| 227 | + | // Colon form — the whole group is one logical SGR. | |
| 228 | + | self.apply_colon_group(group); | |
| 229 | + | i += 1; | |
| 230 | + | continue; | |
| 257 | 231 | } | |
| 258 | - | i += 1; | |
| 232 | + | let p = group.first().copied().unwrap_or(0); | |
| 233 | + | i += self.apply_single(p, &groups, i); | |
| 234 | + | } | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | /// Semicolon-form: `p` came from a single-subparam group. Returns how many | |
| 238 | + | /// group indices were consumed (usually 1, or up to 5 for extended color). | |
| 239 | + | fn apply_single(&mut self, p: u16, groups: &[&[u16]], i: usize) -> usize { | |
| 240 | + | match p { | |
| 241 | + | 0 => { | |
| 242 | + | self.pending_fg = Color::Default; | |
| 243 | + | self.pending_bg = Color::Default; | |
| 244 | + | self.pending_attrs = Attrs::default(); | |
| 245 | + | 1 | |
| 246 | + | } | |
| 247 | + | 1 => { | |
| 248 | + | self.pending_attrs.bold = true; | |
| 249 | + | 1 | |
| 250 | + | } | |
| 251 | + | 2 => { | |
| 252 | + | self.pending_attrs.dim = true; | |
| 253 | + | 1 | |
| 254 | + | } | |
| 255 | + | 3 => { | |
| 256 | + | self.pending_attrs.italic = true; | |
| 257 | + | 1 | |
| 258 | + | } | |
| 259 | + | 4 => { | |
| 260 | + | self.pending_attrs.underline = true; | |
| 261 | + | 1 | |
| 262 | + | } | |
| 263 | + | 7 => { | |
| 264 | + | self.pending_attrs.reverse = true; | |
| 265 | + | 1 | |
| 266 | + | } | |
| 267 | + | 9 => { | |
| 268 | + | self.pending_attrs.strikethrough = true; | |
| 269 | + | 1 | |
| 270 | + | } | |
| 271 | + | 22 => { | |
| 272 | + | self.pending_attrs.bold = false; | |
| 273 | + | self.pending_attrs.dim = false; | |
| 274 | + | 1 | |
| 275 | + | } | |
| 276 | + | 23 => { | |
| 277 | + | self.pending_attrs.italic = false; | |
| 278 | + | 1 | |
| 279 | + | } | |
| 280 | + | 24 => { | |
| 281 | + | self.pending_attrs.underline = false; | |
| 282 | + | 1 | |
| 283 | + | } | |
| 284 | + | 27 => { | |
| 285 | + | self.pending_attrs.reverse = false; | |
| 286 | + | 1 | |
| 287 | + | } | |
| 288 | + | 29 => { | |
| 289 | + | self.pending_attrs.strikethrough = false; | |
| 290 | + | 1 | |
| 291 | + | } | |
| 292 | + | 30..=37 => { | |
| 293 | + | self.pending_fg = Color::Named((p - 30) as u8); | |
| 294 | + | 1 | |
| 295 | + | } | |
| 296 | + | 38 => { | |
| 297 | + | let (color, consumed) = take_semi_extended(groups, i + 1); | |
| 298 | + | if let Some(c) = color { | |
| 299 | + | self.pending_fg = c; | |
| 300 | + | } | |
| 301 | + | 1 + consumed | |
| 302 | + | } | |
| 303 | + | 39 => { | |
| 304 | + | self.pending_fg = Color::Default; | |
| 305 | + | 1 | |
| 306 | + | } | |
| 307 | + | 40..=47 => { | |
| 308 | + | self.pending_bg = Color::Named((p - 40) as u8); | |
| 309 | + | 1 | |
| 310 | + | } | |
| 311 | + | 48 => { | |
| 312 | + | let (color, consumed) = take_semi_extended(groups, i + 1); | |
| 313 | + | if let Some(c) = color { | |
| 314 | + | self.pending_bg = c; | |
| 315 | + | } | |
| 316 | + | 1 + consumed | |
| 317 | + | } | |
| 318 | + | 49 => { | |
| 319 | + | self.pending_bg = Color::Default; | |
| 320 | + | 1 | |
| 321 | + | } | |
| 322 | + | 90..=97 => { | |
| 323 | + | self.pending_fg = Color::Named(((p - 90) + 8) as u8); | |
| 324 | + | 1 | |
| 325 | + | } | |
| 326 | + | 100..=107 => { | |
| 327 | + | self.pending_bg = Color::Named(((p - 100) + 8) as u8); | |
| 328 | + | 1 | |
| 329 | + | } | |
| 330 | + | _ => 1, | |
| 331 | + | } | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | /// Colon-form group: leading value is the SGR code, subsequent subparams | |
| 335 | + | /// carry the extended-color payload. We only recognize 38 (fg) and 48 (bg) | |
| 336 | + | /// here — colon-form underline color is a follow-up. | |
| 337 | + | fn apply_colon_group(&mut self, group: &[u16]) { | |
| 338 | + | let leading = group[0]; | |
| 339 | + | let color = parse_colon_extended(&group[1..]); | |
| 340 | + | match leading { | |
| 341 | + | 38 => { | |
| 342 | + | if let Some(c) = color { | |
| 343 | + | self.pending_fg = c; | |
| 344 | + | } | |
| 345 | + | } | |
| 346 | + | 48 => { | |
| 347 | + | if let Some(c) = color { | |
| 348 | + | self.pending_bg = c; | |
| 349 | + | } | |
| 350 | + | } | |
| 351 | + | _ => {} | |
| 259 | 352 | } | |
| 260 | 353 | } | |
| 261 | 354 | ||
| @@ -372,18 +465,54 @@ | |||
| 372 | 465 | } | |
| 373 | 466 | } | |
| 374 | 467 | ||
| 375 | - | /// Parse the tail of a `38;…` or `48;…` extended-color SGR. Returns the | |
| 376 | - | /// parsed color and the number of parameters consumed *after* the leading | |
| 377 | - | /// `38`/`48`. Both `;5;N` (indexed) and `;2;R;G;B` (truecolor) supported. | |
| 378 | - | fn parse_extended_color(rest: &[u16]) -> Option<(Color, usize)> { | |
| 379 | - | match rest.first().copied()? { | |
| 380 | - | 5 => rest.get(1).map(|&i| (Color::Indexed(i.min(255) as u8), 2)), | |
| 381 | - | 2 => { | |
| 382 | - | let r = rest.get(1).copied().unwrap_or(0).min(255) as u8; | |
| 383 | - | let g = rest.get(2).copied().unwrap_or(0).min(255) as u8; | |
| 384 | - | let b = rest.get(3).copied().unwrap_or(0).min(255) as u8; | |
| 385 | - | Some((Color::Rgb(r, g, b), 4)) | |
| 468 | + | /// Parse extended color from the following semicolon-separated groups. Handles | |
| 469 | + | /// `2;R;G;B` (RGB) and `5;N` (indexed). Returns the parsed color and the | |
| 470 | + | /// number of groups consumed after the leading `38`/`48`. | |
| 471 | + | fn take_semi_extended(groups: &[&[u16]], start: usize) -> (Option<Color>, usize) { | |
| 472 | + | let Some(fmt) = groups.get(start).and_then(|g| g.first().copied()) else { | |
| 473 | + | return (None, 0); | |
| 474 | + | }; | |
| 475 | + | match fmt { | |
| 476 | + | 5 => { | |
| 477 | + | let Some(idx) = groups.get(start + 1).and_then(|g| g.first().copied()) else { | |
| 478 | + | return (None, 1); | |
| 479 | + | }; | |
| 480 | + | (Some(Color::Indexed(idx.min(255) as u8)), 2) | |
| 386 | 481 | } | |
| 482 | + | 2 => { | |
| 483 | + | let r = groups.get(start + 1).and_then(|g| g.first().copied()).unwrap_or(0); | |
| 484 | + | let g = groups.get(start + 2).and_then(|g| g.first().copied()).unwrap_or(0); | |
| 485 | + | let b = groups.get(start + 3).and_then(|g| g.first().copied()).unwrap_or(0); | |
| 486 | + | ( | |
| 487 | + | Some(Color::Rgb(r.min(255) as u8, g.min(255) as u8, b.min(255) as u8)), | |
| 488 | + | 4, | |
| 489 | + | ) | |
| 490 | + | } | |
| 491 | + | _ => (None, 1), | |
| 492 | + | } | |
| 493 | + | } | |
| 494 | + | ||
| 495 | + | /// Parse extended color from a colon-form subparam tail — the bytes after the | |
| 496 | + | /// leading `38`/`48`. `2;colorspace;R;G;B` (5 items) OR `2;R;G;B` (4 items) | |
| 497 | + | /// OR `5;N` (2 items). Colorspace slot is skipped when present. | |
| 498 | + | fn parse_colon_extended(rest: &[u16]) -> Option<Color> { | |
| 499 | + | match rest.first().copied()? { | |
| 500 | + | 5 => rest.get(1).map(|&i| Color::Indexed(i.min(255) as u8)), | |
| 501 | + | 2 => match rest.len() { | |
| 502 | + | // [2, colorspace, R, G, B] | |
| 503 | + | 5 => Some(Color::Rgb( | |
| 504 | + | rest[2].min(255) as u8, | |
| 505 | + | rest[3].min(255) as u8, | |
| 506 | + | rest[4].min(255) as u8, | |
| 507 | + | )), | |
| 508 | + | // [2, R, G, B] | |
| 509 | + | 4 => Some(Color::Rgb( | |
| 510 | + | rest[1].min(255) as u8, | |
| 511 | + | rest[2].min(255) as u8, | |
| 512 | + | rest[3].min(255) as u8, | |
| 513 | + | )), | |
| 514 | + | _ => None, | |
| 515 | + | }, | |
| 387 | 516 | _ => None, | |
| 388 | 517 | } | |
| 389 | 518 | } | |
| @@ -584,3 +713,337 @@ | |||
| 584 | 713 | } | |
| 585 | 714 | } | |
| 586 | 715 | } | |
| 716 | + | ||
| 717 | + | #[cfg(test)] | |
| 718 | + | mod tests { | |
| 719 | + | use super::*; | |
| 720 | + | use shop_vt::Parser; | |
| 721 | + | ||
| 722 | + | /// Feed `bytes` through a fresh Parser into `grid`. | |
| 723 | + | fn feed(grid: &mut Grid, bytes: &[u8]) { | |
| 724 | + | let mut p = Parser::new(); | |
| 725 | + | p.advance(grid, bytes); | |
| 726 | + | } | |
| 727 | + | ||
| 728 | + | fn row_str(grid: &Grid, r: u16) -> String { | |
| 729 | + | grid.row(r) | |
| 730 | + | .iter() | |
| 731 | + | .map(|c| c.c) | |
| 732 | + | .collect::<String>() | |
| 733 | + | .trim_end() | |
| 734 | + | .to_string() | |
| 735 | + | } | |
| 736 | + | ||
| 737 | + | fn assert_cursor(grid: &Grid, row: u16, col: u16) { | |
| 738 | + | let c = grid.cursor(); | |
| 739 | + | assert_eq!( | |
| 740 | + | (c.row, c.col), | |
| 741 | + | (row, col), | |
| 742 | + | "cursor mismatch (wrap_next={})", | |
| 743 | + | c.wrap_next | |
| 744 | + | ); | |
| 745 | + | } | |
| 746 | + | ||
| 747 | + | // ---- basics -------------------------------------------------------- | |
| 748 | + | ||
| 749 | + | #[test] | |
| 750 | + | fn new_grid_is_all_spaces() { | |
| 751 | + | let g = Grid::new(10, 3); | |
| 752 | + | for r in 0..3 { | |
| 753 | + | assert_eq!(row_str(&g, r), ""); | |
| 754 | + | } | |
| 755 | + | assert_cursor(&g, 0, 0); | |
| 756 | + | } | |
| 757 | + | ||
| 758 | + | #[test] | |
| 759 | + | fn print_advances_cursor() { | |
| 760 | + | let mut g = Grid::new(20, 3); | |
| 761 | + | feed(&mut g, b"hello"); | |
| 762 | + | assert_eq!(row_str(&g, 0), "hello"); | |
| 763 | + | assert_cursor(&g, 0, 5); | |
| 764 | + | } | |
| 765 | + | ||
| 766 | + | #[test] | |
| 767 | + | fn cr_lf_move_to_next_row_col_zero() { | |
| 768 | + | let mut g = Grid::new(20, 3); | |
| 769 | + | feed(&mut g, b"one\r\ntwo"); | |
| 770 | + | assert_eq!(row_str(&g, 0), "one"); | |
| 771 | + | assert_eq!(row_str(&g, 1), "two"); | |
| 772 | + | assert_cursor(&g, 1, 3); | |
| 773 | + | } | |
| 774 | + | ||
| 775 | + | #[test] | |
| 776 | + | fn backspace_moves_cursor_back() { | |
| 777 | + | let mut g = Grid::new(20, 3); | |
| 778 | + | feed(&mut g, b"abc\x08"); | |
| 779 | + | assert_cursor(&g, 0, 2); | |
| 780 | + | // BS is non-destructive — 'c' still there. | |
| 781 | + | assert_eq!(row_str(&g, 0), "abc"); | |
| 782 | + | } | |
| 783 | + | ||
| 784 | + | #[test] | |
| 785 | + | fn tab_advances_to_next_multiple_of_eight() { | |
| 786 | + | let mut g = Grid::new(40, 3); | |
| 787 | + | feed(&mut g, b"ab\t"); | |
| 788 | + | assert_cursor(&g, 0, 8); | |
| 789 | + | feed(&mut g, b"c"); | |
| 790 | + | assert_cursor(&g, 0, 9); | |
| 791 | + | } | |
| 792 | + | ||
| 793 | + | // ---- wrap ---------------------------------------------------------- | |
| 794 | + | ||
| 795 | + | #[test] | |
| 796 | + | fn deferred_wrap_on_rightmost_cell() { | |
| 797 | + | let mut g = Grid::new(4, 3); | |
| 798 | + | feed(&mut g, b"ABCD"); | |
| 799 | + | // After 4 chars in a 4-wide grid, cursor is at col 3 with wrap_next. | |
| 800 | + | assert_eq!(row_str(&g, 0), "ABCD"); | |
| 801 | + | let c = g.cursor(); | |
| 802 | + | assert!(c.wrap_next, "should have wrap_next set"); | |
| 803 | + | feed(&mut g, b"E"); | |
| 804 | + | // Next print wraps to row 1 col 0. | |
| 805 | + | assert_eq!(row_str(&g, 1), "E"); | |
| 806 | + | } | |
| 807 | + | ||
| 808 | + | // ---- CSI cursor motion -------------------------------------------- | |
| 809 | + | ||
| 810 | + | #[test] | |
| 811 | + | fn cup_moves_cursor_one_indexed() { | |
| 812 | + | let mut g = Grid::new(20, 5); | |
| 813 | + | feed(&mut g, b"\x1b[3;5H"); | |
| 814 | + | assert_cursor(&g, 2, 4); | |
| 815 | + | } | |
| 816 | + | ||
| 817 | + | #[test] | |
| 818 | + | fn cup_defaults_to_top_left() { | |
| 819 | + | let mut g = Grid::new(20, 5); | |
| 820 | + | feed(&mut g, b"aaa\r\nbbb\r\nccc\x1b[H"); | |
| 821 | + | assert_cursor(&g, 0, 0); | |
| 822 | + | } | |
| 823 | + | ||
| 824 | + | #[test] | |
| 825 | + | fn cuu_cud_cuf_cub_clamp_at_edges() { | |
| 826 | + | let mut g = Grid::new(20, 5); | |
| 827 | + | feed(&mut g, b"\x1b[100A"); // way up — should clamp at 0 | |
| 828 | + | assert_cursor(&g, 0, 0); | |
| 829 | + | feed(&mut g, b"\x1b[100B"); // way down — clamp at rows-1 | |
| 830 | + | assert_cursor(&g, 4, 0); | |
| 831 | + | feed(&mut g, b"\x1b[100C"); // way right — clamp at cols-1 | |
| 832 | + | assert_cursor(&g, 4, 19); | |
| 833 | + | feed(&mut g, b"\x1b[100D"); // way left — clamp at 0 | |
| 834 | + | assert_cursor(&g, 4, 0); | |
| 835 | + | } | |
| 836 | + | ||
| 837 | + | #[test] | |
| 838 | + | fn cha_and_vpa_position_absolutely() { | |
| 839 | + | let mut g = Grid::new(20, 5); | |
| 840 | + | feed(&mut g, b"\x1b[10G\x1b[3d"); | |
| 841 | + | assert_cursor(&g, 2, 9); | |
| 842 | + | } | |
| 843 | + | ||
| 844 | + | // ---- erase --------------------------------------------------------- | |
| 845 | + | ||
| 846 | + | #[test] | |
| 847 | + | fn el0_erases_cursor_to_end() { | |
| 848 | + | let mut g = Grid::new(10, 2); | |
| 849 | + | feed(&mut g, b"ABCDEFGHIJ\x1b[H\x1b[3C\x1b[K"); | |
| 850 | + | assert_eq!(row_str(&g, 0), "ABC"); | |
| 851 | + | } | |
| 852 | + | ||
| 853 | + | #[test] | |
| 854 | + | fn el1_erases_start_to_cursor() { | |
| 855 | + | let mut g = Grid::new(10, 2); | |
| 856 | + | feed(&mut g, b"ABCDEFGHIJ\x1b[H\x1b[3C\x1b[1K"); | |
| 857 | + | // Cells 0..=3 cleared, 4..=9 kept. | |
| 858 | + | assert_eq!(row_str(&g, 0), " EFGHIJ"); | |
| 859 | + | } | |
| 860 | + | ||
| 861 | + | #[test] | |
| 862 | + | fn el2_erases_whole_line() { | |
| 863 | + | let mut g = Grid::new(10, 2); | |
| 864 | + | feed(&mut g, b"ABCDEFGHIJ\x1b[H\x1b[3C\x1b[2K"); | |
| 865 | + | assert_eq!(row_str(&g, 0), ""); | |
| 866 | + | } | |
| 867 | + | ||
| 868 | + | #[test] | |
| 869 | + | fn ed2_erases_whole_screen() { | |
| 870 | + | let mut g = Grid::new(6, 3); | |
| 871 | + | feed(&mut g, b"aaaaaa\r\nbbbbbb\r\ncccccc\x1b[2J"); | |
| 872 | + | for r in 0..3 { | |
| 873 | + | assert_eq!(row_str(&g, r), ""); | |
| 874 | + | } | |
| 875 | + | } | |
| 876 | + | ||
| 877 | + | // ---- SGR ----------------------------------------------------------- | |
| 878 | + | ||
| 879 | + | #[test] | |
| 880 | + | fn sgr_named_fg_and_bg() { | |
| 881 | + | let mut g = Grid::new(10, 1); | |
| 882 | + | feed(&mut g, b"\x1b[31;44mA"); | |
| 883 | + | let cell = g.row(0)[0]; | |
| 884 | + | assert_eq!(cell.fg, Color::Named(1)); // red | |
| 885 | + | assert_eq!(cell.bg, Color::Named(4)); // blue | |
| 886 | + | } | |
| 887 | + | ||
| 888 | + | #[test] | |
| 889 | + | fn sgr_bright_named() { | |
| 890 | + | let mut g = Grid::new(10, 1); | |
| 891 | + | feed(&mut g, b"\x1b[92mA"); | |
| 892 | + | assert_eq!(g.row(0)[0].fg, Color::Named(10)); // bright green = 8+2 | |
| 893 | + | } | |
| 894 | + | ||
| 895 | + | #[test] | |
| 896 | + | fn sgr_indexed_256() { | |
| 897 | + | let mut g = Grid::new(10, 1); | |
| 898 | + | feed(&mut g, b"\x1b[38;5;123mA"); | |
| 899 | + | assert_eq!(g.row(0)[0].fg, Color::Indexed(123)); | |
| 900 | + | } | |
| 901 | + | ||
| 902 | + | #[test] | |
| 903 | + | fn sgr_truecolor_rgb() { | |
| 904 | + | let mut g = Grid::new(10, 1); | |
| 905 | + | feed(&mut g, b"\x1b[38;2;255;128;0mA"); | |
| 906 | + | assert_eq!(g.row(0)[0].fg, Color::Rgb(255, 128, 0)); | |
| 907 | + | } | |
| 908 | + | ||
| 909 | + | #[test] | |
| 910 | + | fn sgr_colon_subparam_rgb() { | |
| 911 | + | // The ITU-T `:` form: `\e[38:2::255:128:0m` — one param with | |
| 912 | + | // subparams. Our SGR parser flattens both forms. | |
| 913 | + | let mut g = Grid::new(10, 1); | |
| 914 | + | feed(&mut g, b"\x1b[38:2::255:128:0mA"); | |
| 915 | + | assert_eq!(g.row(0)[0].fg, Color::Rgb(255, 128, 0)); | |
| 916 | + | } | |
| 917 | + | ||
| 918 | + | #[test] | |
| 919 | + | fn sgr_attrs_bold_italic_underline() { | |
| 920 | + | let mut g = Grid::new(10, 1); | |
| 921 | + | feed(&mut g, b"\x1b[1;3;4mA"); | |
| 922 | + | let a = g.row(0)[0].attrs; | |
| 923 | + | assert!(a.bold && a.italic && a.underline); | |
| 924 | + | } | |
| 925 | + | ||
| 926 | + | #[test] | |
| 927 | + | fn sgr_reset_clears_everything() { | |
| 928 | + | let mut g = Grid::new(10, 1); | |
| 929 | + | feed(&mut g, b"\x1b[1;31;44mA\x1b[mB"); | |
| 930 | + | let a = g.row(0)[0]; | |
| 931 | + | let b = g.row(0)[1]; | |
| 932 | + | assert_eq!(a.fg, Color::Named(1)); | |
| 933 | + | assert!(a.attrs.bold); | |
| 934 | + | assert_eq!(b.fg, Color::Default); | |
| 935 | + | assert_eq!(b.bg, Color::Default); | |
| 936 | + | assert!(!b.attrs.bold); | |
| 937 | + | } | |
| 938 | + | ||
| 939 | + | #[test] | |
| 940 | + | fn sgr_selective_clears() { | |
| 941 | + | let mut g = Grid::new(10, 1); | |
| 942 | + | feed(&mut g, b"\x1b[1;3mA\x1b[22mB\x1b[23mC"); | |
| 943 | + | assert!(g.row(0)[0].attrs.bold && g.row(0)[0].attrs.italic); | |
| 944 | + | // 22 clears bold + dim; italic stays. | |
| 945 | + | assert!(!g.row(0)[1].attrs.bold && g.row(0)[1].attrs.italic); | |
| 946 | + | // 23 clears italic. | |
| 947 | + | assert!(!g.row(0)[2].attrs.italic); | |
| 948 | + | } | |
| 949 | + | ||
| 950 | + | // ---- cursor shape -------------------------------------------------- | |
| 951 | + | ||
| 952 | + | #[test] | |
| 953 | + | fn decscusr_sets_shape() { | |
| 954 | + | let mut g = Grid::new(10, 2); | |
| 955 | + | assert_eq!(g.cursor_shape(), CursorShape::Block); | |
| 956 | + | feed(&mut g, b"\x1b[3 q"); |
Lines truncated