android-rust-cli 0.3.2

A CLI tool for generating Android Rust JNI templates in a single command
use crate::error::{CliError, Result};
use crate::template::{
    TEMPLATE_DIR, TemplateContext, TemplateSource, list_templates_embedded, list_templates_fs,
    render_embedded_dir, render_fs_dir,
};
use crate::ui;
use crate::validation::validate_package_name;
use console::style;
use dialoguer::{Input, Select};
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::time::Duration;

const DEFAULT_PACKAGE: &str = "dev.rodroid.rust";
const DEFAULT_PROJECT_NAME: &str = "my-project";

pub fn new_project(
    name: Option<PathBuf>,
    template: Option<String>,
    template_path: Option<PathBuf>,
    package_name: Option<String>,
    force: bool,
    dry_run: bool,
) -> Result<()> {
    let out_dir = match name {
        Some(n) => n,
        None => PathBuf::from(resolve_project_name()?),
    };
    create_project(
        out_dir,
        template,
        template_path,
        package_name,
        force,
        dry_run,
    )
}

pub fn init_project(
    template: Option<String>,
    template_path: Option<PathBuf>,
    package_name: Option<String>,
    force: bool,
    dry_run: bool,
) -> Result<()> {
    let out_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    create_project(
        out_dir,
        template,
        template_path,
        package_name,
        force,
        dry_run,
    )
}

fn create_project(
    out_dir: PathBuf,
    template: Option<String>,
    template_path: Option<PathBuf>,
    package_name: Option<String>,
    force: bool,
    dry_run: bool,
) -> Result<()> {
    ui::print_banner();

    if !dry_run {
        prepare_output_dir(&out_dir, force)?;
    }

    let template_source = resolve_template(template, template_path)?;
    let package_name = resolve_package_name(package_name)?;
    let project_name = out_dir
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "my-project".to_string());
    let context = TemplateContext::new(package_name.clone(), project_name);

    if dry_run {
        ui::print_dry_run_header();
        println!();
        ui::print_key_value("Directory", &out_dir.display().to_string());
        ui::print_key_value("Template", template_source.name());
        ui::print_key_value("Package", &package_name);
        println!();
        return Ok(());
    }

    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("  {spinner:.cyan} {msg}")
            .unwrap()
            .tick_strings(&["", "", "", "", "", "", "", "", "", ""]),
    );
    pb.enable_steady_tick(Duration::from_millis(80));
    pb.set_message(format!(
        "Creating project with {} template...",
        style(template_source.name()).cyan()
    ));

    match template_source {
        TemplateSource::Embedded(name) => {
            let dir = TEMPLATE_DIR
                .get_dir(&name)
                .ok_or_else(|| CliError::TemplateNotFound {
                    name: name.clone(),
                    available: list_templates_embedded(),
                })?;
            render_embedded_dir(dir, dir.path(), &out_dir, &context, force)?;
        }
        TemplateSource::External { root, name } => {
            let src = root.join(&name);
            render_fs_dir(&src, &out_dir, &context, force)?;
        }
    }

    pb.finish_and_clear();

    let canonical_path = out_dir.canonicalize().unwrap_or(out_dir.clone());
    let display_path = canonical_path.to_string_lossy().replace(r"\\?\", ""); // Fix Windows prefix

    ui::print_completion_message(&display_path, &package_name);

    Ok(())
}

fn prepare_output_dir(out_dir: &PathBuf, force: bool) -> Result<()> {
    if out_dir.exists() {
        if !out_dir.is_dir() {
            return Err(CliError::TargetNotDirectory(out_dir.clone()));
        }
        if !force && !is_dir_empty(out_dir)? {
            return Err(CliError::DirectoryNotEmpty(out_dir.clone()));
        }
    } else {
        fs::create_dir_all(out_dir)?;
    }
    Ok(())
}

fn resolve_template(
    template: Option<String>,
    template_path: Option<PathBuf>,
) -> Result<TemplateSource> {
    if let Some(path) = template_path {
        let names = list_templates_fs(&path)?;
        let name = pick_template_name(template, &names)?;
        return Ok(TemplateSource::External { root: path, name });
    }

    let names = list_templates_embedded();
    let name = pick_template_name(template, &names)?;
    Ok(TemplateSource::Embedded(name))
}

fn pick_template_name(template: Option<String>, names: &[String]) -> Result<String> {
    if let Some(name) = template {
        if !names.iter().any(|t| t == &name) {
            return Err(CliError::TemplateNotFound {
                name,
                available: names.to_vec(),
            });
        }
        return Ok(name);
    }

    if names.is_empty() {
        return Err(CliError::NoTemplatesFound);
    }

    if names.len() == 1 {
        return Ok(names[0].clone());
    }

    if std::io::stdin().is_terminal() {
        println!();
        let selection = Select::new()
            .with_prompt(format!("  {} Select a template", style("?").cyan().bold()))
            .items(names)
            .default(0)
            .interact()
            .map_err(|e| CliError::Other(e.into()))?;
        return Ok(names[selection].clone());
    }

    if names.iter().any(|t| t == "standard") {
        return Ok("standard".to_string());
    }

    Err(CliError::AmbiguousTemplate)
}

fn resolve_package_name(package_name: Option<String>) -> Result<String> {
    if let Some(name) = package_name {
        validate_package_name(&name)?;
        return Ok(name);
    }

    if std::io::stdin().is_terminal() {
        println!();
        let name: String = Input::new()
            .with_prompt(format!("  {} Package name", style("?").cyan().bold()))
            .default(DEFAULT_PACKAGE.to_string())
            .interact_text()
            .map_err(|e| CliError::Other(e.into()))?;
        validate_package_name(&name)?;
        return Ok(name);
    }

    Ok(DEFAULT_PACKAGE.to_string())
}

fn resolve_project_name() -> Result<String> {
    if std::io::stdin().is_terminal() {
        println!();
        let name: String = Input::new()
            .with_prompt(format!("  {} Project name", style("?").cyan().bold()))
            .default(DEFAULT_PROJECT_NAME.to_string())
            .interact_text()
            .map_err(|e| CliError::Other(e.into()))?;
        return Ok(name);
    }

    Ok(DEFAULT_PROJECT_NAME.to_string())
}

fn is_dir_empty(path: &PathBuf) -> Result<bool> {
    Ok(fs::read_dir(path)?.next().is_none())
}