Skip to main content

retch_cli/
logo.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright (C) 2026 l1a
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("macos") => Some(include_bytes!("../assets/logos/macos.png")),
36        Some("windows") => Some(include_bytes!("../assets/logos/windows.png")),
37        _ => Some(include_bytes!("../assets/logos/tux.png")),
38    }
39}
40
41/// Fallback for non-graphics build to provide Tux bytes for Chafa if needed.
42#[cfg(not(feature = "graphics"))]
43pub fn get_embedded_logo(_distro: Option<&str>) -> Option<&'static [u8]> {
44    Some(include_bytes!("../assets/logos/tux.png"))
45}
46
47/// Attempts to detect the current operating system distribution.
48pub fn detect_distro() -> Option<String> {
49    #[cfg(target_os = "macos")]
50    {
51        Some("macos".to_string())
52    }
53    #[cfg(target_os = "windows")]
54    {
55        Some("windows".to_string())
56    }
57    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
58    {
59        if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
60            for line in content.lines() {
61                if line.starts_with("ID=") {
62                    let id = line.trim_start_matches("ID=").trim_matches('"');
63                    return Some(id.to_string());
64                }
65            }
66        }
67        None
68    }
69}
70
71/// Returns a list of strings representing the ASCII art for a given distro.
72///
73/// All ASCII logos are sourced from or compatible with Fastfetch.
74pub fn get_ascii_logo(distro: Option<&str>) -> Vec<String> {
75    let d = distro.map(|s| s.to_lowercase());
76
77    match d.as_deref() {
78        // Arch Linux - exact from Fastfetch
79        Some("arch") => vec![
80            "               -`".to_string(),
81            "                 .o+`".to_string(),
82            "                `ooo/".to_string(),
83            "               `+oooo:".to_string(),
84            "              `+oooooo:".to_string(),
85            "              -+oooooo+:".to_string(),
86            "            `/:-:++oooo+:".to_string(),
87            "           `/++++/+++++++:".to_string(),
88            "          `/++++++++++++++:".to_string(),
89            "         `/+++ooooooooooooo/`".to_string(),
90            "        ./ooosssso++osssssso+`".to_string(),
91            "       .oossssso-````/ossssss+`".to_string(),
92            "      -osssssso.      :ssssssso.".to_string(),
93            "     :osssssss/        osssso+++.".to_string(),
94            "    /ossssssss/        +ssssooo/-".to_string(),
95            "  `/ossssso+/:-        -:/+osssso+-".to_string(),
96            " `+sso+:-`                 `.-/+oso:".to_string(),
97            "`++:.                           `-/+/".to_string(),
98            ".`                                 `".to_string(),
99        ],
100
101        // Debian - exact from Fastfetch
102        Some("debian") => vec![
103            " _,met$$$$$$$$$$gg.".to_string(),
104            "      ,g$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$P.".to_string(),
105            "    ,g$$$$P\"\"       \"\"\"Y$$$$.\".".to_string(),
106            "   ,$$$$P'              `$$$$$$.".to_string(),
107            "',$$$$P       ,ggs.     `$$$$b:".to_string(),
108            "`d$$$$'     ,$P\"'   .    $$$$$$".to_string(),
109            " $$$$P      d$'     ,    $$$$P".to_string(),
110            " $$$$:      $$$.   -    ,d$$$$'".to_string(),
111            " $$$$;      Y$b._   _,d$P'".to_string(),
112            " Y$$$$.    .`\"Y$$$$$$$$P\"'".to_string(),
113            " `$$$$b      \"-.__".to_string(),
114            "  `Y$$$$b".to_string(),
115            "   `Y$$$$.".to_string(),
116            "     `$$$$b.".to_string(),
117            "       `Y$$$$b.".to_string(),
118            "         `\"Y$$b._".to_string(),
119            "             `\"\"\"\"".to_string(),
120        ],
121
122        // Fedora - exact from Fastfetch
123        Some("fedora") => vec![
124            "             .',;::::;,'.".to_string(),
125            "         .';:cccccccccccc:;,.".to_string(),
126            "      .;cccccccccccccccccccccc;.".to_string(),
127            "    .:cccccccccccccccccccccc:.".to_string(),
128            "  .;ccccccccccccc;.:dddl:.;ccccccc;.".to_string(),
129            " .:ccccccccccccc;OWMKOOXMWd;ccccccc:.".to_string(),
130            ".:ccccccccccccc;KMMc;cc;xMMc;ccccccc:.".to_string(),
131            ",cccccccccccccc;MMM.;cc;;WW:;cccccccc,".to_string(),
132            ":cccccccccccccc;MMM.;cccccccccccccccc:".to_string(),
133            ":ccccccc;oxOOOo;MMM000k.;cccccccccccc:".to_string(),
134            "cccccc;0MMKxdd:;MMMkddc.;cccccccccccc;".to_string(),
135            "ccccc;XMO';cccc;MMM.;cccccccccccccccc'".to_string(),
136            "ccccc;MMo;ccccc;MMW.;ccccccccccccccc;".to_string(),
137            "ccccc;0MNc.ccc.xMMd;ccccccccccccccc;".to_string(),
138            "cccccc;dNMWXXXWM0:;cccccccccccccc:,".to_string(),
139            "cccccccc;.:odl:.;cccccccccccccc:,.".to_string(),
140            "ccccccccccccccccccccccccccccc:'.".to_string(),
141            ":ccccccccccccccccccccccc:;,..".to_string(),
142        ],
143
144        // NixOS - exact from Fastfetch
145        Some("nixos") => vec![
146            "          ::::.    ':::::".to_string(),
147            "      ':::::::::.   ':::::::::".to_string(),
148            "  .:::::::::::::     ::::::::::::.".to_string(),
149            "  :::::::::::'        ::::::::::::".to_string(),
150            " .::::::::::'          :::::::::::.".to_string(),
151            " ::::::::::             :::::::::::".to_string(),
152            " :::::::::              :::::::::::".to_string(),
153            " ::::::::                ::::::::::".to_string(),
154            " '::::::.                '::::::::'".to_string(),
155            "  ':::::                  '::::::'".to_string(),
156            "   ':::                    '::::'".to_string(),
157            "    ':                      ':".to_string(),
158        ],
159
160        // Ubuntu - exact from Fastfetch
161        Some("ubuntu") => vec![
162            "                          ....".to_string(),
163            "          ....,,:::::ccccclllclllcc:::;,,....".to_string(),
164            "      ..,;::cccllllllllllllllllllllllllcc::;..".to_string(),
165            "    ..,;::cccllllllllllllllllllllllllllllcc::;..".to_string(),
166            "  ..,;::cccllllllllllllllllllllllllllllllllcc::;..".to_string(),
167            " ..,;::cccllllllllllllllllllllllllllllllllllcc::;..".to_string(),
168            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
169            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
170            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
171            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
172            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
173            "..,;::cccllllllllllllllllllllllllllllllllllllcc::;.".to_string(),
174        ],
175
176        Some("pop") => vec![
177            "\x1b[37m             /////////////\x1b[0m".to_string(),
178            "\x1b[37m         /////////////////////\x1b[0m".to_string(),
179            "\x1b[37m      ///////\x1b[36m*767\x1b[37m////////////////\x1b[0m".to_string(),
180            "\x1b[37m    //////\x1b[36m7676767676*\x1b[37m//////////////\x1b[0m".to_string(),
181            "\x1b[37m   /////\x1b[36m76767\x1b[37m//\x1b[36m7676767\x1b[37m//////////////\x1b[0m".to_string(),
182            "\x1b[37m  /////\x1b[36m767676\x1b[37m///\x1b[36m*76767\x1b[37m///////////////\x1b[0m".to_string(),
183            "\x1b[37m ///////\x1b[36m767676\x1b[37m///\x1b[36m76767\x1b[37m.\x1b[36m///7676*\x1b[37m///////\x1b[0m".to_string(),
184            "\x1b[37m/////////\x1b[36m767676\x1b[37m//\x1b[36m76767\x1b[37m///\x1b[36m767676\x1b[37m////////\x1b[0m".to_string(),
185            "\x1b[37m//////////\x1b[36m767676767676\x1b[37m////\x1b[36m76767\x1b[37m/////////\x1b[0m".to_string(),
186            "\x1b[37m///////////\x1b[36m767676767\x1b[37m//////\x1b[36m7676\x1b[37m//////////\x1b[0m".to_string(),
187            "\x1b[37m////////////,\x1b[36m7676\x1b[37m,///////\x1b[36m767\x1b[37m///////////\x1b[0m".to_string(),
188            "\x1b[37m/////////////*\x1b[36m7676\x1b[37m///////\x1b[36m76\x1b[37m////////////\x1b[0m".to_string(),
189            "\x1b[37m///////////////\x1b[36m7676\x1b[37m////////////////////\x1b[0m".to_string(),
190            "\x1b[37m ///////////////\x1b[36m7676\x1b[37m///\x1b[36m767\x1b[37m////////////\x1b[0m".to_string(),
191            "\x1b[37m  //////////////////////\x1b[36m'\x1b[37m////////////\x1b[0m".to_string(),
192            "\x1b[37m   //////\x1b[36m.7676767676767676767,\x1b[37m//////\x1b[0m".to_string(),
193            "\x1b[37m    /////\x1b[36m767676767676767676767\x1b[37m/////\x1b[0m".to_string(),
194            "\x1b[37m      ///////////////////////////\x1b[0m".to_string(),
195            "\x1b[37m         /////////////////////\x1b[0m".to_string(),
196            "\x1b[37m             /////////////\x1b[0m".to_string(),
197        ],
198
199        Some("manjaro") => vec![
200            "\x1b[32m██████████████████  ████████\x1b[0m".to_string(),
201            "\x1b[32m██████████████████  ████████\x1b[0m".to_string(),
202            "\x1b[32m██████████████████  ████████\x1b[0m".to_string(),
203            "\x1b[32m██████████████████  ████████\x1b[0m".to_string(),
204            "\x1b[32m████████            ████████\x1b[0m".to_string(),
205            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
206            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
207            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
208            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
209            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
210            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
211            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
212            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
213            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
214            "\x1b[32m████████  ████████  ████████\x1b[0m".to_string(),
215        ],
216
217        Some("endeavouros") => vec![
218            "                     \x1b[35m./\x1b[31mo\x1b[34m.\x1b[0m".to_string(),
219            "                   \x1b[35m./\x1b[31msssso\x1b[34m-\x1b[0m".to_string(),
220            "                 \x1b[35m`:\x1b[31mosssssss+\x1b[34m-\x1b[0m".to_string(),
221            "               \x1b[35m`:+\x1b[31msssssssssso\x1b[34m/.\x1b[0m".to_string(),
222            "             \x1b[35m`-/o\x1b[31mssssssssssssso\x1b[34m/.\x1b[0m".to_string(),
223            "           \x1b[35m`-/+\x1b[31msssssssssssssssso\x1b[34m+:`\x1b[0m".to_string(),
224            "         \x1b[35m`-:/+\x1b[31msssssssssssssssssso\x1b[34m+/.\x1b[0m".to_string(),
225            "       \x1b[35m`.://o\x1b[31msssssssssssssssssssso\x1b[34m++-\x1b[0m".to_string(),
226            "      \x1b[35m.://+\x1b[31mssssssssssssssssssssssso\x1b[34m++:\x1b[0m".to_string(),
227            "    \x1b[35m.:///o\x1b[31mssssssssssssssssssssssssso\x1b[34m++:\x1b[0m".to_string(),
228            "  \x1b[35m`:////\x1b[31mssssssssssssssssssssssssssso\x1b[34m+++.\x1b[0m".to_string(),
229            "\x1b[35m`-////+\x1b[31mssssssssssssssssssssssssssso\x1b[34m++++-\x1b[0m".to_string(),
230            " \x1b[35m`..-+\x1b[31moosssssssssssssssssssssssso\x1b[34m+++++/`\x1b[0m".to_string(),
231            "   \x1b[34m./++++++++++++++++++++++++++++++/:.\x1b[0m".to_string(),
232            "  \x1b[34m`:::::::::::::::::::::::::------``\x1b[0m".to_string(),
233            "  \x1b[34m`:::::::::::::::::::::::::------``\x1b[0m".to_string(),
234        ],
235
236        Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => vec![
237            "\x1b[32m           .;ldkO0000Okdl;.\x1b[0m".to_string(),
238            "\x1b[32m       .;d00xl:^''''''^:ok00d;.\x1b[0m".to_string(),
239            "\x1b[32m     .d00l'                'o00d.\x1b[0m".to_string(),
240            "\x1b[32m   .d0Kd'\x1b[37m  Okxol:;,.          \x1b[32m:O0d\x1b[0m".to_string(),
241            "\x1b[32m  .OK\x1b[37mKKK0kOKKKKKKKKKKOxo:,\x1b[32m      lKO.\x1b[0m".to_string(),
242            "\x1b[32m ,0K\x1b[37mKKKKKKKKKKKKKKK0P^\x1b[32m,,,\x1b[37m^dx:\x1b[32m    ;00,\x1b[0m".to_string(),
243            "\x1b[32m.OK\x1b[37mKKKKKKKKKKKKKKKk'\x1b[32m.oOPPb.\x1b[37m'0k.\x1b[32m   cKO.\x1b[0m".to_string(),
244            "\x1b[32m:KK\x1b[37mKKKKKKKKKKKKKKK: \x1b[32mkKx..dd \x1b[37mlKd\x1b[32m   'OK:\x1b[0m".to_string(),
245            "\x1b[32mdKK\x1b[37mKKKKKKKKKOx0KKKd \x1b[32m^0KKKO' \x1b[37mkKKc\x1b[32m   dKd\x1b[0m".to_string(),
246            "\x1b[32mdKK\x1b[37mKKKKKKKKKK;.;oOKx,..\x1b[32m^\x1b[37m..;kKKK0.\x1b[32m  dKd\x1b[0m".to_string(),
247            "\x1b[32m:KK\x1b[37mKKKKKKKKKK0o;...^cdxxOK0O/^^'  \x1b[32m.0K:\x1b[0m".to_string(),
248            "\x1b[32m kKK\x1b[37mKKKKKKKKKKKKK0x;,,......,;od  \x1b[32mlKk\x1b[0m".to_string(),
249            "\x1b[32m '0K\x1b[37mKKKKKKKKKKKKKKKKKKKK00KKOo^  \x1b[32mc00'\x1b[0m".to_string(),
250            "\x1b[32m  'kK\x1b[37mKKOxddxkOO00000Okxoc;''   \x1b[32m.dKk'\x1b[0m".to_string(),
251            "\x1b[32m    l0Ko.                    .c00l'\x1b[0m".to_string(),
252            "\x1b[32m     'l0Kk:.              .;xK0l'\x1b[0m".to_string(),
253            "\x1b[32m        'lkK0xl:;,,,,;:ldO0kl'\x1b[0m".to_string(),
254            "\x1b[32m            '^:ldxkkkkxdl:^'\x1b[0m".to_string(),
255        ],
256
257        Some("macos") => vec![
258            "\x1b[32m                    c.'\x1b[0m".to_string(),
259            "\x1b[32m                 ,xNMM.\x1b[0m".to_string(),
260            "\x1b[32m               .OMMMMo\x1b[0m".to_string(),
261            "\x1b[32m               lMM\"\x1b[0m".to_string(),
262            "\x1b[32m     .;loddo:.  .olloddol;.\x1b[0m".to_string(),
263            "\x1b[32m   cKMMMMMMMMMMNWMMMMMMMMMM0:\x1b[0m".to_string(),
264            "\x1b[33m .KMMMMMMMMMMMMMMMMMMMMMMMWd.\x1b[0m".to_string(),
265            "\x1b[33m XMMMMMMMMMMMMMMMMMMMMMMMX.\x1b[0m".to_string(),
266            "\x1b[31m;MMMMMMMMMMMMMMMMMMMMMMMM:\x1b[0m".to_string(),
267            "\x1b[31m:MMMMMMMMMMMMMMMMMMMMMMMM:\x1b[0m".to_string(),
268            "\x1b[31m.MMMMMMMMMMMMMMMMMMMMMMMMX.\x1b[0m".to_string(),
269            "\x1b[31m kMMMMMMMMMMMMMMMMMMMMMMMMWd.\x1b[0m".to_string(),
270            "\x1b[35m'XMMMMMMMMMMMMMMMMMMMMMMMMMMk\x1b[0m".to_string(),
271            "\x1b[35m 'XMMMMMMMMMMMMMMMMMMMMMMMMK.\x1b[0m".to_string(),
272            "\x1b[34m    kMMMMMMMMMMMMMMMMMMMMMMd\x1b[0m".to_string(),
273            "\x1b[34m     ;KMMMMMMMWXXWMMMMMMMk.\x1b[0m".to_string(),
274            "\x1b[34m       \"cooc*\"    \"*coo'\"\x1b[0m".to_string(),
275        ],
276
277        Some("windows") => vec![
278            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
279            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
280            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
281            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
282            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
283            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
284            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
285            "".to_string(),
286            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
287            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
288            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
289            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
290            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
291            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
292            "\x1b[36m    ####################  ####################\x1b[0m".to_string(),
293        ],
294
295        // Fallback: Tux (Linux)
296        _ => vec![
297            "    .--.".to_string(),
298            "   |o_o |".to_string(),
299            "   |:_/ |".to_string(),
300            "  //   \\ \\".to_string(),
301            " (|     | )".to_string(),
302            "/'\\_   _/`\\".to_string(),
303            "\\___)=(___/".to_string(),
304        ],
305    }
306}
307
308/// Checks if the terminal supports the Kitty inline image protocol.
309pub fn supports_kitty() -> bool {
310    std::env::var("TERM")
311        .map(|t| t == "xterm-kitty")
312        .unwrap_or(false)
313        || std::env::var("TERMINAL_EMULATOR")
314            .map(|t| t == "iterm-kitty" || t == "iTerm.app")
315            .unwrap_or(false)
316        || std::env::var("TERM_PROGRAM")
317            .map(|t| t == "rio")
318            .unwrap_or(false)
319}
320
321/// Checks if the terminal supports the iTerm2 inline image protocol.
322pub fn supports_iterm2() -> bool {
323    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
324        if prog == "iTerm.app" || prog == "WezTerm" || prog == "rio" {
325            return true;
326        }
327    }
328    false
329}
330
331/// Checks if the terminal supports Sixel graphics (heuristic based on environment).
332pub fn supports_sixel() -> bool {
333    if let Ok(term) = std::env::var("TERM") {
334        let term = term.to_lowercase();
335        if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
336            return true;
337        }
338    }
339
340    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
341        if prog == "WezTerm" || prog == "iTerm.app" || prog == "rio" {
342            return true;
343        }
344    }
345
346    if std::env::var("WT_SESSION").is_ok() {
347        return true;
348    }
349
350    false
351}
352
353/// Checks if the `chafa` command-line tool is available in the system path.
354pub fn chafa_available() -> bool {
355    std::process::Command::new("chafa")
356        .arg("--version")
357        .output()
358        .map(|o| o.status.success())
359        .unwrap_or(false)
360}
361
362/// Write embedded logo bytes to a temporary file and return the path.
363fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
364    let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
365    std::fs::write(&temp_path, bytes)?;
366    Ok(temp_path)
367}
368
369/// Attempts to render an image using the `chafa` utility.
370///
371/// Chafa renders images using high-quality Unicode symbols, providing a
372/// good graphical fallback for many terminal emulators.
373pub fn print_with_chafa(path: &std::path::Path) -> bool {
374    let output = std::process::Command::new("chafa")
375        .arg("--format")
376        .arg("symbols")
377        .arg("--size")
378        .arg("40x20")
379        .arg(path)
380        .output();
381
382    match output {
383        Ok(out) if out.status.success() => {
384            print!("{}", String::from_utf8_lossy(&out.stdout));
385            true
386        }
387        Ok(out) => {
388            eprintln!("warning: chafa failed with status: {}", out.status);
389            false
390        }
391        Err(e) => {
392            eprintln!("warning: failed to execute chafa: {}", e);
393            false
394        }
395    }
396}
397
398/// Print logo for a distro following the strict priority:
399/// 1. Real graphic logo (if terminal supports it and embedded logo exists)
400/// 2. Chafa high-quality symbols (if chafa is available)
401/// 3. Real Fastfetch ASCII logo (always available)
402pub fn print_distro_logo(distro: Option<&str>) {
403    print_distro_logo_with_ascii(distro, false);
404}
405
406/// Renders the distribution logo with an option to force ASCII mode.
407///
408/// This is the primary entry point for logo rendering, handling the entire
409/// priority chain from high-res graphics down to text-based ASCII.
410pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool) {
411    if ascii_only {
412        // Force ASCII path
413        let art = get_ascii_logo(distro);
414        for line in art {
415            println!("{}", line);
416        }
417        return;
418    }
419
420    let supports_kitty = supports_kitty();
421    let supports_iterm2 = supports_iterm2();
422    let supports_sixel = supports_sixel();
423    let has_chafa = chafa_available();
424
425    // 1. Try embedded graphical logo (Kitty)
426    #[cfg(feature = "graphics")]
427    if supports_kitty {
428        if let Some(bytes) = get_embedded_logo(distro) {
429            if !bytes.is_empty() {
430                print_graphical_logo(bytes);
431                return;
432            }
433        }
434    }
435
436    // 2. Try embedded graphical logo (iTerm2)
437    #[cfg(feature = "graphics")]
438    if supports_iterm2 {
439        if let Some(bytes) = get_embedded_logo(distro) {
440            if !bytes.is_empty() {
441                print_iterm2_logo(bytes);
442                return;
443            }
444        }
445    }
446
447    // 3. Try embedded graphical logo (Sixel)
448    #[cfg(feature = "graphics")]
449    if supports_sixel {
450        if let Some(bytes) = get_embedded_logo(distro) {
451            if !bytes.is_empty() {
452                print_sixel_logo(bytes);
453                return;
454            }
455        }
456    }
457
458    // 3. Try chafa using embedded distro logo
459    if has_chafa {
460        if let Some(bytes) = get_embedded_logo(distro) {
461            if bytes.len() > 100 {
462                if let Ok(temp_path) = write_temp_logo(bytes) {
463                    if print_with_chafa(&temp_path) {
464                        let _ = std::fs::remove_file(&temp_path);
465                        return;
466                    }
467                    let _ = std::fs::remove_file(&temp_path);
468                }
469            }
470        }
471    }
472
473    // 4. Final fallback: Real Fastfetch ASCII logo
474    let art = get_ascii_logo(distro);
475    for line in art {
476        println!("{}", line);
477    }
478}
479
480/// Renders a raw image buffer using the iTerm2 inline image protocol.
481#[cfg(feature = "graphics")]
482pub fn print_iterm2_logo(image_data: &[u8]) {
483    use base64::Engine;
484    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
485    print!(
486        "\x1b]1337;File=inline=1;preserveAspectRatio=1:{}\x07",
487        encoded
488    );
489    println!(); // iTerm2 typically needs a newline after the logo
490}
491
492/// Loads an image from a file and prints it using the iTerm2 protocol.
493#[cfg(feature = "graphics")]
494pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
495    if let Ok(bytes) = std::fs::read(path) {
496        print_iterm2_logo(&bytes);
497    } else {
498        println!("[Could not read logo for iTerm2 from {}]", path.display());
499    }
500}
501
502/// Placeholder for iTerm2 logo rendering when the `graphics` feature is disabled.
503#[cfg(not(feature = "graphics"))]
504pub fn print_iterm2_logo(_image_data: &[u8]) {
505    println!("[iTerm2 logo support requires --features graphics]");
506}
507
508/// Renders a raw image buffer using the Kitty graphics protocol.
509#[cfg(feature = "graphics")]
510pub fn print_graphical_logo(image_data: &[u8]) {
511    use base64::Engine;
512
513    let (width, height) = image::load_from_memory(image_data)
514        .map(|img| (img.width(), img.height()))
515        .unwrap_or((0, 0));
516
517    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
518
519    if width > 0 && height > 0 {
520        println!("\x1b_Gf=100,s={},v={},a=T;{}\x1b\\", width, height, encoded);
521    } else {
522        println!("\x1b_Gf=100,a=T;{}", encoded);
523    }
524}
525
526/// Renders a raw image buffer (e.g. PNG bytes) using the Sixel graphics protocol.
527#[cfg(feature = "graphics")]
528pub fn print_sixel_logo(image_data: &[u8]) {
529    if let Ok(img) = image::load_from_memory(image_data) {
530        let rgba = img.to_rgba8();
531        let (width, height) = rgba.dimensions();
532        print_sixel_rgba(rgba.as_raw(), width, height);
533    }
534}
535
536/// Renders raw RGBA pixels using the Sixel graphics protocol.
537#[cfg(feature = "graphics")]
538pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
539    use icy_sixel::SixelImage;
540
541    match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
542        Ok(sixel_img) => match sixel_img.encode() {
543            Ok(sixel_str) => {
544                print!("{}", sixel_str);
545            }
546            Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
547        },
548        Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
549    }
550}
551
552/// Placeholder for graphical logo rendering when the `graphics` feature is disabled.
553#[cfg(not(feature = "graphics"))]
554pub fn print_graphical_logo(_image_data: &[u8]) {
555    println!("[Graphical logo support requires --features graphics]");
556}
557
558/// Placeholder for sixel logo rendering when the `graphics` feature is disabled.
559#[cfg(not(feature = "graphics"))]
560pub fn print_sixel_logo(_image_data: &[u8]) {
561    println!("[Sixel logo support requires --features graphics]");
562}
563
564/// Loads an image from a file, resizes it, and prints it using the graphics protocol.
565#[cfg(feature = "graphics")]
566pub fn print_graphical_logo_from_path(path: &std::path::Path) {
567    use image::ImageFormat;
568    match image::open(path) {
569        Ok(img) => {
570            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
571            let mut png_data = Vec::new();
572            if resized
573                .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
574                .is_ok()
575            {
576                print_graphical_logo(&png_data);
577            } else {
578                println!("[Failed to encode logo as PNG]");
579            }
580        }
581        Err(_) => {
582            println!("[Could not load graphical logo from {}]", path.display());
583        }
584    }
585}
586
587/// Loads an image from a file, resizes it, and prints it using the Sixel protocol.
588#[cfg(feature = "graphics")]
589pub fn print_sixel_logo_from_path(path: &std::path::Path) {
590    match image::open(path) {
591        Ok(img) => {
592            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
593            let rgba = resized.to_rgba8();
594            let (width, height) = rgba.dimensions();
595            print_sixel_rgba(rgba.as_raw(), width, height);
596        }
597        Err(_) => {
598            println!("[Could not load logo for Sixel from {}]", path.display());
599        }
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn test_get_ascii_logo_arch() {
609        let logo = get_ascii_logo(Some("arch"));
610        assert!(!logo.is_empty());
611        assert!(logo[0].contains("`"));
612    }
613
614    #[test]
615    fn test_get_ascii_logo_unknown() {
616        let logo = get_ascii_logo(Some("unknown_distro"));
617        assert!(!logo.is_empty());
618        // Should fall back to Tux
619        assert!(logo.iter().any(|line| line.contains("o_o")));
620    }
621
622    #[test]
623    fn test_get_ascii_logo_none() {
624        let logo = get_ascii_logo(None);
625        assert!(!logo.is_empty());
626    }
627
628    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
629
630    struct EnvGuard {
631        _mutex_guard: std::sync::MutexGuard<'static, ()>,
632        old_vars: std::collections::HashMap<&'static str, Option<String>>,
633    }
634
635    impl EnvGuard {
636        fn new(vars_to_mock: &[&'static str]) -> Self {
637            let guard = ENV_LOCK.lock().unwrap();
638            let mut old_vars = std::collections::HashMap::new();
639            for var in vars_to_mock {
640                old_vars.insert(*var, std::env::var(var).ok());
641            }
642            EnvGuard {
643                _mutex_guard: guard,
644                old_vars,
645            }
646        }
647    }
648
649    impl Drop for EnvGuard {
650        fn drop(&mut self) {
651            for (var, value) in &self.old_vars {
652                if let Some(val) = value {
653                    std::env::set_var(var, val);
654                } else {
655                    std::env::remove_var(var);
656                }
657            }
658        }
659    }
660
661    #[test]
662    fn test_supports_kitty_heuristics() {
663        let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
664
665        // Test TERM=xterm-kitty
666        std::env::set_var("TERM", "xterm-kitty");
667        std::env::remove_var("TERMINAL_EMULATOR");
668        std::env::remove_var("TERM_PROGRAM");
669        assert!(supports_kitty());
670
671        // Test TERMINAL_EMULATOR=iterm-kitty
672        std::env::remove_var("TERM");
673        std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
674        assert!(supports_kitty());
675
676        // Test TERMINAL_EMULATOR=iTerm.app
677        std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
678        assert!(supports_kitty());
679
680        // Test TERM_PROGRAM=rio
681        std::env::remove_var("TERMINAL_EMULATOR");
682        std::env::set_var("TERM_PROGRAM", "rio");
683        assert!(supports_kitty());
684
685        // Test clear env -> false
686        std::env::remove_var("TERM_PROGRAM");
687        assert!(!supports_kitty());
688    }
689
690    #[test]
691    fn test_supports_iterm2_heuristics() {
692        let _guard = EnvGuard::new(&["TERM_PROGRAM"]);
693
694        // Test TERM_PROGRAM=iTerm.app
695        std::env::set_var("TERM_PROGRAM", "iTerm.app");
696        assert!(supports_iterm2());
697
698        // Test TERM_PROGRAM=WezTerm
699        std::env::set_var("TERM_PROGRAM", "WezTerm");
700        assert!(supports_iterm2());
701
702        // Test TERM_PROGRAM=rio
703        std::env::set_var("TERM_PROGRAM", "rio");
704        assert!(supports_iterm2());
705
706        // Test TERM_PROGRAM=Apple_Terminal
707        std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
708        assert!(!supports_iterm2());
709
710        // Test clear env -> false
711        std::env::remove_var("TERM_PROGRAM");
712        assert!(!supports_iterm2());
713    }
714
715    #[test]
716    fn test_supports_sixel_heuristics() {
717        let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
718
719        // Clear all to start fresh
720        std::env::remove_var("TERM");
721        std::env::remove_var("TERM_PROGRAM");
722        std::env::remove_var("WT_SESSION");
723        assert!(!supports_sixel());
724
725        // Test TERM=xterm-sixel
726        std::env::set_var("TERM", "xterm-sixel");
727        assert!(supports_sixel());
728
729        // Test TERM=foot
730        std::env::set_var("TERM", "foot");
731        assert!(supports_sixel());
732
733        // Test TERM=mlterm (case variations)
734        std::env::set_var("TERM", "MLTerm");
735        assert!(supports_sixel());
736
737        // Reset TERM, test TERM_PROGRAM=WezTerm
738        std::env::remove_var("TERM");
739        std::env::set_var("TERM_PROGRAM", "WezTerm");
740        assert!(supports_sixel());
741
742        // Test TERM_PROGRAM=iTerm.app
743        std::env::set_var("TERM_PROGRAM", "iTerm.app");
744        assert!(supports_sixel());
745
746        // Test TERM_PROGRAM=rio
747        std::env::set_var("TERM_PROGRAM", "rio");
748        assert!(supports_sixel());
749
750        // Reset TERM_PROGRAM, test WT_SESSION
751        std::env::remove_var("TERM_PROGRAM");
752        std::env::set_var("WT_SESSION", "active");
753        assert!(supports_sixel());
754    }
755
756    #[test]
757    fn test_get_embedded_logo() {
758        let logo = get_embedded_logo(Some("arch"));
759        assert!(logo.is_some());
760        let logo = get_embedded_logo(Some("pop"));
761        assert!(logo.is_some());
762        let logo = get_embedded_logo(Some("manjaro"));
763        assert!(logo.is_some());
764        let logo = get_embedded_logo(Some("endeavouros"));
765        assert!(logo.is_some());
766        let logo = get_embedded_logo(Some("opensuse"));
767        assert!(logo.is_some());
768        let logo = get_embedded_logo(Some("opensuse-leap"));
769        assert!(logo.is_some());
770        let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
771        assert!(logo.is_some());
772        let logo = get_embedded_logo(Some("macos"));
773        assert!(logo.is_some());
774        let logo = get_embedded_logo(Some("windows"));
775        assert!(logo.is_some());
776        let logo = get_embedded_logo(None);
777        assert!(logo.is_some());
778    }
779
780    #[test]
781    fn test_get_ascii_logo_new_distros() {
782        let pop = get_ascii_logo(Some("pop"));
783        assert!(!pop.is_empty());
784        assert!(pop.iter().any(|line| line.contains("767")));
785
786        let manjaro = get_ascii_logo(Some("manjaro"));
787        assert!(!manjaro.is_empty());
788        assert!(manjaro.iter().any(|line| line.contains("████████")));
789
790        let endeavouros = get_ascii_logo(Some("endeavouros"));
791        assert!(!endeavouros.is_empty());
792        assert!(endeavouros.iter().any(|line| line.contains("ssso")));
793
794        let opensuse = get_ascii_logo(Some("opensuse"));
795        assert!(!opensuse.is_empty());
796        assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
797
798        let macos = get_ascii_logo(Some("macos"));
799        assert!(!macos.is_empty());
800        assert!(macos
801            .iter()
802            .any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
803
804        let windows = get_ascii_logo(Some("windows"));
805        assert!(!windows.is_empty());
806        assert!(windows
807            .iter()
808            .any(|line| line.contains("####################  ####################")));
809    }
810}