use std::fmt::Write as _;
use std::fs;
use std::process::Command;
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());
}
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))
}
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}")
}
}
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 {
"#;