ferris_says/
lib.rs

1use regex::Regex;
2use smallvec::*;
3use std::io::{Result, Write};
4use textwrap::fill;
5use unicode_width::UnicodeWidthStr;
6
7const MASCOT: &[u8] = if cfg!(feature = "clippy") {
8    br#"
9        \
10         \
11            __
12           /  \
13           |  |
14           @  @
15           |  |
16           || |/
17           || ||
18           |\_/|
19           \___/
20"#
21} else {
22    br#"
23        \
24         \
25            _~^~^~_
26        \) /  o o  \ (/
27          '_   -   _'
28          / '-----' \
29"#
30};
31
32// A decent number for SmallVec's Buffer Size, not too large
33// but also big enough for most inputs
34const BUFSIZE: usize = 2048;
35
36/// Print out Ferris saying something.
37///
38/// `input` is a string slice that you want to be written out to somewhere
39///
40/// `max_width` is the maximum width of a line of text before it is wrapped
41///
42/// `writer` is anywhere that can be written to using the Writer trait like
43/// STDOUT or STDERR
44///
45/// # Example
46///
47/// The following bit of code will write the byte string to STDOUT
48///
49/// ```rust
50/// use ferris_says::say;
51/// use std::io::{stdout, BufWriter};
52///
53/// let stdout = stdout();
54/// let out = "Hello fellow Rustaceans!";
55/// let width = 24;
56///
57/// let writer = BufWriter::new(stdout.lock());
58/// say(out, width, writer).unwrap();
59/// ```
60///
61/// This will print out:
62///
63/// ```plain
64///  __________________________
65/// < Hello fellow Rustaceans! >
66///  --------------------------
67///         \
68///          \
69///             _~^~^~_
70///         \) /  o o  \ (/
71///           '_   -   _'
72///           / '-----' \
73/// ```
74pub fn say<W>(input: &str, max_width: usize, mut writer: W) -> Result<()>
75where
76    W: Write,
77{
78    // Final output is stored here
79    let mut write_buffer = SmallVec::<[u8; BUFSIZE]>::new();
80
81    // Pre process to merge continuous whitespaces into one space character
82    let input = merge_white_spaces(input);
83
84    // Let textwrap work its magic
85    let wrapped = fill(input.as_str(), max_width);
86
87    let lines: Vec<&str> = wrapped.lines().collect();
88
89    let line_count = lines.len();
90    let actual_width = longest_line(&lines);
91
92    // top box border
93    write_buffer.push(b' ');
94    for _ in 0..(actual_width + 2) {
95        write_buffer.push(b'_');
96    }
97    write_buffer.push(b'\n');
98
99    // inner message
100    for (i, line) in lines.into_iter().enumerate() {
101        if line_count == 1 {
102            write_buffer.extend_from_slice(b"< ");
103        } else if i == 0 {
104            write_buffer.extend_from_slice(b"/ ");
105        } else if i == line_count - 1 {
106            write_buffer.extend_from_slice(b"\\ ");
107        } else {
108            write_buffer.extend_from_slice(b"| ");
109        }
110
111        let line_len = UnicodeWidthStr::width(line);
112        write_buffer.extend_from_slice(line.as_bytes());
113        for _ in line_len..actual_width {
114            write_buffer.push(b' ');
115        }
116
117        if line_count == 1 {
118            write_buffer.extend_from_slice(b" >\n");
119        } else if i == 0 {
120            write_buffer.extend_from_slice(b" \\\n");
121        } else if i == line_count - 1 {
122            write_buffer.extend_from_slice(b" /\n");
123        } else {
124            write_buffer.extend_from_slice(b" |\n");
125        }
126    }
127
128    // bottom box border
129    write_buffer.push(b' ');
130    for _ in 0..(actual_width + 2) {
131        write_buffer.push(b'-');
132    }
133
134    // mascot
135    write_buffer.extend_from_slice(MASCOT);
136
137    writer.write_all(&write_buffer)
138}
139
140fn longest_line(lines: &[&str]) -> usize {
141    lines
142        .iter()
143        .map(|line| UnicodeWidthStr::width(*line))
144        .max()
145        .unwrap_or(0)
146}
147
148/// Merge continues white spaces into one space character while preserving newline characters.
149fn merge_white_spaces(input: &str) -> String {
150    let re = Regex::new(r"([^\S\r\n])+").unwrap();
151    re.replace_all(input, " ").to_string()
152}