mossaic 0.8.1

Draw pixel art in your GitHub contribution graph — a terminal chart, an editor, and a planner that says what today owes
Documentation
//! Embeds every `art/templates/*.art` file into the crate at compile time.
//!
//! The point is that contributing a template is *dropping in a file*. A
//! hand-maintained list in a `.rs` somewhere would work exactly as well until
//! the first contributor forgot to add their line to it, and then the template
//! would be in the repository, reviewed and merged, and invisible to the
//! program — which is a confusing kind of broken to debug.
//!
//! The generated file is a table of `(stem, contents)`. `include_str!` carries
//! the bytes, so nothing is read from disk at run time and an installed binary
//! has its whole built-in catalogue inside it.

use std::path::{Path, PathBuf};
use std::{env, fs};

fn main() {
    let root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("cargo sets this"));
    let dir = root.join("art").join("templates");

    // Re-run when a file is added, removed or edited. The directory itself is
    // listed as well as its contents: without it, *adding* a template does not
    // change any watched path and the catalogue stays stale until something
    // else forces a rebuild.
    println!("cargo:rerun-if-changed={}", dir.display());
    println!("cargo:rerun-if-changed=build.rs");

    let mut found: Vec<(String, PathBuf)> = Vec::new();
    if let Ok(entries) = fs::read_dir(&dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("art") {
                continue;
            }
            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
                continue;
            };
            // The stem is a name someone types after `--template`, so hold it
            // to what a name can be rather than discovering the problem at the
            // command line.
            if !stem
                .chars()
                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
                || stem.is_empty()
            {
                panic!(
                    "{}: a template file name must be lowercase letters, digits and dashes — \
                     it is what you type after --template",
                    path.display()
                );
            }
            println!("cargo:rerun-if-changed={}", path.display());
            found.push((stem.to_string(), path));
        }
    }
    // Sorted, so the catalogue does not depend on the order the filesystem
    // happens to hand back — otherwise `--list-templates` reorders itself
    // between machines and every test of it is flaky somewhere else.
    found.sort_by(|a, b| a.0.cmp(&b.0));

    let mut out = String::from(
        "// Generated by build.rs. Add a template by dropping a .art file in\n\
         // art/templates/ — nothing here is edited by hand.\n\
         pub(crate) static BUILTIN: &[(&str, &str)] = &[\n",
    );
    for (stem, path) in &found {
        out.push_str(&format!(
            "    ({stem:?}, include_str!({:?})),\n",
            path.to_str().expect("a UTF-8 path")
        ));
    }
    out.push_str("];\n");

    let dest = Path::new(&env::var_os("OUT_DIR").expect("cargo sets this")).join("templates.rs");
    fs::write(&dest, out).expect("write the generated catalogue");
}