1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! # term-snip
//!
//! `term-snip` A small utility crate using https://crates.io/crates/console
//! to write to stdout but limited to a given number of lines.
//! The oldest line is removed when writing a new line.
//!
//!## Usage
//!
//!From *examples/five.rs*:
//!
//!```rust
//!use term_snip::TermSnip;
//!
//!use std::{thread, time};
//!
//!/// A simple example writing 15 lines to stdout but only showing
//!/// a maximum of five lines.
//!fn main() {
//!    let half_sec = time::Duration::from_millis(500);
//!    
//!    let mut term = TermSnip::new(5);
//!    for n in 1..15 {
//!        term.write_line(&format!("{} - line number {}", n, n)).unwrap();
//!        
//!        // just to slow down for demonstration
//!        thread::sleep(half_sec);
//!    }
//!}
//!
//!```
//!
//!
//!## Screenshot
//!
//!Screenshot showing above example in action
//!
//!![Screenshot of example five.rs](https://gitlab.com/sorcerersr/term-snip/-/raw/master/screenshot/example_five.gif)
//!
//!Clearing the written lines afterwards (```cargo run --example clear```)
//!
//!![Screenshot of example clear.rs](https://gitlab.com/sorcerersr/term-snip/-/raw/master/screenshot/example_clear.gif)
//!

use std::{collections::VecDeque, io, usize};

mod termwrap;
use termwrap::{ConsoleTermWrap, TermWrap};

/// Representation of a terminal.
pub struct TermSnip<'a> {
    term: Box<dyn TermWrap + 'a>,
    limit: usize,
    lines: VecDeque<String>,
}

impl<'a> TermSnip<'a> {
    /// Creates a TermSnip wich limits output lines to the given limit.
    ///
    /// # Example
    ///
    /// ```rust
    /// use term_snip::TermSnip;
    /// let mut term = TermSnip::new(5);
    /// ```
    ///
    pub fn new(limit: usize) -> TermSnip<'a> {
        TermSnip {
            term: Box::new(ConsoleTermWrap::new()),
            limit,
            lines: VecDeque::new(),
        }
    }

    /// Writes a line to the terminal (stdout).
    pub fn write_line(&mut self, text: &str) -> io::Result<()> {
        // split text into multiple text when it is longer than a line
        let line_len: usize = self.term.size().1.into();
        // if the char count of the text is less than the line length
        // just write it and return
        if text.chars().count() < line_len {
            self.term_write_line(text)?;
            return Ok(());
        }
        // when this code line is reached the text is larger then the line length
        // and must be splitted to be written as multiple lines

        let mut last_text = text;
        while last_text.chars().count() >= line_len {
            let (first, last) = last_text.split_at(line_len);
            last_text = last;
            self.term_write_line(first)?;
        }

        self.term_write_line(last_text)?;

        Ok(())
    }

    /// Delegates the writing to console::Term and manages the line limit.
    fn term_write_line(&mut self, text: &str) -> io::Result<()> {
        self.lines.push_back(text.to_string());
        if self.lines.len() > self.limit {
            self.lines.pop_front();
            self.clear_lines()?;
            for line in &self.lines {
                self.term.write_line(line)?;
            }
        } else {
            self.term.write_line(text)?;
        }
        Ok(())
    }

    /// Clear the lines written with the TermSnip instance
    pub fn clear_lines(&mut self) -> io::Result<()> {
        self.term.clear_last_lines(self.lines.len())?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use mockall::predicate::eq;

    #[test]
    fn test_simple_one_line() {
        let mut termwrap_mock = termwrap::MockTermWrap::new();
        termwrap_mock.expect_size().returning(|| (20, 20));
        termwrap_mock
            .expect_write_line()
            .with(eq("test"))
            .times(1)
            .returning(|_x| Ok(()));

        let mut term_snip = TermSnip {
            term: Box::new(termwrap_mock),
            limit: 5,
            lines: VecDeque::new(),
        };

        term_snip.write_line("test").unwrap();
    }

    #[test]
    fn test_six_lines_with_limit_5() {
        let mut termwrap_mock = termwrap::MockTermWrap::new();
        termwrap_mock.expect_size().returning(|| (20, 20));
        // 10 calls to write_line are expected.
        // what happens is:
        // * lines 0,1,2,3,4 are written to terminal, limit of 5 is reached
        // * next line is about to be written, after limit, so clear_lines is called
        // * lines 1,2,3,4 are written again, so it looks like they are moving
        //   up one line
        // * line 5 is written
        // in total 10 calls to write_line
        termwrap_mock
            .expect_write_line()
            .times(10)
            .returning(|_x| Ok(()));
        termwrap_mock
            .expect_clear_last_lines()
            .with(eq(5))
            .times(1)
            .returning(|_x| Ok(()));

        let mut term_snip = TermSnip {
            term: Box::new(termwrap_mock),
            limit: 5,
            lines: VecDeque::new(),
        };

        // write six lines
        for _n in 0..6 {
            term_snip.write_line("test").unwrap();
        }
    }

    #[test]
    fn test_long_line_split() {
        let mut termwrap_mock = termwrap::MockTermWrap::new();
        // line length of terminal is mocked to 6
        termwrap_mock.expect_size().returning(|| (20, 6));
        // testcase is one long line that needs to be splitted to three lines
        // due to its length
        termwrap_mock
            .expect_write_line()
            .times(3)
            .returning(|_x| Ok(()));

        let mut term_snip = TermSnip {
            term: Box::new(termwrap_mock),
            limit: 5,
            lines: VecDeque::new(),
        };

        // sample text is 17 chars long which should lead to
        // three lines as line length is 6
        let testline = "Lorem ipsum dolor";
        term_snip.write_line(testline).unwrap();
    }
}