purefetch 0.1.8

A fast, fastfetch-style system information tool written entirely in Rust with zero dependencies
//! Generator for `src/logo.rs` from the ASCII-art files in `assets/logos/`.
//!
//! Each `assets/logos/<name>.txt` is:
//!   line 1:  `COLOR: R;G;B`   (the logo's truecolor RGB)
//!   line 2..: the ASCII art, one row per line
//!
//! Add or edit a logo by changing the `.txt` file, then run:
//!     cargo run --example genlogos
//!
//! std only. Shells out to `rustfmt` to canonicalize the output when available.

use std::fmt::Write as _;
use std::fs;
use std::process::Command;

// Display order and the name aliases each logo answers to (first is canonical).
const ORDER: &[(&str, &[&str])] = &[
    ("debian", &["debian"]),
    ("arch", &["arch"]),
    ("ubuntu", &["ubuntu"]),
    ("fedora", &["fedora"]),
    ("mint", &["mint"]),
    ("manjaro", &["manjaro"]),
    ("pop", &["pop"]),
    ("opensuse", &["opensuse"]),
    ("alpine", &["alpine"]),
    ("void", &["void"]),
    ("nixos", &["nixos"]),
    ("gentoo", &["gentoo"]),
    ("endeavouros", &["endeavouros", "endeavour"]),
    ("kali", &["kali"]),
    ("elementary", &["elementary"]),
    ("zorin", &["zorin"]),
    ("artix", &["artix"]),
    ("rocky", &["rocky"]),
    ("almalinux", &["almalinux", "alma"]),
    ("centos", &["centos"]),
    ("devuan", &["devuan"]),
    ("mx", &["mx"]),
    ("garuda", &["garuda"]),
    ("tux", &["tux", "linux", "generic"]),
];

fn main() {
    let root = std::env::args().nth(1).unwrap_or_else(|| ".".to_string());

    let mut entries: Vec<(String, String, Vec<String>, &[&str])> = Vec::new();
    for &(name, aliases) in ORDER {
        let path = format!("{root}/assets/logos/{name}.txt");
        let Some((color, art)) = read_logo(&path) else {
            eprintln!("WARN: missing {name}.txt, skipping");
            continue;
        };
        entries.push((name.to_ascii_uppercase(), color, art, aliases));
    }

    let mut out = String::from(HEADER);

    for (const_name, color, _art, aliases) in &entries {
        let pat = aliases
            .iter()
            .map(|a| format!("\"{a}\""))
            .collect::<Vec<_>>()
            .join(" | ");
        if const_name == "TUX" {
            let _ = writeln!(out, "        {pat} => Logo {{ lines: TUX, sgr: TUX_SGR }},");
        } else {
            let _ = writeln!(
                out,
                "        {pat} => Logo {{ lines: {const_name}, sgr: \"{}\" }},",
                to_sgr(color)
            );
        }
    }
    out.push_str("        _ => return None,\n    })\n}\n\n");

    let tux_color = entries
        .iter()
        .find(|(n, _, _, _)| n == "TUX")
        .map(|(_, c, _, _)| c.clone())
        .unwrap_or_else(|| "236;236;236".to_string());
    let _ = writeln!(out, "const TUX_SGR: &str = \"{}\";\n", to_sgr(&tux_color));

    for (const_name, _color, art, _aliases) in &entries {
        let _ = writeln!(out, "const {const_name}: &[&str] = &[");
        for row in art {
            let _ = writeln!(out, "    {},", rust_str(row));
        }
        out.push_str("];\n\n");
    }

    let dst = format!("{root}/src/logo.rs");
    let text = format!("{}\n", out.trim_end());
    fs::write(&dst, text).expect("write src/logo.rs");
    let _ = Command::new("rustfmt").arg(&dst).status();
    println!("wrote src/logo.rs: {} logos", entries.len());
}

/// Parse a `<name>.txt`: the `COLOR:` line plus the art rows (trailing blank
/// rows trimmed).
fn read_logo(path: &str) -> Option<(String, Vec<String>)> {
    let text = fs::read_to_string(path).ok()?;
    let mut color = String::new();
    let mut art: Vec<String> = Vec::new();
    for line in text.split('\n') {
        if let Some(c) = line.strip_prefix("COLOR:") {
            color = c.trim().to_string();
        } else {
            art.push(line.trim_end().to_string());
        }
    }
    while art.last().is_some_and(|s| s.is_empty()) {
        art.pop();
    }
    Some((color, art))
}

/// Wrap an `R;G;B` triplet as a truecolor foreground SGR (`38;2;R;G;B`);
/// pass through anything that already looks like SGR params.
fn to_sgr(color: &str) -> String {
    let color = color.trim();
    if color.starts_with("38;") || color.starts_with("1;") {
        color.to_string()
    } else {
        format!("38;2;{color}")
    }
}

/// Escape a line as a Rust string literal.
fn rust_str(s: &str) -> String {
    format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}

const HEADER: &str = r#"//! Distro ASCII logos and selection.
//!
//! GENERATED by examples/genlogos.rs from assets/logos/*.txt.
//! Edit the art files and re-run `cargo run --example genlogos`; do not edit by hand.

pub struct Logo {
    pub lines: &'static [&'static str],
    pub sgr: &'static str,
}

/// Resolve a logo selector ("auto", "debian", "none", ...) to a logo.
/// A known name wins; an unknown *explicit* name falls back to the detected
/// distro (matching fastfetch), and finally to the generic Tux logo.
pub fn get(selector: &str) -> Option<Logo> {
    let sel = selector.to_ascii_lowercase();
    if sel == "none" || sel == "off" {
        return None;
    }
    let name = if sel == "auto" { detect_distro() } else { sel };
    Some(
        known(&name)
            .or_else(|| known(&detect_distro()))
            .unwrap_or(Logo { lines: TUX, sgr: TUX_SGR }),
    )
}

/// The `ID` from /etc/os-release, normalized to a known logo name.
fn detect_distro() -> String {
    let id = std::fs::read_to_string("/etc/os-release")
        .ok()
        .and_then(|s| {
            s.lines().find_map(|l| {
                l.strip_prefix("ID=")
                    .map(|v| v.trim().trim_matches('"').to_ascii_lowercase())
            })
        })
        .unwrap_or_default();
    normalize(&id)
}

/// Map os-release IDs to the logo names we ship.
fn normalize(id: &str) -> String {
    if id.starts_with("opensuse") {
        return "opensuse".to_string();
    }
    match id {
        "linuxmint" => "mint",
        "raspbian" | "raspberry-pi-os" => "debian",
        "popos" => "pop",
        "" => "tux",
        other => other,
    }
    .to_string()
}

/// A logo for a known name/alias, or None if unrecognized.
fn known(name: &str) -> Option<Logo> {
    Some(match name {
"#;