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("mx") => Some(include_bytes!("../assets/logos/mx.png")),
36 Some("linuxmint") => Some(include_bytes!("../assets/logos/linuxmint.png")),
37 Some("kali") => Some(include_bytes!("../assets/logos/kali.png")),
38 Some("zorin") => Some(include_bytes!("../assets/logos/zorin.png")),
39 Some("garuda") => Some(include_bytes!("../assets/logos/garuda.png")),
40 Some("macos") => Some(include_bytes!("../assets/logos/macos.png")),
41 Some("windows") => Some(include_bytes!("../assets/logos/windows.png")),
42 _ => Some(include_bytes!("../assets/logos/tux.png")),
43 }
44}
45
46#[cfg(not(feature = "graphics"))]
48pub fn get_embedded_logo(_distro: Option<&str>) -> Option<&'static [u8]> {
49 Some(include_bytes!("../assets/logos/tux.png"))
50}
51
52pub fn detect_distro() -> Option<String> {
54 #[cfg(target_os = "macos")]
55 {
56 Some("macos".to_string())
57 }
58 #[cfg(target_os = "windows")]
59 {
60 Some("windows".to_string())
61 }
62 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
63 {
64 if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
65 for line in content.lines() {
66 if line.starts_with("ID=") {
67 let id = line.trim_start_matches("ID=").trim_matches('"');
68 return Some(id.to_string());
69 }
70 }
71 }
72 None
73 }
74}
75
76pub fn get_ascii_logo(distro: Option<&str>) -> Vec<String> {
80 let d = distro.map(|s| s.to_lowercase());
81
82 match d.as_deref() {
83 Some("arch") => {
84 let logo = include_str!("../assets/logos/arch.txt");
85 logo.lines().map(|s| s.to_string()).collect()
86 }
87 Some("debian") => {
88 let logo = include_str!("../assets/logos/debian.txt");
89 logo.lines().map(|s| s.to_string()).collect()
90 }
91 Some("fedora") => {
92 let logo = include_str!("../assets/logos/fedora.txt");
93 logo.lines().map(|s| s.to_string()).collect()
94 }
95 Some("nixos") => {
96 let logo = include_str!("../assets/logos/nixos.txt");
97 logo.lines().map(|s| s.to_string()).collect()
98 }
99 Some("ubuntu") => {
100 let logo = include_str!("../assets/logos/ubuntu.txt");
101 logo.lines().map(|s| s.to_string()).collect()
102 }
103 Some("pop") => {
104 let logo = include_str!("../assets/logos/pop.txt");
105 logo.lines().map(|s| s.to_string()).collect()
106 }
107 Some("manjaro") => {
108 let logo = include_str!("../assets/logos/manjaro.txt");
109 logo.lines().map(|s| s.to_string()).collect()
110 }
111 Some("endeavouros") => {
112 let logo = include_str!("../assets/logos/endeavouros.txt");
113 logo.lines().map(|s| s.to_string()).collect()
114 }
115 Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
116 let logo = include_str!("../assets/logos/opensuse.txt");
117 logo.lines().map(|s| s.to_string()).collect()
118 }
119 Some("mx") => {
120 let logo = include_str!("../assets/logos/mx.txt");
121 logo.lines().map(|s| s.to_string()).collect()
122 }
123 Some("linuxmint") => {
124 let logo = include_str!("../assets/logos/linuxmint.txt");
125 logo.lines().map(|s| s.to_string()).collect()
126 }
127 Some("kali") => {
128 let logo = include_str!("../assets/logos/kali.txt");
129 logo.lines().map(|s| s.to_string()).collect()
130 }
131 Some("zorin") => {
132 let logo = include_str!("../assets/logos/zorin.txt");
133 logo.lines().map(|s| s.to_string()).collect()
134 }
135 Some("garuda") => {
136 let logo = include_str!("../assets/logos/garuda.txt");
137 logo.lines().map(|s| s.to_string()).collect()
138 }
139 Some("macos") => {
140 let logo = include_str!("../assets/logos/macos.txt");
141 logo.lines().map(|s| s.to_string()).collect()
142 }
143 Some("windows") => {
144 let logo = include_str!("../assets/logos/windows.txt");
145 logo.lines().map(|s| s.to_string()).collect()
146 }
147
148 _ => {
150 let logo = include_str!("../assets/logos/tux.txt");
151 logo.lines().map(|s| s.to_string()).collect()
152 }
153 }
154}
155
156pub fn get_distro_colors(distro: Option<&str>) -> Vec<&'static str> {
158 let d = distro.map(|s| s.to_lowercase());
159 match d.as_deref() {
160 Some("arch") => vec!["\x1b[36m", "\x1b[37m"],
161 Some("debian") => vec!["\x1b[31m", "\x1b[37m"],
162 Some("fedora") => vec!["\x1b[34m", "\x1b[37m"],
163 Some("nixos") => vec![
164 "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m",
165 ],
166 Some("ubuntu") => vec!["\x1b[33m", "\x1b[31m"],
167 Some("pop") => vec!["\x1b[36m", "\x1b[37m"],
168 Some("manjaro") => vec!["\x1b[32m"],
169 Some("endeavouros") => vec!["\x1b[35m", "\x1b[31m", "\x1b[34m"],
170 Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
171 vec!["\x1b[32m", "\x1b[37m"]
172 }
173 Some("mx") => vec!["\x1b[34m", "\x1b[37m"],
174 Some("linuxmint") => vec!["\x1b[32m", "\x1b[37m"],
175 Some("kali") => vec!["\x1b[34m", "\x1b[37m"],
176 Some("zorin") => vec!["\x1b[36m", "\x1b[37m"],
177 Some("garuda") => vec!["\x1b[35m", "\x1b[36m"],
178 Some("macos") => vec![
181 "\x1b[38;5;252m",
182 "\x1b[38;5;250m",
183 "\x1b[38;5;248m",
184 "\x1b[38;5;246m",
185 "\x1b[38;5;244m",
186 ],
187 Some("windows") => vec!["\x1b[36m"],
188 _ => vec!["\x1b[30m", "\x1b[37m", "\x1b[33m"], }
190}
191
192pub fn get_distro_logo_lines(distro: Option<&str>) -> Vec<String> {
194 let raw_lines = get_ascii_logo(distro);
195 let colors = get_distro_colors(distro);
196 let default_color = colors.first().copied().unwrap_or("\x1b[0m");
197
198 raw_lines
199 .into_iter()
200 .map(|line| {
201 let mut formatted = line;
202 for i in 1..=9 {
203 let color_val = colors.get(i - 1).copied().unwrap_or("\x1b[0m");
204 let placeholder = format!("${{{}}}", i);
205 formatted = formatted.replace(&placeholder, color_val);
206 let placeholder_short = format!("${}", i);
207 formatted = formatted.replace(&placeholder_short, color_val);
208 }
209 if !formatted.is_empty() {
210 format!("{}{}\x1b[0m", default_color, formatted)
211 } else {
212 formatted
213 }
214 })
215 .collect()
216}
217
218fn is_rio_terminal() -> bool {
225 if let Ok(term) = std::env::var("TERM") {
226 if term == "rio" || term.starts_with("xterm-rio") {
227 return true;
228 }
229 }
230 std::env::var("TERM_PROGRAM")
231 .map(|t| t == "rio")
232 .unwrap_or(false)
233}
234
235pub fn supports_kitty() -> bool {
237 std::env::var("TERM")
238 .map(|t| t == "xterm-kitty")
239 .unwrap_or(false)
240 || std::env::var("TERMINAL_EMULATOR")
241 .map(|t| t == "iterm-kitty" || t == "iTerm.app")
242 .unwrap_or(false)
243 || is_rio_terminal()
244}
245
246pub fn supports_iterm2() -> bool {
248 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
249 if prog == "iTerm.app" || prog == "WezTerm" {
250 return true;
251 }
252 }
253 is_rio_terminal()
254}
255
256pub fn supports_sixel() -> bool {
258 if let Ok(term) = std::env::var("TERM") {
259 let term = term.to_lowercase();
260 if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
261 return true;
262 }
263 }
264
265 if let Ok(prog) = std::env::var("TERM_PROGRAM") {
266 if prog == "WezTerm" || prog == "iTerm.app" {
267 return true;
268 }
269 }
270
271 if std::env::var("WT_SESSION").is_ok() {
272 return true;
273 }
274
275 is_rio_terminal()
276}
277
278pub const LOGO_MAX_COLS: usize = 45;
286
287pub const LOGO_MAX_ROWS: usize = 10;
289
290pub fn fit_logo_cells(
304 img_w: u32,
305 img_h: u32,
306 cell_w: usize,
307 cell_h: usize,
308 max_cols: usize,
309 max_rows: usize,
310) -> LogoFit {
311 let (max_cols, max_rows) = (max_cols.max(1), max_rows.max(1));
312
313 if img_w == 0 || img_h == 0 || cell_w == 0 || cell_h == 0 {
315 return LogoFit {
316 cols: max_cols,
317 rows: max_rows,
318 width_limited: true,
319 };
320 }
321
322 let (img_w, img_h) = (u64::from(img_w), u64::from(img_h));
323 let box_w = (max_cols * cell_w) as u64;
324 let box_h = (max_rows * cell_h) as u64;
325
326 let width_limited = box_w * img_h <= box_h * img_w;
329 let (disp_w, disp_h) = if width_limited {
330 (box_w, box_w * img_h / img_w) } else {
332 (box_h * img_w / img_h, box_h) };
334
335 LogoFit {
336 cols: (disp_w as usize).div_ceil(cell_w).clamp(1, max_cols),
339 rows: (disp_h as usize).div_ceil(cell_h).clamp(1, max_rows),
340 width_limited,
341 }
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub struct LogoFit {
349 pub cols: usize,
351 pub rows: usize,
353 pub width_limited: bool,
355}
356
357pub fn kitty_placement_spec(fit: LogoFit) -> String {
369 if fit.width_limited {
370 format!("c={}", fit.cols)
371 } else {
372 format!("r={}", fit.rows)
373 }
374}
375
376pub fn terminal_cell_size_px() -> (usize, usize) {
381 #[cfg(unix)]
382 {
383 use std::mem::MaybeUninit;
384 let mut ws: libc::winsize = unsafe { MaybeUninit::zeroed().assume_init() };
385 let ret = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
387 if ret == 0 && ws.ws_row > 0 && ws.ws_col > 0 && ws.ws_xpixel > 0 && ws.ws_ypixel > 0 {
388 return (
389 ws.ws_xpixel as usize / ws.ws_col as usize,
390 ws.ws_ypixel as usize / ws.ws_row as usize,
391 );
392 }
393 }
394 (10, 20)
395}
396
397pub fn logo_cells_for(img_w: u32, img_h: u32) -> LogoFit {
401 let (cell_w, cell_h) = terminal_cell_size_px();
402 fit_logo_cells(img_w, img_h, cell_w, cell_h, LOGO_MAX_COLS, LOGO_MAX_ROWS)
403}
404
405pub fn chafa_size_arg() -> String {
410 format!("{LOGO_MAX_COLS}x{LOGO_MAX_ROWS}")
411}
412
413static CHAFA_SUPPORTS_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
414
415pub fn chafa_supports_probe() -> bool {
417 *CHAFA_SUPPORTS_PROBE.get_or_init(|| {
418 std::process::Command::new("chafa")
419 .args(["--probe", "off", "--version"])
420 .output()
421 .map(|o| o.status.success())
422 .unwrap_or(false)
423 })
424}
425
426pub fn chafa_available() -> bool {
428 std::process::Command::new("chafa")
429 .arg("--version")
430 .output()
431 .map(|o| o.status.success())
432 .unwrap_or(false)
433}
434
435fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
437 let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
438 std::fs::write(&temp_path, bytes)?;
439 Ok(temp_path)
440}
441
442pub fn print_with_chafa(path: &std::path::Path) -> bool {
447 let mut cmd = std::process::Command::new("chafa");
448 cmd.arg("--format")
449 .arg("symbols")
450 .arg("--size")
451 .arg(chafa_size_arg());
452
453 if chafa_supports_probe() {
454 cmd.arg("--probe").arg("off");
455 }
456
457 let output = cmd.arg(path).output();
458
459 match output {
460 Ok(out) if out.status.success() => {
461 print!("{}", String::from_utf8_lossy(&out.stdout));
462 true
463 }
464 Ok(out) => {
465 eprintln!("warning: chafa failed with status: {}", out.status);
466 false
467 }
468 Err(e) => {
469 eprintln!("warning: failed to execute chafa: {}", e);
470 false
471 }
472 }
473}
474
475pub fn get_chafa_logo_lines(path: &std::path::Path) -> Option<Vec<String>> {
477 let mut cmd = std::process::Command::new("chafa");
478 cmd.arg("--format")
479 .arg("symbols")
480 .arg("--size")
481 .arg(chafa_size_arg());
482
483 if chafa_supports_probe() {
484 cmd.arg("--probe").arg("off");
485 }
486
487 let output = cmd.arg(path).output().ok()?;
488 if output.status.success() {
489 let content = String::from_utf8_lossy(&output.stdout);
490 Some(content.lines().map(|s| s.to_string()).collect())
491 } else {
492 None
493 }
494}
495
496pub fn print_distro_logo(distro: Option<&str>) {
501 print_distro_logo_with_ascii(distro, false, false);
502}
503
504pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool, chafa_only: bool) {
512 if ascii_only {
513 let art = get_distro_logo_lines(distro);
515 for line in art {
516 println!("{}", line);
517 }
518 return;
519 }
520
521 let has_chafa = chafa_available();
522
523 if !chafa_only {
524 let supports_kitty = supports_kitty();
525 let supports_iterm2 = supports_iterm2();
526 let supports_sixel = supports_sixel();
527
528 #[cfg(feature = "graphics")]
530 if supports_kitty {
531 if let Some(bytes) = get_embedded_logo(distro) {
532 if !bytes.is_empty() {
533 print_graphical_logo(bytes);
534 return;
535 }
536 }
537 }
538
539 #[cfg(feature = "graphics")]
541 if supports_iterm2 {
542 if let Some(bytes) = get_embedded_logo(distro) {
543 if !bytes.is_empty() {
544 print_iterm2_logo(bytes);
545 return;
546 }
547 }
548 }
549
550 #[cfg(feature = "graphics")]
552 if supports_sixel {
553 if let Some(bytes) = get_embedded_logo(distro) {
554 if !bytes.is_empty() {
555 print_sixel_logo(bytes);
556 return;
557 }
558 }
559 }
560 }
561
562 if has_chafa {
564 if let Some(bytes) = get_embedded_logo(distro) {
565 if bytes.len() > 100 {
566 if let Ok(temp_path) = write_temp_logo(bytes) {
567 if print_with_chafa(&temp_path) {
568 let _ = std::fs::remove_file(&temp_path);
569 return;
570 }
571 let _ = std::fs::remove_file(&temp_path);
572 }
573 }
574 }
575 }
576
577 let art = get_distro_logo_lines(distro);
579 for line in art {
580 println!("{}", line);
581 }
582}
583
584#[cfg(feature = "graphics")]
586pub fn print_iterm2_logo(image_data: &[u8]) {
587 use base64::Engine;
588
589 let (width, height) = image::load_from_memory(image_data)
590 .map(|img| (img.width(), img.height()))
591 .unwrap_or((0, 0));
592
593 let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
594
595 if width > 0 && height > 0 {
599 let fit = logo_cells_for(width, height);
600 print!(
601 "\x1b]1337;File=inline=1;width={};height={};preserveAspectRatio=1:{}\x07",
602 fit.cols, fit.rows, encoded
603 );
604 } else {
605 print!(
606 "\x1b]1337;File=inline=1;height={};preserveAspectRatio=1:{}\x07",
607 LOGO_MAX_ROWS, encoded
608 );
609 }
610 println!(); }
612
613#[cfg(feature = "graphics")]
615pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
616 if let Ok(bytes) = std::fs::read(path) {
617 print_iterm2_logo(&bytes);
618 } else {
619 println!("[Could not read logo for iTerm2 from {}]", path.display());
620 }
621}
622
623#[cfg(not(feature = "graphics"))]
625pub fn print_iterm2_logo(_image_data: &[u8]) {
626 println!("[iTerm2 logo support requires --features graphics]");
627}
628
629#[cfg(feature = "graphics")]
631pub fn print_graphical_logo(image_data: &[u8]) {
632 use base64::Engine;
633
634 let (width, height) = image::load_from_memory(image_data)
635 .map(|img| (img.width(), img.height()))
636 .unwrap_or((0, 0));
637
638 let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
639
640 if width > 0 && height > 0 {
641 let spec = kitty_placement_spec(logo_cells_for(width, height));
646 println!(
647 "\x1b_Gf=100,s={},v={},{},a=T;{}\x1b\\",
648 width, height, spec, encoded
649 );
650 } else {
651 println!("\x1b_Gf=100,a=T;{}", encoded);
652 }
653}
654
655#[cfg(feature = "graphics")]
657pub fn print_sixel_logo(image_data: &[u8]) {
658 if let Ok(img) = image::load_from_memory(image_data) {
659 let fit = logo_cells_for(img.width(), img.height());
663 let (cell_w, cell_h) = terminal_cell_size_px();
664 let resized = img.resize(
665 (fit.cols * cell_w) as u32,
666 (fit.rows * cell_h) as u32,
667 image::imageops::FilterType::Triangle,
668 );
669 let rgba = resized.to_rgba8();
670 let (width, height) = rgba.dimensions();
671 print_sixel_rgba(rgba.as_raw(), width, height);
672 }
673}
674
675#[cfg(feature = "graphics")]
677pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
678 use icy_sixel::SixelImage;
679
680 match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
681 Ok(sixel_img) => match sixel_img.encode() {
682 Ok(sixel_str) => {
683 print!("{}", sixel_str);
684 }
685 Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
686 },
687 Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
688 }
689}
690
691#[cfg(not(feature = "graphics"))]
693pub fn print_graphical_logo(_image_data: &[u8]) {
694 println!("[Graphical logo support requires --features graphics]");
695}
696
697#[cfg(not(feature = "graphics"))]
699pub fn print_sixel_logo(_image_data: &[u8]) {
700 println!("[Sixel logo support requires --features graphics]");
701}
702
703#[cfg(feature = "graphics")]
705pub fn print_graphical_logo_from_path(path: &std::path::Path) {
706 use image::ImageFormat;
707 match image::open(path) {
708 Ok(img) => {
709 let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
710 let mut png_data = Vec::new();
711 if resized
712 .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
713 .is_ok()
714 {
715 print_graphical_logo(&png_data);
716 } else {
717 println!("[Failed to encode logo as PNG]");
718 }
719 }
720 Err(_) => {
721 println!("[Could not load graphical logo from {}]", path.display());
722 }
723 }
724}
725
726#[cfg(feature = "graphics")]
728pub fn print_sixel_logo_from_path(path: &std::path::Path) {
729 match image::open(path) {
730 Ok(img) => {
731 let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
732 let rgba = resized.to_rgba8();
733 let (width, height) = rgba.dimensions();
734 print_sixel_rgba(rgba.as_raw(), width, height);
735 }
736 Err(_) => {
737 println!("[Could not load logo for Sixel from {}]", path.display());
738 }
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745
746 #[test]
747 fn test_get_ascii_logo_arch() {
748 let logo = get_ascii_logo(Some("arch"));
749 assert!(!logo.is_empty());
750 assert!(logo[0].contains("`"));
751 }
752
753 #[test]
754 fn test_get_ascii_logo_unknown() {
755 let logo = get_ascii_logo(Some("unknown_distro"));
756 assert!(!logo.is_empty());
757 assert!(logo
759 .iter()
760 .any(|line| line.contains("o${2}_${3}o") || line.contains("o_o")));
761 }
762
763 #[test]
764 fn test_get_ascii_logo_none() {
765 let logo = get_ascii_logo(None);
766 assert!(!logo.is_empty());
767 }
768
769 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
770
771 struct EnvGuard {
772 _mutex_guard: std::sync::MutexGuard<'static, ()>,
773 old_vars: std::collections::HashMap<&'static str, Option<String>>,
774 }
775
776 impl EnvGuard {
777 fn new(vars_to_mock: &[&'static str]) -> Self {
778 let guard = ENV_LOCK.lock().unwrap();
779 let mut old_vars = std::collections::HashMap::new();
780 for var in vars_to_mock {
781 old_vars.insert(*var, std::env::var(var).ok());
782 }
783 EnvGuard {
784 _mutex_guard: guard,
785 old_vars,
786 }
787 }
788 }
789
790 impl Drop for EnvGuard {
791 fn drop(&mut self) {
792 for (var, value) in &self.old_vars {
793 if let Some(val) = value {
794 std::env::set_var(var, val);
795 } else {
796 std::env::remove_var(var);
797 }
798 }
799 }
800 }
801
802 #[test]
803 fn test_supports_kitty_heuristics() {
804 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
805
806 std::env::set_var("TERM", "xterm-kitty");
808 std::env::remove_var("TERMINAL_EMULATOR");
809 std::env::remove_var("TERM_PROGRAM");
810 assert!(supports_kitty());
811
812 std::env::remove_var("TERM");
814 std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
815 assert!(supports_kitty());
816
817 std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
819 assert!(supports_kitty());
820
821 std::env::remove_var("TERMINAL_EMULATOR");
823 std::env::set_var("TERM_PROGRAM", "rio");
824 assert!(supports_kitty());
825
826 std::env::remove_var("TERM_PROGRAM");
828 assert!(!supports_kitty());
829 }
830
831 #[test]
832 fn test_supports_iterm2_heuristics() {
833 let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM"]);
837 std::env::remove_var("TERM");
838
839 std::env::set_var("TERM_PROGRAM", "iTerm.app");
841 assert!(supports_iterm2());
842
843 std::env::set_var("TERM_PROGRAM", "WezTerm");
845 assert!(supports_iterm2());
846
847 std::env::set_var("TERM_PROGRAM", "rio");
849 assert!(supports_iterm2());
850
851 std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
853 assert!(!supports_iterm2());
854
855 std::env::remove_var("TERM_PROGRAM");
857 assert!(!supports_iterm2());
858 }
859
860 #[test]
861 fn test_supports_sixel_heuristics() {
862 let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
863
864 std::env::remove_var("TERM");
866 std::env::remove_var("TERM_PROGRAM");
867 std::env::remove_var("WT_SESSION");
868 assert!(!supports_sixel());
869
870 std::env::set_var("TERM", "xterm-sixel");
872 assert!(supports_sixel());
873
874 std::env::set_var("TERM", "foot");
876 assert!(supports_sixel());
877
878 std::env::set_var("TERM", "MLTerm");
880 assert!(supports_sixel());
881
882 std::env::remove_var("TERM");
884 std::env::set_var("TERM_PROGRAM", "WezTerm");
885 assert!(supports_sixel());
886
887 std::env::set_var("TERM_PROGRAM", "iTerm.app");
889 assert!(supports_sixel());
890
891 std::env::set_var("TERM_PROGRAM", "rio");
893 assert!(supports_sixel());
894
895 std::env::remove_var("TERM_PROGRAM");
897 std::env::set_var("WT_SESSION", "active");
898 assert!(supports_sixel());
899 }
900
901 #[test]
902 fn test_get_embedded_logo() {
903 let logo = get_embedded_logo(Some("arch"));
904 assert!(logo.is_some());
905 let logo = get_embedded_logo(Some("pop"));
906 assert!(logo.is_some());
907 let logo = get_embedded_logo(Some("manjaro"));
908 assert!(logo.is_some());
909 let logo = get_embedded_logo(Some("endeavouros"));
910 assert!(logo.is_some());
911 let logo = get_embedded_logo(Some("opensuse"));
912 assert!(logo.is_some());
913 let logo = get_embedded_logo(Some("opensuse-leap"));
914 assert!(logo.is_some());
915 let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
916 assert!(logo.is_some());
917 let logo = get_embedded_logo(Some("mx"));
918 assert!(logo.is_some());
919 let logo = get_embedded_logo(Some("linuxmint"));
920 assert!(logo.is_some());
921 let logo = get_embedded_logo(Some("kali"));
922 assert!(logo.is_some());
923 let logo = get_embedded_logo(Some("zorin"));
924 assert!(logo.is_some());
925 let logo = get_embedded_logo(Some("garuda"));
926 assert!(logo.is_some());
927 let logo = get_embedded_logo(Some("macos"));
928 assert!(logo.is_some());
929 let logo = get_embedded_logo(Some("windows"));
930 assert!(logo.is_some());
931 let logo = get_embedded_logo(None);
932 assert!(logo.is_some());
933 }
934
935 #[test]
936 fn test_get_ascii_logo_new_distros() {
937 let pop = get_ascii_logo(Some("pop"));
938 assert!(!pop.is_empty());
939 assert!(pop.iter().any(|line| line.contains("767")));
940
941 let manjaro = get_ascii_logo(Some("manjaro"));
942 assert!(!manjaro.is_empty());
943 assert!(manjaro.iter().any(|line| line.contains("████████")));
944
945 let endeavouros = get_ascii_logo(Some("endeavouros"));
946 assert!(!endeavouros.is_empty());
947 assert!(endeavouros.iter().any(|line| line.contains("ssso")));
948
949 let opensuse = get_ascii_logo(Some("opensuse"));
950 assert!(!opensuse.is_empty());
951 assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
952
953 let macos = get_ascii_logo(Some("macos"));
954 assert!(!macos.is_empty());
955 assert!(macos
956 .iter()
957 .any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
958
959 let windows = get_ascii_logo(Some("windows"));
960 assert!(!windows.is_empty());
961 assert!(windows
962 .iter()
963 .any(|line| line.contains("AEEEtttt::::ztF") || line.contains("tt:::tt333EE3")));
964
965 let mx = get_ascii_logo(Some("mx"));
966 assert!(!mx.is_empty());
967 assert!(mx
968 .iter()
969 .any(|line| line.contains("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMM")));
970
971 let linuxmint = get_ascii_logo(Some("linuxmint"));
972 assert!(!linuxmint.is_empty());
973 assert!(linuxmint.iter().any(|line| line.contains("oOOOOOOOOOOo")));
974
975 let kali = get_ascii_logo(Some("kali"));
976 assert!(!kali.is_empty());
977 assert!(kali.iter().any(|line| line.contains(":ccc")));
978
979 let zorin = get_ascii_logo(Some("zorin"));
980 assert!(!zorin.is_empty());
981 assert!(zorin
982 .iter()
983 .any(|line| line.contains("osssssssssssssssssssso")));
984
985 let garuda = get_ascii_logo(Some("garuda"));
986 assert!(!garuda.is_empty());
987 assert!(garuda.iter().any(|line| line.contains("888:8898898")));
988 }
989
990 #[test]
993 fn test_rio_detected_from_term_when_term_program_is_absent() {
994 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
997 std::env::remove_var("TERM_PROGRAM");
998 std::env::remove_var("TERMINAL_EMULATOR");
999 std::env::set_var("TERM", "xterm-rio");
1000
1001 assert!(is_rio_terminal());
1002 assert!(supports_kitty());
1003 assert!(supports_iterm2());
1004 assert!(supports_sixel());
1005 }
1006
1007 #[test]
1008 fn test_rio_still_detected_from_term_program() {
1009 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
1011 std::env::remove_var("TERMINAL_EMULATOR");
1012 std::env::set_var("TERM", "xterm-256color");
1013 std::env::set_var("TERM_PROGRAM", "rio");
1014
1015 assert!(is_rio_terminal());
1016 assert!(supports_kitty());
1017 }
1018
1019 #[test]
1020 fn test_non_rio_term_is_not_matched() {
1021 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
1023 std::env::remove_var("TERM_PROGRAM");
1024 std::env::remove_var("TERMINAL_EMULATOR");
1025 for term in ["xterm-256color", "screen", "linux", "rioja"] {
1026 std::env::set_var("TERM", term);
1027 assert!(!is_rio_terminal(), "{term} should not be detected as Rio");
1028 }
1029 }
1030
1031 #[test]
1034 fn test_fit_logo_cells_preserves_aspect_for_wide_image() {
1035 let fit = fit_logo_cells(384, 108, 10, 20, 45, 10);
1039 assert_eq!(fit.cols, 45);
1040 assert_eq!(fit.rows, 7);
1041 assert!(fit.width_limited);
1042 assert_eq!(kitty_placement_spec(fit), "c=45");
1044 }
1045
1046 #[test]
1047 fn test_fit_logo_cells_preserves_aspect_for_tall_image() {
1048 let fit = fit_logo_cells(291, 384, 10, 20, 45, 10);
1050 assert_eq!(fit.rows, 10);
1051 assert!(
1052 fit.cols < 45,
1053 "tall image should not fill the width, got {}",
1054 fit.cols
1055 );
1056 assert!(!fit.width_limited);
1057 assert_eq!(kitty_placement_spec(fit), "r=10");
1058 }
1059
1060 #[test]
1061 fn test_fit_logo_cells_accounts_for_non_square_cells() {
1062 let fit = fit_logo_cells(256, 256, 10, 20, 45, 10);
1065 assert_eq!((fit.cols, fit.rows), (20, 10));
1066 let sq = fit_logo_cells(256, 256, 10, 10, 45, 10);
1067 assert_eq!((sq.cols, sq.rows), (10, 10));
1068 }
1069
1070 #[test]
1071 fn test_fit_logo_cells_never_exceeds_the_box() {
1072 for (w, h) in [(384, 108), (291, 384), (256, 256), (4000, 3), (3, 4000)] {
1074 let fit = fit_logo_cells(w, h, 10, 20, 45, 10);
1075 assert!((1..=45).contains(&fit.cols), "{w}x{h} -> {} cols", fit.cols);
1076 assert!((1..=10).contains(&fit.rows), "{w}x{h} -> {} rows", fit.rows);
1077 }
1078 }
1079
1080 #[test]
1081 fn test_fit_logo_cells_handles_degenerate_input() {
1082 for fit in [
1085 fit_logo_cells(0, 0, 10, 20, 45, 10),
1086 fit_logo_cells(384, 108, 0, 20, 45, 10),
1087 fit_logo_cells(384, 108, 10, 0, 45, 10),
1088 ] {
1089 assert_eq!((fit.cols, fit.rows), (45, 10));
1090 }
1091 }
1092
1093 #[test]
1094 fn test_chafa_size_arg_matches_the_shared_box() {
1095 assert_eq!(chafa_size_arg(), format!("{LOGO_MAX_COLS}x{LOGO_MAX_ROWS}"));
1097 assert_eq!(chafa_size_arg(), "45x10");
1098 }
1099}