indicatif/
in_memory.rs

1use std::fmt::{Debug, Formatter, Write as _};
2use std::io::Write as _;
3use std::sync::{Arc, Mutex};
4
5use vt100::Parser;
6
7use crate::TermLike;
8
9/// A thin wrapper around [`vt100::Parser`].
10///
11/// This is just an [`Arc`] around its internal state, so it can be freely cloned.
12#[cfg_attr(docsrs, doc(cfg(feature = "in_memory")))]
13#[derive(Debug, Clone)]
14pub struct InMemoryTerm {
15    state: Arc<Mutex<InMemoryTermState>>,
16}
17
18impl InMemoryTerm {
19    pub fn new(rows: u16, cols: u16) -> InMemoryTerm {
20        assert!(rows > 0, "rows must be > 0");
21        assert!(cols > 0, "cols must be > 0");
22        InMemoryTerm {
23            state: Arc::new(Mutex::new(InMemoryTermState::new(rows, cols))),
24        }
25    }
26
27    pub fn reset(&self) {
28        let mut state = self.state.lock().unwrap();
29        *state = InMemoryTermState::new(state.height, state.width);
30    }
31
32    pub fn contents(&self) -> String {
33        let state = self.state.lock().unwrap();
34
35        // For some reason, the `Screen::contents` method doesn't include newlines in what it
36        // returns, making it useless for our purposes. So we need to manually reconstruct the
37        // contents by iterating over the rows in the terminal buffer.
38        let mut rows = state
39            .parser
40            .screen()
41            .rows(0, state.width)
42            .collect::<Vec<_>>();
43
44        // Reverse the rows and trim empty lines from the end
45        rows = rows
46            .into_iter()
47            .rev()
48            .skip_while(|line| line.is_empty())
49            .map(|line| line.trim_end().to_string())
50            .collect();
51
52        // Un-reverse the rows and join them up with newlines
53        rows.reverse();
54        rows.join("\n")
55    }
56
57    pub fn contents_formatted(&self) -> Vec<u8> {
58        let state = self.state.lock().unwrap();
59
60        // For some reason, the `Screen::contents` method doesn't include newlines in what it
61        // returns, making it useless for our purposes. So we need to manually reconstruct the
62        // contents by iterating over the rows in the terminal buffer.
63        let mut rows = state
64            .parser
65            .screen()
66            .rows_formatted(0, state.width)
67            .collect::<Vec<_>>();
68
69        // Reverse the rows and trim empty lines from the end
70        rows = rows
71            .into_iter()
72            .rev()
73            .skip_while(|line| line.is_empty())
74            .collect();
75
76        // Un-reverse the rows
77        rows.reverse();
78
79        // Calculate buffer size
80        let reset = b"";
81        let len = rows.iter().map(|line| line.len() + reset.len() + 1).sum();
82
83        // Join rows up with reset codes and newlines
84        let mut contents = rows.iter().fold(Vec::with_capacity(len), |mut acc, cur| {
85            acc.extend_from_slice(cur);
86            acc.extend_from_slice(reset);
87            acc.push(b'\n');
88            acc
89        });
90
91        // Remove last newline again, but leave the reset code
92        contents.truncate(len.saturating_sub(1));
93        contents
94    }
95
96    pub fn moves_since_last_check(&self) -> String {
97        let mut s = String::new();
98        for line in std::mem::take(&mut self.state.lock().unwrap().history) {
99            writeln!(s, "{line:?}").unwrap();
100        }
101        s
102    }
103}
104
105impl TermLike for InMemoryTerm {
106    fn width(&self) -> u16 {
107        self.state.lock().unwrap().width
108    }
109
110    fn height(&self) -> u16 {
111        self.state.lock().unwrap().height
112    }
113
114    fn move_cursor_up(&self, n: usize) -> std::io::Result<()> {
115        match n {
116            0 => Ok(()),
117            _ => {
118                let mut state = self.state.lock().unwrap();
119                state.history.push(Move::Up(n));
120                state.write_str(&format!("\x1b[{n}A"))
121            }
122        }
123    }
124
125    fn move_cursor_down(&self, n: usize) -> std::io::Result<()> {
126        match n {
127            0 => Ok(()),
128            _ => {
129                let mut state = self.state.lock().unwrap();
130                state.history.push(Move::Down(n));
131                state.write_str(&format!("\x1b[{n}B"))
132            }
133        }
134    }
135
136    fn move_cursor_right(&self, n: usize) -> std::io::Result<()> {
137        match n {
138            0 => Ok(()),
139            _ => {
140                let mut state = self.state.lock().unwrap();
141                state.history.push(Move::Right(n));
142                state.write_str(&format!("\x1b[{n}C"))
143            }
144        }
145    }
146
147    fn move_cursor_left(&self, n: usize) -> std::io::Result<()> {
148        match n {
149            0 => Ok(()),
150            _ => {
151                let mut state = self.state.lock().unwrap();
152                state.history.push(Move::Left(n));
153                state.write_str(&format!("\x1b[{n}D"))
154            }
155        }
156    }
157
158    fn write_line(&self, s: &str) -> std::io::Result<()> {
159        let mut state = self.state.lock().unwrap();
160        state.history.push(Move::Str(s.into()));
161        state.history.push(Move::NewLine);
162
163        // Don't try to handle writing lines with additional newlines embedded in them - it's not
164        // worth the extra code for something that indicatif doesn't even do. May revisit in future.
165        debug_assert!(
166            s.lines().count() <= 1,
167            "calling write_line with embedded newlines is not allowed"
168        );
169
170        // vte100 needs the full \r\n sequence to jump to the next line and reset the cursor to
171        // the beginning of the line. Be flexible and take either \n or \r\n
172        state.write_str(s)?;
173        state.write_str("\r\n")
174    }
175
176    fn write_str(&self, s: &str) -> std::io::Result<()> {
177        let mut state = self.state.lock().unwrap();
178        state.history.push(Move::Str(s.into()));
179        state.write_str(s)
180    }
181
182    fn clear_line(&self) -> std::io::Result<()> {
183        let mut state = self.state.lock().unwrap();
184        state.history.push(Move::Clear);
185        state.write_str("\r\x1b[2K")
186    }
187
188    fn flush(&self) -> std::io::Result<()> {
189        let mut state = self.state.lock().unwrap();
190        state.history.push(Move::Flush);
191        state.parser.flush()
192    }
193}
194
195struct InMemoryTermState {
196    width: u16,
197    height: u16,
198    parser: vt100::Parser,
199    history: Vec<Move>,
200}
201
202impl InMemoryTermState {
203    pub(crate) fn new(rows: u16, cols: u16) -> InMemoryTermState {
204        InMemoryTermState {
205            width: cols,
206            height: rows,
207            parser: Parser::new(rows, cols, 0),
208            history: vec![],
209        }
210    }
211
212    pub(crate) fn write_str(&mut self, s: &str) -> std::io::Result<()> {
213        self.parser.write_all(s.as_bytes())
214    }
215}
216
217impl Debug for InMemoryTermState {
218    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
219        f.debug_struct("InMemoryTermState").finish_non_exhaustive()
220    }
221}
222
223#[derive(Debug, PartialEq, Clone)]
224enum Move {
225    Up(usize),
226    Down(usize),
227    Left(usize),
228    Right(usize),
229    Str(String),
230    NewLine,
231    Clear,
232    Flush,
233}
234
235#[cfg(test)]
236mod test {
237    use super::*;
238
239    fn cursor_pos(in_mem: &InMemoryTerm) -> (u16, u16) {
240        in_mem
241            .state
242            .lock()
243            .unwrap()
244            .parser
245            .screen()
246            .cursor_position()
247    }
248
249    #[test]
250    fn line_wrapping() {
251        let in_mem = InMemoryTerm::new(10, 5);
252        assert_eq!(cursor_pos(&in_mem), (0, 0));
253
254        in_mem.write_str("ABCDE").unwrap();
255        assert_eq!(in_mem.contents(), "ABCDE");
256        assert_eq!(cursor_pos(&in_mem), (0, 5));
257        assert_eq!(
258            in_mem.moves_since_last_check(),
259            r#"Str("ABCDE")
260"#
261        );
262
263        // Should wrap onto next line
264        in_mem.write_str("FG").unwrap();
265        assert_eq!(in_mem.contents(), "ABCDE\nFG");
266        assert_eq!(cursor_pos(&in_mem), (1, 2));
267        assert_eq!(
268            in_mem.moves_since_last_check(),
269            r#"Str("FG")
270"#
271        );
272
273        in_mem.write_str("HIJ").unwrap();
274        assert_eq!(in_mem.contents(), "ABCDE\nFGHIJ");
275        assert_eq!(cursor_pos(&in_mem), (1, 5));
276        assert_eq!(
277            in_mem.moves_since_last_check(),
278            r#"Str("HIJ")
279"#
280        );
281    }
282
283    #[test]
284    fn write_line() {
285        let in_mem = InMemoryTerm::new(10, 5);
286        assert_eq!(cursor_pos(&in_mem), (0, 0));
287
288        in_mem.write_line("A").unwrap();
289        assert_eq!(in_mem.contents(), "A");
290        assert_eq!(cursor_pos(&in_mem), (1, 0));
291        assert_eq!(
292            in_mem.moves_since_last_check(),
293            r#"Str("A")
294NewLine
295"#
296        );
297
298        in_mem.write_line("B").unwrap();
299        assert_eq!(in_mem.contents(), "A\nB");
300        assert_eq!(cursor_pos(&in_mem), (2, 0));
301        assert_eq!(
302            in_mem.moves_since_last_check(),
303            r#"Str("B")
304NewLine
305"#
306        );
307
308        in_mem.write_line("Longer than cols").unwrap();
309        assert_eq!(in_mem.contents(), "A\nB\nLonge\nr tha\nn col\ns");
310        assert_eq!(cursor_pos(&in_mem), (6, 0));
311        assert_eq!(
312            in_mem.moves_since_last_check(),
313            r#"Str("Longer than cols")
314NewLine
315"#
316        );
317    }
318
319    #[test]
320    fn basic_functionality() {
321        let in_mem = InMemoryTerm::new(10, 80);
322
323        in_mem.write_line("This is a test line").unwrap();
324        assert_eq!(in_mem.contents(), "This is a test line");
325        assert_eq!(
326            in_mem.moves_since_last_check(),
327            r#"Str("This is a test line")
328NewLine
329"#
330        );
331
332        in_mem.write_line("And another line!").unwrap();
333        assert_eq!(in_mem.contents(), "This is a test line\nAnd another line!");
334        assert_eq!(
335            in_mem.moves_since_last_check(),
336            r#"Str("And another line!")
337NewLine
338"#
339        );
340
341        in_mem.move_cursor_up(1).unwrap();
342        in_mem.write_str("TEST").unwrap();
343
344        assert_eq!(in_mem.contents(), "This is a test line\nTESTanother line!");
345        assert_eq!(
346            in_mem.moves_since_last_check(),
347            r#"Up(1)
348Str("TEST")
349"#
350        );
351    }
352
353    #[test]
354    fn newlines() {
355        let in_mem = InMemoryTerm::new(10, 10);
356        in_mem.write_line("LINE ONE").unwrap();
357        in_mem.write_line("LINE TWO").unwrap();
358        in_mem.write_line("").unwrap();
359        in_mem.write_line("LINE FOUR").unwrap();
360
361        assert_eq!(in_mem.contents(), "LINE ONE\nLINE TWO\n\nLINE FOUR");
362
363        assert_eq!(
364            in_mem.moves_since_last_check(),
365            r#"Str("LINE ONE")
366NewLine
367Str("LINE TWO")
368NewLine
369Str("")
370NewLine
371Str("LINE FOUR")
372NewLine
373"#
374        );
375    }
376
377    #[test]
378    fn cursor_zero_movement() {
379        let in_mem = InMemoryTerm::new(10, 80);
380        in_mem.write_line("LINE ONE").unwrap();
381        assert_eq!(cursor_pos(&in_mem), (1, 0));
382
383        // Check that moving zero rows/cols does not actually move cursor
384        in_mem.move_cursor_up(0).unwrap();
385        assert_eq!(cursor_pos(&in_mem), (1, 0));
386
387        in_mem.move_cursor_down(0).unwrap();
388        assert_eq!(cursor_pos(&in_mem), (1, 0));
389
390        in_mem.move_cursor_right(1).unwrap();
391        assert_eq!(cursor_pos(&in_mem), (1, 1));
392
393        in_mem.move_cursor_left(0).unwrap();
394        assert_eq!(cursor_pos(&in_mem), (1, 1));
395
396        in_mem.move_cursor_right(0).unwrap();
397        assert_eq!(cursor_pos(&in_mem), (1, 1));
398    }
399}