use alacritty_terminal::{ event::VoidListener, index::Column, term::{Config, test::TermSize}, vte::ansi::Processor, }; use super::*; use crate::term::marks::{self, Mark}; /// What the parser gets from `chunks`, each one read. fn passed(chunks: &[&[u8]]) -> Vec { let mut prompts = Prompts::default(); let mut out = Vec::new(); for chunk in chunks { out.extend_from_slice(prompts.pass(chunk)); } out } /// A terminal `columns` wide and `lines` high after `bytes` passed through the rewrite. fn parsed(bytes: &[u8], columns: usize, lines: usize) -> Term { let mut term = Term::new( Config::default(), &TermSize::new(columns, lines), VoidListener, ); let mut parser: Processor = Processor::new(); parser.advance(&mut term, &passed(&[bytes])); term } /// The target of the link at a cell. fn target_of(term: &Term, line: i32, column: usize) -> Option { let cell = &term.grid()[Line(line)][Column(column)]; cell.hyperlink().map(|link| link.uri().to_string()) } #[test] fn a_mark_becomes_its_prompt_s_numbered_link() { assert_eq!( passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x1b\\x"]), b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;id=\x7f0i;\x7f\x07\x1b\\x" ); assert_eq!( passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x07ls\x1b]133;C\x07out"]), b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;id=\x7f0i;\x7f\x07ls\x1b]8;;\x07out", "the input runs from B to C" ); assert_eq!( passed(&[b"\x1b]133;A;k=i\x07\x1b]133;D;1\x18"]), b"\x1b]8;id=\x7f1;\x7f\x07\x1b]8;;\x07\x18" ); assert_eq!( passed(&[b"\x1b]133;N\x07\x1b]133;C\x07\x1b]133;A\x07"]), b"\x1b]8;id=\x7f1;\x7f\x07\x1b]8;;\x07\x1b]8;id=\x7f2;\x7f\x07" ); assert_eq!( passed(&[b"\x1b]133;D;0\x07"]), b"\x1b]133;D;0\x07", "an end with no prompt open passes" ); } #[test] fn the_rewrite_reads_a_mark_as_the_session_hears_it() { let bodies = [ "A", "A;k=i", "N", "B", "I", "C;x", "D;1", "D;abc", "D", "", "AB", "L", "P;k=i", "Q", "a", ]; for body in bodies { let heard = marks::read(body.as_bytes()).map(|mark| match mark { Mark::Prompt(_) => Edge::Prompt, Mark::Input => Edge::Input, Mark::Output | Mark::Done(_) => Edge::End, }); assert_eq!(mark(body.as_bytes()), heard, "{body}"); } } #[test] fn a_mark_split_across_reads_comes_out_whole() { let whole: &[u8] = b"a\x1b]133;A;k=i\x07\x1b]8;;file:///w\x1b\\w\x1b]8;;\x1b\\$ \x1b]133;B\x1b\\b"; let once = passed(&[whole]); for cut in 0..=whole.len() { let (first, second) = whole.split_at(cut); assert_eq!(passed(&[first, second]), once, "cut at {cut}"); } let bytes: Vec<&[u8]> = whole.chunks(1).collect(); assert_eq!(passed(&bytes), once); } #[test] fn other_sequences_pass_as_they_came() { for bytes in [ "\x1b]133;AB\x07x", "\x1b]1337;A\x07", "\x1b]133;P;k=i\x07", "\x1b]133;\x07", "\x1b]0;133;A\x07", "\x1b[31m\x1bc", "\x1b]8;;file:///x\x07x\x1b]8;;\x07", "text", ] { assert_eq!(passed(&[bytes.as_bytes()]), bytes.as_bytes(), "{bytes:?}"); } let mut long = b"\x1b]133;A;".to_vec(); long.extend([b'x'; MOST]); long.push(0x07); assert_eq!(passed(&[&long]), long, "past the scanner's bound"); let mut skipped = b"\x1b]133;A".to_vec(); skipped.extend([0; 3 * MOST]); skipped.push(0x07); assert_eq!( passed(&[&skipped]), skipped, "skipped controls bound the bytes held" ); } #[test] fn a_long_escape_of_controls_goes_on_as_it_comes() { let mut whole = vec![ESC]; whole.extend(vec![0; 3 << 20]); whole.extend_from_slice(b"[0mREADY"); let mut prompts = Prompts::default(); let mut out = Vec::new(); for chunk in whole.chunks(64 << 10) { out.extend_from_slice(prompts.pass(chunk)); assert!(prompts.held() <= 1, "only the ESC waits"); } assert_eq!(out.len(), whole.len()); assert!(out.ends_with(b"\x1b[0mREADY")); } #[test] fn a_program_s_link_inside_a_prompt_carries_its_number() { let whole = parsed( b"\x1b]133;A\x07\x1b]8;;file:///work\x1b\\/work $ \x1b]8;;\x1b\\\x1b]133;B\x07", 12, 2, ); assert_eq!( prompt(&whole.grid()[Line(0)]), Some(1), "a prompt all in a link" ); assert_eq!(target_of(&whole, 0, 6).as_deref(), Some("file:///work")); let first = b"\x1b]133;A\x07\x1b]8;;file:///w\x1b\\/work\r\n\x1b]8;;\x1b\\$ \x1b]133;B\x07\r\no\r\no\r\no"; let term = parsed(first, 8, 3); assert_eq!(jump(&term, true), Some(2), "its linked first row starts it"); let open = parsed( b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]133;B\x07z", 8, 2, ); assert_eq!( prompt(&open.grid()[Line(0)]), Some(1), "a prompt inside a link left open" ); let (y, z) = ( &open.grid()[Line(0)][Column(1)], &open.grid()[Line(0)][Column(2)], ); assert_eq!(part(z), Some(Part::Input(0)), "the input inside it"); assert_eq!( target_of(&open, 0, 2).as_deref(), Some("file:///x"), "its own target past the prompt" ); assert!( y.hyperlink() .is_some_and(|link| link.id().starts_with(NUMBERED)) ); } #[test] fn a_long_target_numbers_a_prompt_in_either_order() { let target = format!("https://e.test/{}", "a".repeat(249)); let open = format!("\x1b]8;;{target}\x1b\\"); let before = format!("{open}x\x1b]133;A\x07y\x1b]133;B\x07z\x1b]8;;\x1b\\"); let inside = format!("x\x1b]133;A\x07{open}y\x1b]133;B\x07z\x1b]8;;\x1b\\"); for bytes in [before, inside] { let term = parsed(bytes.as_bytes(), 8, 2); let row = &term.grid()[Line(0)]; assert_eq!(prompt(row), Some(1), "{bytes:?}"); assert!( prompted(&row[Column(1)]) && !prompted(&row[Column(2)]), "the prompt's cell alone: {bytes:?}" ); assert_eq!(part(&row[Column(2)]), Some(Part::Input(0)), "{bytes:?}"); assert_eq!(target_of(&term, 0, 2).as_deref(), Some(target.as_str())); } } #[test] fn controls_after_an_osc_s_closing_escape_run_where_vte_runs_them() { let whole: &[u8] = b"LEFT\x1b]0;title\x1b\r\n\\RIGHT"; for cut in 0..=whole.len() { let (first, second) = whole.split_at(cut); assert_eq!(passed(&[first, second]), whole, "cut at {cut}"); } let term = parsed(whole, 8, 2); let line = |at: i32| -> String { term.grid()[Line(at)] .into_iter() .map(|cell| cell.c) .collect() }; assert_eq!((line(0).trim_end(), line(1).trim_end()), ("LEFT", "RIGHT")); let marked = parsed(b"\x1b]133;A\x1b\r\n\\$", 8, 2); assert_eq!(prompt(&marked.grid()[Line(1)]), Some(1)); } #[test] fn del_and_high_bytes_in_an_escape_go_on_after_it() { assert_eq!( passed(&[b"\x1b\x7f]133;A\x07$\x1b\xff", b"]133;B\x07"]), b"\x1b\x7f]8;id=\x7f1;\x7f\x07$\x1b\xff]8;id=\x7f0i;\x7f\x07" ); } #[test] fn controls_vte_runs_inside_an_escape_go_on_before_the_link() { assert_eq!( passed(&[b"\x1b\n\x07]133;A\x07$"]), b"\n\x07\x1b]8;id=\x7f1;\x7f\x07$" ); } #[test] fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_its_own() { let term = parsed( b"x\r\n\x1b]133;A\x07\x1b[31m$\x1b[0m \x1b]133;B\x07ls", 8, 2, ); assert_eq!(prompt(&term.grid()[Line(0)]), None); assert_eq!(prompt(&term.grid()[Line(1)]), Some(1)); assert_eq!(target_of(&term, 1, 1).as_deref(), Some(LINK)); assert_eq!( part(&term.grid()[Line(1)][Column(2)]), Some(Part::Input(0)), "input" ); let ran = parsed( b"\x1b]133;A\x07$\x1b]133;B\x07ls\x1b]133;C\x07\r\nout", 8, 2, ); assert_eq!(target_of(&ran, 1, 0), None, "output"); } #[test] fn a_program_s_own_link_and_the_prompt_s_keep_each_other() { let term = parsed( b"\x1b]133;A\x07\x1b]8;;file:///work\x1b\\/work\x1b]8;;\x1b\\ $ \x1b]133;B\x07", 12, 2, ); assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///work")); assert_eq!(target_of(&term, 0, 6).as_deref(), Some(LINK)); assert_eq!(prompt(&term.grid()[Line(0)]), Some(1)); let term = parsed( b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]8;;\x1b\\z\x1b]133;B\x07", 12, 2, ); assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///x")); assert_eq!( target_of(&term, 0, 1).as_deref(), Some("file:///x"), "a prompt inside a link keeps it" ); assert_eq!(target_of(&term, 0, 2).as_deref(), Some(LINK)); } /// Prompts on lines 0, 3 and 6 of a history four lines high, the last on the screen. fn history() -> Term { let mut bytes = Vec::new(); for _ in 0..3 { bytes.extend_from_slice( b"\x1b]133;A\x07$ \x1b]133;B\x07c\r\n\x1b]133;C\x07o\r\no\r\n\x1b]133;D;0\x07", ); } bytes.extend_from_slice(b"\x1b]133;A\x07$ \x1b]133;B\x07"); parsed(&bytes, 8, 4) } #[test] fn up_and_down_go_to_the_prompts_beside_the_view_s_top() { let mut term = history(); assert_eq!(term.grid().history_size(), 6); // From the bottom: the prompt at the view's top is passed, the one before it found. assert_eq!(jump(&term, true), Some(3)); term.scroll_display(Scroll::Delta(3)); assert_eq!(jump(&term, true), Some(6)); term.scroll_display(Scroll::Delta(3)); assert_eq!(jump(&term, true), None, "no prompt above the first"); assert_eq!(jump(&term, false), Some(3)); term.scroll_display(Scroll::Delta(-3)); assert_eq!( jump(&term, false), Some(0), "a prompt on the screen means the bottom" ); } #[test] fn a_prompt_over_rows_starts_at_its_first_blank_rows_within_it_too() { let bytes = b"\x1b]133;A\x07FIRST\r\n\r\nLAST $ \x1b]133;B\x07\r\nout1\r\nout2\r\nout3\r\nout4"; let mut term = parsed(bytes, 8, 3); assert_eq!(term.grid().history_size(), 4); assert_eq!(jump(&term, true), Some(4), "FIRST, not LAST"); term.scroll_display(Scroll::Delta(4)); assert_eq!(jump(&term, false), None, "LAST is the same prompt"); let next = parsed(b"\x1b]133;A\x07$ \x1b]133;B\x07\r\n\x1b]133;A\x07$ ", 8, 2); assert_eq!( (prompt(&next.grid()[Line(0)]), prompt(&next.grid()[Line(1)])), (Some(1), Some(2)), "an empty line's next prompt is its own" ); } #[test] fn a_prompt_keeps_its_start_through_a_resize() { let bytes = b"\x1b]133;A\x07~/folder $ \x1b]133;B\x07ls\r\nout\r\nout\r\nout\r\n\x1b]133;A\x07$ "; let mut term = parsed(bytes, 12, 3); term.resize(TermSize::new(6, 3)); let rows = term.topmost_line().0..=term.bottommost_line().0; let numbers: Vec> = rows.map(|line| prompt(&term.grid()[Line(line)])).collect(); let first = numbers.iter().position(|number| *number == Some(1)); // The first prompt wraps over two rows; the view jumps to the first of them. assert_eq!( numbers.iter().filter(|number| **number == Some(1)).count(), 2 ); let offset = jump(&term, true).expect("a prompt above"); let top = term.topmost_line().0 + first.expect("prompt 1") as i32; assert_eq!(offset as i32, -top); } #[test] fn input_carries_the_commands_ended_before_it() { let line = b"\x1b]133;A\x07$\x1b]133;B\x07ab\x1b]133;A\x07R\x1b]133;B\x07c"; let term = parsed(line, 8, 2); let at = |column: usize| part(&term.grid()[Line(0)][Column(column)]); assert_eq!( (at(0), at(1), at(3)), ( Some(Part::Prompt(1)), Some(Part::Input(0)), Some(Part::Prompt(2)) ) ); assert_eq!( at(4), Some(Part::Input(0)), "a prompt drawn again keeps the line" ); let next = parsed( b"\x1b]133;A\x07$\x1b]133;B\x07a\x1b]133;C\x07\x1b]133;D\x07\x1b]133;A\x07$\x1b]133;B\x07b", 8, 2, ); assert_eq!( part(&next.grid()[Line(0)][Column(3)]), Some(Part::Input(2)), "C and D each end it" ); }