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 {
337 (box_w, (box_w * img_h).div_ceil(img_w)) } else {
339 ((box_h * img_w).div_ceil(img_h), box_h) };
341
342 LogoFit {
343 cols: (disp_w as usize).div_ceil(cell_w).clamp(1, max_cols),
346 rows: (disp_h as usize).div_ceil(cell_h).clamp(1, max_rows),
347 width_limited,
348 }
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355pub struct LogoFit {
356 pub cols: usize,
358 pub rows: usize,
360 pub width_limited: bool,
362}
363
364pub fn kitty_placement_spec(fit: LogoFit) -> String {
376 if fit.width_limited {
377 format!("c={}", fit.cols)
378 } else {
379 format!("r={}", fit.rows)
380 }
381}
382
383pub fn terminal_cell_size_px() -> (usize, usize) {
388 #[cfg(unix)]
389 {
390 use std::mem::MaybeUninit;
391 let mut ws: libc::winsize = unsafe { MaybeUninit::zeroed().assume_init() };
392 let ret = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
394 if ret == 0 && ws.ws_row > 0 && ws.ws_col > 0 && ws.ws_xpixel > 0 && ws.ws_ypixel > 0 {
395 return (
396 ws.ws_xpixel as usize / ws.ws_col as usize,
397 ws.ws_ypixel as usize / ws.ws_row as usize,
398 );
399 }
400 }
401 (10, 20)
402}
403
404pub fn logo_cells_for(img_w: u32, img_h: u32) -> LogoFit {
408 let (cell_w, cell_h) = terminal_cell_size_px();
409 fit_logo_cells(img_w, img_h, cell_w, cell_h, LOGO_MAX_COLS, LOGO_MAX_ROWS)
410}
411
412pub fn chafa_size_arg() -> String {
417 format!("{LOGO_MAX_COLS}x{LOGO_MAX_ROWS}")
418}
419
420static CHAFA_SUPPORTS_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
421
422pub fn chafa_supports_probe() -> bool {
424 *CHAFA_SUPPORTS_PROBE.get_or_init(|| {
425 std::process::Command::new("chafa")
426 .args(["--probe", "off", "--version"])
427 .output()
428 .map(|o| o.status.success())
429 .unwrap_or(false)
430 })
431}
432
433pub fn chafa_available() -> bool {
435 std::process::Command::new("chafa")
436 .arg("--version")
437 .output()
438 .map(|o| o.status.success())
439 .unwrap_or(false)
440}
441
442fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
444 let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
445 std::fs::write(&temp_path, bytes)?;
446 Ok(temp_path)
447}
448
449pub fn print_with_chafa(path: &std::path::Path) -> bool {
454 let mut cmd = std::process::Command::new("chafa");
455 cmd.arg("--format")
456 .arg("symbols")
457 .arg("--size")
458 .arg(chafa_size_arg());
459
460 if chafa_supports_probe() {
461 cmd.arg("--probe").arg("off");
462 }
463
464 let output = cmd.arg(path).output();
465
466 match output {
467 Ok(out) if out.status.success() => {
468 print!("{}", String::from_utf8_lossy(&out.stdout));
469 true
470 }
471 Ok(out) => {
472 eprintln!("warning: chafa failed with status: {}", out.status);
473 false
474 }
475 Err(e) => {
476 eprintln!("warning: failed to execute chafa: {}", e);
477 false
478 }
479 }
480}
481
482pub fn get_chafa_logo_lines(path: &std::path::Path) -> Option<Vec<String>> {
484 let mut cmd = std::process::Command::new("chafa");
485 cmd.arg("--format")
486 .arg("symbols")
487 .arg("--size")
488 .arg(chafa_size_arg());
489
490 if chafa_supports_probe() {
491 cmd.arg("--probe").arg("off");
492 }
493
494 let output = cmd.arg(path).output().ok()?;
495 if output.status.success() {
496 let content = String::from_utf8_lossy(&output.stdout);
497 Some(content.lines().map(|s| s.to_string()).collect())
498 } else {
499 None
500 }
501}
502
503pub fn print_distro_logo(distro: Option<&str>) {
508 print_distro_logo_with_ascii(distro, false, false);
509}
510
511pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool, chafa_only: bool) {
519 if ascii_only {
520 let art = get_distro_logo_lines(distro);
522 for line in art {
523 println!("{}", line);
524 }
525 return;
526 }
527
528 let has_chafa = chafa_available();
529
530 if !chafa_only {
531 let supports_kitty = supports_kitty();
532 let supports_iterm2 = supports_iterm2();
533 let supports_sixel = supports_sixel();
534
535 #[cfg(feature = "graphics")]
537 if supports_kitty {
538 if let Some(bytes) = get_embedded_logo(distro) {
539 if !bytes.is_empty() {
540 print_graphical_logo(bytes);
541 return;
542 }
543 }
544 }
545
546 #[cfg(feature = "graphics")]
548 if supports_iterm2 {
549 if let Some(bytes) = get_embedded_logo(distro) {
550 if !bytes.is_empty() {
551 print_iterm2_logo(bytes);
552 return;
553 }
554 }
555 }
556
557 #[cfg(feature = "graphics")]
559 if supports_sixel {
560 if let Some(bytes) = get_embedded_logo(distro) {
561 if !bytes.is_empty() {
562 print_sixel_logo(bytes);
563 return;
564 }
565 }
566 }
567 }
568
569 if has_chafa {
571 if let Some(bytes) = get_embedded_logo(distro) {
572 if bytes.len() > 100 {
573 if let Ok(temp_path) = write_temp_logo(bytes) {
574 if print_with_chafa(&temp_path) {
575 let _ = std::fs::remove_file(&temp_path);
576 return;
577 }
578 let _ = std::fs::remove_file(&temp_path);
579 }
580 }
581 }
582 }
583
584 let art = get_distro_logo_lines(distro);
586 for line in art {
587 println!("{}", line);
588 }
589}
590
591#[cfg(feature = "graphics")]
593pub fn print_iterm2_logo(image_data: &[u8]) {
594 use base64::Engine;
595
596 let (width, height) = image::load_from_memory(image_data)
597 .map(|img| (img.width(), img.height()))
598 .unwrap_or((0, 0));
599
600 let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
601
602 if width > 0 && height > 0 {
606 let fit = logo_cells_for(width, height);
607 print!(
608 "\x1b]1337;File=inline=1;width={};height={};preserveAspectRatio=1:{}\x07",
609 fit.cols, fit.rows, encoded
610 );
611 } else {
612 print!(
613 "\x1b]1337;File=inline=1;height={};preserveAspectRatio=1:{}\x07",
614 LOGO_MAX_ROWS, encoded
615 );
616 }
617 println!(); }
619
620#[cfg(feature = "graphics")]
622pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
623 if let Ok(bytes) = std::fs::read(path) {
624 print_iterm2_logo(&bytes);
625 } else {
626 println!("[Could not read logo for iTerm2 from {}]", path.display());
627 }
628}
629
630#[cfg(not(feature = "graphics"))]
632pub fn print_iterm2_logo(_image_data: &[u8]) {
633 println!("[iTerm2 logo support requires --features graphics]");
634}
635
636#[cfg(feature = "graphics")]
638pub fn print_graphical_logo(image_data: &[u8]) {
639 use base64::Engine;
640
641 let (width, height) = image::load_from_memory(image_data)
642 .map(|img| (img.width(), img.height()))
643 .unwrap_or((0, 0));
644
645 let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
646
647 if width > 0 && height > 0 {
648 let spec = kitty_placement_spec(logo_cells_for(width, height));
653 println!(
654 "\x1b_Gf=100,s={},v={},{},a=T;{}\x1b\\",
655 width, height, spec, encoded
656 );
657 } else {
658 println!("\x1b_Gf=100,a=T;{}", encoded);
659 }
660}
661
662#[cfg(feature = "graphics")]
664pub fn print_sixel_logo(image_data: &[u8]) {
665 if let Ok(img) = image::load_from_memory(image_data) {
666 let fit = logo_cells_for(img.width(), img.height());
670 let (cell_w, cell_h) = terminal_cell_size_px();
671 let resized = img.resize(
672 (fit.cols * cell_w) as u32,
673 (fit.rows * cell_h) as u32,
674 image::imageops::FilterType::Triangle,
675 );
676 let rgba = resized.to_rgba8();
677 let (width, height) = rgba.dimensions();
678 print_sixel_rgba(rgba.as_raw(), width, height);
679 }
680}
681
682#[cfg(feature = "graphics")]
684pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
685 use icy_sixel::SixelImage;
686
687 match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
688 Ok(sixel_img) => match sixel_img.encode() {
689 Ok(sixel_str) => {
690 print!("{}", sixel_str);
691 }
692 Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
693 },
694 Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
695 }
696}
697
698#[cfg(not(feature = "graphics"))]
700pub fn print_graphical_logo(_image_data: &[u8]) {
701 println!("[Graphical logo support requires --features graphics]");
702}
703
704#[cfg(not(feature = "graphics"))]
706pub fn print_sixel_logo(_image_data: &[u8]) {
707 println!("[Sixel logo support requires --features graphics]");
708}
709
710#[cfg(feature = "graphics")]
712pub fn print_graphical_logo_from_path(path: &std::path::Path) {
713 use image::ImageFormat;
714 match image::open(path) {
715 Ok(img) => {
716 let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
717 let mut png_data = Vec::new();
718 if resized
719 .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
720 .is_ok()
721 {
722 print_graphical_logo(&png_data);
723 } else {
724 println!("[Failed to encode logo as PNG]");
725 }
726 }
727 Err(_) => {
728 println!("[Could not load graphical logo from {}]", path.display());
729 }
730 }
731}
732
733#[cfg(feature = "graphics")]
735pub fn print_sixel_logo_from_path(path: &std::path::Path) {
736 match image::open(path) {
737 Ok(img) => {
738 let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
739 let rgba = resized.to_rgba8();
740 let (width, height) = rgba.dimensions();
741 print_sixel_rgba(rgba.as_raw(), width, height);
742 }
743 Err(_) => {
744 println!("[Could not load logo for Sixel from {}]", path.display());
745 }
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752
753 #[test]
754 fn test_get_ascii_logo_arch() {
755 let logo = get_ascii_logo(Some("arch"));
756 assert!(!logo.is_empty());
757 assert!(logo[0].contains("`"));
758 }
759
760 #[test]
761 fn test_get_ascii_logo_unknown() {
762 let logo = get_ascii_logo(Some("unknown_distro"));
763 assert!(!logo.is_empty());
764 assert!(logo
766 .iter()
767 .any(|line| line.contains("o${2}_${3}o") || line.contains("o_o")));
768 }
769
770 #[test]
771 fn test_get_ascii_logo_none() {
772 let logo = get_ascii_logo(None);
773 assert!(!logo.is_empty());
774 }
775
776 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
777
778 struct EnvGuard {
779 _mutex_guard: std::sync::MutexGuard<'static, ()>,
780 old_vars: std::collections::HashMap<&'static str, Option<String>>,
781 }
782
783 impl EnvGuard {
784 fn new(vars_to_mock: &[&'static str]) -> Self {
785 let guard = ENV_LOCK.lock().unwrap();
786 let mut old_vars = std::collections::HashMap::new();
787 for var in vars_to_mock {
788 old_vars.insert(*var, std::env::var(var).ok());
789 }
790 EnvGuard {
791 _mutex_guard: guard,
792 old_vars,
793 }
794 }
795 }
796
797 impl Drop for EnvGuard {
798 fn drop(&mut self) {
799 for (var, value) in &self.old_vars {
800 if let Some(val) = value {
801 std::env::set_var(var, val);
802 } else {
803 std::env::remove_var(var);
804 }
805 }
806 }
807 }
808
809 #[test]
810 fn test_supports_kitty_heuristics() {
811 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
812
813 std::env::set_var("TERM", "xterm-kitty");
815 std::env::remove_var("TERMINAL_EMULATOR");
816 std::env::remove_var("TERM_PROGRAM");
817 assert!(supports_kitty());
818
819 std::env::remove_var("TERM");
821 std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
822 assert!(supports_kitty());
823
824 std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
826 assert!(supports_kitty());
827
828 std::env::remove_var("TERMINAL_EMULATOR");
830 std::env::set_var("TERM_PROGRAM", "rio");
831 assert!(supports_kitty());
832
833 std::env::remove_var("TERM_PROGRAM");
835 assert!(!supports_kitty());
836 }
837
838 #[test]
839 fn test_supports_iterm2_heuristics() {
840 let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM"]);
844 std::env::remove_var("TERM");
845
846 std::env::set_var("TERM_PROGRAM", "iTerm.app");
848 assert!(supports_iterm2());
849
850 std::env::set_var("TERM_PROGRAM", "WezTerm");
852 assert!(supports_iterm2());
853
854 std::env::set_var("TERM_PROGRAM", "rio");
856 assert!(supports_iterm2());
857
858 std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
860 assert!(!supports_iterm2());
861
862 std::env::remove_var("TERM_PROGRAM");
864 assert!(!supports_iterm2());
865 }
866
867 #[test]
868 fn test_supports_sixel_heuristics() {
869 let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
870
871 std::env::remove_var("TERM");
873 std::env::remove_var("TERM_PROGRAM");
874 std::env::remove_var("WT_SESSION");
875 assert!(!supports_sixel());
876
877 std::env::set_var("TERM", "xterm-sixel");
879 assert!(supports_sixel());
880
881 std::env::set_var("TERM", "foot");
883 assert!(supports_sixel());
884
885 std::env::set_var("TERM", "MLTerm");
887 assert!(supports_sixel());
888
889 std::env::remove_var("TERM");
891 std::env::set_var("TERM_PROGRAM", "WezTerm");
892 assert!(supports_sixel());
893
894 std::env::set_var("TERM_PROGRAM", "iTerm.app");
896 assert!(supports_sixel());
897
898 std::env::set_var("TERM_PROGRAM", "rio");
900 assert!(supports_sixel());
901
902 std::env::remove_var("TERM_PROGRAM");
904 std::env::set_var("WT_SESSION", "active");
905 assert!(supports_sixel());
906 }
907
908 #[test]
909 fn test_get_embedded_logo() {
910 let logo = get_embedded_logo(Some("arch"));
911 assert!(logo.is_some());
912 let logo = get_embedded_logo(Some("pop"));
913 assert!(logo.is_some());
914 let logo = get_embedded_logo(Some("manjaro"));
915 assert!(logo.is_some());
916 let logo = get_embedded_logo(Some("endeavouros"));
917 assert!(logo.is_some());
918 let logo = get_embedded_logo(Some("opensuse"));
919 assert!(logo.is_some());
920 let logo = get_embedded_logo(Some("opensuse-leap"));
921 assert!(logo.is_some());
922 let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
923 assert!(logo.is_some());
924 let logo = get_embedded_logo(Some("mx"));
925 assert!(logo.is_some());
926 let logo = get_embedded_logo(Some("linuxmint"));
927 assert!(logo.is_some());
928 let logo = get_embedded_logo(Some("kali"));
929 assert!(logo.is_some());
930 let logo = get_embedded_logo(Some("zorin"));
931 assert!(logo.is_some());
932 let logo = get_embedded_logo(Some("garuda"));
933 assert!(logo.is_some());
934 let logo = get_embedded_logo(Some("macos"));
935 assert!(logo.is_some());
936 let logo = get_embedded_logo(Some("windows"));
937 assert!(logo.is_some());
938 let logo = get_embedded_logo(None);
939 assert!(logo.is_some());
940 }
941
942 #[test]
943 fn test_get_ascii_logo_new_distros() {
944 let pop = get_ascii_logo(Some("pop"));
945 assert!(!pop.is_empty());
946 assert!(pop.iter().any(|line| line.contains("767")));
947
948 let manjaro = get_ascii_logo(Some("manjaro"));
949 assert!(!manjaro.is_empty());
950 assert!(manjaro.iter().any(|line| line.contains("████████")));
951
952 let endeavouros = get_ascii_logo(Some("endeavouros"));
953 assert!(!endeavouros.is_empty());
954 assert!(endeavouros.iter().any(|line| line.contains("ssso")));
955
956 let opensuse = get_ascii_logo(Some("opensuse"));
957 assert!(!opensuse.is_empty());
958 assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
959
960 let macos = get_ascii_logo(Some("macos"));
961 assert!(!macos.is_empty());
962 assert!(macos
963 .iter()
964 .any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
965
966 let windows = get_ascii_logo(Some("windows"));
967 assert!(!windows.is_empty());
968 assert!(windows
969 .iter()
970 .any(|line| line.contains("AEEEtttt::::ztF") || line.contains("tt:::tt333EE3")));
971
972 let mx = get_ascii_logo(Some("mx"));
973 assert!(!mx.is_empty());
974 assert!(mx
975 .iter()
976 .any(|line| line.contains("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMM")));
977
978 let linuxmint = get_ascii_logo(Some("linuxmint"));
979 assert!(!linuxmint.is_empty());
980 assert!(linuxmint.iter().any(|line| line.contains("oOOOOOOOOOOo")));
981
982 let kali = get_ascii_logo(Some("kali"));
983 assert!(!kali.is_empty());
984 assert!(kali.iter().any(|line| line.contains(":ccc")));
985
986 let zorin = get_ascii_logo(Some("zorin"));
987 assert!(!zorin.is_empty());
988 assert!(zorin
989 .iter()
990 .any(|line| line.contains("osssssssssssssssssssso")));
991
992 let garuda = get_ascii_logo(Some("garuda"));
993 assert!(!garuda.is_empty());
994 assert!(garuda.iter().any(|line| line.contains("888:8898898")));
995 }
996
997 #[test]
1000 fn test_rio_detected_from_term_when_term_program_is_absent() {
1001 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
1004 std::env::remove_var("TERM_PROGRAM");
1005 std::env::remove_var("TERMINAL_EMULATOR");
1006 std::env::set_var("TERM", "xterm-rio");
1007
1008 assert!(is_rio_terminal());
1009 assert!(supports_kitty());
1010 assert!(supports_iterm2());
1011 assert!(supports_sixel());
1012 }
1013
1014 #[test]
1015 fn test_rio_still_detected_from_term_program() {
1016 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
1018 std::env::remove_var("TERMINAL_EMULATOR");
1019 std::env::set_var("TERM", "xterm-256color");
1020 std::env::set_var("TERM_PROGRAM", "rio");
1021
1022 assert!(is_rio_terminal());
1023 assert!(supports_kitty());
1024 }
1025
1026 #[test]
1027 fn test_non_rio_term_is_not_matched() {
1028 let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
1030 std::env::remove_var("TERM_PROGRAM");
1031 std::env::remove_var("TERMINAL_EMULATOR");
1032 for term in ["xterm-256color", "screen", "linux", "rioja"] {
1033 std::env::set_var("TERM", term);
1034 assert!(!is_rio_terminal(), "{term} should not be detected as Rio");
1035 }
1036 }
1037
1038 #[test]
1041 fn test_fit_logo_cells_preserves_aspect_for_wide_image() {
1042 let fit = fit_logo_cells(384, 108, 10, 20, 45, 10);
1046 assert_eq!(fit.cols, 45);
1047 assert_eq!(fit.rows, 7);
1048 assert!(fit.width_limited);
1049 assert_eq!(kitty_placement_spec(fit), "c=45");
1051 }
1052
1053 #[test]
1054 fn test_fit_logo_cells_preserves_aspect_for_tall_image() {
1055 let fit = fit_logo_cells(291, 384, 10, 20, 45, 10);
1057 assert_eq!(fit.rows, 10);
1058 assert!(
1059 fit.cols < 45,
1060 "tall image should not fill the width, got {}",
1061 fit.cols
1062 );
1063 assert!(!fit.width_limited);
1064 assert_eq!(kitty_placement_spec(fit), "r=10");
1065 }
1066
1067 #[test]
1068 fn test_fit_logo_cells_accounts_for_non_square_cells() {
1069 let fit = fit_logo_cells(256, 256, 10, 20, 45, 10);
1072 assert_eq!((fit.cols, fit.rows), (20, 10));
1073 let sq = fit_logo_cells(256, 256, 10, 10, 45, 10);
1074 assert_eq!((sq.cols, sq.rows), (10, 10));
1075 }
1076
1077 #[test]
1078 fn test_fit_logo_cells_never_exceeds_the_box() {
1079 for (w, h) in [(384, 108), (291, 384), (256, 256), (4000, 3), (3, 4000)] {
1081 let fit = fit_logo_cells(w, h, 10, 20, 45, 10);
1082 assert!((1..=45).contains(&fit.cols), "{w}x{h} -> {} cols", fit.cols);
1083 assert!((1..=10).contains(&fit.rows), "{w}x{h} -> {} rows", fit.rows);
1084 }
1085 }
1086
1087 const SHIPPED_ASSET_DIMENSIONS: &[(u32, u32)] = &[
1093 (384, 117), (291, 384), (384, 384), (384, 108), (256, 256), (256, 150), (313, 384), (256, 232), (384, 121), (384, 163), (384, 135), (256, 222), ];
1106
1107 #[test]
1108 fn test_fit_logo_cells_reservation_is_never_smaller_than_the_drawn_image() {
1109 for &(w, h) in SHIPPED_ASSET_DIMENSIONS {
1118 for (cell_w, cell_h) in [(10usize, 20usize), (7, 15), (22, 51), (9, 18)] {
1119 let fit = fit_logo_cells(w, h, cell_w, cell_h, LOGO_MAX_COLS, LOGO_MAX_ROWS);
1120 let (box_w, box_h) = (
1121 (LOGO_MAX_COLS * cell_w) as u64,
1122 (LOGO_MAX_ROWS * cell_h) as u64,
1123 );
1124 let (w, h) = (u64::from(w), u64::from(h));
1125 let (res_w, res_h) = ((fit.cols * cell_w) as u64, (fit.rows * cell_h) as u64);
1126 if fit.width_limited {
1127 assert!(
1129 res_w >= box_w,
1130 "{w}x{h} cells {cell_w}x{cell_h}: width short"
1131 );
1132 assert!(
1136 res_h * w >= box_w * h,
1137 "{w}x{h} cells {cell_w}x{cell_h}: reserved {res_h}px < drawn {:.2}px",
1138 (box_w * h) as f64 / w as f64
1139 );
1140 } else {
1141 assert!(
1143 res_h >= box_h,
1144 "{w}x{h} cells {cell_w}x{cell_h}: height short"
1145 );
1146 assert!(
1147 res_w * h >= box_h * w,
1148 "{w}x{h} cells {cell_w}x{cell_h}: reserved {res_w}px < drawn {:.2}px",
1149 (box_h * w) as f64 / h as f64
1150 );
1151 }
1152 }
1153 }
1154 }
1155
1156 #[test]
1157 fn test_fit_logo_cells_handles_degenerate_input() {
1158 for fit in [
1161 fit_logo_cells(0, 0, 10, 20, 45, 10),
1162 fit_logo_cells(384, 108, 0, 20, 45, 10),
1163 fit_logo_cells(384, 108, 10, 0, 45, 10),
1164 ] {
1165 assert_eq!((fit.cols, fit.rows), (45, 10));
1166 }
1167 }
1168
1169 #[test]
1170 fn test_chafa_size_arg_matches_the_shared_box() {
1171 assert_eq!(chafa_size_arg(), format!("{LOGO_MAX_COLS}x{LOGO_MAX_ROWS}"));
1173 assert_eq!(chafa_size_arg(), "45x10");
1174 }
1175}