malvin 0.2.4

Non-interactive research and coding agent
use std::io::{self, Write};

use clap::CommandFactory;
use clap::builder::Command;

use super::Cli;

fn subcommand_by_name<'a>(root: &'a Command, name: &str) -> Option<&'a Command> {
    root.get_subcommands().find(|sub| sub.get_name() == name)
}

fn positional_value_name(arg: &clap::Arg) -> &str {
    arg.get_value_names()
        .and_then(|names| names.first())
        .map_or_else(|| arg.get_id().as_str(), clap::builder::Str::as_str)
}

fn positional_token(arg: &clap::Arg) -> String {
    let name = positional_value_name(arg);
    if arg.is_required_set() {
        format!("<{name}>")
    } else {
        format!("[{name}]")
    }
}

fn format_positional_usage_tokens(sub: &Command) -> Vec<String> {
    sub.get_arguments()
        .filter(|arg| arg.is_positional())
        .map(positional_token)
        .collect()
}

fn push_about_block(lines: &mut Vec<String>, sub: &Command) {
    if let Some(about) = sub.get_about() {
        lines.push(about.to_string());
        lines.push(String::new());
    }
}

fn subcommand_usage_line(sub: &Command) -> String {
    let usage_args = format_positional_usage_tokens(sub);
    if usage_args.is_empty() {
        format!("Usage: malvin {} [OPTION]...", sub.get_name())
    } else {
        format!(
            "Usage: malvin {} [OPTION]... {}",
            sub.get_name(),
            usage_args.join(" ")
        )
    }
}

fn argument_help_line(arg: &clap::Arg) -> String {
    let token = positional_token(arg);
    let help = arg
        .get_help()
        .map(std::string::ToString::to_string)
        .unwrap_or_default();
    format!("  {token}  {help}")
}

fn append_arguments_block(lines: &mut Vec<String>, sub: &Command) {
    let positionals: Vec<_> = sub
        .get_arguments()
        .filter(|arg| arg.is_positional())
        .collect();
    if positionals.is_empty() {
        return;
    }
    lines.push(String::new());
    lines.push("Arguments:".to_string());
    for arg in positionals {
        lines.push(argument_help_line(arg));
    }
}

fn append_short_help_epilog(lines: &mut Vec<String>, name: &str) {
    lines.push(String::new());
    lines.push(format!("Use malvin {name} --help to see options."));
}

fn subcommand_short_help_lines(root: &Command, name: &str) -> Option<Vec<String>> {
    let sub = subcommand_by_name(root, name)?;
    let mut lines = Vec::new();
    push_about_block(&mut lines, sub);
    lines.push(subcommand_usage_line(sub));
    append_arguments_block(&mut lines, sub);
    append_short_help_epilog(&mut lines, name);
    Some(lines)
}

pub fn render_subcommand_short_help(name: &str) -> Option<String> {
    let root = Cli::command();
    subcommand_short_help_lines(&root, name).map(|lines| format!("{}\n", lines.join("\n")))
}

pub fn print_subcommand_short_help(name: &str) -> io::Result<()> {
    let text = render_subcommand_short_help(name).unwrap_or_default();
    io::stdout().lock().write_all(text.as_bytes())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn render_subcommand_short_help_unknown_returns_none() {
        assert!(render_subcommand_short_help("code").is_none());
        assert!(render_subcommand_short_help("delight").is_none());
    }

    #[test]
    fn render_subcommand_short_help_inspire_omits_options() {
        let help = render_subcommand_short_help("inspire").expect("inspire subcommand");
        assert!(help.contains("Explore creative boundaries"));
        assert!(help.contains("Usage: malvin inspire [OPTION]... [REQUEST]"));
        assert!(help.contains("Arguments:"));
        assert!(help.contains("malvin inspire --help"));
        assert!(!help.contains("Options:"));
        assert!(!help.contains("--model"));
    }

    #[test]
    fn print_subcommand_short_help_invokes_stdout_path() {
        print_subcommand_short_help("inspire").expect("stdout");
    }

    #[test]
    fn kiss_cov_commands_help_subcommand_symbols() {}
}