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#[cfg(feature = "graphics")]
9pub fn get_embedded_logo(distro: Option<&str>) -> Option<&'static [u8]> {
10    let d = distro.map(|s| s.to_lowercase());
11    match d.as_deref() {
12        Some("arch") => Some(include_bytes!("../assets/logos/arch.png")),
13        Some("debian") => Some(include_bytes!("../assets/logos/debian.png")),
14        Some("fedora") => Some(include_bytes!("../assets/logos/fedora.png")),
15        Some("nixos") => Some(include_bytes!("../assets/logos/nixos.png")),
16        Some("ubuntu") => Some(include_bytes!("../assets/logos/ubuntu.png")),
17        _ => Some(include_bytes!("../assets/logos/tux.png")),
18    }
19}
20
21pub fn detect_distro() -> Option<String> {
22    if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
23        for line in content.lines() {
24            if line.starts_with("ID=") {
25                let id = line.trim_start_matches("ID=").trim_matches('"');
26                return Some(id.to_string());
27            }
28        }
29    }
30    None
31}
32
33pub fn get_ascii_logo(distro: Option<&str>) -> Vec<String> {
34    let d = distro.map(|s| s.to_lowercase());
35
36    match d.as_deref() {
37        // Arch Linux - exact from Fastfetch
38        Some("arch") => vec![
39            "               -`".to_string(),
40            "                 .o+`".to_string(),
41            "                `ooo/".to_string(),
42            "               `+oooo:".to_string(),
43            "              `+oooooo:".to_string(),
44            "              -+oooooo+:".to_string(),
45            "            `/:-:++oooo+:".to_string(),
46            "           `/++++/+++++++:".to_string(),
47            "          `/++++++++++++++:".to_string(),
48            "         `/+++ooooooooooooo/`".to_string(),
49            "        ./ooosssso++osssssso+`".to_string(),
50            "       .oossssso-````/ossssss+`".to_string(),
51            "      -osssssso.      :ssssssso.".to_string(),
52            "     :osssssss/        osssso+++.".to_string(),
53            "    /ossssssss/        +ssssooo/-".to_string(),
54            "  `/ossssso+/:-        -:/+osssso+-".to_string(),
55            " `+sso+:-`                 `.-/+oso:".to_string(),
56            "`++:.                           `-/+/".to_string(),
57            ".`                                 `".to_string(),
58        ],
59
60        // Debian - exact from Fastfetch
61        Some("debian") => vec![
62            " _,met$$$$$$$$$$gg.".to_string(),
63            "      ,g$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$P.".to_string(),
64            "    ,g$$$$P\"\"       \"\"\"Y$$$$.\".".to_string(),
65            "   ,$$$$P'              `$$$$$$.".to_string(),
66            "',$$$$P       ,ggs.     `$$$$b:".to_string(),
67            "`d$$$$'     ,$P\"'   .    $$$$$$".to_string(),
68            " $$$$P      d$'     ,    $$$$P".to_string(),
69            " $$$$:      $$$.   -    ,d$$$$'".to_string(),
70            " $$$$;      Y$b._   _,d$P'".to_string(),
71            " Y$$$$.    .`\"Y$$$$$$$$P\"'".to_string(),
72            " `$$$$b      \"-.__".to_string(),
73            "  `Y$$$$b".to_string(),
74            "   `Y$$$$.".to_string(),
75            "     `$$$$b.".to_string(),
76            "       `Y$$$$b.".to_string(),
77            "         `\"Y$$b._".to_string(),
78            "             `\"\"\"\"".to_string(),
79        ],
80
81        // Fedora - exact from Fastfetch
82        Some("fedora") => vec![
83            "             .',;::::;,'.".to_string(),
84            "         .';:cccccccccccc:;,.".to_string(),
85            "      .;cccccccccccccccccccccc;.".to_string(),
86            "    .:cccccccccccccccccccccccccc:.".to_string(),
87            "  .;ccccccccccccc;.:dddl:.;ccccccc;.".to_string(),
88            " .:ccccccccccccc;OWMKOOXMWd;ccccccc:.".to_string(),
89            ".:ccccccccccccc;KMMc;cc;xMMc;ccccccc:.".to_string(),
90            ",cccccccccccccc;MMM.;cc;;WW:;cccccccc,".to_string(),
91            ":cccccccccccccc;MMM.;cccccccccccccccc:".to_string(),
92            ":ccccccc;oxOOOo;MMM000k.;cccccccccccc:".to_string(),
93            "cccccc;0MMKxdd:;MMMkddc.;cccccccccccc;".to_string(),
94            "ccccc;XMO';cccc;MMM.;cccccccccccccccc'".to_string(),
95            "ccccc;MMo;ccccc;MMW.;ccccccccccccccc;".to_string(),
96            "ccccc;0MNc.ccc.xMMd;ccccccccccccccc;".to_string(),
97            "cccccc;dNMWXXXWM0:;cccccccccccccc:,".to_string(),
98            "cccccccc;.:odl:.;cccccccccccccc:,.".to_string(),
99            "ccccccccccccccccccccccccccccc:'.".to_string(),
100            ":ccccccccccccccccccccccc:;,..".to_string(),
101            " ':cccccccccccccccc::;,.".to_string(),
102        ],
103
104        // Ubuntu - exact from Fastfetch
105        Some("ubuntu") => vec![
106            "              ....".to_string(),
107            "           .',:clooo:  .:looooo:.".to_string(),
108            "        .;looooooooc  .oooooooooo'".to_string(),
109            "     .;looooool:,''.  :ooooooooooc".to_string(),
110            "    ;looool;.         'oooooooooo,".to_string(),
111            "   ;clool'             .cooooooc.  ,,".to_string(),
112            "      ...                ......  .:oo,".to_string(),
113            " .;clol:,.                        .loooo'".to_string(),
114            ":ooooooooo,                        'ooool".to_string(),
115            "'ooooooooooo.                        loooo.".to_string(),
116            "'ooooooooool                         coooo.".to_string(),
117            " ,loooooooc.                        .loooo.".to_string(),
118            "   .,;;;'.                          ;ooooc".to_string(),
119            "       ...                         ,ooool.".to_string(),
120            "    .cooooc.              ..',,'.  .cooo.".to_string(),
121            "      ;ooooo:.           ;oooooooc.  :l.".to_string(),
122            "       .coooooc,..      coooooooooo.".to_string(),
123            "         .:ooooooolc:. .ooooooooooo'".to_string(),
124            "           .':loooooo;  ,oooooooooc".to_string(),
125            "               ..';::c'  .;loooo:'".to_string(),
126        ],
127
128        // NixOS - exact from Fastfetch (ASCII version)
129        Some("nixos") => vec![
130            "  \\  \\ /".to_string(),
131            "   \\  \\ /".to_string(),
132            "    \\  \\ /".to_string(),
133            "     \\  \\ /".to_string(),
134            "      \\  \\ /".to_string(),
135            "       \\  \\ /".to_string(),
136            "        \\  \\ /".to_string(),
137            "         \\  \\ /".to_string(),
138            "          \\  \\ /".to_string(),
139            "           \\  \\ /".to_string(),
140            "            \\  \\ /".to_string(),
141            "             \\  \\ /".to_string(),
142            "              \\  \\ /".to_string(),
143            "               \\  \\ /".to_string(),
144            "                \\  \\ /".to_string(),
145            "                 \\  \\ /".to_string(),
146            "                  \\  \\ /".to_string(),
147            "                   \\  \\ /".to_string(),
148            "                    \\  \\ /".to_string(),
149        ],
150
151        // Tux (default)
152        _ => vec![
153            "    .--.".to_string(),
154            "   |o_o |".to_string(),
155            "   |:_/ |".to_string(),
156            "  //   \\ \\".to_string(),
157            " (|     | )".to_string(),
158            "'/_   _/'".to_string(),
159            "  |___|".to_string(),
160        ],
161    }
162}
163
164pub fn print_ascii_logo(logo: &[String]) {
165    for line in logo {
166        println!("{}", line);
167    }
168}
169
170pub fn supports_graphical_logo() -> bool {
171    if let Ok(term) = std::env::var("TERM") {
172        if term.contains("kitty") {
173            return true;
174        }
175    }
176    if let Ok(program) = std::env::var("TERM_PROGRAM") {
177        if program.to_lowercase().contains("iterm") {
178            return true;
179        }
180    }
181    false
182}
183
184/// Check if chafa is available in PATH
185pub fn chafa_available() -> bool {
186    std::process::Command::new("chafa")
187        .arg("--version")
188        .output()
189        .map(|o| o.status.success())
190        .unwrap_or(false)
191}
192
193/// Write embedded logo bytes to a temporary file and return the path
194fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
195    let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
196    std::fs::write(&temp_path, bytes)?;
197    Ok(temp_path)
198}
199
200/// Try to print logo using chafa (high quality symbols)
201pub fn print_with_chafa(path: &std::path::Path) -> bool {
202    let output = std::process::Command::new("chafa")
203        .arg("--format")
204        .arg("symbols")
205        .arg("--size")
206        .arg("40x20")
207        .arg(path)
208        .output();
209
210    match output {
211        Ok(out) if out.status.success() => {
212            print!("{}", String::from_utf8_lossy(&out.stdout));
213            true
214        }
215        _ => false,
216    }
217}
218
219/// Print logo for a distro following the strict priority:
220/// 1. Real graphic logo (if terminal supports it and embedded logo exists)
221/// 2. Chafa high-quality symbols (if chafa is available and user logo exists)
222/// 3. Real Fastfetch ASCII logo (always available)
223pub fn print_distro_logo(distro: Option<&str>) {
224    print_distro_logo_with_ascii(distro, false);
225}
226
227/// Same as `print_distro_logo` but allows forcing ASCII mode
228pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool) {
229    if ascii_only {
230        // Force ASCII path
231        let art = get_ascii_logo(distro);
232        for line in art {
233            println!("{}", line);
234        }
235        return;
236    }
237
238    let supports_graphics = supports_graphical_logo();
239    let has_chafa = chafa_available();
240
241    // 1. Try embedded graphical logo (only if feature is enabled)
242    #[cfg(feature = "graphics")]
243    if supports_graphics {
244        if let Some(bytes) = get_embedded_logo(distro) {
245            if !bytes.is_empty() {
246                print_graphical_logo(bytes);
247                return;
248            }
249        }
250    }
251
252    // 2. Try chafa using embedded distro logo
253    if has_chafa {
254        if let Some(bytes) = get_embedded_logo(distro) {
255            if bytes.len() > 100 {
256                if let Ok(temp_path) = write_temp_logo(bytes) {
257                    if print_with_chafa(&temp_path) {
258                        let _ = std::fs::remove_file(&temp_path);
259                        return;
260                    }
261                    let _ = std::fs::remove_file(&temp_path);
262                }
263            }
264        }
265    }
266
267    // 3. Final fallback: Real Fastfetch ASCII logo
268    let art = get_ascii_logo(distro);
269    for line in art {
270        println!("{}", line);
271    }
272}
273
274#[cfg(feature = "graphics")]
275pub fn print_graphical_logo(image_data: &[u8]) {
276    use base64::Engine;
277
278    let (width, height) = image::load_from_memory(image_data)
279        .map(|img| (img.width(), img.height()))
280        .unwrap_or((0, 0));
281
282    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
283
284    if width > 0 && height > 0 {
285        println!("\x1b_Gf=100,s={},v={},a=T;{}\x1b\\", width, height, encoded);
286    } else {
287        println!("\x1b_Gf=100,a=T;{}", encoded);
288    }
289}
290
291#[cfg(not(feature = "graphics"))]
292pub fn print_graphical_logo(_image_data: &[u8]) {
293    println!("[Graphical logo support requires --features graphics]");
294}
295
296#[cfg(feature = "graphics")]
297use image::ImageFormat;
298
299#[cfg(feature = "graphics")]
300pub fn print_graphical_logo_from_path(path: &std::path::Path) {
301    match image::open(path) {
302        Ok(img) => {
303            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
304            let mut png_data = Vec::new();
305            if resized
306                .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
307                .is_ok()
308            {
309                print_graphical_logo(&png_data);
310            } else {
311                println!("[Failed to encode logo as PNG]");
312            }
313        }
314        Err(_) => {
315            println!("[Could not load graphical logo from {}]", path.display());
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_get_ascii_logo_arch() {
326        let logo = get_ascii_logo(Some("arch"));
327        assert!(!logo.is_empty());
328        assert!(logo[0].contains("`"));
329    }
330
331    #[test]
332    fn test_get_ascii_logo_unknown() {
333        let logo = get_ascii_logo(Some("unknown_distro"));
334        assert!(!logo.is_empty());
335        // Should fall back to Tux
336        assert!(logo.iter().any(|line| line.contains("o_o")));
337    }
338
339    #[test]
340    fn test_get_ascii_logo_none() {
341        let logo = get_ascii_logo(None);
342        assert!(!logo.is_empty());
343    }
344}