fresh-editor 0.2.11

A lightweight, fast terminal-based text editor with LSP support and TypeScript plugins
Documentation
//! Build script for Fresh editor
//!
//! Generates TypeScript type definitions from Rust op definitions.
//! JSON Schema for configuration is now generated via `cargo run --features dev-bins --bin generate_schema`.

use std::fs;
use std::path::Path;

fn main() {
    // Rerun if locales change
    println!("cargo::rerun-if-changed=locales");

    // Rerun if themes change
    println!("cargo::rerun-if-changed=themes");

    // On Windows, embed the application icon into the .exe
    #[cfg(target_os = "windows")]
    {
        let ico_path = Path::new("../../docs/icons/windows/app.ico");
        if ico_path.exists() {
            let mut res = winresource::WindowsResource::new();
            res.set_icon(ico_path.to_str().unwrap());
            if let Err(e) = res.compile() {
                eprintln!("Warning: Failed to embed Windows icon: {}", e);
            }
        }
    }

    // Always generate locale_options.rs - it's required by config.rs at compile time
    // This must run even during publish verification since the include!() macro needs it
    if let Err(e) = generate_locale_options() {
        eprintln!("Warning: Failed to generate locale options: {}", e);
    }

    // Always generate builtin_themes.rs - embeds all themes from themes/ directory
    if let Err(e) = generate_builtin_themes() {
        eprintln!("Warning: Failed to generate builtin themes: {}", e);
    }

    // Generate plugins content hash for cache invalidation
    #[cfg(feature = "embed-plugins")]
    {
        println!("cargo::rerun-if-changed=plugins");
        if let Err(e) = generate_plugins_hash() {
            eprintln!("Warning: Failed to generate plugins hash: {}", e);
        }
    }
}

/// Generate a hash of all plugin files for cache invalidation
#[cfg(feature = "embed-plugins")]
fn generate_plugins_hash() -> Result<(), Box<dyn std::error::Error>> {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::Hasher;

    let plugins_dir = Path::new("plugins");
    let mut hasher = DefaultHasher::new();

    // Hash all files in the plugins directory recursively
    hash_directory(plugins_dir, &mut hasher)?;

    let hash = format!("{:016x}", hasher.finish());

    let out_dir = std::env::var("OUT_DIR")?;
    let dest_path = Path::new(&out_dir).join("plugins_hash.txt");
    fs::write(&dest_path, &hash)?;

    println!("cargo::warning=Generated plugins hash: {}", hash);
    Ok(())
}

#[cfg(feature = "embed-plugins")]
fn hash_directory(dir: &Path, hasher: &mut impl std::hash::Hasher) -> std::io::Result<()> {
    use std::hash::Hash;

    if !dir.exists() {
        return Ok(());
    }

    let mut entries: Vec<_> = fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
    // Sort for deterministic ordering
    entries.sort_by_key(|e| e.path());

    for entry in entries {
        let path = entry.path();
        // Hash the relative path
        path.strip_prefix("plugins").unwrap_or(&path).hash(hasher);

        if path.is_dir() {
            hash_directory(&path, hasher)?;
        } else {
            // Hash file contents
            let contents = fs::read(&path)?;
            contents.hash(hasher);
        }
    }

    Ok(())
}

/// Generate a Rust file with all theme files embedded from themes/ directory recursively
fn generate_builtin_themes() -> Result<(), Box<dyn std::error::Error>> {
    let themes_dir = Path::new("themes");
    let mut themes: Vec<(String, String, String)> = Vec::new(); // (name, pack, relative_path)

    // Recursively collect all .json files
    collect_theme_files(themes_dir, "", &mut themes)?;

    // Sort by pack then name for consistent output
    themes.sort_by(|a, b| (&a.1, &a.0).cmp(&(&b.1, &b.0)));

    // Generate Rust code
    let out_dir = std::env::var("OUT_DIR")?;
    let dest_path = Path::new(&out_dir).join("builtin_themes.rs");

    let theme_entries: Vec<String> = themes
        .iter()
        .map(|(name, pack, rel_path)| {
            format!(
                r#"    BuiltinTheme {{
        name: "{}",
        pack: "{}",
        json: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/themes/{}")),
    }}"#,
                name, pack, rel_path
            )
        })
        .collect();

    let content = format!(
        r#"// Auto-generated by build.rs from themes/**/*.json files
// DO NOT EDIT MANUALLY

/// All builtin themes embedded at compile time.
pub const BUILTIN_THEMES: &[BuiltinTheme] = &[
{}
];
"#,
        theme_entries.join(",\n")
    );

    fs::write(&dest_path, content)?;

    println!("cargo::warning=Generated {} builtin themes", themes.len());

    Ok(())
}

/// Recursively collect theme files from a directory
fn collect_theme_files(
    dir: &Path,
    pack: &str,
    themes: &mut Vec<(String, String, String)>,
) -> std::io::Result<()> {
    if !dir.exists() {
        return Ok(());
    }

    let mut entries: Vec<_> = fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
    // Sort for deterministic ordering
    entries.sort_by_key(|e| e.path());

    for entry in entries {
        let path = entry.path();

        if path.is_dir() {
            // Recurse into subdirectory, updating pack name
            let dir_name = path.file_name().unwrap().to_string_lossy();
            let new_pack = if pack.is_empty() {
                dir_name.to_string()
            } else {
                format!("{}/{}", pack, dir_name)
            };
            collect_theme_files(&path, &new_pack, themes)?;
        } else if path.extension().is_some_and(|ext| ext == "json") {
            // Found a theme file
            let name = path.file_stem().unwrap().to_string_lossy().to_string();
            // Build path with forward slashes for cross-platform include_str!
            let rel_path = path
                .strip_prefix("themes")
                .unwrap()
                .components()
                .map(|c| c.as_os_str().to_string_lossy())
                .collect::<Vec<_>>()
                .join("/");
            themes.push((name, pack.to_string(), rel_path));
        }
    }

    Ok(())
}

/// Generate a Rust file with the list of available locales from the locales directory
fn generate_locale_options() -> Result<(), Box<dyn std::error::Error>> {
    let locales_dir = Path::new("locales");

    // Read all .json files in the locales directory
    let mut locales: Vec<String> = fs::read_dir(locales_dir)?
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let path = entry.path();
            if path.extension()? == "json" {
                path.file_stem()?.to_str().map(|s| s.to_string())
            } else {
                None
            }
        })
        .collect();

    // Sort alphabetically for consistent output
    locales.sort();

    // Generate Rust code
    let out_dir = std::env::var("OUT_DIR")?;
    let dest_path = Path::new(&out_dir).join("locale_options.rs");

    let locale_entries: Vec<String> = locales.iter().map(|l| format!("Some(\"{}\")", l)).collect();

    let content = format!(
        r#"// Auto-generated by build.rs from locales/*.json files
// DO NOT EDIT MANUALLY

/// Available locale options for the settings dropdown
/// None (null) means auto-detect from environment
pub const GENERATED_LOCALE_OPTIONS: &[Option<&str>] = &[
    None, // Auto-detect
    {}
];
"#,
        locale_entries.join(",\n    ")
    );

    // Note: OUT_DIR files don't need write_if_changed since cargo handles them specially,
    // but it doesn't hurt to use it for consistency
    fs::write(&dest_path, content)?;

    println!(
        "cargo::warning=Generated locale options with {} locales",
        locales.len()
    );

    Ok(())
}