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//!
38//!
39//!Clearing the written lines afterwards (```cargo run --example clear```)
40//!
41//!
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}