purefetch 0.1.2

A fast, fastfetch-style system information tool written entirely in Rust with zero dependencies
#!/usr/bin/env python3
"""Generate src/logo.rs from the ASCII-art files in assets/logos/.

Each assets/logos/<name>.txt is:
    line 1: "COLOR: R;G;B"   (ANSI truecolor triplet for the logo)
    line 2..: the ASCII art, one row per line

To add or edit a distro logo: change the .txt file and re-run this script.
No external dependencies.

    python3 scripts/genlogos.py            # run from the repo root

On first run it bootstraps assets/logos/{debian,tux}.txt from the existing
src/logo.rs so the hand-made Debian and Tux art is preserved.
"""
import os
import re
import subprocess
import sys

# Display order and the name aliases each logo answers to (first is canonical).
ORDER = [
    ("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"]),
]


def extract_array(src, const_name):
    """Pull the string contents of a `const NAME: &[&str] = &[ ... ];` block."""
    m = re.search(r"const " + const_name + r": &\[&str\] = &\[(.*?)\];", src, re.S)
    if not m:
        return None
    lines = []
    for ln in m.group(1).splitlines():
        mm = re.match(r'\s*r#"(.*)"#,\s*$', ln)
        if mm:
            lines.append(mm.group(1))
    return lines


def bootstrap(root):
    """Create debian.txt / tux.txt from the current src/logo.rs if missing."""
    logos = os.path.join(root, "assets", "logos")
    os.makedirs(logos, exist_ok=True)
    seed = {"debian": "215;7;81", "tux": "236;236;236"}
    src_path = os.path.join(root, "src", "logo.rs")
    if not os.path.exists(src_path):
        return
    src = open(src_path).read()
    for name, color in seed.items():
        path = os.path.join(logos, name + ".txt")
        if os.path.exists(path):
            continue
        art = extract_array(src, name.upper())
        if not art:
            continue
        with open(path, "w") as f:
            f.write("COLOR: %s\n" % color)
            f.write("\n".join(art) + "\n")
        print("bootstrapped %s.txt" % name)


def read_logo(path):
    lines = open(path).read().split("\n")
    color = ""
    art = []
    for ln in lines:
        if ln.startswith("COLOR:"):
            color = ln.split(":", 1)[1].strip()
        else:
            art.append(ln.rstrip())
    # drop trailing empty rows
    while art and art[-1] == "":
        art.pop()
    return color, art


def rust_str(s):
    return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'


def to_sgr(color):
    # Color files store an "R;G;B" triplet; wrap it as a truecolor fg SGR
    # (ESC[38;2;R;G;Bm). Pass through anything that already looks like SGR params.
    color = color.strip()
    if color.startswith("38;") or color.startswith("1;"):
        return color
    return "38;2;" + color


def main():
    root = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
    bootstrap(root)
    logos = os.path.join(root, "assets", "logos")

    entries = []  # (const_name, color, art, aliases)
    for name, aliases in ORDER:
        path = os.path.join(logos, name + ".txt")
        if not os.path.exists(path):
            print("WARN: missing %s.txt, skipping" % name, file=sys.stderr)
            continue
        color, art = read_logo(path)
        entries.append((name.upper(), color, art, aliases))

    out = []
    out.append("//! Distro ASCII logos and selection.")
    out.append("//!")
    out.append("//! GENERATED by scripts/genlogos.py from assets/logos/*.txt.")
    out.append("//! Edit the art files and re-run the script; do not edit this file by hand.")
    out.append("")
    out.append("pub struct Logo {")
    out.append("    pub lines: &'static [&'static str],")
    out.append("    pub sgr: &'static str,")
    out.append("}")
    out.append("")
    out.append("/// Resolve a logo selector (\"auto\", \"debian\", \"none\", ...) to a logo.")
    out.append("/// A known name wins; an unknown *explicit* name falls back to the detected")
    out.append("/// distro (matching fastfetch), and finally to the generic Tux logo.")
    out.append("pub fn get(selector: &str) -> Option<Logo> {")
    out.append("    let sel = selector.to_ascii_lowercase();")
    out.append('    if sel == "none" || sel == "off" {')
    out.append("        return None;")
    out.append("    }")
    out.append('    let name = if sel == "auto" { detect_distro() } else { sel };')
    out.append("    Some(")
    out.append("        known(&name)")
    out.append("            .or_else(|| known(&detect_distro()))")
    out.append("            .unwrap_or(Logo { lines: TUX, sgr: TUX_SGR }),")
    out.append("    )")
    out.append("}")
    out.append("")
    out.append("/// The `ID` from /etc/os-release, normalized to a known logo name.")
    out.append("fn detect_distro() -> String {")
    out.append('    let id = std::fs::read_to_string("/etc/os-release")')
    out.append("        .ok()")
    out.append("        .and_then(|s| {")
    out.append("            s.lines().find_map(|l| {")
    out.append('                l.strip_prefix("ID=")')
    out.append('                    .map(|v| v.trim().trim_matches(\'"\').to_ascii_lowercase())')
    out.append("            })")
    out.append("        })")
    out.append("        .unwrap_or_default();")
    out.append("    normalize(&id)")
    out.append("}")
    out.append("")
    out.append("/// Map os-release IDs to the logo names we ship.")
    out.append("fn normalize(id: &str) -> String {")
    out.append('    if id.starts_with("opensuse") {')
    out.append('        return "opensuse".to_string();')
    out.append("    }")
    out.append("    match id {")
    out.append('        "linuxmint" => "mint",')
    out.append('        "raspbian" | "raspberry-pi-os" => "debian",')
    out.append('        "popos" => "pop",')
    out.append('        "" => "tux",')
    out.append("        other => other,")
    out.append("    }")
    out.append("    .to_string()")
    out.append("}")
    out.append("")
    out.append("/// A logo for a known name/alias, or None if unrecognized.")
    out.append("fn known(name: &str) -> Option<Logo> {")
    out.append("    Some(match name {")
    for const_name, color, _art, aliases in entries:
        pat = " | ".join('"%s"' % a for a in aliases)
        if const_name == "TUX":
            out.append("        %s => Logo { lines: TUX, sgr: TUX_SGR }," % pat)
        else:
            out.append(
                '        %s => Logo { lines: %s, sgr: "%s" },'
                % (pat, const_name, to_sgr(color))
            )
    out.append("        _ => return None,")
    out.append("    })")
    out.append("}")
    out.append("")
    # TUX color as a named const (used by both the match arm and the fallback).
    tux_color = next((c for n, c, _a, _al in entries if n == "TUX"), "236;236;236")
    out.append('const TUX_SGR: &str = "%s";' % to_sgr(tux_color))
    out.append("")
    for const_name, _color, art, _aliases in entries:
        out.append("const %s: &[&str] = &[" % const_name)
        for row in art:
            out.append("    %s," % rust_str(row))
        out.append("];")
        out.append("")

    text = "\n".join(out).rstrip("\n") + "\n"
    dst = os.path.join(root, "src", "logo.rs")
    with open(dst, "w") as f:
        f.write(text)
    # Canonicalize with rustfmt when it is available on PATH.
    try:
        subprocess.run(["rustfmt", dst], check=False)
    except FileNotFoundError:
        pass
    print("wrote src/logo.rs: %d logos" % len(entries))


if __name__ == "__main__":
    main()