1use std::borrow::Cow;
27use std::fmt::Write as _;
28use std::io::IsTerminal;
29
30const CRAB: [&str; 43] = [
33 "...................oooooooooo..oooooooooo...................",
34 "...................owwwwwwhho..owwwwwwhho...................",
35 "...................owwwwwwhho..owwwwwwhho...................",
36 "...................owwwwwwhho..owwwwwwhho...................",
37 "...................ohhhhkkhho..ohhhhkkhho...................",
38 "...................ohhhhkkhho..ohhhhkkhho...................",
39 "...................ohhkkkkhho..ohhkkkkhho...................",
40 "...................ohhkkkkhho..ohhkkkkhho...................",
41 "...................ohhkkkdhho..ohhkkkdhho...................",
42 "...................ophhhhhhhoooophhhhhhho...................",
43 ".................whohhhhhhhhoppohhhhhhhho...................",
44 "...oooo.........wphooooooooooppoooooooooo............oooo...",
45 "..o.mmmoooo....wwphhhpppppppwppppppppppmooo......oooommmoo..",
46 ".o.phhhpmmo...pwwwhhhpppppppwppppppppppmmmoo.....omphhhpmoo.",
47 "o.mpppppmmo..ppwwwwwwwwwwwwwwpppppwwwwwbmmmoo....ompppppmmoo",
48 "oompppppmmo..ppwwwwwwwwwwwwwwpppppwwwwwbbmmmoo...ompppppmm.o",
49 "o.mmmmmmmbo..pppwwwwwwwwwwwwpppppppppbwwbbmmmoo..ombmmmmmmoo",
50 "oommmmmmmmo..ppppwwwwwwwwwwppppppppppbwwbbbmmmo..ommmmmmmmo.",
51 ".ommmmmmmmoooppppppppppppppppppppppppbwwbbbmmmoo.ommmmmmmmo.",
52 ".o.dmmmmmo.ooppppppppddppppppppppppppbwwmmbmmmoo.o.mmmmmmdo.",
53 "..o.omm.ooobobppppppppppmmppppppppppbbbbbbdddddoooo.omm.oo..",
54 "...ooo.o..obobbppppppppppppppppppppbbddbbbdddddoopoooo.o....",
55 ".....oo...ooobbbbppppppppppppppppbbbbbbbbbdddddodoo..oo.....",
56 "...........oobbbbbrrrrpppppppppbbbbbbbrrrrdddddooo..........",
57 "............obbbbbpprrbbbbkbbbbbkbbbbbrrdddddddo............",
58 "............ombbbbbbbbbbbbbkkkkkbbbbbbbbbbdddddo............",
59 "............oombbbbbbbbbbbbbbbbbbbbbbbbbbmddddoo............",
60 "............oommbbbbbbbbbbbbbbbbbbbbbbbbmmddddoo............",
61 ".............ommmbbbbbbbbbbbbbbbbbbbbbbmmmddddo.............",
62 ".............oommmmbbbbbbbbbbbbbbbbbbmmmmmdddoo.............",
63 "..............oommmmmbbbbbbbbbbbbbbmmmmmmmddoo..............",
64 "...............oommmmmmmmmmmmmmmmmmmmmmmmmdoo...............",
65 "................oommmmmmmmmmmmmmmmmmmmmmmmoo................",
66 ".................ooommmmmmmmmdddddddddddooooo...............",
67 "................oobooommmmmmmdddddddddooooodo...............",
68 "...............ooboo.oooommmmddddddoooooo.odoo..............",
69 "..............ooboo..oooooooooooooooodoo..oodoo.............",
70 "..............oboo....oopoo........oboo....oopoo............",
71 ".............oobo......oopo.......oobo......oopo............",
72 "............ooboo.......opoo.....ooboo.......opoo...........",
73 "........owwwwooo........ooowwowwwwooo........ooowwwwo.......",
74 "........obbbboo..........ooppobbbboo..........ooppppo.......",
75 "........oooooo............ooooooooo............oooooo.......",
76];
77
78const TAGLINE: &str = "Google tools from the terminal, at maximum performance";
81
82const ASCII_BANNER: &str = concat!(
86 env!("CARGO_PKG_VERSION"),
87 "\n\n",
88 " _~^~^~^~_\n",
91 " \\) / (o) (o) \\ (/\n",
92 " '_ \\_/ _'\n",
93 " \\ '-----' /\n",
94 "Google tools from the terminal, at maximum performance",
95);
96
97fn rgb(cell: char) -> Option<[u8; 3]> {
103 Some(match cell {
104 'o' => [0x3B, 0x1D, 0x0B],
105 's' => [0x7C, 0x2D, 0x12],
106 'd' => [0xC2, 0x41, 0x0C],
107 'm' => [0xEA, 0x58, 0x0C],
108 'b' => [0xF9, 0x73, 0x16],
109 'p' => [0xFF, 0x8A, 0x3D],
110 'w' => [0xFF, 0xD9, 0xB0],
111 'h' => [0xFF, 0xF3, 0xE4],
112 'k' => [0x1A, 0x12, 0x0B],
113 'r' => [0xEA, 0x43, 0x35],
114 _ => return None,
115 })
116}
117
118pub fn version_banner() -> Cow<'static, str> {
126 if !graphics_enabled() {
127 return Cow::Borrowed(ASCII_BANNER);
128 }
129
130 let mut banner = String::with_capacity(ASCII_BANNER.len() + CRAB.len() * 64);
131 banner.push_str(env!("CARGO_PKG_VERSION"));
132 banner.push_str("\n\n");
133 render_truecolor(&mut banner);
134 banner.push('\n');
137 banner.push_str(TAGLINE);
138 Cow::Owned(banner)
139}
140
141fn graphics_enabled() -> bool {
153 if std::env::var_os("NO_COLOR").is_some() {
154 return false;
155 }
156 if std::env::var("CLICOLOR_FORCE").is_ok_and(|force| force != "0") {
157 return true;
158 }
159 std::io::stdout().is_terminal() && truecolor_signalled(terminal_signals())
160}
161
162fn terminal_signals() -> (Option<String>, Option<String>, bool) {
165 let term = std::env::var("TERM").ok().filter(|value| !value.is_empty());
166 let colorterm = std::env::var("COLORTERM").ok().filter(|v| !v.is_empty());
167 (term, colorterm, windows_terminal())
168}
169
170fn truecolor_signalled(signals: (Option<String>, Option<String>, bool)) -> bool {
181 let (term, colorterm, windows_terminal) = signals;
182 if term.as_deref() == Some("dumb") {
186 return false;
187 }
188 if matches!(colorterm.as_deref(), Some("truecolor" | "24bit")) {
189 return true;
190 }
191 if let Some(term) = term.as_deref()
194 && (term.contains("direct") || term.contains("truecolor"))
195 {
196 return true;
197 }
198 if cfg!(windows) {
199 return windows_terminal;
200 }
201 false
202}
203
204#[cfg(windows)]
208fn windows_terminal() -> bool {
209 std::env::var_os("WT_SESSION").is_some()
210}
211
212#[cfg(not(windows))]
216fn windows_terminal() -> bool {
217 false
218}
219
220fn render_truecolor(out: &mut String) {
229 let mut foreground: Option<[u8; 3]> = None;
230 let mut background: Option<[u8; 3]> = None;
231
232 for pair in CRAB.chunks(2) {
233 let top = pair[0].as_bytes();
234 let bottom = pair.get(1).map(|row| row.as_bytes());
235 for column in 0..CRAB[0].len() {
236 let top_pixel = rgb(top[column] as char);
237 let bottom_pixel = bottom.and_then(|row| rgb(row[column] as char));
238
239 if foreground != top_pixel {
240 foreground = top_pixel;
241 set_colour(out, 38, top_pixel);
242 }
243 if background != bottom_pixel {
244 background = bottom_pixel;
245 set_colour(out, 48, bottom_pixel);
246 }
247 out.push(if top_pixel.is_none() && bottom_pixel.is_none() {
250 ' '
251 } else {
252 UPPER_HALF_BLOCK
253 });
254 }
255 out.push_str(RESET);
257 out.push('\n');
258 }
259}
260
261fn set_colour(out: &mut String, layer: u8, pixel: Option<[u8; 3]>) {
266 match pixel {
267 Some([red, green, blue]) => {
268 let _ = write!(out, "\u{1b}[{layer};2;{red};{green};{blue}m");
269 }
270 None => {
271 let _ = write!(out, "\u{1b}[{}m", layer + 1);
272 }
273 }
274}
275
276const UPPER_HALF_BLOCK: char = '\u{2580}';
279
280const RESET: &str = "\u{1b}[0m";
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
291 fn every_pixel_cell_maps_to_a_colour() {
292 for (row_index, row) in CRAB.iter().enumerate() {
293 for (column_index, cell) in row.chars().enumerate() {
294 if cell != '.' && rgb(cell).is_none() {
295 panic!("CRAB[{row_index}][{column_index}] = {cell:?} has no palette arm");
296 }
297 }
298 }
299 }
300
301 #[test]
304 fn the_map_is_a_rectangular_grid() {
305 assert!(!CRAB.is_empty());
308 let width = CRAB[0].len();
309 for row in CRAB {
310 assert_eq!(row.len(), width, "ragged crab row: {row:?}");
311 }
312 assert_eq!(CRAB.len().div_ceil(2), 22);
315 }
316
317 #[test]
320 fn the_colour_render_draws_every_row_as_a_reset_terminated_line() {
321 let mut rendered = String::new();
322 render_truecolor(&mut rendered);
323
324 let lines: Vec<&str> = rendered.lines().collect();
325 assert_eq!(lines.len(), CRAB.len().div_ceil(2));
326 let widest = lines[CRAB.len() / 2 - 1];
329 assert!(widest.contains(UPPER_HALF_BLOCK));
330 for line in lines {
331 assert!(line.ends_with(RESET), "row not reset: {line:?}");
332 assert!(line.contains(UPPER_HALF_BLOCK));
333 }
334 }
335
336 #[test]
339 fn truecolor_is_detected_from_the_documented_signals() {
340 let absent = (None, None, false);
341 assert!(!truecolor_signalled(absent));
342 assert!(truecolor_signalled((None, Some("truecolor".into()), false)));
343 assert!(truecolor_signalled((None, Some("24bit".into()), false)));
344 assert!(truecolor_signalled((
347 Some("foot-direct".into()),
348 None,
349 false
350 )));
351 assert!(!truecolor_signalled((
352 Some("xterm-kitty".into()),
353 None,
354 false
355 )));
356 assert!(truecolor_signalled((
357 Some("xterm-kitty".into()),
358 Some("truecolor".into()),
359 false
360 )));
361 assert!(!truecolor_signalled((None, Some(String::new()), false)));
364 assert!(!truecolor_signalled((
365 Some("dumb".into()),
366 Some("truecolor".into()),
367 false
368 )));
369 }
370
371 #[test]
374 fn the_plain_banner_keeps_semver_first_and_the_crab_intact() {
375 let banner = version_banner();
376 let first_line = banner.lines().next().expect("banner has a first line");
377 assert_eq!(first_line, env!("CARGO_PKG_VERSION"));
378 assert!(banner.contains("(o) (o)"));
379 assert!(banner.contains("\\_/"));
380 assert!(banner.ends_with(TAGLINE));
381 }
382}