rusteal 0.6.0

Rust bindings for Unreal Engine 5.8.2
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Default)]
pub struct GlobalConfig {
    #[serde(default)]
    pub engine: EngineSection,
}

#[derive(Deserialize, Serialize, Default)]
pub struct EngineSection {
    #[serde(default)]
    pub path: String,
}

pub fn path() -> PathBuf {
    let base = dirs::config_dir().unwrap_or_else(|| {
        eprintln!("Error: this system has no configuration directory.");
        std::process::exit(1);
    });

    base.join("rusteal").join("config.toml")
}

pub fn load() -> GlobalConfig {
    let path = path();

    let Ok(text) = std::fs::read_to_string(&path) else {
        return GlobalConfig::default();
    };

    toml::from_str(&text).unwrap_or_else(|e| {
        eprintln!("Error: cannot parse {}: {e}", path.display());
        std::process::exit(1);
    })
}

pub fn save(config: &GlobalConfig) {
    let path = path();

    if let Some(dir) = path.parent() {
        std::fs::create_dir_all(dir)
            .unwrap_or_else(|e| panic!("Failed to create {}: {e}", dir.display()));
    }

    let text = toml::to_string(config).expect("serializable config");

    std::fs::write(&path, text)
        .unwrap_or_else(|e| panic!("Failed to write {}: {e}", path.display()));
}

pub fn engine_path() -> PathBuf {
    let mut config = load();

    if config.engine.path.trim().is_empty() {
        config.engine.path = ask_engine_path();
        save(&config);
        eprintln!("  Saved to {}", path().display());
    }

    let engine = PathBuf::from(&config.engine.path);

    if !is_engine_root(&engine) {
        eprintln!(
            "Error: {} is not an Unreal Engine root (no Engine/Build/BatchFiles).",
            engine.display()
        );

        eprintln!("  Fix [engine].path in {}.", path().display());
        std::process::exit(1);
    }

    engine
}

fn ask_engine_path() -> String {
    if !std::io::stdin().is_terminal() {
        eprintln!("Error: no engine configured.");

        eprintln!(
            "  Set [engine].path in {} to your Unreal Engine root.",
            path().display()
        );

        std::process::exit(1);
    }

    loop {
        eprint!("Unreal Engine root (the directory with Engine/ and Templates/): ");
        std::io::stderr().flush().ok();
        let mut line = String::new();

        if std::io::stdin().read_line(&mut line).unwrap_or(0) == 0 {
            std::process::exit(1);
        }

        let answer = line.trim().trim_end_matches(['/', '\\']).to_string();

        if is_engine_root(Path::new(&answer)) {
            return answer;
        }

        eprintln!("  {answer} has no Engine/Build/BatchFiles; try again.");
    }
}

fn is_engine_root(dir: &Path) -> bool {
    dir.join("Engine/Build/BatchFiles").is_dir()
}