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 with color placeholders.
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            "         `/+++o${2}oooooooo${1}oooo/`".to_string(),
90            "        ./${2}ooosssso++osssssso${1}+`".to_string(),
91            "${2}       .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            "        ${2}_,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\"'   ${1}.${2}    $$$$$$".to_string(),
109            " $$$$P      d$'     ${1},${2}    $$$$P".to_string(),
110            " $$$$:      $$$.   ${1}-${2}    ,d$$$$'".to_string(),
111            " $$$$;      Y$b._   _,d$P'".to_string(),
112            " Y$$$$.    ${1}`.${2}`\"Y$$$$$$$$P\"'".to_string(),
113            " `$$$$b      ${1}\"-.__".to_string(),
114            "  ${2}`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            "    .:cccccccccccccccccccccccccc:.".to_string(),
128            "  .;ccccccccccccc;${2}.:dddl:${1};ccccccc;.".to_string(),
129            " .:ccccccccccccc;${2}OWMKOOXMWd${1};ccccccc:.".to_string(),
130            ".:ccccccccccccc;${2}KMMc${1};cc;${2}xMMc${1};ccccccc:.".to_string(),
131            ",cccccccccccccc;${2}MMM.${1};cc;${2};WW:${1};cccccccc,".to_string(),
132            ":cccccccccccccc;${2}MMM.${1};cccccccccccccccc:".to_string(),
133            ":ccccccc;${2}oxOOOo${1};${2}MMM000k.${1};cccccccccccc:".to_string(),
134            "cccccc;${2}0MMKxdd:${1};${2}MMMkddc.${1};cccccccccccc;".to_string(),
135            "ccccc;${2}XMO'${1};cccc;${2}MMM.${1};cccccccccccccccc'".to_string(),
136            "ccccc;${2}MMo${1};ccccc;${2}MMW.${1};ccccccccccccccc;".to_string(),
137            "ccccc;${2}0MNc.${1}ccc${2}.xMMd${1};ccccccccccccccc;".to_string(),
138            "cccccc;${2}dNMWXXXWM0:${1};cccccccccccccc:,".to_string(),
139            "cccccccc;${2}.:odl:${1};cccccccccccccc:,.".to_string(),
140            "ccccccccccccccccccccccccccccc:'.".to_string(),
141            ":ccccccccccccccccccccccc:;,..".to_string(),
142            " ':cccccccccccccccc::;,.".to_string(),
143        ],
144
145        // NixOS - exact from Fastfetch
146        Some("nixos") => vec![
147            "          ${1}▗▄▄▄       ${2}▗▄▄▄▄    ▄▄▄▖".to_string(),
148            "          ${1}▜███▙       ${2}▜███▙  ▟███▛".to_string(),
149            "           ${1}▜███▙       ${2}▜███▙▟███▛".to_string(),
150            "            ${1}▜███▙       ${2}▜██████▛".to_string(),
151            "     ${1}▟█████████████████▙ ${2}▜████▛     ${3}▟▙".to_string(),
152            "    ${1}▟███████████████████▙ ${2}▜███▙    ${3}▟██▙".to_string(),
153            "           ${6}▄▄▄▄▖           ${2}▜███▙  ${3}▟███▛".to_string(),
154            "          ${6}▟███▛             ${2}▜██▛ ${3}▟███▛".to_string(),
155            "         ${6}▟███▛               ${2}▜▛ ${3}▟███▛".to_string(),
156            "${6}▟███████████▛                  ${3}▟██████████▙".to_string(),
157            "${6}▜██████████▛                  ${3}▟███████████▛".to_string(),
158            "      ${6}▟███▛ ${5}▟▙               ${3}▟███▛".to_string(),
159            "     ${6}▟███▛ ${5}▟██▙             ${3}▟███▛".to_string(),
160            "    ${6}▟███▛  ${5}▜███▙           ${3}▝▀▀▀▀".to_string(),
161            "    ${6}▜██▛    ${5}▜███▙ ${4}▜██████████████████▛".to_string(),
162            "     ${6}▜▛     ${5}▟████▙ ${4}▜████████████████▛".to_string(),
163            "           ${5}▟██████▙         ${4}▜███▙".to_string(),
164            "          ${5}▟███▛▜███▙         ${4}▜███▙".to_string(),
165            "         ${5}▟███▛  ▜███▙         ${4}▜███▙".to_string(),
166            "         ${5}▝▀▀▀    ▀▀▀▀▘         ${4}▀▀▀▘".to_string(),
167        ],
168
169        // Ubuntu - exact from Fastfetch
170        Some("ubuntu") => vec![
171            "                             ....".to_string(),
172            "              ${2}.',:clooo:  ${1}.:looooo:.".to_string(),
173            "           ${2}.;looooooooc  ${1}.oooooooooo'".to_string(),
174            "        ${2}.;looooool:,''.  ${1}:ooooooooooc".to_string(),
175            "       ${2};looool;.         ${1}'oooooooooo,".to_string(),
176            "      ${2};clool'             ${1}.cooooooc.  ${2},,".to_string(),
177            "         ${2}...                ${1}......  ${2}.:oo,".to_string(),
178            "  ${1}.;clol:,.                        ${2}.loooo'".to_string(),
179            " ${1}:ooooooooo,                        ${2}'ooool".to_string(),
180            "${1}'ooooooooooo.                        ${2}loooo.".to_string(),
181            "${1}'ooooooooool                         ${2}coooo.".to_string(),
182            " ${1},loooooooc.                        ${2}.loooo.".to_string(),
183            "   ${1}.,;;;'.                          ${2};ooooc".to_string(),
184            "       ${2}...                         ${2},ooool.".to_string(),
185            "    ${2}.cooooc.              ${1}..',,'.  ${2}.cooo.".to_string(),
186            "      ${2};ooooo:.           ${1};oooooooc.  ${2}:l.".to_string(),
187            "       ${2}.coooooc,..      ${1}coooooooooo.".to_string(),
188            "         ${2}.:ooooooolc:. ${1}.ooooooooooo'".to_string(),
189            "           ${2}.':loooooo;  ${1},oooooooooc".to_string(),
190            "               ${2}..';::c'  ${1}.;loooo:'".to_string(),
191        ],
192
193        Some("pop") => vec![
194            "             /////////////".to_string(),
195            "         /////////////////////".to_string(),
196            "      ///////${2}*767${1}////////////////".to_string(),
197            "    //////${2}7676767676*${1}//////////////".to_string(),
198            "   /////${2}76767${1}//${2}7676767${1}//////////////".to_string(),
199            "  /////${2}767676${1}///${2}*76767${1}///////////////".to_string(),
200            " ///////${2}767676${1}///${2}76767${1}.///${2}7676*${1}///////".to_string(),
201            "/////////${2}767676${1}//${2}76767${1}///${2}767676${1}////////".to_string(),
202            "//////////${2}76767676767${1}////${2}76767${1}/////////".to_string(),
203            "///////////${2}76767676${1}//////${2}7676${1}//////////".to_string(),
204            "////////////,${2}7676${1},///////${2}767${1}///////////".to_string(),
205            "/////////////*${2}7676${1}///////${2}76${1}////////////".to_string(),
206            "///////////////${2}7676${1}////////////////////".to_string(),
207            " ///////////////${2}7676${1}///${2}767${1}////////////".to_string(),
208            "  //////////////////////${2}'${1}////////////".to_string(),
209            "   //////${2}.7676767676767676767,${1}//////".to_string(),
210            "    /////${2}767676767676767676767${1}/////".to_string(),
211            "      ///////////////////////////".to_string(),
212            "         /////////////////////".to_string(),
213            "             /////////////".to_string(),
214        ],
215
216        Some("manjaro") => vec![
217            "██████████████████  ████████".to_string(),
218            "██████████████████  ████████".to_string(),
219            "██████████████████  ████████".to_string(),
220            "██████████████████  ████████".to_string(),
221            "████████            ████████".to_string(),
222            "████████  ████████  ████████".to_string(),
223            "████████  ████████  ████████".to_string(),
224            "████████  ████████  ████████".to_string(),
225            "████████  ████████  ████████".to_string(),
226            "████████  ████████  ████████".to_string(),
227            "████████  ████████  ████████".to_string(),
228            "████████  ████████  ████████".to_string(),
229            "████████  ████████  ████████".to_string(),
230            "████████  ████████  ████████".to_string(),
231        ],
232
233        Some("endeavouros") => vec![
234            "                     ${2}./${1}o${3}.".to_string(),
235            "                   ${2}./${1}sssso${3}-".to_string(),
236            "                 ${2}`:${1}osssssss+${3}-".to_string(),
237            "               ${2}`:+${1}sssssssssso${3}/.".to_string(),
238            "             ${2}`-/o${1}ssssssssssssso${3}/.".to_string(),
239            "           ${2}`-/+${1}sssssssssssssssso${3}+:`".to_string(),
240            "         ${2}`-:/+${1}sssssssssssssssssso${3}+/.".to_string(),
241            "       ${2}`.://o${1}sssssssssssssssssssso${3}++-".to_string(),
242            "      ${2}.://+${1}ssssssssssssssssssssssso${3}++:".to_string(),
243            "    ${2}.:///o${1}ssssssssssssssssssssssssso${3}++:".to_string(),
244            "  ${2}`:////${1}ssssssssssssssssssssssssssso${3}+++.".to_string(),
245            "${2}`-////+${1}ssssssssssssssssssssssssssso${3}++++-".to_string(),
246            " ${2}`..-+${1}oosssssssssssssssssssssssso${3}+++++/`".to_string(),
247            "   ${3}./++++++++++++++++++++++++++++++/:.".to_string(),
248            "  `:::::::::::::::::::::::::------``".to_string(),
249        ],
250
251        Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => vec![
252            "           ${2}.;ldkO0000Okdl;.".to_string(),
253            "       .;d00xl:^''''''^:ok00d;.".to_string(),
254            "     .d00l'                'o00d.".to_string(),
255            "   .d0Kd'${1}  Okxol:;,.          ${2}:O0d".to_string(),
256            "  .OK${1}KKK0kOKKKKKKKKKKOxo:,      ${2}lKO.".to_string(),
257            " ,0K${1}KKKKKKKKKKKKKKK0P^${2},,,${1}^dx:${2}    ;00,".to_string(),
258            ".OK${1}KKKKKKKKKKKKKKKk'${2}.oOPPb.${1}'0k.${2}   cKO.".to_string(),
259            ":KK${1}KKKKKKKKKKKKKKK: ${2}kKx..dd ${1}lKd${2}   'OK:".to_string(),
260            "dKK${1}KKKKKKKKKOx0KKKd ${2}^0KKKO' ${1}kKKc${2}   dKd".to_string(),
261            "dKK${1}KKKKKKKKKK;.;oOKx,..${2}^${1}..;kKKK0.${2}  dKd".to_string(),
262            ":KK${1}KKKKKKKKKK0o;...^cdxxOK0O/^^'  ${2}.0K:".to_string(),
263            " kKK${1}KKKKKKKKKKKKK0x;,,......,;od  ${2}lKk".to_string(),
264            " '0K${1}KKKKKKKKKKKKKKKKKKKK00KKOo^  ${2}c00'".to_string(),
265            "  'kK${1}KKOxddxkOO00000Okxoc;''   ${2}.dKk'".to_string(),
266            "    l0Ko.                    .c00l'".to_string(),
267            "     'l0Kk:.              .;xK0l'".to_string(),
268            "        'lkK0xl:;,,,,;:ldO0kl'".to_string(),
269            "            '^:ldxkkkkxdl:^'".to_string(),
270        ],
271
272        Some("macos") => vec![
273            "                     ..'".to_string(),
274            "                 ,xNMM.".to_string(),
275            "               .OMMMMo".to_string(),
276            "               lMM\"".to_string(),
277            "     .;loddo:.  .olloddol;.".to_string(),
278            "   cKMMMMMMMMMMNWMMMMMMMMMM0:".to_string(),
279            " ${2}.KMMMMMMMMMMMMMMMMMMMMMMMWd.".to_string(),
280            " XMMMMMMMMMMMMMMMMMMMMMMMX.".to_string(),
281            "${3};MMMMMMMMMMMMMMMMMMMMMMMM:".to_string(),
282            ":MMMMMMMMMMMMMMMMMMMMMMMM:".to_string(),
283            "${4}.MMMMMMMMMMMMMMMMMMMMMMMMX.".to_string(),
284            " kMMMMMMMMMMMMMMMMMMMMMMMMWd.".to_string(),
285            " ${5}'XMMMMMMMMMMMMMMMMMMMMMMMMMMk".to_string(),
286            "  'XMMMMMMMMMMMMMMMMMMMMMMMMK.".to_string(),
287            "    ${6}kMMMMMMMMMMMMMMMMMMMMMMd".to_string(),
288            "     ;KMMMMMMMWXXWMMMMMMMk.".to_string(),
289            "       \"cooc*\"    \"*coo'\"".to_string(),
290        ],
291
292        Some("windows") => vec![
293            "${1}        ,.=:!!t3Z3z.,".to_string(),
294            "       :tt:::tt333EE3".to_string(),
295            "${1}       Et:::ztt33EEEL${2} @Ee.,      ..,".to_string(),
296            "${1}      ;tt:::tt333EE7${2} ;EEEEEEttttt33#".to_string(),
297            "${1}     :Et:::zt333EEQ.${2} $EEEEEttttt33QL".to_string(),
298            "${1}     it::::tt333EEF${2} @EEEEEEttttt33F".to_string(),
299            "${1}    ;3=*^```\"*4EEV${2} :EEEEEEttttt33@.".to_string(),
300            "${3}    ,.=::::!t=., ${1}`${2} @EEEEEEtttz33QF".to_string(),
301            "${3}   ;::::::::zt33)${2}   \"4EEEtttji3P*".to_string(),
302            "${3}  :t::::::::tt33.${4}:Z3z..${2}  ``${4} ,..g.".to_string(),
303            "${3}  i::::::::zt33F${4} AEEEtttt::::ztF".to_string(),
304            "${3} ;:::::::::t33V${4} ;EEEttttt::::t3".to_string(),
305            "${3} E::::::::zt33L${4} @EEEtttt::::z3F".to_string(),
306            "${3}{3=*^```\"*4E3)${4} ;EEEtttt:::::tZ`".to_string(),
307            "${3}             `${4} :EEEEtttt::::z7".to_string(),
308            "                 \"VEzjt:;;z>*`".to_string(),
309        ],
310
311        // Fallback: Tux (Linux)
312        _ => vec![
313            "    ${2}.--.".to_string(),
314            "   ${2}|${3}o${2}_${3}o${2} |".to_string(),
315            "   ${2}|${3}:${2}_/ |".to_string(),
316            "  ${2}//   \\ \\".to_string(),
317            " (|     | )".to_string(),
318            "/'\\_   _/`\\".to_string(),
319            "\\___)=(___/".to_string(),
320        ],
321    }
322}
323
324/// Returns dynamic ANSI color arrays for a given distribution.
325pub fn get_distro_colors(distro: Option<&str>) -> Vec<&'static str> {
326    let d = distro.map(|s| s.to_lowercase());
327    match d.as_deref() {
328        Some("arch") => vec!["\x1b[36m", "\x1b[37m"],
329        Some("debian") => vec!["\x1b[31m", "\x1b[37m"],
330        Some("fedora") => vec!["\x1b[34m", "\x1b[37m"],
331        Some("nixos") => vec![
332            "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m",
333        ],
334        Some("ubuntu") => vec!["\x1b[33m", "\x1b[31m"],
335        Some("pop") => vec!["\x1b[36m", "\x1b[37m"],
336        Some("manjaro") => vec!["\x1b[32m"],
337        Some("endeavouros") => vec!["\x1b[35m", "\x1b[31m", "\x1b[34m"],
338        Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
339            vec!["\x1b[32m", "\x1b[37m"]
340        }
341        Some("macos") => vec!["\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[35m", "\x1b[34m"],
342        Some("windows") => vec!["\x1b[36m"],
343        _ => vec!["\x1b[30m", "\x1b[37m", "\x1b[33m"], // Tux
344    }
345}
346
347/// Interpolates placeholders `${1}`...`${9}` or `$1`...`$9` with dynamic ANSI colors and appends a reset at the end.
348pub fn get_distro_logo_lines(distro: Option<&str>) -> Vec<String> {
349    let raw_lines = get_ascii_logo(distro);
350    let colors = get_distro_colors(distro);
351    let default_color = colors.first().copied().unwrap_or("\x1b[0m");
352
353    raw_lines
354        .into_iter()
355        .map(|line| {
356            let mut formatted = line;
357            for i in 1..=9 {
358                let color_val = colors.get(i - 1).copied().unwrap_or("\x1b[0m");
359                let placeholder = format!("${{{}}}", i);
360                formatted = formatted.replace(&placeholder, color_val);
361                let placeholder_short = format!("${}", i);
362                formatted = formatted.replace(&placeholder_short, color_val);
363            }
364            if !formatted.is_empty() {
365                format!("{}{}\x1b[0m", default_color, formatted)
366            } else {
367                formatted
368            }
369        })
370        .collect()
371}
372
373/// Checks if the terminal supports the Kitty inline image protocol.
374pub fn supports_kitty() -> bool {
375    std::env::var("TERM")
376        .map(|t| t == "xterm-kitty")
377        .unwrap_or(false)
378        || std::env::var("TERMINAL_EMULATOR")
379            .map(|t| t == "iterm-kitty" || t == "iTerm.app")
380            .unwrap_or(false)
381        || std::env::var("TERM_PROGRAM")
382            .map(|t| t == "rio")
383            .unwrap_or(false)
384}
385
386/// Checks if the terminal supports the iTerm2 inline image protocol.
387pub fn supports_iterm2() -> bool {
388    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
389        if prog == "iTerm.app" || prog == "WezTerm" || prog == "rio" {
390            return true;
391        }
392    }
393    false
394}
395
396/// Checks if the terminal supports Sixel graphics (heuristic based on environment).
397pub fn supports_sixel() -> bool {
398    if let Ok(term) = std::env::var("TERM") {
399        let term = term.to_lowercase();
400        if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
401            return true;
402        }
403    }
404
405    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
406        if prog == "WezTerm" || prog == "iTerm.app" || prog == "rio" {
407            return true;
408        }
409    }
410
411    if std::env::var("WT_SESSION").is_ok() {
412        return true;
413    }
414
415    false
416}
417
418/// Checks if the `chafa` command-line tool is available in the system path.
419pub fn chafa_available() -> bool {
420    std::process::Command::new("chafa")
421        .arg("--version")
422        .output()
423        .map(|o| o.status.success())
424        .unwrap_or(false)
425}
426
427/// Write embedded logo bytes to a temporary file and return the path.
428fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
429    let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
430    std::fs::write(&temp_path, bytes)?;
431    Ok(temp_path)
432}
433
434/// Attempts to render an image using the `chafa` utility.
435///
436/// Chafa renders images using high-quality Unicode symbols, providing a
437/// good graphical fallback for many terminal emulators.
438pub fn print_with_chafa(path: &std::path::Path) -> bool {
439    let output = std::process::Command::new("chafa")
440        .arg("--format")
441        .arg("symbols")
442        .arg("--size")
443        .arg("40x20")
444        .arg(path)
445        .output();
446
447    match output {
448        Ok(out) if out.status.success() => {
449            print!("{}", String::from_utf8_lossy(&out.stdout));
450            true
451        }
452        Ok(out) => {
453            eprintln!("warning: chafa failed with status: {}", out.status);
454            false
455        }
456        Err(e) => {
457            eprintln!("warning: failed to execute chafa: {}", e);
458            false
459        }
460    }
461}
462
463/// Attempts to get Chafa output as a list of lines.
464pub fn get_chafa_logo_lines(path: &std::path::Path) -> Option<Vec<String>> {
465    let output = std::process::Command::new("chafa")
466        .arg("--format")
467        .arg("symbols")
468        .arg("--size")
469        .arg("40x20")
470        .arg(path)
471        .output()
472        .ok()?;
473    if output.status.success() {
474        let content = String::from_utf8_lossy(&output.stdout);
475        Some(content.lines().map(|s| s.to_string()).collect())
476    } else {
477        None
478    }
479}
480
481/// Print logo for a distro following the strict priority:
482/// 1. Real graphic logo (if terminal supports it and embedded logo exists)
483/// 2. Chafa high-quality symbols (if chafa is available)
484/// 3. Real Fastfetch ASCII logo (always available)
485pub fn print_distro_logo(distro: Option<&str>) {
486    print_distro_logo_with_ascii(distro, false, false);
487}
488
489/// Renders the distribution logo with options to force ASCII or Chafa mode.
490///
491/// This is the primary entry point for logo rendering, handling the entire
492/// priority chain from high-res graphics down to text-based ASCII.
493/// - `ascii_only`: skip all graphical protocols and render the ASCII art directly.
494/// - `chafa_only`: skip Kitty/iTerm2/Sixel and go straight to Chafa (falls back
495///   to ASCII if Chafa is unavailable). `ascii_only` takes precedence.
496pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool, chafa_only: bool) {
497    if ascii_only {
498        // Force ASCII path
499        let art = get_distro_logo_lines(distro);
500        for line in art {
501            println!("{}", line);
502        }
503        return;
504    }
505
506    let has_chafa = chafa_available();
507
508    if !chafa_only {
509        let supports_kitty = supports_kitty();
510        let supports_iterm2 = supports_iterm2();
511        let supports_sixel = supports_sixel();
512
513        // 1. Try embedded graphical logo (Kitty)
514        #[cfg(feature = "graphics")]
515        if supports_kitty {
516            if let Some(bytes) = get_embedded_logo(distro) {
517                if !bytes.is_empty() {
518                    print_graphical_logo(bytes);
519                    return;
520                }
521            }
522        }
523
524        // 2. Try embedded graphical logo (iTerm2)
525        #[cfg(feature = "graphics")]
526        if supports_iterm2 {
527            if let Some(bytes) = get_embedded_logo(distro) {
528                if !bytes.is_empty() {
529                    print_iterm2_logo(bytes);
530                    return;
531                }
532            }
533        }
534
535        // 3. Try embedded graphical logo (Sixel)
536        #[cfg(feature = "graphics")]
537        if supports_sixel {
538            if let Some(bytes) = get_embedded_logo(distro) {
539                if !bytes.is_empty() {
540                    print_sixel_logo(bytes);
541                    return;
542                }
543            }
544        }
545    }
546
547    // 4. Try chafa using embedded distro logo
548    if has_chafa {
549        if let Some(bytes) = get_embedded_logo(distro) {
550            if bytes.len() > 100 {
551                if let Ok(temp_path) = write_temp_logo(bytes) {
552                    if print_with_chafa(&temp_path) {
553                        let _ = std::fs::remove_file(&temp_path);
554                        return;
555                    }
556                    let _ = std::fs::remove_file(&temp_path);
557                }
558            }
559        }
560    }
561
562    // 5. Final fallback: Real Fastfetch ASCII logo
563    let art = get_distro_logo_lines(distro);
564    for line in art {
565        println!("{}", line);
566    }
567}
568
569/// Renders a raw image buffer using the iTerm2 inline image protocol.
570#[cfg(feature = "graphics")]
571pub fn print_iterm2_logo(image_data: &[u8]) {
572    use base64::Engine;
573    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
574    print!(
575        "\x1b]1337;File=inline=1;preserveAspectRatio=1:{}\x07",
576        encoded
577    );
578    println!(); // iTerm2 typically needs a newline after the logo
579}
580
581/// Loads an image from a file and prints it using the iTerm2 protocol.
582#[cfg(feature = "graphics")]
583pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
584    if let Ok(bytes) = std::fs::read(path) {
585        print_iterm2_logo(&bytes);
586    } else {
587        println!("[Could not read logo for iTerm2 from {}]", path.display());
588    }
589}
590
591/// Placeholder for iTerm2 logo rendering when the `graphics` feature is disabled.
592#[cfg(not(feature = "graphics"))]
593pub fn print_iterm2_logo(_image_data: &[u8]) {
594    println!("[iTerm2 logo support requires --features graphics]");
595}
596
597/// Renders a raw image buffer using the Kitty graphics protocol.
598#[cfg(feature = "graphics")]
599pub fn print_graphical_logo(image_data: &[u8]) {
600    use base64::Engine;
601
602    let (width, height) = image::load_from_memory(image_data)
603        .map(|img| (img.width(), img.height()))
604        .unwrap_or((0, 0));
605
606    let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
607
608    if width > 0 && height > 0 {
609        println!("\x1b_Gf=100,s={},v={},a=T;{}\x1b\\", width, height, encoded);
610    } else {
611        println!("\x1b_Gf=100,a=T;{}", encoded);
612    }
613}
614
615/// Renders a raw image buffer (e.g. PNG bytes) using the Sixel graphics protocol.
616#[cfg(feature = "graphics")]
617pub fn print_sixel_logo(image_data: &[u8]) {
618    if let Ok(img) = image::load_from_memory(image_data) {
619        let rgba = img.to_rgba8();
620        let (width, height) = rgba.dimensions();
621        print_sixel_rgba(rgba.as_raw(), width, height);
622    }
623}
624
625/// Renders raw RGBA pixels using the Sixel graphics protocol.
626#[cfg(feature = "graphics")]
627pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
628    use icy_sixel::SixelImage;
629
630    match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
631        Ok(sixel_img) => match sixel_img.encode() {
632            Ok(sixel_str) => {
633                print!("{}", sixel_str);
634            }
635            Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
636        },
637        Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
638    }
639}
640
641/// Placeholder for graphical logo rendering when the `graphics` feature is disabled.
642#[cfg(not(feature = "graphics"))]
643pub fn print_graphical_logo(_image_data: &[u8]) {
644    println!("[Graphical logo support requires --features graphics]");
645}
646
647/// Placeholder for sixel logo rendering when the `graphics` feature is disabled.
648#[cfg(not(feature = "graphics"))]
649pub fn print_sixel_logo(_image_data: &[u8]) {
650    println!("[Sixel logo support requires --features graphics]");
651}
652
653/// Loads an image from a file, resizes it, and prints it using the graphics protocol.
654#[cfg(feature = "graphics")]
655pub fn print_graphical_logo_from_path(path: &std::path::Path) {
656    use image::ImageFormat;
657    match image::open(path) {
658        Ok(img) => {
659            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
660            let mut png_data = Vec::new();
661            if resized
662                .write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
663                .is_ok()
664            {
665                print_graphical_logo(&png_data);
666            } else {
667                println!("[Failed to encode logo as PNG]");
668            }
669        }
670        Err(_) => {
671            println!("[Could not load graphical logo from {}]", path.display());
672        }
673    }
674}
675
676/// Loads an image from a file, resizes it, and prints it using the Sixel protocol.
677#[cfg(feature = "graphics")]
678pub fn print_sixel_logo_from_path(path: &std::path::Path) {
679    match image::open(path) {
680        Ok(img) => {
681            let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
682            let rgba = resized.to_rgba8();
683            let (width, height) = rgba.dimensions();
684            print_sixel_rgba(rgba.as_raw(), width, height);
685        }
686        Err(_) => {
687            println!("[Could not load logo for Sixel from {}]", path.display());
688        }
689    }
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    #[test]
697    fn test_get_ascii_logo_arch() {
698        let logo = get_ascii_logo(Some("arch"));
699        assert!(!logo.is_empty());
700        assert!(logo[0].contains("`"));
701    }
702
703    #[test]
704    fn test_get_ascii_logo_unknown() {
705        let logo = get_ascii_logo(Some("unknown_distro"));
706        assert!(!logo.is_empty());
707        // Should fall back to Tux
708        assert!(logo
709            .iter()
710            .any(|line| line.contains("o${2}_${3}o") || line.contains("o_o")));
711    }
712
713    #[test]
714    fn test_get_ascii_logo_none() {
715        let logo = get_ascii_logo(None);
716        assert!(!logo.is_empty());
717    }
718
719    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
720
721    struct EnvGuard {
722        _mutex_guard: std::sync::MutexGuard<'static, ()>,
723        old_vars: std::collections::HashMap<&'static str, Option<String>>,
724    }
725
726    impl EnvGuard {
727        fn new(vars_to_mock: &[&'static str]) -> Self {
728            let guard = ENV_LOCK.lock().unwrap();
729            let mut old_vars = std::collections::HashMap::new();
730            for var in vars_to_mock {
731                old_vars.insert(*var, std::env::var(var).ok());
732            }
733            EnvGuard {
734                _mutex_guard: guard,
735                old_vars,
736            }
737        }
738    }
739
740    impl Drop for EnvGuard {
741        fn drop(&mut self) {
742            for (var, value) in &self.old_vars {
743                if let Some(val) = value {
744                    std::env::set_var(var, val);
745                } else {
746                    std::env::remove_var(var);
747                }
748            }
749        }
750    }
751
752    #[test]
753    fn test_supports_kitty_heuristics() {
754        let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
755
756        // Test TERM=xterm-kitty
757        std::env::set_var("TERM", "xterm-kitty");
758        std::env::remove_var("TERMINAL_EMULATOR");
759        std::env::remove_var("TERM_PROGRAM");
760        assert!(supports_kitty());
761
762        // Test TERMINAL_EMULATOR=iterm-kitty
763        std::env::remove_var("TERM");
764        std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
765        assert!(supports_kitty());
766
767        // Test TERMINAL_EMULATOR=iTerm.app
768        std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
769        assert!(supports_kitty());
770
771        // Test TERM_PROGRAM=rio
772        std::env::remove_var("TERMINAL_EMULATOR");
773        std::env::set_var("TERM_PROGRAM", "rio");
774        assert!(supports_kitty());
775
776        // Test clear env -> false
777        std::env::remove_var("TERM_PROGRAM");
778        assert!(!supports_kitty());
779    }
780
781    #[test]
782    fn test_supports_iterm2_heuristics() {
783        let _guard = EnvGuard::new(&["TERM_PROGRAM"]);
784
785        // Test TERM_PROGRAM=iTerm.app
786        std::env::set_var("TERM_PROGRAM", "iTerm.app");
787        assert!(supports_iterm2());
788
789        // Test TERM_PROGRAM=WezTerm
790        std::env::set_var("TERM_PROGRAM", "WezTerm");
791        assert!(supports_iterm2());
792
793        // Test TERM_PROGRAM=rio
794        std::env::set_var("TERM_PROGRAM", "rio");
795        assert!(supports_iterm2());
796
797        // Test TERM_PROGRAM=Apple_Terminal
798        std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
799        assert!(!supports_iterm2());
800
801        // Test clear env -> false
802        std::env::remove_var("TERM_PROGRAM");
803        assert!(!supports_iterm2());
804    }
805
806    #[test]
807    fn test_supports_sixel_heuristics() {
808        let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
809
810        // Clear all to start fresh
811        std::env::remove_var("TERM");
812        std::env::remove_var("TERM_PROGRAM");
813        std::env::remove_var("WT_SESSION");
814        assert!(!supports_sixel());
815
816        // Test TERM=xterm-sixel
817        std::env::set_var("TERM", "xterm-sixel");
818        assert!(supports_sixel());
819
820        // Test TERM=foot
821        std::env::set_var("TERM", "foot");
822        assert!(supports_sixel());
823
824        // Test TERM=mlterm (case variations)
825        std::env::set_var("TERM", "MLTerm");
826        assert!(supports_sixel());
827
828        // Reset TERM, test TERM_PROGRAM=WezTerm
829        std::env::remove_var("TERM");
830        std::env::set_var("TERM_PROGRAM", "WezTerm");
831        assert!(supports_sixel());
832
833        // Test TERM_PROGRAM=iTerm.app
834        std::env::set_var("TERM_PROGRAM", "iTerm.app");
835        assert!(supports_sixel());
836
837        // Test TERM_PROGRAM=rio
838        std::env::set_var("TERM_PROGRAM", "rio");
839        assert!(supports_sixel());
840
841        // Reset TERM_PROGRAM, test WT_SESSION
842        std::env::remove_var("TERM_PROGRAM");
843        std::env::set_var("WT_SESSION", "active");
844        assert!(supports_sixel());
845    }
846
847    #[test]
848    fn test_get_embedded_logo() {
849        let logo = get_embedded_logo(Some("arch"));
850        assert!(logo.is_some());
851        let logo = get_embedded_logo(Some("pop"));
852        assert!(logo.is_some());
853        let logo = get_embedded_logo(Some("manjaro"));
854        assert!(logo.is_some());
855        let logo = get_embedded_logo(Some("endeavouros"));
856        assert!(logo.is_some());
857        let logo = get_embedded_logo(Some("opensuse"));
858        assert!(logo.is_some());
859        let logo = get_embedded_logo(Some("opensuse-leap"));
860        assert!(logo.is_some());
861        let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
862        assert!(logo.is_some());
863        let logo = get_embedded_logo(Some("macos"));
864        assert!(logo.is_some());
865        let logo = get_embedded_logo(Some("windows"));
866        assert!(logo.is_some());
867        let logo = get_embedded_logo(None);
868        assert!(logo.is_some());
869    }
870
871    #[test]
872    fn test_get_ascii_logo_new_distros() {
873        let pop = get_ascii_logo(Some("pop"));
874        assert!(!pop.is_empty());
875        assert!(pop.iter().any(|line| line.contains("767")));
876
877        let manjaro = get_ascii_logo(Some("manjaro"));
878        assert!(!manjaro.is_empty());
879        assert!(manjaro.iter().any(|line| line.contains("████████")));
880
881        let endeavouros = get_ascii_logo(Some("endeavouros"));
882        assert!(!endeavouros.is_empty());
883        assert!(endeavouros.iter().any(|line| line.contains("ssso")));
884
885        let opensuse = get_ascii_logo(Some("opensuse"));
886        assert!(!opensuse.is_empty());
887        assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
888
889        let macos = get_ascii_logo(Some("macos"));
890        assert!(!macos.is_empty());
891        assert!(macos
892            .iter()
893            .any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
894
895        let windows = get_ascii_logo(Some("windows"));
896        assert!(!windows.is_empty());
897        assert!(windows
898            .iter()
899            .any(|line| line.contains("AEEEtttt::::ztF") || line.contains("tt:::tt333EE3")));
900    }
901}