clippy_output/
lib.rs

1//! ```
2//! use clippy_output::ClippyOutput;
3//!
4//! let mut clippy = ClippyOutput::new(50);
5//! clippy
6//!     .add_str("It looks like you're creating a project in\nRust. Would you like some help with that?");
7//! clippy.finish();
8//!
9//! let output: String = clippy.collect();
10//! println!("{}", output);
11//! ```
12//! ```none
13//! /‾‾\
14//! |  |
15//! @  @
16//! || |/
17//! || ||
18//! |\_/|
19//! \___/
20//!   /\
21//! /‾  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\
22//! | It looks like you're creating a project in     |
23//! | Rust. Would you like some help with that?      |
24//! \________________________________________________/
25//! ```
26
27use std::mem::take;
28use textwrap::wrap;
29
30const CLIPPY_ART: &str = r#"/‾‾\
31|  |
32@  @
33|| |/
34|| ||
35|\_/|
36\___/
37  /\
38"#;
39
40/// Inputs a string and outputs ascii art of Clippy saying the text.
41///
42/// Call `add_str()` to input strings and call `finish()` at the end after all text as been added.
43/// `ClippyOutput` implements `Iterator` to return the output string.
44#[derive(Default, Clone, PartialOrd, PartialEq)]
45pub struct ClippyOutput {
46    buf: String,
47
48    // output_width >= 5
49    output_width: u16,
50
51    // Incomplete last line without vertical bars
52    line: String,
53
54    // Length of line in chars. Since this is the number of characters, the displayed width may be
55    // different. For example, combining characters will cause lines to appear shorter.
56    // line_char_length <= self.output_width - 4
57    line_char_length: u16,
58}
59
60impl ClippyOutput {
61    pub fn new(mut output_width: u16) -> Self {
62        if output_width < 5 {
63            output_width = 5;
64        }
65
66        let mut s = CLIPPY_ART.to_string() + "/‾  ";
67        for _ in 0..output_width - 5 {
68            s.push('‾');
69        }
70        s.push_str("\\\n");
71        Self {
72            buf: s,
73            output_width,
74            line: String::new(),
75            line_char_length: 0,
76        }
77    }
78
79    /// Adds text to be processed.
80    pub fn add_str(&mut self, s: &str) {
81        let lines = wrap(s, usize::min((self.output_width - 4) as usize, 2000));
82
83        for (i, line) in lines.iter().enumerate() {
84            for char in line.chars() {
85                if char == '\n' {
86                    ClippyOutput::add_string_to_buffer(
87                        &mut self.buf,
88                        &self.line,
89                        self.output_width - 4 - self.line_char_length,
90                    );
91                    self.line.clear();
92                    self.line_char_length = 0;
93                } else {
94                    self.line.push(char);
95                    self.line_char_length += 1;
96                }
97
98                if self.line_char_length == self.output_width - 4 {
99                    ClippyOutput::add_string_to_buffer(&mut self.buf, &self.line, 0);
100                    self.line.clear();
101                    self.line_char_length = 0;
102                }
103            }
104
105            if i < lines.len() - 1 {
106                ClippyOutput::add_string_to_buffer(
107                    &mut self.buf,
108                    &self.line,
109                    self.output_width - 4 - self.line_char_length,
110                );
111                self.line.clear();
112                self.line_char_length = 0;
113            }
114        }
115    }
116
117    fn add_string_to_buffer(buf: &mut String, line: &str, space_count: u16) {
118        if !line.is_empty() {
119            buf.push_str("| ");
120            buf.push_str(&line);
121            for _ in 0..space_count {
122                buf.push(' ');
123            }
124            buf.push_str(" |\n");
125        }
126    }
127
128    /// Appends the last line of the speech bubble.
129    ///
130    /// `add_str()` or `finish()` should not be called after `finish()` was called.
131    pub fn finish(&mut self) {
132        if !self.line.is_empty() {
133            ClippyOutput::add_string_to_buffer(
134                &mut self.buf,
135                &self.line,
136                self.output_width - 4 - self.line.chars().count() as u16,
137            );
138            self.line.clear();
139            self.line_char_length = 0;
140        }
141
142        self.buf.push('\\');
143        for _ in 0..self.output_width - 2 {
144            self.buf.push('_');
145        }
146        self.buf.push_str("/\n");
147    }
148}
149
150impl Iterator for ClippyOutput {
151    type Item = String;
152
153    /// Returns `Some` if there is a string remaining in the buffer. Returns `None` if the buffer is
154    /// clear.
155    fn next(&mut self) -> Option<String> {
156        let result = take(&mut self.buf);
157        if result.is_empty() {
158            None
159        } else {
160            Some(result)
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn clippy_output() {
171        {
172            // Minimum output width
173            let mut clippy = ClippyOutput::new(0);
174            let result: String = clippy.by_ref().collect();
175            assert_eq!(result, CLIPPY_ART.to_string() + "/‾  \\\n");
176
177            clippy.finish();
178            let result: String = clippy.by_ref().collect();
179            assert_eq!(result, "\\___/\n");
180        }
181        {
182            // Add empty string
183            let mut clippy = ClippyOutput::new(0);
184            clippy.add_str("");
185            clippy.finish();
186            let result: String = clippy.collect();
187            assert_eq!(result, CLIPPY_ART.to_string() + "/‾  \\\n\\___/\n");
188        }
189        {
190            let mut clippy = ClippyOutput::new(0);
191            clippy.add_str("a");
192            let result: String = clippy.by_ref().collect();
193            assert_eq!(result, CLIPPY_ART.to_string() + "/‾  \\\n| a |\n");
194
195            clippy.finish();
196            let result: String = clippy.by_ref().collect();
197            assert_eq!(result, "\\___/\n");
198
199            // Add string after finish()
200            clippy.add_str("a");
201            clippy.finish();
202            let result: String = clippy.collect();
203            assert_eq!(result, "| a |\n\\___/\n");
204        }
205        {
206            // Output width = 6
207            let mut clippy = ClippyOutput::new(6);
208            clippy.add_str("aa");
209            clippy.finish();
210
211            let result: String = clippy.collect();
212            assert_eq!(
213                result,
214                CLIPPY_ART.to_string() + "/‾  ‾\\\n| aa |\n\\____/\n"
215            );
216        }
217        {
218            // Wrap long line
219            let mut clippy = ClippyOutput::new(6);
220            clippy.add_str("aaa");
221            clippy.finish();
222
223            let result: String = clippy.collect();
224            assert_eq!(
225                result,
226                CLIPPY_ART.to_string() + "/‾  ‾\\\n| aa |\n| a  |\n\\____/\n"
227            );
228        }
229        {
230            // Append string
231            let mut clippy = ClippyOutput::new(6);
232            clippy.add_str("a");
233            let result: String = clippy.by_ref().collect();
234            assert_eq!(result, CLIPPY_ART.to_string() + "/‾  ‾\\\n");
235
236            clippy.add_str("a");
237            clippy.finish();
238            let result: String = clippy.collect();
239            assert_eq!(result, "| aa |\n\\____/\n");
240        }
241        {
242            // Newline in string
243            let mut clippy = ClippyOutput::new(6);
244            clippy.add_str("a\n");
245            let result: String = clippy.by_ref().collect();
246            assert_eq!(result, CLIPPY_ART.to_string() + "/‾  ‾\\\n| a  |\n");
247
248            clippy.add_str("b");
249            clippy.finish();
250            let result: String = clippy.collect();
251            assert_eq!(result, "| b  |\n\\____/\n");
252        }
253        {
254            // Word wrapping
255            let mut clippy = ClippyOutput::new(7);
256            clippy.add_str("a");
257            clippy.add_str("\n");
258            clippy.add_str("aaaa\n");
259            clippy.add_str("b");
260            clippy.finish();
261            let result: String = clippy.collect();
262            assert_eq!(
263                result,
264                CLIPPY_ART.to_string() + "/‾  ‾‾\\\n| a   |\n| aaa |\n| a   |\n| b   |\n\\_____/\n"
265            );
266        }
267    }
268}