purefetch 0.1.10

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:  `COLORS: <sgr> <sgr> ...`  (space-separated SGR params, one per color)
//!   line 2..: the ASCII art; `$1`..`$9` select the Nth color (any text before the
//!             first marker uses the first color). A literal `$` is any `$` not
//!             followed by a digit.
//!
//! 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"]),
];

/// One logo's data collected from its `.txt`, ready to emit into `logo.rs`.
struct Entry {
    const_name: String,
    colors: Vec<String>,
    art: Vec<String>,
    aliases: &'static [&'static str],
}

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

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

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

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

    let tux_colors = entries
        .iter()
        .find(|e| e.const_name == "TUX")
        .map(|e| colors_literal(&e.colors))
        .unwrap_or_else(|| "&[\"38;2;236;236;236\"]".to_string());
    let _ = writeln!(out, "const TUX_COLORS: &[&str] = {tux_colors};\n");

    for e in &entries {
        let _ = writeln!(out, "const {}: &[&str] = &[", e.const_name);
        for row in &e.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 `COLORS:` line (space-separated SGR params) plus the
/// art rows (trailing blank rows trimmed).
fn read_logo(path: &str) -> Option<(Vec<String>, Vec<String>)> {
    let text = fs::read_to_string(path).ok()?;
    let mut colors: Vec<String> = Vec::new();
    let mut art: Vec<String> = Vec::new();
    for line in text.split('\n') {
        if let Some(c) = line.strip_prefix("COLORS:") {
            colors = c.split_whitespace().map(str::to_string).collect();
        } else {
            art.push(line.trim_end().to_string());
        }
    }
    while art.last().is_some_and(|s| s.is_empty()) {
        art.pop();
    }
    Some((colors, art))
}

/// Render a color list as a Rust `&[&str]` literal, e.g. `&["31", "37"]`.
fn colors_literal(colors: &[String]) -> String {
    let inner = colors
        .iter()
        .map(|c| format!("\"{c}\""))
        .collect::<Vec<_>>()
        .join(", ");
    format!("&[{inner}]")
}

/// 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.
//!
//! The distro logo art is from neofetch and fastfetch (both MIT); see CREDITS.md.

pub struct Logo {
    pub lines: &'static [&'static str],
    /// SGR params for the logo's colors. The art selects them with `$1`..`$9`
    /// markers (any text before the first marker uses the first color); resolved
    /// in `main`. A literal `$` in the art is any `$` not followed by a digit.
    pub colors: &'static [&'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, colors: TUX_COLORS }),
    )
}

/// 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 {
"#;