Skip to main content

retch_cli/
logo.rs

1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! ASCII and graphical logo definitions and rendering.
5//!
6//! Contains embedded distro logos and logic for rendering them
7//! as text or images (e.g., Sixel, Kitty, iTerm).
8
9// Exact Fastfetch ASCII logos (intact, unmodified)
10// Source: https://github.com/fastfetch-cli/fastfetch/src/logo/ascii/
11
12// Embedded distro logos (PNG)
13// Place real logos in assets/logos/<distro>.png
14// Example: assets/logos/arch.png, assets/logos/fedora.png, assets/logos/tux.png
15
16/// Returns the raw PNG bytes for an embedded distro logo.
17///
18/// If the distro is not recognized, it falls back to the Tux (Linux) logo.
19/// Only available when the `graphics` feature is enabled.
20#[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/// Fallback for non-graphics build to provide Tux bytes for Chafa if needed.
47#[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
52/// Attempts to detect the current operating system distribution.
53pub 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
76///// Returns a list of strings representing the ASCII art for a given distro with color placeholders.
77///
78/// All ASCII logos are sourced from or compatible with Fastfetch.
79pub 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        // Fallback: Tux (Linux)
149        _ => {
150            let logo = include_str!("../assets/logos/tux.txt");
151            logo.lines().map(|s| s.to_string()).collect()
152        }
153    }
154}
155
156/// Returns dynamic ANSI color arrays for a given distribution.
157pub 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        // Grayscale (silver) ramp, matching the modern monochrome Apple logo rather than the
179        // legacy rainbow. 256-colour greys (light→medium) read on light and dark terminals.
180        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"], // Tux
189    }
190}
191
192/// Interpolates placeholders `${1}`...`${9}` or `$1`...`$9` with dynamic ANSI colors and appends a reset at the end.
193pub 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
218/// Checks if the terminal supports the Kitty inline image protocol.
219pub fn supports_kitty() -> bool {
220    std::env::var("TERM")
221        .map(|t| t == "xterm-kitty")
222        .unwrap_or(false)
223        || std::env::var("TERMINAL_EMULATOR")
224            .map(|t| t == "iterm-kitty" || t == "iTerm.app")
225            .unwrap_or(false)
226        || std::env::var("TERM_PROGRAM")
227            .map(|t| t == "rio")
228            .unwrap_or(false)
229}
230
231/// Checks if the terminal supports the iTerm2 inline image protocol.
232pub fn supports_iterm2() -> bool {
233    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
234        if prog == "iTerm.app" || prog == "WezTerm" || prog == "rio" {
235            return true;
236        }
237    }
238    false
239}
240
241/// Checks if the terminal supports Sixel graphics (heuristic based on environment).
242pub fn supports_sixel() -> bool {
243    if let Ok(term) = std::env::var("TERM") {
244        let term = term.to_lowercase();
245        if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
246            return true;
247        }
248    }
249
250    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
251        if prog == "WezTerm" || prog == "iTerm.app" || prog == "rio" {
252            return true;
253        }
254    }
255
256    if std::env::var("WT_SESSION").is_ok() {
257        return true;
258    }
259
260    false
261}
262
263static CHAFA_SUPPORTS_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
264
265/// Checks if the `chafa` command-line tool supports the `--probe` option.
266pub fn chafa_supports_probe() -> bool {
267    *CHAFA_SUPPORTS_PROBE.get_or_init(|| {
268        std::process::Command::new("chafa")
269            .args(["--probe", "off", "--version"])
270            .output()
271            .map(|o| o.status.success())
272            .unwrap_or(false)
273    })
274}
275
276/// Checks if the `chafa` command-line tool is available in the system path.
277pub fn chafa_available() -> bool {
278    std::process::Command::new("chafa")
279        .arg("--version")
280        .output()
281        .map(|o| o.status.success())
282        .unwrap_or(false)
283}
284
285/// Write embedded logo bytes to a temporary file and return the path.
286fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
287    let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
288    std::fs::write(&temp_path, bytes)?;
289    Ok(temp_path)
290}
291
292/// Attempts to render an image using the `chafa` utility.
293///
294/// Chafa renders images using high-quality Unicode symbols, providing a
295/// good graphical fallback for many terminal emulators.
296pub fn print_with_chafa(path: &std::path::Path) -> bool {
297    let mut cmd = std::process::Command::new("chafa");
298    cmd.arg("--format")
299        .arg("symbols")
300        .arg("--size")
301        .arg("28x10");
302
303    if chafa_supports_probe() {
304        cmd.arg("--probe").arg("off");
305    }
306
307    let output = cmd.arg(path).output();
308
309    match output {
310        Ok(out) if out.status.success() => {
311            print!("{}", String::from_utf8_lossy(&out.stdout));
312            true
313        }
314        Ok(out) => {
315            eprintln!("warning: chafa failed with status: {}", out.status);
316            false
317        }
318        Err(e) => {
319            eprintln!("warning: failed to execute chafa: {}", e);
320            false
321        }
322    }
323}
324
325/// Attempts to get Chafa output as a list of lines.
326pub fn get_chafa_logo_lines(path: &std::path::Path) -> Option<Vec<String>> {
327    let mut cmd = std::process::Command::new("chafa");
328    cmd.arg("--format")
329        .arg("symbols")
330        .arg("--size")
331        .arg("28x10");
332
333    if chafa_supports_probe() {
334        cmd.arg("--probe").arg("off");
335    }
336
337    let output = cmd.arg(path).output().ok()?;
338    if output.status.success() {
339        let content = String::from_utf8_lossy(&output.stdout);
340        Some(content.lines().map(|s| s.to_string()).collect())
341    } else {
342        None
343    }
344}
345
346/// Print logo for a distro following the strict priority:
347/// 1. Real graphic logo (if terminal supports it and embedded logo exists)
348/// 2. Chafa high-quality symbols (if chafa is available)
349/// 3. Real Fastfetch ASCII logo (always available)
350pub fn print_distro_logo(distro: Option<&str>) {
351    print_distro_logo_with_ascii(distro, false, false);
352}
353
354/// Renders the distribution logo with options to force ASCII or Chafa mode.
355///
356/// This is the primary entry point for logo rendering, handling the entire
357/// priority chain from high-res graphics down to text-based ASCII.
358/// - `ascii_only`: skip all graphical protocols and render the ASCII art directly.
359/// - `chafa_only`: skip Kitty/iTerm2/Sixel and go straight to Chafa (falls back
360///   to ASCII if Chafa is unavailable). `ascii_only` takes precedence.
361pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool, chafa_only: bool) {
362    if ascii_only {
363        // Force ASCII path
364        let art = get_distro_logo_lines(distro);
365        for line in art {
366            println!("{}", line);
367        }
368        return;
369    }
370
371    let has_chafa = chafa_available();
372
373    if !chafa_only {
374        let supports_kitty = supports_kitty();
375        let supports_iterm2 = supports_iterm2();
376        let supports_sixel = supports_sixel();
377
378        // 1. Try embedded graphical logo (Kitty)
379        #[cfg(feature = "graphics")]
380        if supports_kitty {
381            if let Some(bytes) = get_embedded_logo(distro) {
382                if !bytes.is_empty() {
383                    print_graphical_logo(bytes);
384                    return;
385                }
386            }
387        }
388
389        // 2. Try embedded graphical logo (iTerm2)
390        #[cfg(feature = "graphics")]
391        if supports_iterm2 {
392            if let Some(bytes) = get_embedded_logo(distro) {
393                if !bytes.is_empty() {
394                    print_iterm2_logo(bytes);
395                    return;
396                }
397            }
398        }
399
400        // 3. Try embedded graphical logo (Sixel)
401        #[cfg(feature = "graphics")]
402        if supports_sixel {
403            if let Some(bytes) = get_embedded_logo(distro) {
404                if !bytes.is_empty() {
405                    print_sixel_logo(bytes);
406                    return;
407                }
408            }
409        }
410    }
411
412    // 4. Try chafa using embedded distro logo
413    if has_chafa {
414        if let Some(bytes) = get_embedded_logo(distro) {
415            if bytes.len() > 100 {
416                if let Ok(temp_path) = write_temp_logo(bytes) {
417                    if print_with_chafa(&temp_path) {
418                        let _ = std::fs::remove_file(&temp_path);
419                        return;
420                    }
421                    let _ = std::fs::remove_file(&temp_path);
422                }
423            }
424        }
425    }
426
427    // 5. Final fallback: Real Fastfetch ASCII logo
428    let art = get_distro_logo_lines(distro);
429    for line in art {
430        println!("{}", line);
431    }
432}
433
434/// Renders a raw image buffer using the iTerm2 inline image protocol.
435#[cfg(feature = "graphics")]
436pub fn print_iterm2_logo(image_data: &[u8]) {
437    use base64::Engine;
438    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
439    print!(
440        "\x1b]1337;File=inline=1;height=10;preserveAspectRatio=1:{}\x07",
441        encoded
442    );
443    println!(); // iTerm2 typically needs a newline after the logo
444}
445
446/// Loads an image from a file and prints it using the iTerm2 protocol.
447#[cfg(feature = "graphics")]
448pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
449    if let Ok(bytes) = std::fs::read(path) {
450        print_iterm2_logo(&bytes);
451    } else {
452        println!("[Could not read logo for iTerm2 from {}]", path.display());
453    }
454}
455
456/// Placeholder for iTerm2 logo rendering when the `graphics` feature is disabled.
457#[cfg(not(feature = "graphics"))]
458pub fn print_iterm2_logo(_image_data: &[u8]) {
459    println!("[iTerm2 logo support requires --features graphics]");
460}
461
462/// Renders a raw image buffer using the Kitty graphics protocol.
463#[cfg(feature = "graphics")]
464pub fn print_graphical_logo(image_data: &[u8]) {
465    use base64::Engine;
466
467    let (width, height) = image::load_from_memory(image_data)
468        .map(|img| (img.width(), img.height()))
469        .unwrap_or((0, 0));
470
471    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
472
473    if width > 0 && height > 0 {
474        println!(
475            "\x1b_Gf=100,s={},v={},c=26,r=10,a=T;{}\x1b\\",
476            width, height, encoded
477        );
478    } else {
479        println!("\x1b_Gf=100,a=T;{}", encoded);
480    }
481}
482
483/// Renders a raw image buffer (e.g. PNG bytes) using the Sixel graphics protocol.
484#[cfg(feature = "graphics")]
485pub fn print_sixel_logo(image_data: &[u8]) {
486    if let Ok(img) = image::load_from_memory(image_data) {
487        let resized = img.resize(240, 200, image::imageops::FilterType::Triangle);
488        let rgba = resized.to_rgba8();
489        let (width, height) = rgba.dimensions();
490        print_sixel_rgba(rgba.as_raw(), width, height);
491    }
492}
493
494/// Renders raw RGBA pixels using the Sixel graphics protocol.
495#[cfg(feature = "graphics")]
496pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
497    use icy_sixel::SixelImage;
498
499    match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
500        Ok(sixel_img) => match sixel_img.encode() {
501            Ok(sixel_str) => {
502                print!("{}", sixel_str);
503            }
504            Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
505        },
506        Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
507    }
508}
509
510/// Placeholder for graphical logo rendering when the `graphics` feature is disabled.
511#[cfg(not(feature = "graphics"))]
512pub fn print_graphical_logo(_image_data: &[u8]) {
513    println!("[Graphical logo support requires --features graphics]");
514}
515
516/// Placeholder for sixel logo rendering when the `graphics` feature is disabled.
517#[cfg(not(feature = "graphics"))]
518pub fn print_sixel_logo(_image_data: &[u8]) {
519    println!("[Sixel logo support requires --features graphics]");
520}
521
522/// Loads an image from a file, resizes it, and prints it using the graphics protocol.
523#[cfg(feature = "graphics")]
524pub fn print_graphical_logo_from_path(path: &std::path::Path) {
525    use image::ImageFormat;
526    match image::open(path) {
527        Ok(img) => {
528            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
529            let mut png_data = Vec::new();
530            if resized
531                .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
532                .is_ok()
533            {
534                print_graphical_logo(&png_data);
535            } else {
536                println!("[Failed to encode logo as PNG]");
537            }
538        }
539        Err(_) => {
540            println!("[Could not load graphical logo from {}]", path.display());
541        }
542    }
543}
544
545/// Loads an image from a file, resizes it, and prints it using the Sixel protocol.
546#[cfg(feature = "graphics")]
547pub fn print_sixel_logo_from_path(path: &std::path::Path) {
548    match image::open(path) {
549        Ok(img) => {
550            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
551            let rgba = resized.to_rgba8();
552            let (width, height) = rgba.dimensions();
553            print_sixel_rgba(rgba.as_raw(), width, height);
554        }
555        Err(_) => {
556            println!("[Could not load logo for Sixel from {}]", path.display());
557        }
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564
565    #[test]
566    fn test_get_ascii_logo_arch() {
567        let logo = get_ascii_logo(Some("arch"));
568        assert!(!logo.is_empty());
569        assert!(logo[0].contains("`"));
570    }
571
572    #[test]
573    fn test_get_ascii_logo_unknown() {
574        let logo = get_ascii_logo(Some("unknown_distro"));
575        assert!(!logo.is_empty());
576        // Should fall back to Tux
577        assert!(logo
578            .iter()
579            .any(|line| line.contains("o${2}_${3}o") || line.contains("o_o")));
580    }
581
582    #[test]
583    fn test_get_ascii_logo_none() {
584        let logo = get_ascii_logo(None);
585        assert!(!logo.is_empty());
586    }
587
588    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
589
590    struct EnvGuard {
591        _mutex_guard: std::sync::MutexGuard<'static, ()>,
592        old_vars: std::collections::HashMap<&'static str, Option<String>>,
593    }
594
595    impl EnvGuard {
596        fn new(vars_to_mock: &[&'static str]) -> Self {
597            let guard = ENV_LOCK.lock().unwrap();
598            let mut old_vars = std::collections::HashMap::new();
599            for var in vars_to_mock {
600                old_vars.insert(*var, std::env::var(var).ok());
601            }
602            EnvGuard {
603                _mutex_guard: guard,
604                old_vars,
605            }
606        }
607    }
608
609    impl Drop for EnvGuard {
610        fn drop(&mut self) {
611            for (var, value) in &self.old_vars {
612                if let Some(val) = value {
613                    std::env::set_var(var, val);
614                } else {
615                    std::env::remove_var(var);
616                }
617            }
618        }
619    }
620
621    #[test]
622    fn test_supports_kitty_heuristics() {
623        let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
624
625        // Test TERM=xterm-kitty
626        std::env::set_var("TERM", "xterm-kitty");
627        std::env::remove_var("TERMINAL_EMULATOR");
628        std::env::remove_var("TERM_PROGRAM");
629        assert!(supports_kitty());
630
631        // Test TERMINAL_EMULATOR=iterm-kitty
632        std::env::remove_var("TERM");
633        std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
634        assert!(supports_kitty());
635
636        // Test TERMINAL_EMULATOR=iTerm.app
637        std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
638        assert!(supports_kitty());
639
640        // Test TERM_PROGRAM=rio
641        std::env::remove_var("TERMINAL_EMULATOR");
642        std::env::set_var("TERM_PROGRAM", "rio");
643        assert!(supports_kitty());
644
645        // Test clear env -> false
646        std::env::remove_var("TERM_PROGRAM");
647        assert!(!supports_kitty());
648    }
649
650    #[test]
651    fn test_supports_iterm2_heuristics() {
652        let _guard = EnvGuard::new(&["TERM_PROGRAM"]);
653
654        // Test TERM_PROGRAM=iTerm.app
655        std::env::set_var("TERM_PROGRAM", "iTerm.app");
656        assert!(supports_iterm2());
657
658        // Test TERM_PROGRAM=WezTerm
659        std::env::set_var("TERM_PROGRAM", "WezTerm");
660        assert!(supports_iterm2());
661
662        // Test TERM_PROGRAM=rio
663        std::env::set_var("TERM_PROGRAM", "rio");
664        assert!(supports_iterm2());
665
666        // Test TERM_PROGRAM=Apple_Terminal
667        std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
668        assert!(!supports_iterm2());
669
670        // Test clear env -> false
671        std::env::remove_var("TERM_PROGRAM");
672        assert!(!supports_iterm2());
673    }
674
675    #[test]
676    fn test_supports_sixel_heuristics() {
677        let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
678
679        // Clear all to start fresh
680        std::env::remove_var("TERM");
681        std::env::remove_var("TERM_PROGRAM");
682        std::env::remove_var("WT_SESSION");
683        assert!(!supports_sixel());
684
685        // Test TERM=xterm-sixel
686        std::env::set_var("TERM", "xterm-sixel");
687        assert!(supports_sixel());
688
689        // Test TERM=foot
690        std::env::set_var("TERM", "foot");
691        assert!(supports_sixel());
692
693        // Test TERM=mlterm (case variations)
694        std::env::set_var("TERM", "MLTerm");
695        assert!(supports_sixel());
696
697        // Reset TERM, test TERM_PROGRAM=WezTerm
698        std::env::remove_var("TERM");
699        std::env::set_var("TERM_PROGRAM", "WezTerm");
700        assert!(supports_sixel());
701
702        // Test TERM_PROGRAM=iTerm.app
703        std::env::set_var("TERM_PROGRAM", "iTerm.app");
704        assert!(supports_sixel());
705
706        // Test TERM_PROGRAM=rio
707        std::env::set_var("TERM_PROGRAM", "rio");
708        assert!(supports_sixel());
709
710        // Reset TERM_PROGRAM, test WT_SESSION
711        std::env::remove_var("TERM_PROGRAM");
712        std::env::set_var("WT_SESSION", "active");
713        assert!(supports_sixel());
714    }
715
716    #[test]
717    fn test_get_embedded_logo() {
718        let logo = get_embedded_logo(Some("arch"));
719        assert!(logo.is_some());
720        let logo = get_embedded_logo(Some("pop"));
721        assert!(logo.is_some());
722        let logo = get_embedded_logo(Some("manjaro"));
723        assert!(logo.is_some());
724        let logo = get_embedded_logo(Some("endeavouros"));
725        assert!(logo.is_some());
726        let logo = get_embedded_logo(Some("opensuse"));
727        assert!(logo.is_some());
728        let logo = get_embedded_logo(Some("opensuse-leap"));
729        assert!(logo.is_some());
730        let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
731        assert!(logo.is_some());
732        let logo = get_embedded_logo(Some("mx"));
733        assert!(logo.is_some());
734        let logo = get_embedded_logo(Some("linuxmint"));
735        assert!(logo.is_some());
736        let logo = get_embedded_logo(Some("kali"));
737        assert!(logo.is_some());
738        let logo = get_embedded_logo(Some("zorin"));
739        assert!(logo.is_some());
740        let logo = get_embedded_logo(Some("garuda"));
741        assert!(logo.is_some());
742        let logo = get_embedded_logo(Some("macos"));
743        assert!(logo.is_some());
744        let logo = get_embedded_logo(Some("windows"));
745        assert!(logo.is_some());
746        let logo = get_embedded_logo(None);
747        assert!(logo.is_some());
748    }
749
750    #[test]
751    fn test_get_ascii_logo_new_distros() {
752        let pop = get_ascii_logo(Some("pop"));
753        assert!(!pop.is_empty());
754        assert!(pop.iter().any(|line| line.contains("767")));
755
756        let manjaro = get_ascii_logo(Some("manjaro"));
757        assert!(!manjaro.is_empty());
758        assert!(manjaro.iter().any(|line| line.contains("████████")));
759
760        let endeavouros = get_ascii_logo(Some("endeavouros"));
761        assert!(!endeavouros.is_empty());
762        assert!(endeavouros.iter().any(|line| line.contains("ssso")));
763
764        let opensuse = get_ascii_logo(Some("opensuse"));
765        assert!(!opensuse.is_empty());
766        assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
767
768        let macos = get_ascii_logo(Some("macos"));
769        assert!(!macos.is_empty());
770        assert!(macos
771            .iter()
772            .any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
773
774        let windows = get_ascii_logo(Some("windows"));
775        assert!(!windows.is_empty());
776        assert!(windows
777            .iter()
778            .any(|line| line.contains("AEEEtttt::::ztF") || line.contains("tt:::tt333EE3")));
779
780        let mx = get_ascii_logo(Some("mx"));
781        assert!(!mx.is_empty());
782        assert!(mx
783            .iter()
784            .any(|line| line.contains("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMM")));
785
786        let linuxmint = get_ascii_logo(Some("linuxmint"));
787        assert!(!linuxmint.is_empty());
788        assert!(linuxmint.iter().any(|line| line.contains("oOOOOOOOOOOo")));
789
790        let kali = get_ascii_logo(Some("kali"));
791        assert!(!kali.is_empty());
792        assert!(kali.iter().any(|line| line.contains(":ccc")));
793
794        let zorin = get_ascii_logo(Some("zorin"));
795        assert!(!zorin.is_empty());
796        assert!(zorin
797            .iter()
798            .any(|line| line.contains("osssssssssssssssssssso")));
799
800        let garuda = get_ascii_logo(Some("garuda"));
801        assert!(!garuda.is_empty());
802        assert!(garuda.iter().any(|line| line.contains("888:8898898")));
803    }
804}