1#[cfg(feature = "graphics")]
21pub fn get_embedded_logo(distro: Option<&str>) -> Option<&'static [u8]> {
22 let d = distro.map(|s| s.to_lowercase());
23 match d.as_deref() {
24 Some("arch") => Some(include_bytes!("../assets/logos/arch.png")),
25 Some("debian") => Some(include_bytes!("../assets/logos/debian.png")),
26 Some("fedora") => Some(include_bytes!("../assets/logos/fedora.png")),
27 Some("nixos") => Some(include_bytes!("../assets/logos/nixos.png")),
28 Some("ubuntu") => Some(include_bytes!("../assets/logos/ubuntu.png")),
29 Some("pop") => Some(include_bytes!("../assets/logos/pop.png")),
30 Some("manjaro") => Some(include_bytes!("../assets/logos/manjaro.png")),
31 Some("endeavouros") => Some(include_bytes!("../assets/logos/endeavouros.png")),
32 Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
33 Some(include_bytes!("../assets/logos/opensuse.png"))
34 }
35 Some("macos") => Some(include_bytes!("../assets/logos/macos.png")),
36 Some("windows") => Some(include_bytes!("../assets/logos/windows.png")),
37 _ => Some(include_bytes!("../assets/logos/tux.png")),
38 }
39}
40
41#[cfg(not(feature = "graphics"))]
43pub fn get_embedded_logo(_distro: Option<&str>) -> Option<&'static [u8]> {
44 Some(include_bytes!("../assets/logos/tux.png"))
45}
46
47pub fn detect_distro() -> Option<String> {
49 #[cfg(target_os = "macos")]
50 {
51 Some("macos".to_string())
52 }
53 #[cfg(target_os = "windows")]
54 {
55 Some("windows".to_string())
56 }
57 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
58 {
59 if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
60 for line in content.lines() {
61 if line.starts_with("ID=") {
62 let id = line.trim_start_matches("ID=").trim_matches('"');
63 return Some(id.to_string());
64 }
65 }
66 }
67 None
68 }
69}
70
71pub fn get_ascii_logo(distro: Option<&str>) -> Vec<String> {
75 let d = distro.map(|s| s.to_lowercase());
76
77 match d.as_deref() {
78 Some("arch") => {
79 let logo = include_str!("../assets/logos/arch.txt");
80 logo.lines().map(|s| s.to_string()).collect()
81 }
82 Some("debian") => {
83 let logo = include_str!("../assets/logos/debian.txt");
84 logo.lines().map(|s| s.to_string()).collect()
85 }
86 Some("fedora") => {
87 let logo = include_str!("../assets/logos/fedora.txt");
88 logo.lines().map(|s| s.to_string()).collect()
89 }
90 Some("nixos") => {
91 let logo = include_str!("../assets/logos/nixos.txt");
92 logo.lines().map(|s| s.to_string()).collect()
93 }
94 Some("ubuntu") => {
95 let logo = include_str!("../assets/logos/ubuntu.txt");
96 logo.lines().map(|s| s.to_string()).collect()
97 }
98 Some("pop") => {
99 let logo = include_str!("../assets/logos/pop.txt");
100 logo.lines().map(|s| s.to_string()).collect()
101 }
102 Some("manjaro") => {
103 let logo = include_str!("../assets/logos/manjaro.txt");
104 logo.lines().map(|s| s.to_string()).collect()
105 }
106 Some("endeavouros") => {
107 let logo = include_str!("../assets/logos/endeavouros.txt");
108 logo.lines().map(|s| s.to_string()).collect()
109 }
110 Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
111 let logo = include_str!("../assets/logos/opensuse.txt");
112 logo.lines().map(|s| s.to_string()).collect()
113 }
114 Some("macos") => {
115 let logo = include_str!("../assets/logos/macos.txt");
116 logo.lines().map(|s| s.to_string()).collect()
117 }
118 Some("windows") => {
119 let logo = include_str!("../assets/logos/windows.txt");
120 logo.lines().map(|s| s.to_string()).collect()
121 }
122
123 _ => {
125 let logo = include_str!("../assets/logos/tux.txt");
126 logo.lines().map(|s| s.to_string()).collect()
127 }
128 }
129}
130
131pub fn get_distro_colors(distro: Option<&str>) -> Vec<&'static str> {
133 let d = distro.map(|s| s.to_lowercase());
134 match d.as_deref() {
135 Some("arch") => vec!["\x1b[36m", "\x1b[37m"],
136 Some("debian") => vec!["\x1b[31m", "\x1b[37m"],
137 Some("fedora") => vec!["\x1b[34m", "\x1b[37m"],
138 Some("nixos") => vec![
139 "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m",
140 ],
141 Some("ubuntu") => vec!["\x1b[33m", "\x1b[31m"],
142 Some("pop") => vec!["\x1b[36m", "\x1b[37m"],
143 Some("manjaro") => vec!["\x1b[32m"],
144 Some("endeavouros") => vec!["\x1b[35m", "\x1b[31m", "\x1b[34m"],
145 Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
146 vec!["\x1b[32m", "\x1b[37m"]
147 }
148 Some("macos") => vec!["\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[35m", "\x1b[34m"],
149 Some("windows") => vec!["\x1b[36m"],
150 _ => vec!["\x1b[30m", "\x1b[37m", "\x1b[33m"], }
152}
153
154pub fn get_distro_logo_lines(distro: Option<&str>) -> Vec<String> {
156 let raw_lines = get_ascii_logo(distro);
157 let colors = get_distro_colors(distro);
158 let default_color = colors.first().copied().unwrap_or("\x1b[0m");
159
160 raw_lines
161 .into_iter()
162 .map(|line| {
163 let mut formatted = line;
164 for i in 1..=9 {
165 let color_val = colors.get(i - 1).copied().unwrap_or("\x1b[0m");
166 let placeholder = format!("${{{}}}", i);
167 formatted = formatted.replace(&placeholder, color_val);
168 let placeholder_short = format!("${}", i);
169 formatted = formatted.replace(&placeholder_short, color_val);
170 }
171 if !formatted.is_empty() {
172 format!("{}{}\x1b[0m", default_color, formatted)
173 } else {
174 formatted
175 }
176 })
177 .collect()
178}
179
180pub fn supports_kitty() -> bool {
182 std::env::var("TERM")
183 .map(|t| t == "xterm-kitty")
184 .unwrap_or(false)
185 || std::env::var("TERMINAL_EMULATOR")
186 .map(|t| t == "iterm-kitty" || t == "iTerm.app")
187 .unwrap_or(false)
188 || std::env::var("TERM_PROGRAM")
189 .map(|t| t == "rio")
190 .unwrap_or(false)
191}
192
193pub fn supports_iterm2() -> bool {
195 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
196 if prog == "iTerm.app" || prog == "WezTerm" || prog == "rio" {
197 return true;
198 }
199 }
200 false
201}
202
203pub fn supports_sixel() -> bool {
205 if let Ok(term) = std::env::var("TERM") {
206 let term = term.to_lowercase();
207 if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
208 return true;
209 }
210 }
211
212 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
213 if prog == "WezTerm" || prog == "iTerm.app" || prog == "rio" {
214 return true;
215 }
216 }
217
218 if std::env::var("WT_SESSION").is_ok() {
219 return true;
220 }
221
222 false
223}
224
225pub fn chafa_available() -> bool {
227 std::process::Command::new("chafa")
228 .arg("--version")
229 .output()
230 .map(|o| o.status.success())
231 .unwrap_or(false)
232}
233
234fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
236 let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
237 std::fs::write(&temp_path, bytes)?;
238 Ok(temp_path)
239}
240
241pub fn print_with_chafa(path: &std::path::Path) -> bool {
246 let output = std::process::Command::new("chafa")
247 .arg("--format")
248 .arg("symbols")
249 .arg("--size")
250 .arg("40x20")
251 .arg(path)
252 .output();
253
254 match output {
255 Ok(out) if out.status.success() => {
256 print!("{}", String::from_utf8_lossy(&out.stdout));
257 true
258 }
259 Ok(out) => {
260 eprintln!("warning: chafa failed with status: {}", out.status);
261 false
262 }
263 Err(e) => {
264 eprintln!("warning: failed to execute chafa: {}", e);
265 false
266 }
267 }
268}
269
270pub fn get_chafa_logo_lines(path: &std::path::Path) -> Option<Vec<String>> {
272 let output = std::process::Command::new("chafa")
273 .arg("--format")
274 .arg("symbols")
275 .arg("--size")
276 .arg("40x20")
277 .arg(path)
278 .output()
279 .ok()?;
280 if output.status.success() {
281 let content = String::from_utf8_lossy(&output.stdout);
282 Some(content.lines().map(|s| s.to_string()).collect())
283 } else {
284 None
285 }
286}
287
288pub fn print_distro_logo(distro: Option<&str>) {
293 print_distro_logo_with_ascii(distro, false, false);
294}
295
296pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool, chafa_only: bool) {
304 if ascii_only {
305 let art = get_distro_logo_lines(distro);
307 for line in art {
308 println!("{}", line);
309 }
310 return;
311 }
312
313 let has_chafa = chafa_available();
314
315 if !chafa_only {
316 let supports_kitty = supports_kitty();
317 let supports_iterm2 = supports_iterm2();
318 let supports_sixel = supports_sixel();
319
320 #[cfg(feature = "graphics")]
322 if supports_kitty {
323 if let Some(bytes) = get_embedded_logo(distro) {
324 if !bytes.is_empty() {
325 print_graphical_logo(bytes);
326 return;
327 }
328 }
329 }
330
331 #[cfg(feature = "graphics")]
333 if supports_iterm2 {
334 if let Some(bytes) = get_embedded_logo(distro) {
335 if !bytes.is_empty() {
336 print_iterm2_logo(bytes);
337 return;
338 }
339 }
340 }
341
342 #[cfg(feature = "graphics")]
344 if supports_sixel {
345 if let Some(bytes) = get_embedded_logo(distro) {
346 if !bytes.is_empty() {
347 print_sixel_logo(bytes);
348 return;
349 }
350 }
351 }
352 }
353
354 if has_chafa {
356 if let Some(bytes) = get_embedded_logo(distro) {
357 if bytes.len() > 100 {
358 if let Ok(temp_path) = write_temp_logo(bytes) {
359 if print_with_chafa(&temp_path) {
360 let _ = std::fs::remove_file(&temp_path);
361 return;
362 }
363 let _ = std::fs::remove_file(&temp_path);
364 }
365 }
366 }
367 }
368
369 let art = get_distro_logo_lines(distro);
371 for line in art {
372 println!("{}", line);
373 }
374}
375
376#[cfg(feature = "graphics")]
378pub fn print_iterm2_logo(image_data: &[u8]) {
379 use base64::Engine;
380 let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
381 print!(
382 "\x1b]1337;File=inline=1;preserveAspectRatio=1:{}\x07",
383 encoded
384 );
385 println!(); }
387
388#[cfg(feature = "graphics")]
390pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
391 if let Ok(bytes) = std::fs::read(path) {
392 print_iterm2_logo(&bytes);
393 } else {
394 println!("[Could not read logo for iTerm2 from {}]", path.display());
395 }
396}
397
398#[cfg(not(feature = "graphics"))]
400pub fn print_iterm2_logo(_image_data: &[u8]) {
401 println!("[iTerm2 logo support requires --features graphics]");
402}
403
404#[cfg(feature = "graphics")]
406pub fn print_graphical_logo(image_data: &[u8]) {
407 use base64::Engine;
408
409 let (width, height) = image::load_from_memory(image_data)
410 .map(|img| (img.width(), img.height()))
411 .unwrap_or((0, 0));
412
413 let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
414
415 if width > 0 && height > 0 {
416 println!("\x1b_Gf=100,s={},v={},a=T;{}\x1b\\", width, height, encoded);
417 } else {
418 println!("\x1b_Gf=100,a=T;{}", encoded);
419 }
420}
421
422#[cfg(feature = "graphics")]
424pub fn print_sixel_logo(image_data: &[u8]) {
425 if let Ok(img) = image::load_from_memory(image_data) {
426 let rgba = img.to_rgba8();
427 let (width, height) = rgba.dimensions();
428 print_sixel_rgba(rgba.as_raw(), width, height);
429 }
430}
431
432#[cfg(feature = "graphics")]
434pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
435 use icy_sixel::SixelImage;
436
437 match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
438 Ok(sixel_img) => match sixel_img.encode() {
439 Ok(sixel_str) => {
440 print!("{}", sixel_str);
441 }
442 Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
443 },
444 Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
445 }
446}
447
448#[cfg(not(feature = "graphics"))]
450pub fn print_graphical_logo(_image_data: &[u8]) {
451 println!("[Graphical logo support requires --features graphics]");
452}
453
454#[cfg(not(feature = "graphics"))]
456pub fn print_sixel_logo(_image_data: &[u8]) {
457 println!("[Sixel logo support requires --features graphics]");
458}
459
460#[cfg(feature = "graphics")]
462pub fn print_graphical_logo_from_path(path: &std::path::Path) {
463 use image::ImageFormat;
464 match image::open(path) {
465 Ok(img) => {
466 let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
467 let mut png_data = Vec::new();
468 if resized
469 .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
470 .is_ok()
471 {
472 print_graphical_logo(&png_data);
473 } else {
474 println!("[Failed to encode logo as PNG]");
475 }
476 }
477 Err(_) => {
478 println!("[Could not load graphical logo from {}]", path.display());
479 }
480 }
481}
482
483#[cfg(feature = "graphics")]
485pub fn print_sixel_logo_from_path(path: &std::path::Path) {
486 match image::open(path) {
487 Ok(img) => {
488 let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
489 let rgba = resized.to_rgba8();
490 let (width, height) = rgba.dimensions();
491 print_sixel_rgba(rgba.as_raw(), width, height);
492 }
493 Err(_) => {
494 println!("[Could not load logo for Sixel from {}]", path.display());
495 }
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_get_ascii_logo_arch() {
505 let logo = get_ascii_logo(Some("arch"));
506 assert!(!logo.is_empty());
507 assert!(logo[0].contains("`"));
508 }
509
510 #[test]
511 fn test_get_ascii_logo_unknown() {
512 let logo = get_ascii_logo(Some("unknown_distro"));
513 assert!(!logo.is_empty());
514 assert!(logo
516 .iter()
517 .any(|line| line.contains("o${2}_${3}o") || line.contains("o_o")));
518 }
519
520 #[test]
521 fn test_get_ascii_logo_none() {
522 let logo = get_ascii_logo(None);
523 assert!(!logo.is_empty());
524 }
525
526 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
527
528 struct EnvGuard {
529 _mutex_guard: std::sync::MutexGuard<'static, ()>,
530 old_vars: std::collections::HashMap<&'static str, Option<String>>,
531 }
532
533 impl EnvGuard {
534 fn new(vars_to_mock: &[&'static str]) -> Self {
535 let guard = ENV_LOCK.lock().unwrap();
536 let mut old_vars = std::collections::HashMap::new();
537 for var in vars_to_mock {
538 old_vars.insert(*var, std::env::var(var).ok());
539 }
540 EnvGuard {
541 _mutex_guard: guard,
542 old_vars,
543 }
544 }
545 }
546
547 impl Drop for EnvGuard {
548 fn drop(&mut self) {
549 for (var, value) in &self.old_vars {
550 if let Some(val) = value {
551 std::env::set_var(var, val);
552 } else {
553 std::env::remove_var(var);
554 }
555 }
556 }
557 }
558
559 #[test]
560 fn test_supports_kitty_heuristics() {
561 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
562
563 std::env::set_var("TERM", "xterm-kitty");
565 std::env::remove_var("TERMINAL_EMULATOR");
566 std::env::remove_var("TERM_PROGRAM");
567 assert!(supports_kitty());
568
569 std::env::remove_var("TERM");
571 std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
572 assert!(supports_kitty());
573
574 std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
576 assert!(supports_kitty());
577
578 std::env::remove_var("TERMINAL_EMULATOR");
580 std::env::set_var("TERM_PROGRAM", "rio");
581 assert!(supports_kitty());
582
583 std::env::remove_var("TERM_PROGRAM");
585 assert!(!supports_kitty());
586 }
587
588 #[test]
589 fn test_supports_iterm2_heuristics() {
590 let _guard = EnvGuard::new(&["TERM_PROGRAM"]);
591
592 std::env::set_var("TERM_PROGRAM", "iTerm.app");
594 assert!(supports_iterm2());
595
596 std::env::set_var("TERM_PROGRAM", "WezTerm");
598 assert!(supports_iterm2());
599
600 std::env::set_var("TERM_PROGRAM", "rio");
602 assert!(supports_iterm2());
603
604 std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
606 assert!(!supports_iterm2());
607
608 std::env::remove_var("TERM_PROGRAM");
610 assert!(!supports_iterm2());
611 }
612
613 #[test]
614 fn test_supports_sixel_heuristics() {
615 let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
616
617 std::env::remove_var("TERM");
619 std::env::remove_var("TERM_PROGRAM");
620 std::env::remove_var("WT_SESSION");
621 assert!(!supports_sixel());
622
623 std::env::set_var("TERM", "xterm-sixel");
625 assert!(supports_sixel());
626
627 std::env::set_var("TERM", "foot");
629 assert!(supports_sixel());
630
631 std::env::set_var("TERM", "MLTerm");
633 assert!(supports_sixel());
634
635 std::env::remove_var("TERM");
637 std::env::set_var("TERM_PROGRAM", "WezTerm");
638 assert!(supports_sixel());
639
640 std::env::set_var("TERM_PROGRAM", "iTerm.app");
642 assert!(supports_sixel());
643
644 std::env::set_var("TERM_PROGRAM", "rio");
646 assert!(supports_sixel());
647
648 std::env::remove_var("TERM_PROGRAM");
650 std::env::set_var("WT_SESSION", "active");
651 assert!(supports_sixel());
652 }
653
654 #[test]
655 fn test_get_embedded_logo() {
656 let logo = get_embedded_logo(Some("arch"));
657 assert!(logo.is_some());
658 let logo = get_embedded_logo(Some("pop"));
659 assert!(logo.is_some());
660 let logo = get_embedded_logo(Some("manjaro"));
661 assert!(logo.is_some());
662 let logo = get_embedded_logo(Some("endeavouros"));
663 assert!(logo.is_some());
664 let logo = get_embedded_logo(Some("opensuse"));
665 assert!(logo.is_some());
666 let logo = get_embedded_logo(Some("opensuse-leap"));
667 assert!(logo.is_some());
668 let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
669 assert!(logo.is_some());
670 let logo = get_embedded_logo(Some("macos"));
671 assert!(logo.is_some());
672 let logo = get_embedded_logo(Some("windows"));
673 assert!(logo.is_some());
674 let logo = get_embedded_logo(None);
675 assert!(logo.is_some());
676 }
677
678 #[test]
679 fn test_get_ascii_logo_new_distros() {
680 let pop = get_ascii_logo(Some("pop"));
681 assert!(!pop.is_empty());
682 assert!(pop.iter().any(|line| line.contains("767")));
683
684 let manjaro = get_ascii_logo(Some("manjaro"));
685 assert!(!manjaro.is_empty());
686 assert!(manjaro.iter().any(|line| line.contains("████████")));
687
688 let endeavouros = get_ascii_logo(Some("endeavouros"));
689 assert!(!endeavouros.is_empty());
690 assert!(endeavouros.iter().any(|line| line.contains("ssso")));
691
692 let opensuse = get_ascii_logo(Some("opensuse"));
693 assert!(!opensuse.is_empty());
694 assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
695
696 let macos = get_ascii_logo(Some("macos"));
697 assert!(!macos.is_empty());
698 assert!(macos
699 .iter()
700 .any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
701
702 let windows = get_ascii_logo(Some("windows"));
703 assert!(!windows.is_empty());
704 assert!(windows
705 .iter()
706 .any(|line| line.contains("AEEEtttt::::ztF") || line.contains("tt:::tt333EE3")));
707 }
708}