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"]),
];
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());
}
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))
}
fn colors_literal(colors: &[String]) -> String {
let inner = colors
.iter()
.map(|c| format!("\"{c}\""))
.collect::<Vec<_>>()
.join(", ");
format!("&[{inner}]")
}
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 {
"#;