forkctl 0.0.9

Control audited StGit downstream patch stacks
use crate::protocol::ColorMode;
use anstyle::{AnsiColor, Style};
use clap::{Command, CommandFactory};
use comfy_table::{ContentArrangement, Table, presets::UTF8_FULL_CONDENSED};
use std::fmt::Write as _;
use std::io::{self, Write};

const HEADING: Style = Style::new()
    .bold()
    .fg_color(Some(anstyle::Color::Ansi(AnsiColor::Cyan)));
const OPTION: Style = Style::new()
    .bold()
    .fg_color(Some(anstyle::Color::Ansi(AnsiColor::Green)));
const VALUE: Style = Style::new()
    .bold()
    .fg_color(Some(anstyle::Color::Ansi(AnsiColor::Yellow)));
const MUTED: Style = Style::new().fg_color(Some(anstyle::Color::Ansi(AnsiColor::BrightBlack)));

pub fn try_emit<C: CommandFactory>() -> io::Result<bool> {
    let args = std::env::args_os()
        .map(|arg| arg.to_string_lossy().into_owned())
        .collect::<Vec<_>>();
    let wants_help = args.len() == 1 || args.iter().any(|arg| arg == "-h" || arg == "--help");
    if !wants_help {
        return Ok(false);
    }
    let color = color_mode(&args);
    let mut root = C::command();
    root.build();
    let command = select_command(root, &args[1..]);
    let output = render(command);
    let choice = match color {
        ColorMode::Auto => anstream::ColorChoice::Auto,
        ColorMode::Always => anstream::ColorChoice::Always,
        ColorMode::Never => anstream::ColorChoice::Never,
    };
    let mut stream = anstream::AutoStream::new(io::stdout().lock(), choice);
    stream.write_all(output.as_bytes())?;
    stream.flush()?;
    Ok(true)
}

fn select_command(mut command: Command, args: &[String]) -> Command {
    for value in args {
        if value == "-h" || value == "--help" {
            break;
        }
        if value.starts_with('-') {
            continue;
        }
        let Some(next) = command
            .get_subcommands()
            .find(|subcommand| subcommand.get_name() == value)
            .cloned()
        else {
            continue;
        };
        command = next;
    }
    command
}

fn color_mode(args: &[String]) -> ColorMode {
    for (index, arg) in args.iter().enumerate() {
        let value = arg.strip_prefix("--color=").or_else(|| {
            (arg == "--color" || arg == "-c")
                .then(|| args.get(index + 1))
                .flatten()
                .map(String::as_str)
        });
        match value {
            Some("always") => return ColorMode::Always,
            Some("never") => return ColorMode::Never,
            Some("auto") => return ColorMode::Auto,
            _ => {}
        }
    }
    ColorMode::Auto
}

pub fn render(mut command: Command) -> String {
    command.build();
    let mut output = String::new();
    let usage = command.render_usage().to_string();
    let _ = write!(
        output,
        "{}{}{}\n\n",
        HEADING.render(),
        usage.trim(),
        HEADING.render_reset()
    );
    if let Some(about) = command.get_about() {
        output.push_str(&about.to_string());
        output.push_str("\n\n");
    }
    let subcommands = command
        .get_subcommands()
        .filter(|subcommand| !subcommand.is_hide_set())
        .map(|subcommand| {
            vec![
                styled(HEADING, subcommand.get_name()),
                subcommand
                    .get_about()
                    .map_or_else(String::new, ToString::to_string),
            ]
        })
        .collect::<Vec<_>>();
    if !subcommands.is_empty() {
        section(&mut output, "Commands", subcommands);
    }
    let positionals = command
        .get_positionals()
        .filter(|arg| !arg.is_hide_set())
        .map(|arg| {
            vec![
                String::new(),
                styled(VALUE, &value_label(arg)),
                String::new(),
                description(arg),
            ]
        })
        .collect::<Vec<_>>();
    if !positionals.is_empty() {
        section(&mut output, "Arguments", positionals);
    }
    let mut headings = Vec::<String>::new();
    for arg in command
        .get_arguments()
        .filter(|arg| !arg.is_positional() && !arg.is_hide_set() && arg.get_id().as_str() != "help")
    {
        let heading = arg
            .get_help_heading()
            .map_or_else(|| "Options".to_string(), ToString::to_string);
        if !headings.contains(&heading) {
            headings.push(heading);
        }
    }
    if command
        .get_arguments()
        .any(|arg| arg.get_id().as_str() == "help")
    {
        headings.push("Help".into());
    }
    for heading in headings {
        let rows = command
            .get_arguments()
            .filter(|arg| {
                if heading == "Help" {
                    return arg.get_id().as_str() == "help";
                }
                !arg.is_positional()
                    && !arg.is_hide_set()
                    && arg.get_id().as_str() != "help"
                    && arg.get_help_heading().map_or("Options", |value| value) == heading
            })
            .map(|arg| {
                vec![
                    arg.get_short()
                        .map_or_else(String::new, |value| styled(OPTION, &format!("-{value}"))),
                    arg.get_long()
                        .map_or_else(String::new, |value| styled(OPTION, &format!("--{value}"))),
                    styled(VALUE, &value_label(arg)),
                    description(arg),
                ]
            })
            .collect::<Vec<_>>();
        if !rows.is_empty() {
            section(&mut output, &heading, rows);
        }
    }
    output
}

fn section(output: &mut String, title: &str, rows: Vec<Vec<String>>) {
    let _ = writeln!(
        output,
        "{}{}{}",
        HEADING.render(),
        title,
        HEADING.render_reset()
    );
    let mut table = Table::new();
    table
        .load_preset(UTF8_FULL_CONDENSED)
        .set_content_arrangement(ContentArrangement::Dynamic);
    for row in rows {
        table.add_row(row);
    }
    let _ = writeln!(output, "{table}");
}

fn value_label(arg: &clap::Arg) -> String {
    if matches!(
        arg.get_action(),
        clap::ArgAction::SetTrue
            | clap::ArgAction::SetFalse
            | clap::ArgAction::Help
            | clap::ArgAction::Version
    ) {
        return String::new();
    }
    let names = arg
        .get_value_names()
        .map(|names| {
            names
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(" ")
        })
        .unwrap_or_default();
    let choices = arg
        .get_possible_values()
        .into_iter()
        .filter(|value| !value.is_hide_set())
        .map(|value| value.get_name().to_string())
        .collect::<Vec<_>>();
    if choices.is_empty() {
        names
    } else {
        format!("[{}]", choices.join("|"))
    }
}

fn description(arg: &clap::Arg) -> String {
    let mut description = arg.get_help().map_or_else(String::new, ToString::to_string);
    let defaults = arg
        .get_default_values()
        .iter()
        .map(|value| value.to_string_lossy())
        .collect::<Vec<_>>();
    if !defaults.is_empty()
        && !matches!(
            arg.get_action(),
            clap::ArgAction::SetTrue | clap::ArgAction::SetFalse
        )
    {
        if !description.is_empty() {
            description.push(' ');
        }
        description.push_str(&styled(
            MUTED,
            &format!("[default: {}]", defaults.join(", ")),
        ));
    }
    if arg.is_required_set() {
        if !description.is_empty() {
            description.push(' ');
        }
        description.push_str(&styled(MUTED, "[required]"));
    }
    description
}

fn styled(style: Style, value: &str) -> String {
    if value.is_empty() {
        String::new()
    } else {
        format!("{}{value}{}", style.render(), style.render_reset())
    }
}