liora-icons-ionic 0.1.20

Bundled Ionicons icon paths rendered through Liora native GPUI icon primitives.
use std::env;
use std::fs;
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};

fn main() {
    if let Err(error) = try_main() {
        println!("cargo:error=failed to generate icon bindings: {error}");
        std::process::exit(1);
    }
}

fn try_main() -> io::Result<()> {
    let out_dir = env::var_os("OUT_DIR")
        .map(PathBuf::from)
        .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "OUT_DIR is not set"))?;
    let svg_dir = Path::new("assets/svgs");

    if !svg_dir.is_dir() || fs::read_dir(svg_dir).map_or(true, |mut d| d.next().is_none()) {
        println!("cargo:warning=No SVG icons found. Run the crate sync script.");
    }

    let mut entries = Vec::new();
    if svg_dir.is_dir() {
        for entry in fs::read_dir(svg_dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.extension().is_some_and(|e| e == "svg") {
                let stem = path_component_to_str(path.file_stem(), &path, "file stem")?;
                let file_name = path_component_to_str(path.file_name(), &path, "file name")?;
                entries.push((to_pascal_case(stem), file_name.to_string()));
            }
        }
    }
    entries.sort_by(|a, b| a.0.cmp(&b.0));

    let generated = out_dir.join("generated.rs");
    let mut f = fs::File::create(&generated)?;
    writeln!(f, "// Auto-generated — {} icons", entries.len())?;
    writeln!(f, "/// Strongly typed names for bundled SVG icon assets.")?;
    writeln!(f, "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]")?;
    writeln!(f, "pub enum IconName {{")?;
    for (variant, file) in &entries {
        let kebab = file.strip_suffix(".svg").unwrap_or(file);
        writeln!(f, "    /// The `{}` icon asset.", kebab)?;
        writeln!(f, "    {},", variant)?;
    }
    writeln!(f, "}}")?;
    writeln!(f, "impl IconName {{")?;
    writeln!(f, "    /// Returns every icon in generated enum order.")?;
    writeln!(f, "    pub const fn all() -> &'static [IconName] {{")?;
    writeln!(f, "        &[")?;
    for (variant, _) in &entries {
        writeln!(f, "            IconName::{},", variant)?;
    }
    writeln!(f, "        ]")?;
    writeln!(f, "    }}")?;
    writeln!(
        f,
        "    /// Returns the bundled SVG file name for this icon."
    )?;
    writeln!(f, "    pub fn file(&self) -> &'static str {{")?;
    writeln!(f, "        match self {{")?;
    for (variant, file) in &entries {
        writeln!(f, "            IconName::{} => {:?},", variant, file)?;
    }
    writeln!(f, "        }}")?;
    writeln!(f, "    }}")?;
    writeln!(f, "    /// Returns the embedded SVG source for this icon.")?;
    writeln!(f, "    pub fn svg_source(&self) -> &'static str {{")?;
    writeln!(f, "        match self {{")?;
    for (variant, file) in &entries {
        writeln!(
            f,
            "            IconName::{} => include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/assets/svgs/{}\")),",
            variant, file
        )?;
    }
    writeln!(f, "        }}")?;
    writeln!(f, "    }}")?;
    writeln!(f, "}}")?;
    println!("cargo:rerun-if-changed=assets/svgs/");
    Ok(())
}

fn path_component_to_str<'a>(
    component: Option<&'a std::ffi::OsStr>,
    path: &Path,
    label: &str,
) -> io::Result<&'a str> {
    component
        .and_then(|component| component.to_str())
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidData,
                format!("icon SVG {} is not valid UTF-8: {}", label, path.display()),
            )
        })
}

fn to_pascal_case(value: &str) -> String {
    let mut out = String::new();
    for part in value.split(|ch: char| !ch.is_ascii_alphanumeric()) {
        if part.is_empty() {
            continue;
        }
        let mut chars = part.chars();
        if let Some(first) = chars.next() {
            out.extend(first.to_uppercase());
            out.push_str(chars.as_str());
        }
    }
    if out.is_empty() {
        out.push_str("Icon");
    }
    if out.as_bytes().first().is_some_and(u8::is_ascii_digit) {
        out.insert(0, 'I');
    }
    out
}