term_snip/
lib.rs

1//! # term-snip
2//!
3//! `term-snip` A small utility crate using https://crates.io/crates/console
4//! to write to stdout but limited to a given number of lines.
5//! The oldest line is removed when writing a new line.
6//!
7//!## Usage
8//!
9//!From *examples/five.rs*:
10//!
11//!```rust
12//!use term_snip::TermSnip;
13//!
14//!use std::{thread, time};
15//!
16//!/// A simple example writing 15 lines to stdout but only showing
17//!/// a maximum of five lines.
18//!fn main() {
19//!    let half_sec = time::Duration::from_millis(500);
20//!    
21//!    let mut term = TermSnip::new(5);
22//!    for n in 1..15 {
23//!        term.write_line(&format!("{} - line number {}", n, n)).unwrap();
24//!        
25//!        // just to slow down for demonstration
26//!        thread::sleep(half_sec);
27//!    }
28//!}
29//!
30//!```
31//!
32//!
33//!## Screenshot
34//!
35//!Screenshot showing above example in action
36//!
37//!![Screenshot of example five.rs](https://gitlab.com/sorcerersr/term-snip/-/raw/master/screenshot/example_five.gif)
38//!
39//!Clearing the written lines afterwards (```cargo run --example clear```)
40//!
41//!![Screenshot of example clear.rs](https://gitlab.com/sorcerersr/term-snip/-/raw/master/screenshot/example_clear.gif)
42//!
43
44use std::{collections::VecDeque, io, usize};
45
46mod termwrap;
47use termwrap::{ConsoleTermWrap, TermWrap};
48
49/// Representation of a terminal.
50pub struct TermSnip<'a> {
51    term: Box<dyn TermWrap + 'a>,
52    limit: usize,
53    lines: VecDeque<String>,
54}
55
56impl<'a> TermSnip<'a> {
57    /// Creates a TermSnip wich limits output lines to the given limit.
58    ///
59    /// # Example
60    ///
61    /// ```rust
62    /// use term_snip::TermSnip;
63    /// let mut term = TermSnip::new(5);
64    /// ```
65    ///
66    pub fn new(limit: usize) -> TermSnip<'a> {
67        TermSnip {
68            term: Box::new(ConsoleTermWrap::new()),
69            limit,
70            lines: VecDeque::new(),
71        }
72    }
73
74    /// Writes a line to the terminal (stdout).
75    pub fn write_line(&mut self, text: &str) -> io::Result<()> {
76        // split text into multiple text when it is longer than a line
77        let line_len: usize = self.term.size().1.into();
78        // if the char count of the text is less than the line length
79        // just write it and return
80        if text.chars().count() < line_len {
81            self.term_write_line(text)?;
82            return Ok(());
83        }
84        // when this code line is reached the text is larger then the line length
85        // and must be splitted to be written as multiple lines
86
87        let mut last_text = text;
88        while last_text.chars().count() >= line_len {
89            let (first, last) = last_text.split_at(line_len);
90            last_text = last;
91            self.term_write_line(first)?;
92        }
93
94        self.term_write_line(last_text)?;
95
96        Ok(())
97    }
98
99    /// Delegates the writing to console::Term and manages the line limit.
100    fn term_write_line(&mut self, text: &str) -> io::Result<()> {
101        self.lines.push_back(text.to_string());
102        if self.lines.len() > self.limit {
103            self.lines.pop_front();
104            self.clear_lines()?;
105            for line in &self.lines {
106                self.term.write_line(line)?;
107            }
108        } else {
109            self.term.write_line(text)?;
110        }
111        Ok(())
112    }
113
114    /// Clear the lines written with the TermSnip instance
115    pub fn clear_lines(&mut self) -> io::Result<()> {
116        self.term.clear_last_lines(self.lines.len())?;
117        Ok(())
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use mockall::predicate::eq;
125
126    #[test]
127    fn test_simple_one_line() {
128        let mut termwrap_mock = termwrap::MockTermWrap::new();
129        termwrap_mock.expect_size().returning(|| (20, 20));
130        termwrap_mock
131            .expect_write_line()
132            .with(eq("test"))
133            .times(1)
134            .returning(|_x| Ok(()));
135
136        let mut term_snip = TermSnip {
137            term: Box::new(termwrap_mock),
138            limit: 5,
139            lines: VecDeque::new(),
140        };
141
142        term_snip.write_line("test").unwrap();
143    }
144
145    #[test]
146    fn test_six_lines_with_limit_5() {
147        let mut termwrap_mock = termwrap::MockTermWrap::new();
148        termwrap_mock.expect_size().returning(|| (20, 20));
149        // 10 calls to write_line are expected.
150        // what happens is:
151        // * lines 0,1,2,3,4 are written to terminal, limit of 5 is reached
152        // * next line is about to be written, after limit, so clear_lines is called
153        // * lines 1,2,3,4 are written again, so it looks like they are moving
154        //   up one line
155        // * line 5 is written
156        // in total 10 calls to write_line
157        termwrap_mock
158            .expect_write_line()
159            .times(10)
160            .returning(|_x| Ok(()));
161        termwrap_mock
162            .expect_clear_last_lines()
163            .with(eq(5))
164            .times(1)
165            .returning(|_x| Ok(()));
166
167        let mut term_snip = TermSnip {
168            term: Box::new(termwrap_mock),
169            limit: 5,
170            lines: VecDeque::new(),
171        };
172
173        // write six lines
174        for _n in 0..6 {
175            term_snip.write_line("test").unwrap();
176        }
177    }
178
179    #[test]
180    fn test_long_line_split() {
181        let mut termwrap_mock = termwrap::MockTermWrap::new();
182        // line length of terminal is mocked to 6
183        termwrap_mock.expect_size().returning(|| (20, 6));
184        // testcase is one long line that needs to be splitted to three lines
185        // due to its length
186        termwrap_mock
187            .expect_write_line()
188            .times(3)
189            .returning(|_x| Ok(()));
190
191        let mut term_snip = TermSnip {
192            term: Box::new(termwrap_mock),
193            limit: 5,
194            lines: VecDeque::new(),
195        };
196
197        // sample text is 17 chars long which should lead to
198        // three lines as line length is 6
199        let testline = "Lorem ipsum dolor";
200        term_snip.write_line(testline).unwrap();
201    }
202}