use std::fmt::{Debug, Formatter, Write as _}; use std::io::Write as _; use std::sync::{Arc, Mutex}; use vt100::Parser; use crate::TermLike; /// A thin wrapper around [`vt100::Parser`]. /// /// This is just an [`Arc`] around its internal state, so it can be freely cloned. #[cfg_attr(docsrs, doc(cfg(feature = "in_memory")))] #[derive(Debug, Clone)] pub struct InMemoryTerm { state: Arc>, } impl InMemoryTerm { pub fn new(rows: u16, cols: u16) -> InMemoryTerm { assert!(rows > 0, "rows must be > 0"); assert!(cols > 0, "cols must be > 0"); InMemoryTerm { state: Arc::new(Mutex::new(InMemoryTermState::new(rows, cols))), } } pub fn reset(&self) { let mut state = self.state.lock().unwrap(); *state = InMemoryTermState::new(state.height, state.width); } pub fn contents(&self) -> String { let state = self.state.lock().unwrap(); // For some reason, the `Screen::contents` method doesn't include newlines in what it // returns, making it useless for our purposes. So we need to manually reconstruct the // contents by iterating over the rows in the terminal buffer. let mut rows = state .parser .screen() .rows(0, state.width) .collect::>(); // Reverse the rows and trim empty lines from the end rows = rows .into_iter() .rev() .skip_while(|line| line.is_empty()) .map(|line| line.trim_end().to_string()) .collect(); // Un-reverse the rows and join them up with newlines rows.reverse(); rows.join("\n") } pub fn contents_formatted(&self) -> Vec { let state = self.state.lock().unwrap(); // For some reason, the `Screen::contents` method doesn't include newlines in what it // returns, making it useless for our purposes. So we need to manually reconstruct the // contents by iterating over the rows in the terminal buffer. let mut rows = state .parser .screen() .rows_formatted(0, state.width) .collect::>(); // Reverse the rows and trim empty lines from the end rows = rows .into_iter() .rev() .skip_while(|line| line.is_empty()) .collect(); // Un-reverse the rows rows.reverse(); // Calculate buffer size let reset = b""; let len = rows.iter().map(|line| line.len() + reset.len() + 1).sum(); // Join rows up with reset codes and newlines let mut contents = rows.iter().fold(Vec::with_capacity(len), |mut acc, cur| { acc.extend_from_slice(cur); acc.extend_from_slice(reset); acc.push(b'\n'); acc }); // Remove last newline again, but leave the reset code contents.truncate(len.saturating_sub(1)); contents } pub fn moves_since_last_check(&self) -> String { let mut s = String::new(); for line in std::mem::take(&mut self.state.lock().unwrap().history) { writeln!(s, "{line:?}").unwrap(); } s } } impl TermLike for InMemoryTerm { fn width(&self) -> u16 { self.state.lock().unwrap().width } fn height(&self) -> u16 { self.state.lock().unwrap().height } fn move_cursor_up(&self, n: usize) -> std::io::Result<()> { match n { 0 => Ok(()), _ => { let mut state = self.state.lock().unwrap(); state.history.push(Move::Up(n)); state.write_str(&format!("\x1b[{n}A")) } } } fn move_cursor_down(&self, n: usize) -> std::io::Result<()> { match n { 0 => Ok(()), _ => { let mut state = self.state.lock().unwrap(); state.history.push(Move::Down(n)); state.write_str(&format!("\x1b[{n}B")) } } } fn move_cursor_right(&self, n: usize) -> std::io::Result<()> { match n { 0 => Ok(()), _ => { let mut state = self.state.lock().unwrap(); state.history.push(Move::Right(n)); state.write_str(&format!("\x1b[{n}C")) } } } fn move_cursor_left(&self, n: usize) -> std::io::Result<()> { match n { 0 => Ok(()), _ => { let mut state = self.state.lock().unwrap(); state.history.push(Move::Left(n)); state.write_str(&format!("\x1b[{n}D")) } } } fn write_line(&self, s: &str) -> std::io::Result<()> { let mut state = self.state.lock().unwrap(); state.history.push(Move::Str(s.into())); state.history.push(Move::NewLine); // Don't try to handle writing lines with additional newlines embedded in them - it's not // worth the extra code for something that indicatif doesn't even do. May revisit in future. debug_assert!( s.lines().count() <= 1, "calling write_line with embedded newlines is not allowed" ); // vte100 needs the full \r\n sequence to jump to the next line and reset the cursor to // the beginning of the line. Be flexible and take either \n or \r\n state.write_str(s)?; state.write_str("\r\n") } fn write_str(&self, s: &str) -> std::io::Result<()> { let mut state = self.state.lock().unwrap(); state.history.push(Move::Str(s.into())); state.write_str(s) } fn clear_line(&self) -> std::io::Result<()> { let mut state = self.state.lock().unwrap(); state.history.push(Move::Clear); state.write_str("\r\x1b[2K") } fn flush(&self) -> std::io::Result<()> { let mut state = self.state.lock().unwrap(); state.history.push(Move::Flush); state.parser.flush() } } struct InMemoryTermState { width: u16, height: u16, parser: vt100::Parser, history: Vec, } impl InMemoryTermState { pub(crate) fn new(rows: u16, cols: u16) -> InMemoryTermState { InMemoryTermState { width: cols, height: rows, parser: Parser::new(rows, cols, 0), history: vec![], } } pub(crate) fn write_str(&mut self, s: &str) -> std::io::Result<()> { self.parser.write_all(s.as_bytes()) } } impl Debug for InMemoryTermState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("InMemoryTermState").finish_non_exhaustive() } } #[derive(Debug, PartialEq, Clone)] enum Move { Up(usize), Down(usize), Left(usize), Right(usize), Str(String), NewLine, Clear, Flush, } #[cfg(test)] mod test { use super::*; fn cursor_pos(in_mem: &InMemoryTerm) -> (u16, u16) { in_mem .state .lock() .unwrap() .parser .screen() .cursor_position() } #[test] fn line_wrapping() { let in_mem = InMemoryTerm::new(10, 5); assert_eq!(cursor_pos(&in_mem), (0, 0)); in_mem.write_str("ABCDE").unwrap(); assert_eq!(in_mem.contents(), "ABCDE"); assert_eq!(cursor_pos(&in_mem), (0, 5)); assert_eq!( in_mem.moves_since_last_check(), r#"Str("ABCDE") "# ); // Should wrap onto next line in_mem.write_str("FG").unwrap(); assert_eq!(in_mem.contents(), "ABCDE\nFG"); assert_eq!(cursor_pos(&in_mem), (1, 2)); assert_eq!( in_mem.moves_since_last_check(), r#"Str("FG") "# ); in_mem.write_str("HIJ").unwrap(); assert_eq!(in_mem.contents(), "ABCDE\nFGHIJ"); assert_eq!(cursor_pos(&in_mem), (1, 5)); assert_eq!( in_mem.moves_since_last_check(), r#"Str("HIJ") "# ); } #[test] fn write_line() { let in_mem = InMemoryTerm::new(10, 5); assert_eq!(cursor_pos(&in_mem), (0, 0)); in_mem.write_line("A").unwrap(); assert_eq!(in_mem.contents(), "A"); assert_eq!(cursor_pos(&in_mem), (1, 0)); assert_eq!( in_mem.moves_since_last_check(), r#"Str("A") NewLine "# ); in_mem.write_line("B").unwrap(); assert_eq!(in_mem.contents(), "A\nB"); assert_eq!(cursor_pos(&in_mem), (2, 0)); assert_eq!( in_mem.moves_since_last_check(), r#"Str("B") NewLine "# ); in_mem.write_line("Longer than cols").unwrap(); assert_eq!(in_mem.contents(), "A\nB\nLonge\nr tha\nn col\ns"); assert_eq!(cursor_pos(&in_mem), (6, 0)); assert_eq!( in_mem.moves_since_last_check(), r#"Str("Longer than cols") NewLine "# ); } #[test] fn basic_functionality() { let in_mem = InMemoryTerm::new(10, 80); in_mem.write_line("This is a test line").unwrap(); assert_eq!(in_mem.contents(), "This is a test line"); assert_eq!( in_mem.moves_since_last_check(), r#"Str("This is a test line") NewLine "# ); in_mem.write_line("And another line!").unwrap(); assert_eq!(in_mem.contents(), "This is a test line\nAnd another line!"); assert_eq!( in_mem.moves_since_last_check(), r#"Str("And another line!") NewLine "# ); in_mem.move_cursor_up(1).unwrap(); in_mem.write_str("TEST").unwrap(); assert_eq!(in_mem.contents(), "This is a test line\nTESTanother line!"); assert_eq!( in_mem.moves_since_last_check(), r#"Up(1) Str("TEST") "# ); } #[test] fn newlines() { let in_mem = InMemoryTerm::new(10, 10); in_mem.write_line("LINE ONE").unwrap(); in_mem.write_line("LINE TWO").unwrap(); in_mem.write_line("").unwrap(); in_mem.write_line("LINE FOUR").unwrap(); assert_eq!(in_mem.contents(), "LINE ONE\nLINE TWO\n\nLINE FOUR"); assert_eq!( in_mem.moves_since_last_check(), r#"Str("LINE ONE") NewLine Str("LINE TWO") NewLine Str("") NewLine Str("LINE FOUR") NewLine "# ); } #[test] fn cursor_zero_movement() { let in_mem = InMemoryTerm::new(10, 80); in_mem.write_line("LINE ONE").unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 0)); // Check that moving zero rows/cols does not actually move cursor in_mem.move_cursor_up(0).unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 0)); in_mem.move_cursor_down(0).unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 0)); in_mem.move_cursor_right(1).unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 1)); in_mem.move_cursor_left(0).unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 1)); in_mem.move_cursor_right(0).unwrap(); assert_eq!(cursor_pos(&in_mem), (1, 1)); } }