use std::fmt::Write as _;
use alacritty_terminal::{
Term,
grid::{Dimensions, Row},
index::{Column, Line},
term::{
TermMode,
cell::{Cell, Flags},
},
vte::ansi::{Color, NamedColor},
};
const STYLE_FLAGS: Flags = Flags::BOLD
.union(Flags::DIM)
.union(Flags::ITALIC)
.union(Flags::INVERSE)
.union(Flags::HIDDEN)
.union(Flags::STRIKEOUT)
.union(Flags::ALL_UNDERLINES);
#[derive(PartialEq)]
struct Sgr {
fg: Color,
bg: Color,
flags: Flags,
underline: Option<Color>,
}
impl Sgr {
fn reset() -> Self {
Self {
fg: Color::Named(NamedColor::Foreground),
bg: Color::Named(NamedColor::Background),
flags: Flags::empty(),
underline: None,
}
}
fn of(cell: &Cell) -> Self {
Self {
fg: cell.fg,
bg: cell.bg,
flags: cell.flags.intersection(STYLE_FLAGS),
underline: cell.underline_color(),
}
}
}
pub fn formatted<T>(term: &Term<T>) -> (Vec<u8>, (u16, u16), bool) {
let grid = term.grid();
let cols = grid.columns();
let offset = grid.display_offset() as i32;
let mut buf = String::new();
buf.push_str("\x1b[0m");
let mut state = Sgr::reset();
for row in 0..grid.screen_lines() {
let line = &grid[Line(row as i32 - offset)];
cup(&mut buf, row, 0);
let mut col = 0;
while col < cols {
let cell = &line[Column(col)];
if paired_spacer(line, col) {
col += 1;
continue;
}
sync_sgr(&mut buf, &mut state, cell);
if paired_wide(line, col, cols) {
push_char(&mut buf, cell);
col += 2;
continue;
}
if cell
.flags
.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
{
buf.push(' ');
col += 1;
continue;
}
if cell.c == '\t' {
buf.push(' ');
cup(&mut buf, row, col);
buf.push('\t');
cup(&mut buf, row, col + 1);
push_zerowidth(&mut buf, cell);
col += 1;
continue;
}
push_char(&mut buf, cell);
col += 1;
}
}
let cursor = &grid.cursor;
let point = cursor.point;
if offset == 0 && cursor.input_needs_wrap {
let last = cols - 1;
let line = &grid[point.line];
let (base_col, base) = if paired_spacer(line, last) {
(last - 1, &line[Column(last - 1)])
} else {
(last, &line[Column(last)])
};
cup(&mut buf, point.line.0.max(0) as usize, base_col);
sync_sgr(&mut buf, &mut state, base);
let unwritable =
base.c == '\t' || (base_col == last && base.flags.contains(Flags::WIDE_CHAR));
if unwritable {
buf.push(' ');
} else {
buf.push(base.c);
}
push_zerowidth(&mut buf, base);
} else {
cup(&mut buf, point.line.0.max(0) as usize, point.column.0);
}
buf.push_str("\x1b[0m");
buf.push_str(if term.mode().contains(TermMode::SHOW_CURSOR) {
"\x1b[?25h"
} else {
"\x1b[?25l"
});
(
buf.into_bytes(),
(point.line.0.max(0) as u16, point.column.0 as u16),
!term.mode().contains(TermMode::SHOW_CURSOR),
)
}
pub fn contents<T>(term: &Term<T>) -> String {
let grid = term.grid();
let cols = grid.columns();
let offset = grid.display_offset() as i32;
let mut out = String::new();
for row in 0..grid.screen_lines() {
if row > 0 {
out.push('\n');
}
let row_start = out.len();
let line = &grid[Line(row as i32 - offset)];
for col in 0..cols {
let cell = &line[Column(col)];
if paired_spacer(line, col) {
continue;
}
if cell.c == '\t'
|| (!paired_wide(line, col, cols)
&& cell
.flags
.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER))
{
out.push(' ');
} else {
out.push(cell.c);
}
push_zerowidth(&mut out, cell);
}
while out.len() > row_start && out.ends_with(' ') {
out.pop();
}
}
out
}
fn cup(buf: &mut String, row: usize, col: usize) {
let _ = write!(buf, "\x1b[{};{}H", row + 1, col + 1);
}
fn paired_spacer(line: &Row<Cell>, col: usize) -> bool {
line[Column(col)].flags.contains(Flags::WIDE_CHAR_SPACER)
&& col > 0
&& line[Column(col - 1)].flags.contains(Flags::WIDE_CHAR)
}
fn paired_wide(line: &Row<Cell>, col: usize, cols: usize) -> bool {
line[Column(col)].flags.contains(Flags::WIDE_CHAR)
&& col + 1 < cols
&& line[Column(col + 1)]
.flags
.contains(Flags::WIDE_CHAR_SPACER)
}
fn push_char(buf: &mut String, cell: &Cell) {
buf.push(cell.c);
push_zerowidth(buf, cell);
}
fn push_zerowidth(buf: &mut String, cell: &Cell) {
if let Some(zerowidth) = cell.zerowidth() {
buf.extend(zerowidth.iter());
}
}
fn sync_sgr(buf: &mut String, state: &mut Sgr, cell: &Cell) {
let want = Sgr::of(cell);
if *state == want {
return;
}
buf.push_str("\x1b[0");
if want.flags.contains(Flags::BOLD) {
buf.push_str(";1");
}
if want.flags.contains(Flags::DIM) {
buf.push_str(";2");
}
if want.flags.contains(Flags::ITALIC) {
buf.push_str(";3");
}
if want.flags.contains(Flags::UNDERLINE) {
buf.push_str(";4");
} else if want.flags.contains(Flags::DOUBLE_UNDERLINE) {
buf.push_str(";4:2");
} else if want.flags.contains(Flags::UNDERCURL) {
buf.push_str(";4:3");
} else if want.flags.contains(Flags::DOTTED_UNDERLINE) {
buf.push_str(";4:4");
} else if want.flags.contains(Flags::DASHED_UNDERLINE) {
buf.push_str(";4:5");
}
if want.flags.contains(Flags::INVERSE) {
buf.push_str(";7");
}
if want.flags.contains(Flags::HIDDEN) {
buf.push_str(";8");
}
if want.flags.contains(Flags::STRIKEOUT) {
buf.push_str(";9");
}
push_color(buf, want.fg, 30);
push_color(buf, want.bg, 40);
if let Some(color) = want.underline {
push_underline_color(buf, color);
}
buf.push('m');
*state = want;
}
fn push_color(buf: &mut String, color: Color, base: u16) {
let base = base as usize;
match color {
Color::Named(named) => match named as usize {
i @ 0..=7 => {
let _ = write!(buf, ";{}", base + i);
}
i @ 8..=15 => {
let _ = write!(buf, ";{}", base + 60 + i - 8);
}
i @ 259..=266 => {
let _ = write!(buf, ";{}", base + i - 259);
}
_ => {}
},
Color::Indexed(i) => {
let _ = write!(buf, ";{};5;{i}", base + 8);
}
Color::Spec(rgb) => {
let _ = write!(buf, ";{};2;{};{};{}", base + 8, rgb.r, rgb.g, rgb.b);
}
}
}
fn push_underline_color(buf: &mut String, color: Color) {
match color {
Color::Indexed(i) => {
let _ = write!(buf, ";58;5;{i}");
}
Color::Spec(rgb) => {
let _ = write!(buf, ";58;2;{};{};{}", rgb.r, rgb.g, rgb.b);
}
Color::Named(_) => {}
}
}
#[cfg(test)]
mod tests {
use alacritty_terminal::{
event::VoidListener,
grid::Scroll,
term::{Config, test::TermSize},
vte::ansi::Processor,
};
use super::*;
use crate::testutil::{CORPUS_COLS, CORPUS_LINES};
const WRAP_ARTIFACTS: Flags = Flags::WRAPLINE.union(Flags::LEADING_WIDE_CHAR_SPACER);
fn new_term(lines: usize, cols: usize) -> Term<VoidListener> {
Term::new(Config::default(), &TermSize::new(cols, lines), VoidListener)
}
fn parse(bytes: &[u8], lines: usize, cols: usize) -> Term<VoidListener> {
let mut term = new_term(lines, cols);
let mut parser: Processor = Processor::new();
parser.advance(&mut term, bytes);
term
}
fn assert_same_screen(source: &Term<VoidListener>, replay: &Term<VoidListener>, case: &str) {
let sgrid = source.grid();
let rgrid = replay.grid();
assert_eq!(
sgrid.screen_lines(),
rgrid.screen_lines(),
"{case}: line count"
);
assert_eq!(sgrid.columns(), rgrid.columns(), "{case}: column count");
let cols = sgrid.columns();
let offset = sgrid.display_offset() as i32;
let cursor = &sgrid.cursor;
for row in 0..sgrid.screen_lines() {
let sline = &sgrid[Line(row as i32 - offset)];
let rline = &rgrid[Line(row as i32)];
for col in 0..cols {
let s = &sline[Column(col)];
let r = &rline[Column(col)];
let paired_spacer = paired_spacer(sline, col);
let paired_wide = paired_wide(sline, col, cols);
let orphan_wide = s.flags.contains(Flags::WIDE_CHAR) && !paired_wide;
let orphan_spacer = s.flags.contains(Flags::WIDE_CHAR_SPACER) && !paired_spacer;
let phantom_tab = offset == 0
&& cursor.input_needs_wrap
&& cursor.point.line == Line(row as i32)
&& col == cols - 1
&& s.c == '\t';
let expected_c = if orphan_wide || orphan_spacer || phantom_tab {
' '
} else {
s.c
};
let mut mask = WRAP_ARTIFACTS.complement();
if orphan_wide {
mask.remove(Flags::WIDE_CHAR);
}
if orphan_spacer {
mask.remove(Flags::WIDE_CHAR_SPACER);
}
let expected_flags = s.flags.intersection(mask);
let expected_zerowidth: &[char] = if orphan_wide {
&[]
} else {
s.zerowidth().unwrap_or(&[])
};
assert_eq!(r.c, expected_c, "{case}: char at ({row},{col})");
assert_eq!(r.fg, s.fg, "{case}: fg at ({row},{col})");
assert_eq!(r.bg, s.bg, "{case}: bg at ({row},{col})");
assert_eq!(r.flags, expected_flags, "{case}: flags at ({row},{col})");
assert_eq!(
r.zerowidth().unwrap_or(&[]),
expected_zerowidth,
"{case}: zerowidth at ({row},{col})"
);
assert_eq!(
r.underline_color(),
s.underline_color(),
"{case}: underline color at ({row},{col})"
);
}
}
}
fn round_trip(source: &Term<VoidListener>, case: &str) -> Term<VoidListener> {
let (bytes, pos, hidden) = formatted(source);
let mut replay = new_term(source.grid().screen_lines(), source.grid().columns());
let mut parser: Processor = Processor::new();
parser.advance(&mut replay, &bytes);
assert_same_screen(source, &replay, case);
let scursor = &source.grid().cursor;
let rcursor = &replay.grid().cursor;
assert_eq!(
pos,
(
scursor.point.line.0.max(0) as u16,
scursor.point.column.0 as u16
),
"{case}: returned cursor tuple"
);
assert_eq!(
hidden,
!source.mode().contains(TermMode::SHOW_CURSOR),
"{case}: returned visibility"
);
assert_eq!(
replay.mode().contains(TermMode::SHOW_CURSOR),
source.mode().contains(TermMode::SHOW_CURSOR),
"{case}: replayed cursor visibility"
);
assert_eq!(
rcursor.point, scursor.point,
"{case}: replayed cursor position"
);
if source.grid().display_offset() == 0 {
assert_eq!(
rcursor.input_needs_wrap, scursor.input_needs_wrap,
"{case}: replayed pending-wrap state"
);
}
assert_eq!(contents(source), contents(&replay), "{case}: contents");
replay
}
macro_rules! corpus_oracle {
($name:ident, $file:literal) => {
#[test]
fn $name() {
let source = parse(
include_bytes!(concat!("../tests/corpus/", $file)),
CORPUS_LINES,
CORPUS_COLS,
);
round_trip(&source, $file);
}
};
}
corpus_oracle!(corpus_codex_resume, "codex_resume.bin");
corpus_oracle!(corpus_tmux_split, "tmux_split.bin");
corpus_oracle!(corpus_vim_session, "vim_session.bin");
corpus_oracle!(corpus_less_altscreen, "less_altscreen.bin");
corpus_oracle!(corpus_top_live, "top_live.bin");
corpus_oracle!(corpus_shell_colors, "shell_colors.bin");
corpus_oracle!(corpus_build_log, "build_log.bin");
corpus_oracle!(corpus_wide_emoji, "wide_emoji.bin");
corpus_oracle!(corpus_dec_scrollregion, "dec_scrollregion.bin");
#[test]
fn corpus_scrolled_viewport() {
let mut source = parse(
include_bytes!("../tests/corpus/codex_resume.bin"),
CORPUS_LINES,
CORPUS_COLS,
);
source.scroll_display(Scroll::Delta(10));
assert!(
source.grid().display_offset() > 0,
"premise: fixture retains scrollback to scroll into"
);
round_trip(&source, "codex_resume.bin scrolled");
source.scroll_display(Scroll::Top);
round_trip(&source, "codex_resume.bin scrolled to top");
}
#[test]
fn sgr_reset_boundaries() {
let source = parse(
b"\x1b[1;31;44mred on blue\x1b[0m plain\r\n\x1b[7mreverse to eol",
4,
20,
);
assert_eq!(
source.grid()[Line(0)][Column(11)].fg,
Color::Named(NamedColor::Foreground),
"premise: SGR 0 restored default fg"
);
round_trip(&source, "sgr_reset_boundaries");
}
#[test]
fn default_color_restoration() {
let source = parse(b"\x1b[31;44mA\x1b[39mB\x1b[49mC", 2, 10);
let cell = &source.grid()[Line(0)][Column(1)];
assert_eq!(
cell.fg,
Color::Named(NamedColor::Foreground),
"premise: 39 reset fg"
);
assert_eq!(
cell.bg,
Color::Named(NamedColor::Blue),
"premise: bg survived 39"
);
round_trip(&source, "default_color_restoration");
}
#[test]
fn wide_char_spacers_not_double_emitted() {
let source = parse("abæ¼¢å—c🙂d".as_bytes(), 2, 20);
assert!(
source.grid()[Line(0)][Column(3)]
.flags
.contains(Flags::WIDE_CHAR_SPACER),
"premise: spacer follows the wide glyph"
);
round_trip(&source, "wide_char_spacers_not_double_emitted");
}
#[test]
fn wide_char_ending_in_last_column() {
let mut bytes = b"\x1b[1;9H".to_vec();
bytes.extend("æ¼¢".as_bytes());
let source = parse(&bytes, 3, 10);
assert!(
source.grid()[Line(0)][Column(9)]
.flags
.contains(Flags::WIDE_CHAR_SPACER),
"premise: spacer in the last column"
);
assert!(
source.grid().cursor.input_needs_wrap,
"premise: pending wrap over the spacer"
);
round_trip(&source, "wide_char_ending_in_last_column");
}
#[test]
fn wide_char_wrapped_from_last_column() {
let mut bytes = b"\x1b[1;10H".to_vec();
bytes.extend("æ¼¢".as_bytes());
let source = parse(&bytes, 3, 10);
assert!(
source.grid()[Line(0)][Column(9)]
.flags
.contains(Flags::LEADING_WIDE_CHAR_SPACER),
"premise: leading spacer left behind"
);
assert_eq!(
source.grid()[Line(1)][Column(0)].c,
'æ¼¢',
"premise: glyph wrapped"
);
round_trip(&source, "wide_char_wrapped_from_last_column");
}
#[test]
fn zerowidth_combining_marks() {
let source = parse(
"e\u{301}x æ¼¢\u{20d7} a\u{300}\u{301}\u{308}".as_bytes(),
2,
20,
);
assert_eq!(
source.grid()[Line(0)][Column(0)].zerowidth(),
Some(&['\u{301}'][..]),
"premise: mark stored as cell extra"
);
assert_eq!(
source.grid()[Line(0)][Column(3)].zerowidth(),
Some(&['\u{20d7}'][..]),
"premise: mark attached to the wide cell, not its spacer"
);
round_trip(&source, "zerowidth_combining_marks");
}
#[test]
fn erased_with_attrs_styled_blanks() {
let source = parse(b"\x1b[44m\x1b[2J\x1b[3;3Hx\x1b[45m\x1b[K", 6, 12);
assert_eq!(
source.grid()[Line(5)][Column(11)].bg,
Color::Named(NamedColor::Blue),
"premise: ED filled with colored background"
);
assert_eq!(
source.grid()[Line(2)][Column(5)].bg,
Color::Named(NamedColor::Magenta),
"premise: EL filled with a different background"
);
round_trip(&source, "erased_with_attrs_styled_blanks");
}
#[test]
fn cursor_phantom_column() {
let source = parse(b"0123456789", 3, 10);
assert!(
source.grid().cursor.input_needs_wrap,
"premise: pending wrap set"
);
let replay = round_trip(&source, "cursor_phantom_column");
assert_eq!(
replay.grid()[Line(1)][Column(0)].c,
' ',
"no wrap during replay"
);
}
#[test]
fn cursor_position_and_visibility() {
let hidden = parse(b"hello\x1b[?25l\x1b[2;4H", 4, 10);
let (_, pos, hide) = formatted(&hidden);
assert_eq!(pos, (1, 3));
assert!(hide);
round_trip(&hidden, "cursor_hidden");
let visible = parse(b"hello\x1b[3;2H", 4, 10);
let (_, _, hide) = formatted(&visible);
assert!(!hide);
round_trip(&visible, "cursor_visible");
}
#[test]
fn colors_named_and_bright() {
let mut bytes = Vec::new();
for i in 30..=37 {
bytes.extend(format!("\x1b[{i}mA").into_bytes());
}
for i in 90..=97 {
bytes.extend(format!("\x1b[{i}mB").into_bytes());
}
bytes.extend(b"\r\n");
for i in 40..=47 {
bytes.extend(format!("\x1b[{i}mC").into_bytes());
}
for i in 100..=107 {
bytes.extend(format!("\x1b[{i}mD").into_bytes());
}
let source = parse(&bytes, 3, 20);
round_trip(&source, "colors_named_and_bright");
}
#[test]
fn colors_indexed_256() {
let mut bytes = Vec::new();
for i in [0u8, 7, 15, 16, 123, 231, 232, 255] {
bytes.extend(format!("\x1b[38;5;{i}mx\x1b[48;5;{i}my").into_bytes());
}
let source = parse(&bytes, 2, 20);
round_trip(&source, "colors_indexed_256");
}
#[test]
fn colors_rgb() {
let source = parse(
b"\x1b[38;2;1;2;3mA\x1b[48;2;250;128;0mB\x1b[38;2;255;255;255;48;2;0;0;0mC",
2,
10,
);
round_trip(&source, "colors_rgb");
}
#[test]
fn attributes_each() {
let cases: &[(&str, Flags)] = &[
("1", Flags::BOLD),
("2", Flags::DIM),
("3", Flags::ITALIC),
("4", Flags::UNDERLINE),
("4:2", Flags::DOUBLE_UNDERLINE),
("4:3", Flags::UNDERCURL),
("4:4", Flags::DOTTED_UNDERLINE),
("4:5", Flags::DASHED_UNDERLINE),
("7", Flags::INVERSE),
("8", Flags::HIDDEN),
("9", Flags::STRIKEOUT),
];
let mut bytes = Vec::new();
for (sgr, _) in cases {
bytes.extend(format!("\x1b[{sgr}mA\x1b[0m").into_bytes());
}
let source = parse(&bytes, 2, 20);
for (col, (sgr, flag)) in cases.iter().enumerate() {
assert!(
source.grid()[Line(0)][Column(col)].flags.contains(*flag),
"premise: SGR {sgr} stored its flag"
);
}
round_trip(&source, "attributes_each");
}
#[test]
fn attr_blink_not_modeled() {
let source = parse(b"\x1b[5mslow\x1b[6mfast\x1b[25m off", 2, 20);
assert_eq!(
source.grid()[Line(0)][Column(0)].flags,
Flags::empty(),
"premise: backend dropped blink entirely"
);
round_trip(&source, "attr_blink_not_modeled");
}
#[test]
fn underline_color_extras() {
let source = parse(b"\x1b[4;58;5;99mA\x1b[4;58;2;10;20;30mB\x1b[59mC", 2, 10);
assert_eq!(
source.grid()[Line(0)][Column(0)].underline_color(),
Some(Color::Indexed(99)),
"premise: underline color stored as extra"
);
round_trip(&source, "underline_color_extras");
}
#[test]
fn tab_cells_keep_erased_attrs() {
let source = parse(b"\x1b[44m\x1b[2J\x1b[Ha\tb", 3, 20);
assert_eq!(
source.grid()[Line(0)][Column(1)].c,
'\t',
"premise: tab stored in cell"
);
assert_eq!(
source.grid()[Line(0)][Column(1)].bg,
Color::Named(NamedColor::Blue),
"premise: tab cell kept the BCE background"
);
round_trip(&source, "tab_cells_keep_erased_attrs");
}
#[test]
fn dec_line_drawing_charset() {
let source = parse(b"\x1b(0lqk\x1b(B done", 2, 12);
assert_eq!(
source.grid()[Line(0)][Column(0)].c,
'┌',
"premise: charset translated"
);
round_trip(&source, "dec_line_drawing_charset");
}
#[test]
fn alt_screen_visible_grid() {
let source = parse(b"primary\x1b[?1049h\x1b[2;2Halt content\x1b[31mred", 4, 20);
assert!(
source.mode().contains(TermMode::ALT_SCREEN),
"premise: on alt screen"
);
round_trip(&source, "alt_screen_visible_grid");
}
#[test]
fn orphan_spacer_after_ech() {
let mut bytes = "æ¼¢".as_bytes().to_vec();
bytes.extend(b"\x1b[1;1H\x1b[1X");
let source = parse(&bytes, 2, 10);
let line = &source.grid()[Line(0)];
assert!(
line[Column(1)].flags.contains(Flags::WIDE_CHAR_SPACER)
&& !line[Column(0)].flags.contains(Flags::WIDE_CHAR),
"premise: ECH orphaned the spacer"
);
round_trip(&source, "orphan_spacer_after_ech");
}
#[test]
fn orphan_wide_after_ech() {
let mut bytes = "æ¼¢".as_bytes().to_vec();
bytes.extend(b"\x1b[1;2H\x1b[1X");
let source = parse(&bytes, 2, 10);
let line = &source.grid()[Line(0)];
assert!(
line[Column(0)].flags.contains(Flags::WIDE_CHAR)
&& !line[Column(1)].flags.contains(Flags::WIDE_CHAR_SPACER),
"premise: ECH orphaned the wide glyph"
);
round_trip(&source, "orphan_wide_after_ech");
}
#[test]
fn contents_plain_text() {
let source = parse("one\r\ntwo æ¼¢\u{301}å—\r\n\tx".as_bytes(), 4, 12);
assert_eq!(contents(&source), "one\ntwo æ¼¢\u{301}å—\n x\n");
}
struct XorShift64(u64);
impl XorShift64 {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn escape_soup(seed: u64, tokens: usize, lines: u64, cols: u64) -> Vec<u8> {
const WIDE: [char; 6] = ['æ¼¢', 'å—', '🙂', '🚀', 'ä¸', 'æ–‡'];
const COMBINING: [char; 4] = ['\u{300}', '\u{301}', '\u{308}', '\u{20d7}'];
const SGR: [&str; 30] = [
"0", "1", "2", "3", "4", "4:2", "4:3", "4:4", "4:5", "5", "7", "8", "9", "21", "22",
"23", "24", "25", "27", "28", "29", "31", "33", "35", "37", "39", "44", "49", "97",
"104",
];
let mut rng = XorShift64(seed);
let mut out = String::new();
for _ in 0..tokens {
match rng.below(24) {
0..=6 => {
for _ in 0..=rng.below(6) {
out.push((b' ' + rng.below(95) as u8) as char);
}
}
7 => out.push(WIDE[rng.below(WIDE.len() as u64) as usize]),
8 => out.push(COMBINING[rng.below(COMBINING.len() as u64) as usize]),
9..=11 => {
let _ = write!(out, "\x1b[{}m", SGR[rng.below(SGR.len() as u64) as usize]);
}
12 => {
let _ = match rng.below(5) {
0 => write!(out, "\x1b[38;5;{}m", rng.below(256)),
1 => write!(out, "\x1b[48;5;{}m", rng.below(256)),
2 => write!(
out,
"\x1b[38;2;{};{};{}m",
rng.below(256),
rng.below(256),
rng.below(256)
),
3 => write!(
out,
"\x1b[48;2;{};{};{}m",
rng.below(256),
rng.below(256),
rng.below(256)
),
_ => write!(out, "\x1b[58;5;{}m", rng.below(256)),
};
}
13..=15 => match rng.below(8) {
0 => {
let _ = write!(
out,
"\x1b[{};{}H",
1 + rng.below(lines),
1 + rng.below(cols)
);
}
1 => {
let _ = write!(out, "\x1b[{}A", 1 + rng.below(4));
}
2 => {
let _ = write!(out, "\x1b[{}B", 1 + rng.below(4));
}
3 => {
let _ = write!(out, "\x1b[{}C", 1 + rng.below(8));
}
4 => {
let _ = write!(out, "\x1b[{}D", 1 + rng.below(8));
}
5 => out.push('\r'),
6 => out.push('\n'),
_ => out.push('\x08'),
},
16 => {
let _ = write!(out, "\x1b[{}J", rng.below(3));
}
17 => {
let _ = write!(out, "\x1b[{}K", rng.below(3));
}
18 => {
let n = 1 + rng.below(4);
let _ = match rng.below(3) {
0 => write!(out, "\x1b[{n}X"),
1 => write!(out, "\x1b[{n}P"),
_ => write!(out, "\x1b[{n}@"),
};
}
19 => {
let _ = match rng.below(4) {
0 => {
let top = 1 + rng.below(lines / 2);
write!(out, "\x1b[{};{}r", top, top + rng.below(lines / 2))
}
1 => write!(out, "\x1b[{}S", 1 + rng.below(3)),
2 => write!(out, "\x1b[{}T", 1 + rng.below(3)),
_ => write!(out, "\x1bM"),
};
}
20 => out.push_str(if rng.below(2) == 0 { "\x1b7" } else { "\x1b8" }),
21 => {
let mode = ["?25", "?7", "?6"][rng.below(3) as usize];
let hl = if rng.below(2) == 0 { 'h' } else { 'l' };
let _ = write!(out, "\x1b[{mode}{hl}");
}
22 => {
let n = 1 + rng.below(3);
let _ = if rng.below(2) == 0 {
write!(out, "\x1b[{n}L")
} else {
write!(out, "\x1b[{n}M")
};
}
_ => {
if rng.below(2) == 0 {
out.push('\x1b');
} else {
let _ = write!(out, "\x1b[{}", rng.below(100));
}
}
}
}
out.push('\t');
out.into_bytes()
}
#[test]
fn escape_soup_round_trip() {
let mut wide_cells = 0usize;
let mut colored_cells = 0usize;
for seed in [
0x9e3779b97f4a7c15u64,
0xdeadbeefcafef00d,
0x0123456789abcdef,
] {
let bytes = escape_soup(seed, 4000, 24, 80);
let source = parse(&bytes, 24, 80);
for row in 0..24 {
let line = &source.grid()[Line(row)];
for col in 0..80 {
let cell = &line[Column(col)];
wide_cells += usize::from(cell.flags.contains(Flags::WIDE_CHAR));
colored_cells += usize::from(cell.fg != Color::Named(NamedColor::Foreground));
}
}
round_trip(&source, &format!("escape_soup seed {seed:#x}"));
}
assert!(wide_cells > 0, "soup produced no wide cells");
assert!(colored_cells > 0, "soup produced no colored cells");
}
}