import os
import re
import subprocess
import sys
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):
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):
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())
while art and art[-1] == "":
art.pop()
return color, art
def rust_str(s):
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
def to_sgr(color):
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 = [] 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 = 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)
try:
subprocess.run(["rustfmt", dst], check=False)
except FileNotFoundError:
pass
print("wrote src/logo.rs: %d logos" % len(entries))
if __name__ == "__main__":
main()