1use std::cmp::max;
2
3use itertools::Itertools;
4use unicode_width::UnicodeWidthStr;
5
6const CLIPPY: &str = "
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
32const TEXT_BUBBLE_LEFT_SIDE: &str = " ⣿⡇ ";
33const TEXT_BUBBLE_TOP_LEFT: &str = " ⣴⡾⠿";
34const TEXT_BUBBLE_TOP_SIDE: &str = "⠿";
35const TEXT_BUBBLE_TOP_RIGHT: &str = "⠿⢷⣦";
36const TEXT_BUBBLE_RIGHT_SIDE: &str = " ⢸⣿";
37const TEXT_BUBBLE_BOTTOM_RIGHT: &str = "⣾⠟";
38const TEXT_BUBBLE_BOTTOM_SIDE: &str = "⣶";
39const TEXT_BUBBLE_BOTTOM_LEFT: &str = " ⠙⣿⡆ ⣴⣶";
40const TEXT_BUBBLE_TAIL: &str = " ⢰⣿ ⢀⣠⣾⠟⠋
41 ⣠⣿⠃ ⢀⣠⣤⣾⠟⠋
42 ⢿⣷⡾⠿⠟⠛⠉
43";
44
45pub fn clippy() -> String {
46 CLIPPY.to_string()
47}
48
49pub fn clippy_say(text: impl ToString) -> String {
50 let mut clippy = CLIPPY.to_string();
51
52 let speech_bubble = say(text);
54
55 let clippy_height = clippy.lines().count();
57 let speech_bubble_height = speech_bubble.lines().count();
58
59 let difference = speech_bubble_height.saturating_sub(clippy_height);
62 for _ in 0..difference {
63 clippy.insert(0, '\n');
64 }
65
66 horizontal_stack(clippy, speech_bubble)
67}
68
69pub fn say(text: impl ToString) -> String {
70 let minimum_text_width = TEXT_BUBBLE_BOTTOM_LEFT.width() + TEXT_BUBBLE_BOTTOM_RIGHT.width()
71 - TEXT_BUBBLE_LEFT_SIDE.width()
72 - TEXT_BUBBLE_RIGHT_SIDE.width();
73
74 debug_assert_eq!(TEXT_BUBBLE_LEFT_SIDE.width(), 7);
75
76 let text = text.to_string();
77 let mut full_width = longest_line_size(&text);
79 if full_width < minimum_text_width {
80 full_width = minimum_text_width
81 }
82
83 let mut output = String::new();
84 output.push_str(&top(full_width));
85 output.push_str(&empty_line(full_width));
86
87 for line in text.lines() {
88 output.push_str(&speech_line(line, full_width));
89 }
90 output.push_str(&empty_line(full_width));
91 output.push_str(&bottom(full_width));
92 output
93}
94
95fn top(text_width: usize) -> String {
96 let speech_bubble_width = speech_bubble_width(text_width);
97 let top_sides_needed = speech_bubble_width
98 .saturating_sub(TEXT_BUBBLE_TOP_LEFT.width() + TEXT_BUBBLE_TOP_RIGHT.width());
99
100 let top_side = TEXT_BUBBLE_TOP_SIDE.repeat(top_sides_needed);
101 format!("{TEXT_BUBBLE_TOP_LEFT}{top_side}{TEXT_BUBBLE_TOP_RIGHT}\n")
102}
103
104fn bottom(text_width: usize) -> String {
105 let speech_bubble_width = speech_bubble_width(text_width);
106 let bottom_sides_needed = speech_bubble_width
107 .saturating_sub(TEXT_BUBBLE_BOTTOM_LEFT.width() + TEXT_BUBBLE_BOTTOM_RIGHT.width());
108
109 let bottom_side = TEXT_BUBBLE_BOTTOM_SIDE.repeat(bottom_sides_needed);
110 format!("{TEXT_BUBBLE_BOTTOM_LEFT}{bottom_side}{TEXT_BUBBLE_BOTTOM_RIGHT}\n{TEXT_BUBBLE_TAIL}")
111}
112
113fn speech_line(text: &str, text_width: usize) -> String {
114 let mut padded_text = text.to_string();
115 padded_text.push_str(&" ".repeat(text_width.saturating_sub(padded_text.width())));
116 format!("{TEXT_BUBBLE_LEFT_SIDE}{padded_text}{TEXT_BUBBLE_RIGHT_SIDE}\n",)
117}
118
119fn empty_line(text_width: usize) -> String {
120 speech_line("", text_width)
121}
122
123fn longest_line_size(text: &str) -> usize {
124 text.lines().map(|s| s.width()).max().map_or(0, |v| v)
125}
126
127fn speech_bubble_width(text_width: usize) -> usize {
128 TEXT_BUBBLE_LEFT_SIDE.width() + text_width + TEXT_BUBBLE_RIGHT_SIDE.width()
129}
130
131fn horizontal_stack(str1: impl ToString, str2: impl ToString) -> String {
132 let stack1: Vec<String> = str1.to_string().lines().map_into().collect_vec();
134 let stack2: Vec<String> = str2.to_string().lines().map_into().collect_vec();
135 let tallest_stack_height = max(stack1.len(), stack2.len());
136
137 let stack1_longest_line_length = longest_line_size(&str1.to_string());
140
141 let mut output = String::new();
142 for i in 0..tallest_stack_height {
143 let stack1_str = stack1.get(i).map_or("", |v| v);
144 let stack2_str = stack2.get(i).map_or("", |v| v);
145
146 let spaces_to_add = stack1_longest_line_length - stack1_str.width();
148
149 output.push_str(stack1_str);
151 output.push_str(&" ".repeat(spaces_to_add));
152 output.push_str(stack2_str);
153 output.push('\n');
154 }
155 output.pop();
157
158 output
159}
160
161#[cfg(test)]
162mod test {
163 use super::{clippy_say, horizontal_stack, longest_line_size, say};
164 use test_case::test_case;
165
166 #[test_case("hello", 5; "with 1 line")]
167 #[test_case("hello\ngoodbye", 7; "with 2 lines")]
168 #[test_case("hello\ngoodbye\n", 7; "with extra newline")]
169 fn longest_line(text: &str, expected: usize) {
170 let actual = longest_line_size(text);
171 assert_eq!(expected, actual);
172 }
173
174 #[test_case("123\n123", "456\n456", "123456\n123456"; "with no formatting")]
175 #[test_case("12345\n123", "678\n456", "12345678\n123 456"; "with formatting")]
176 #[test_case("123\n123\n123", "456\n456", "123456\n123456\n123"; "with more lines in str1")]
177 #[test_case("123\n123", "456\n456\n456", "123456\n123456\n 456"; "with more lines in str2")]
178 fn stack(str1: &str, str2: &str, expected: &str) {
179 let actual = horizontal_stack(str1, str2);
180 assert_eq!(expected, actual);
181 }
182
183 #[test]
184 fn test_say_smol() {
185 let text = "smol";
186 let expected = format!(
187 " ⣴⡾⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢷⣦
188 ⣿⡇ ⢸⣿
189 ⣿⡇ {text} ⢸⣿
190 ⣿⡇ ⢸⣿
191 ⠙⣿⡆ ⣴⣶⣾⠟
192 ⢰⣿ ⢀⣠⣾⠟⠋
193 ⣠⣿⠃ ⢀⣠⣤⣾⠟⠋
194 ⢿⣷⡾⠿⠟⠛⠉
195"
196 );
197 let actual = say(text);
198
199 println!("{expected}");
200 println!("{actual}");
201
202 assert_eq!(expected, actual)
203 }
204
205 #[test]
206 fn test_say() {
207 let text = "This is a test message on it's own";
208 let expected = format!(
209 " ⣴⡾⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢷⣦
210 ⣿⡇ ⢸⣿
211 ⣿⡇ {text} ⢸⣿
212 ⣿⡇ ⢸⣿
213 ⠙⣿⡆ ⣴⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣶⣾⠟
214 ⢰⣿ ⢀⣠⣾⠟⠋
215 ⣠⣿⠃ ⢀⣠⣤⣾⠟⠋
216 ⢿⣷⡾⠿⠟⠛⠉
217"
218 );
219 let actual = say(text);
220
221 println!("{expected}");
222 println!("{actual}");
223
224 assert_eq!(expected, actual)
225 }
226
227 #[test]
228 fn test_clippy_say() {
229 let text = "This is a test message on it's own";
230 let actual = clippy_say(text);
231 println!("{}", actual)
232 }
233}