bile 0.1.1

A simple build system, structured around Lua scripts.
//! CLI Frontend to [`bile`].

use std::io::{Write as _, stdout};
use std::path::Path;

use bile::api::LibBuilder;
use bile::builder::{Builder, collect_builders_from_table};
use clap::builder::Styles;
use clap::builder::styling::Style;
use clap::{Arg, Command, ValueHint, crate_authors, crate_description, crate_name, crate_version};
use color_eyre::Report;
use mlua::prelude::*;

/// Template used for formatting the main [`clap`] help message.
const HELP_TEMPLATE: &str = "\
{name} v{version}
{tab}Authored by {author}

OVERVIEW: {name}
{tab}{about}

USAGE: {usage}
{positionals}
OPTIONS:
{options}

SUBCOMMANDS:
{subcommands}
";

/// Template used for formatting the [`clap`] help message for subcommands.
const SUBCOMMAND_HELP_TEMPLATE: &str = "\
OVERVIEW: {name}
{tab}{about}

USAGE: {usage}
{positionals}
OPTIONS:
{options}
";

/// Template used for formatting the [`clap`] help message for subcommands that have subcommands.
const SUBCOMMAND_WITH_SUBCOMMANDS_HELP_TEMPLATE: &str = "\
OVERVIEW: {name}
{tab}{about}

USAGE: {usage}
{positionals}
TARGETS:
{subcommands}

OPTIONS:
{options}
";

/// Main entry point to the program.
fn main() -> Result<(), Report> {
    color_eyre::install()?;

    let file_arg = Arg::new("file")
        .short('f')
        .long("file")
        .default_value("build.lua")
        .value_hint(ValueHint::FilePath)
        .help("The path to the build.lua file to load build methods from.");

    let mut command = Command::default().arg(file_arg);

    // For posterity's sake: The following splits off a version of our command
    // and modifies it so that it will 'just work'. At least enough to get us
    // the path to `file`. Then we apply a handful of error combinators to
    // ensure a safe result is returned.
    let pre_args = command
        .clone()
        // Try our very best to parse -f / --file
        .ignore_errors(true)
        .disable_help_flag(true)
        .try_get_matches()
        .ok()
        .and_then(|matches| matches.get_one::<String>("file").cloned())
        .unwrap_or_else(|| "build.lua".to_owned());

    let lua = LibBuilder::init_lua().map_err(Report::new)?;
    // LibBuilder::add_to_lua_globals(&lua).map_err(|a| color_eyre::Report::new(a))?;
    let table = {
        let script = lua.load(Path::new(&pre_args));
        script.call::<LuaTable>(())?
    };

    let builders = collect_builders_from_table(&table);
    let exec = Command::default()
        .name("exec")
        .subcommands(builders.iter().map(Builder::make_command))
        .subcommand_required(true)
        .disable_help_subcommand(true)
        .arg_required_else_help(true)
        .help_template(SUBCOMMAND_WITH_SUBCOMMANDS_HELP_TEMPLATE)
        .about("Executes a command from the build.lua file.");

    let list = Command::default()
        .name("list")
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .num_args(0)
                .help_heading("v"),
        )
        .help_template(SUBCOMMAND_HELP_TEMPLATE)
        .about("Lists the detected commands from the build.lua file.");

    command = command
        .name(crate_name!())
        .version(crate_version!())
        .about(crate_description!())
        .author(crate_authors!())
        .help_template(HELP_TEMPLATE)
        .styles(Styles::plain().placeholder(Style::new().dimmed()))
        .subcommand_required(true)
        .arg_required_else_help(true)
        .allow_missing_positional(true)
        .subcommand(exec)
        .subcommand(list);

    let args = command.get_matches();

    match args.subcommand() {
        Some(("exec", extras)) => match extras.subcommand() {
            Some((subcommand, subcommand_extras)) => {
                // We consume our builders here
                if let Some(find) = builders.into_iter().find(|named| named.name == subcommand) {
                    find.execute()?;
                }
            }
            // Automatically handled for us by clap
            None => return Err(Report::msg("Supposedly unreachable branch executed")),
        },
        Some(("list", _extras)) => {
            let mut out = stdout().lock();
            writeln!(out, "COMMANDS:")?;
            let longest_len = builders
                .iter()
                .map(|a| a.name.len())
                .max()
                .ok_or_else(|| Report::msg("{pre_args:?}"))?;
            for builder in builders {
                if let Some(description) = builder.description {
                    writeln!(
                        out,
                        "  {:<width$}  {description}",
                        builder.name,
                        width = longest_len
                    )?;
                } else {
                    writeln!(out, "  {}", builder.name)?;
                }
            }
            out.flush()?;
            drop(out);
        }
        // These cases are already handled by clap
        None | Some(_) => {
            return Err(Report::msg("Supposedly unreachable branch executed"));
        }
    }

    Ok(())
}