Skip to main content

retch_cli/
logo.rs

1// Exact Fastfetch ASCII logos (intact, unmodified)
2// Source: https://github.com/fastfetch-cli/fastfetch/src/logo/ascii/
3
4// Embedded distro logos (PNG)
5// Place real logos in assets/logos/<distro>.png
6// Example: assets/logos/arch.png, assets/logos/fedora.png, assets/logos/tux.png
7
8/// Returns the raw PNG bytes for an embedded distro logo.
9///
10/// If the distro is not recognized, it falls back to the Tux (Linux) logo.
11/// Only available when the `graphics` feature is enabled.
12#[cfg(feature = "graphics")]
13pub fn get_embedded_logo(distro: Option<&str>) -> Option<&'static [u8]> {
14    let d = distro.map(|s| s.to_lowercase());
15    match d.as_deref() {
16        Some("arch") => Some(include_bytes!("../assets/logos/arch.png")),
17        Some("debian") => Some(include_bytes!("../assets/logos/debian.png")),
18        Some("fedora") => Some(include_bytes!("../assets/logos/fedora.png")),
19        Some("nixos") => Some(include_bytes!("../assets/logos/nixos.png")),
20        Some("ubuntu") => Some(include_bytes!("../assets/logos/ubuntu.png")),
21        _ => Some(include_bytes!("../assets/logos/tux.png")),
22    }
23}
24
25/// Fallback for non-graphics build to provide Tux bytes for Chafa if needed.
26#[cfg(not(feature = "graphics"))]
27pub fn get_embedded_logo(_distro: Option<&str>) -> Option<&'static [u8]> {
28    Some(include_bytes!("../assets/logos/tux.png"))
29}
30
31/// Attempts to detect the current Linux distribution by reading `/etc/os-release`.
32pub fn detect_distro() -> Option<String> {
33    if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
34        for line in content.lines() {
35            if line.starts_with("ID=") {
36                let id = line.trim_start_matches("ID=").trim_matches('"');
37                return Some(id.to_string());
38            }
39        }
40    }
41    None
42}
43
44/// Returns a list of strings representing the ASCII art for a given distro.
45///
46/// All ASCII logos are sourced from or compatible with Fastfetch.
47pub fn get_ascii_logo(distro: Option<&str>) -> Vec<String> {
48    let d = distro.map(|s| s.to_lowercase());
49
50    match d.as_deref() {
51        // Arch Linux - exact from Fastfetch
52        Some("arch") => vec![
53            "               -`".to_string(),
54            "                 .o+`".to_string(),
55            "                `ooo/".to_string(),
56            "               `+oooo:".to_string(),
57            "              `+oooooo:".to_string(),
58            "              -+oooooo+:".to_string(),
59            "            `/:-:++oooo+:".to_string(),
60            "           `/++++/+++++++:".to_string(),
61            "          `/++++++++++++++:".to_string(),
62            "         `/+++ooooooooooooo/`".to_string(),
63            "        ./ooosssso++osssssso+`".to_string(),
64            "       .oossssso-````/ossssss+`".to_string(),
65            "      -osssssso.      :ssssssso.".to_string(),
66            "     :osssssss/        osssso+++.".to_string(),
67            "    /ossssssss/        +ssssooo/-".to_string(),
68            "  `/ossssso+/:-        -:/+osssso+-".to_string(),
69            " `+sso+:-`                 `.-/+oso:".to_string(),
70            "`++:.                           `-/+/".to_string(),
71            ".`                                 `".to_string(),
72        ],
73
74        // Debian - exact from Fastfetch
75        Some("debian") => vec![
76            " _,met$$$$$$$$$$gg.".to_string(),
77            "      ,g$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$P.".to_string(),
78            "    ,g$$$$P\"\"       \"\"\"Y$$$$.\".".to_string(),
79            "   ,$$$$P'              `$$$$$$.".to_string(),
80            "',$$$$P       ,ggs.     `$$$$b:".to_string(),
81            "`d$$$$'     ,$P\"'   .    $$$$$$".to_string(),
82            " $$$$P      d$'     ,    $$$$P".to_string(),
83            " $$$$:      $$$.   -    ,d$$$$'".to_string(),
84            " $$$$;      Y$b._   _,d$P'".to_string(),
85            " Y$$$$.    .`\"Y$$$$$$$$P\"'".to_string(),
86            " `$$$$b      \"-.__".to_string(),
87            "  `Y$$$$b".to_string(),
88            "   `Y$$$$.".to_string(),
89            "     `$$$$b.".to_string(),
90            "       `Y$$$$b.".to_string(),
91            "         `\"Y$$b._".to_string(),
92            "             `\"\"\"\"".to_string(),
93        ],
94
95        // Fedora - exact from Fastfetch
96        Some("fedora") => vec![
97            "             .',;::::;,'.".to_string(),
98            "         .';:cccccccccccc:;,.".to_string(),
99            "      .;cccccccccccccccccccccc;.".to_string(),
100            "    .:cccccccccccccccccccccc:.".to_string(),
101            "  .;ccccccccccccc;.:dddl:.;ccccccc;.".to_string(),
102            " .:ccccccccccccc;OWMKOOXMWd;ccccccc:.".to_string(),
103            ".:ccccccccccccc;KMMc;cc;xMMc;ccccccc:.".to_string(),
104            ",cccccccccccccc;MMM.;cc;;WW:;cccccccc,".to_string(),
105            ":cccccccccccccc;MMM.;cccccccccccccccc:".to_string(),
106            ":ccccccc;oxOOOo;MMM000k.;cccccccccccc:".to_string(),
107            "cccccc;0MMKxdd:;MMMkddc.;cccccccccccc;".to_string(),
108            "ccccc;XMO';cccc;MMM.;cccccccccccccccc'".to_string(),
109            "ccccc;MMo;ccccc;MMW.;ccccccccccccccc;".to_string(),
110            "ccccc;0MNc.ccc.xMMd;ccccccccccccccc;".to_string(),
111            "cccccc;dNMWXXXWM0:;cccccccccccccc:,".to_string(),
112            "cccccccc;.:odl:.;cccccccccccccc:,.".to_string(),
113            "ccccccccccccccccccccccccccccc:'.".to_string(),
114            ":ccccccccccccccccccccccc:;,..".to_string(),
115        ],
116
117        // NixOS - exact from Fastfetch
118        Some("nixos") => vec![
119            "          ::::.    ':::::".to_string(),
120            "      ':::::::::.   ':::::::::".to_string(),
121            "  .:::::::::::::     ::::::::::::.".to_string(),
122            "  :::::::::::'        ::::::::::::".to_string(),
123            " .::::::::::'          :::::::::::.".to_string(),
124            " ::::::::::             :::::::::::".to_string(),
125            " :::::::::              :::::::::::".to_string(),
126            " ::::::::                ::::::::::".to_string(),
127            " '::::::.                '::::::::'".to_string(),
128            "  ':::::                  '::::::'".to_string(),
129            "   ':::                    '::::'".to_string(),
130            "    ':                      ':".to_string(),
131        ],
132
133        // Ubuntu - exact from Fastfetch
134        Some("ubuntu") => vec![
135            "                          ....".to_string(),
136            "          ....,,:::::ccccclllclllcc:::;,,....".to_string(),
137            "      ..,;::cccllllllllllllllllllllllllcc::;..".to_string(),
138            "    ..,;::cccllllllllllllllllllllllllllllcc::;..".to_string(),
139            "  ..,;::cccllllllllllllllllllllllllllllllllcc::;..".to_string(),
140            " ..,;::cccllllllllllllllllllllllllllllllllllcc::;..".to_string(),
141            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
142            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
143            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
144            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
145            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
146            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
147        ],
148
149        // Fallback: Tux (Linux)
150        _ => vec![
151            "    .--.".to_string(),
152            "   |o_o |".to_string(),
153            "   |:_/ |".to_string(),
154            "  //   \\ \\".to_string(),
155            " (|     | )".to_string(),
156            "/'\\_   _/`\\".to_string(),
157            "\\___)=(___/".to_string(),
158        ],
159    }
160}
161
162/// Checks if the terminal supports the Kitty inline image protocol.
163pub fn supports_kitty() -> bool {
164    std::env::var("TERM")
165        .map(|t| t == "xterm-kitty")
166        .unwrap_or(false)
167        || std::env::var("TERMINAL_EMULATOR")
168            .map(|t| t == "iterm-kitty" || t == "iTerm.app")
169            .unwrap_or(false)
170        || std::env::var("TERM_PROGRAM")
171            .map(|t| t == "rio")
172            .unwrap_or(false)
173}
174
175/// Checks if the terminal supports the iTerm2 inline image protocol.
176pub fn supports_iterm2() -> bool {
177    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
178        if prog == "iTerm.app" || prog == "WezTerm" || prog == "rio" {
179            return true;
180        }
181    }
182    false
183}
184
185/// Checks if the terminal supports Sixel graphics (heuristic based on environment).
186pub fn supports_sixel() -> bool {
187    if let Ok(term) = std::env::var("TERM") {
188        let term = term.to_lowercase();
189        if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
190            return true;
191        }
192    }
193
194    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
195        if prog == "WezTerm" || prog == "iTerm.app" || prog == "rio" {
196            return true;
197        }
198    }
199
200    if std::env::var("WT_SESSION").is_ok() {
201        return true;
202    }
203
204    false
205}
206
207/// Checks if the `chafa` command-line tool is available in the system path.
208pub fn chafa_available() -> bool {
209    std::process::Command::new("chafa")
210        .arg("--version")
211        .output()
212        .map(|o| o.status.success())
213        .unwrap_or(false)
214}
215
216/// Write embedded logo bytes to a temporary file and return the path.
217fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
218    let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
219    std::fs::write(&temp_path, bytes)?;
220    Ok(temp_path)
221}
222
223/// Attempts to render an image using the `chafa` utility.
224///
225/// Chafa renders images using high-quality Unicode symbols, providing a
226/// good graphical fallback for many terminal emulators.
227pub fn print_with_chafa(path: &std::path::Path) -> bool {
228    let output = std::process::Command::new("chafa")
229        .arg("--format")
230        .arg("symbols")
231        .arg("--size")
232        .arg("40x20")
233        .arg(path)
234        .output();
235
236    match output {
237        Ok(out) if out.status.success() => {
238            print!("{}", String::from_utf8_lossy(&out.stdout));
239            true
240        }
241        _ => false,
242    }
243}
244
245/// Print logo for a distro following the strict priority:
246/// 1. Real graphic logo (if terminal supports it and embedded logo exists)
247/// 2. Chafa high-quality symbols (if chafa is available)
248/// 3. Real Fastfetch ASCII logo (always available)
249pub fn print_distro_logo(distro: Option<&str>) {
250    print_distro_logo_with_ascii(distro, false);
251}
252
253/// Renders the distribution logo with an option to force ASCII mode.
254///
255/// This is the primary entry point for logo rendering, handling the entire
256/// priority chain from high-res graphics down to text-based ASCII.
257pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool) {
258    if ascii_only {
259        // Force ASCII path
260        let art = get_ascii_logo(distro);
261        for line in art {
262            println!("{}", line);
263        }
264        return;
265    }
266
267    let supports_kitty = supports_kitty();
268    let supports_iterm2 = supports_iterm2();
269    let supports_sixel = supports_sixel();
270    let has_chafa = chafa_available();
271
272    // 1. Try embedded graphical logo (Kitty)
273    #[cfg(feature = "graphics")]
274    if supports_kitty {
275        if let Some(bytes) = get_embedded_logo(distro) {
276            if !bytes.is_empty() {
277                print_graphical_logo(bytes);
278                return;
279            }
280        }
281    }
282
283    // 2. Try embedded graphical logo (iTerm2)
284    #[cfg(feature = "graphics")]
285    if supports_iterm2 {
286        if let Some(bytes) = get_embedded_logo(distro) {
287            if !bytes.is_empty() {
288                print_iterm2_logo(bytes);
289                return;
290            }
291        }
292    }
293
294    // 3. Try embedded graphical logo (Sixel)
295    #[cfg(feature = "graphics")]
296    if supports_sixel {
297        if let Some(bytes) = get_embedded_logo(distro) {
298            if !bytes.is_empty() {
299                print_sixel_logo(bytes);
300                return;
301            }
302        }
303    }
304
305    // 3. Try chafa using embedded distro logo
306    if has_chafa {
307        if let Some(bytes) = get_embedded_logo(distro) {
308            if bytes.len() > 100 {
309                if let Ok(temp_path) = write_temp_logo(bytes) {
310                    if print_with_chafa(&temp_path) {
311                        let _ = std::fs::remove_file(&temp_path);
312                        return;
313                    }
314                    let _ = std::fs::remove_file(&temp_path);
315                }
316            }
317        }
318    }
319
320    // 4. Final fallback: Real Fastfetch ASCII logo
321    let art = get_ascii_logo(distro);
322    for line in art {
323        println!("{}", line);
324    }
325}
326
327/// Renders a raw image buffer using the iTerm2 inline image protocol.
328#[cfg(feature = "graphics")]
329pub fn print_iterm2_logo(image_data: &[u8]) {
330    use base64::Engine;
331    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
332    print!(
333        "\x1b]1337;File=inline=1;preserveAspectRatio=1:{}\x07",
334        encoded
335    );
336    println!(); // iTerm2 typically needs a newline after the logo
337}
338
339/// Loads an image from a file and prints it using the iTerm2 protocol.
340#[cfg(feature = "graphics")]
341pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
342    if let Ok(bytes) = std::fs::read(path) {
343        print_iterm2_logo(&bytes);
344    } else {
345        println!("[Could not read logo for iTerm2 from {}]", path.display());
346    }
347}
348
349/// Placeholder for iTerm2 logo rendering when the `graphics` feature is disabled.
350#[cfg(not(feature = "graphics"))]
351pub fn print_iterm2_logo(_image_data: &[u8]) {
352    println!("[iTerm2 logo support requires --features graphics]");
353}
354
355/// Renders a raw image buffer using the Kitty graphics protocol.
356#[cfg(feature = "graphics")]
357pub fn print_graphical_logo(image_data: &[u8]) {
358    use base64::Engine;
359
360    let (width, height) = image::load_from_memory(image_data)
361        .map(|img| (img.width(), img.height()))
362        .unwrap_or((0, 0));
363
364    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
365
366    if width > 0 && height > 0 {
367        println!("\x1b_Gf=100,s={},v={},a=T;{}\x1b\\", width, height, encoded);
368    } else {
369        println!("\x1b_Gf=100,a=T;{}", encoded);
370    }
371}
372
373/// Renders a raw image buffer (e.g. PNG bytes) using the Sixel graphics protocol.
374#[cfg(feature = "graphics")]
375pub fn print_sixel_logo(image_data: &[u8]) {
376    if let Ok(img) = image::load_from_memory(image_data) {
377        let rgba = img.to_rgba8();
378        let (width, height) = rgba.dimensions();
379        print_sixel_rgba(rgba.as_raw(), width, height);
380    }
381}
382
383/// Renders raw RGBA pixels using the Sixel graphics protocol.
384#[cfg(feature = "graphics")]
385pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
386    use icy_sixel::SixelImage;
387
388    match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
389        Ok(sixel_img) => match sixel_img.encode() {
390            Ok(sixel_str) => {
391                print!("{}", sixel_str);
392            }
393            Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
394        },
395        Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
396    }
397}
398
399/// Placeholder for graphical logo rendering when the `graphics` feature is disabled.
400#[cfg(not(feature = "graphics"))]
401pub fn print_graphical_logo(_image_data: &[u8]) {
402    println!("[Graphical logo support requires --features graphics]");
403}
404
405/// Placeholder for sixel logo rendering when the `graphics` feature is disabled.
406#[cfg(not(feature = "graphics"))]
407pub fn print_sixel_logo(_image_data: &[u8]) {
408    println!("[Sixel logo support requires --features graphics]");
409}
410
411/// Loads an image from a file, resizes it, and prints it using the graphics protocol.
412#[cfg(feature = "graphics")]
413pub fn print_graphical_logo_from_path(path: &std::path::Path) {
414    use image::ImageFormat;
415    match image::open(path) {
416        Ok(img) => {
417            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
418            let mut png_data = Vec::new();
419            if resized
420                .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
421                .is_ok()
422            {
423                print_graphical_logo(&png_data);
424            } else {
425                println!("[Failed to encode logo as PNG]");
426            }
427        }
428        Err(_) => {
429            println!("[Could not load graphical logo from {}]", path.display());
430        }
431    }
432}
433
434/// Loads an image from a file, resizes it, and prints it using the Sixel protocol.
435#[cfg(feature = "graphics")]
436pub fn print_sixel_logo_from_path(path: &std::path::Path) {
437    match image::open(path) {
438        Ok(img) => {
439            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
440            let rgba = resized.to_rgba8();
441            let (width, height) = rgba.dimensions();
442            print_sixel_rgba(rgba.as_raw(), width, height);
443        }
444        Err(_) => {
445            println!("[Could not load logo for Sixel from {}]", path.display());
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_get_ascii_logo_arch() {
456        let logo = get_ascii_logo(Some("arch"));
457        assert!(!logo.is_empty());
458        assert!(logo[0].contains("`"));
459    }
460
461    #[test]
462    fn test_get_ascii_logo_unknown() {
463        let logo = get_ascii_logo(Some("unknown_distro"));
464        assert!(!logo.is_empty());
465        // Should fall back to Tux
466        assert!(logo.iter().any(|line| line.contains("o_o")));
467    }
468
469    #[test]
470    fn test_get_ascii_logo_none() {
471        let logo = get_ascii_logo(None);
472        assert!(!logo.is_empty());
473    }
474}