clap-tui 0.1.0

Auto-generate a TUI from clap commands
Documentation
use std::collections::{BTreeMap, HashSet};

use crate::input::ActiveTab;
use crate::spec::{ArgSpec, CommandPath, CommandSpec};

use super::{
    form::{self, OrderedArg},
    tree::{self, TreeItem, TreeRow},
};

pub(crate) fn visible_sidebar_rows(
    root: &CommandSpec,
    expanded: &HashSet<String>,
    search: &str,
) -> Vec<TreeRow> {
    tree::tree_rows(root, expanded, search)
}

pub(crate) fn visible_sidebar_items(
    root: &CommandSpec,
    expanded: &HashSet<String>,
    search: &str,
) -> Vec<TreeItem> {
    tree::tree_items(root, expanded, search)
}

pub(crate) fn visible_form_args<'a>(
    root: &'a CommandSpec,
    selected_path: &CommandPath,
    active_tab: ActiveTab,
) -> Vec<OrderedArg<'a>> {
    form::visible_args_for_path(root, selected_path, active_tab)
}

pub(crate) fn visible_form_arg_pairs<'a>(args: &[OrderedArg<'a>]) -> Vec<(usize, &'a ArgSpec)> {
    form::visible_arg_pairs(args)
}

pub(crate) fn active_form_field<'a>(
    args: &'a [OrderedArg<'a>],
    selected_arg_index: usize,
) -> Option<&'a OrderedArg<'a>> {
    args.iter()
        .find(|item| item.order_index == selected_arg_index)
}

pub(crate) fn first_invalid_visible_field<'a>(
    args: &'a [OrderedArg<'a>],
    field_errors: &BTreeMap<String, String>,
) -> Option<&'a OrderedArg<'a>> {
    args.iter()
        .find(|item| field_errors.contains_key(&item.arg.id))
}

#[cfg(test)]
mod tests {
    use std::collections::{BTreeMap, HashSet};

    use clap::Command;

    use super::{
        active_form_field, first_invalid_visible_field, visible_form_args, visible_sidebar_items,
    };
    use crate::input::ActiveTab;
    use crate::spec::{ArgKind, ArgSpec, CommandPath, CommandSpec, ValueCardinality};

    fn arg(id: &str, name: &str) -> ArgSpec {
        ArgSpec {
            id: id.to_string(),
            display_name: name.to_string(),
            help: None,
            required: false,
            kind: ArgKind::Option,
            default_values: Vec::new(),
            choices: Vec::new(),
            position: None,
            value_cardinality: ValueCardinality::One,
            value_hint: None,
            ..ArgSpec::default()
        }
    }

    #[test]
    fn active_form_field_resolves_selected_visible_arg() {
        let command = CommandSpec {
            name: "tool".to_string(),
            version: None,
            about: None,
            help: String::new(),
            args: vec![arg("alpha", "--alpha"), arg("beta", "--beta")],
            subcommands: Vec::new(),
            ..CommandSpec::default()
        };
        let args = visible_form_args(&command, &CommandPath::default(), ActiveTab::Inputs);

        let active = active_form_field(&args, 1).expect("selected field");

        assert_eq!(active.arg.id, "beta");
    }

    #[test]
    fn visible_sidebar_items_follow_search_projection() {
        let root = CommandSpec::from_command(&Command::new("tool").subcommand(
            Command::new("release").subcommand(Command::new("deploy").visible_alias("ship")),
        ));

        let items = visible_sidebar_items(&root, &HashSet::new(), "ship");

        assert_eq!(items.len(), 2);
        assert_eq!(items[0].path.as_slice(), &["release".to_string()]);
        assert_eq!(
            items[1].path.as_slice(),
            &["release".to_string(), "deploy".to_string()]
        );
    }

    #[test]
    fn first_invalid_visible_field_follows_form_order() {
        let command = CommandSpec {
            name: "tool".to_string(),
            version: None,
            about: None,
            help: String::new(),
            args: vec![arg("alpha", "--alpha"), arg("beta", "--beta")],
            subcommands: Vec::new(),
            ..CommandSpec::default()
        };
        let args = visible_form_args(&command, &CommandPath::default(), ActiveTab::Inputs);
        let field_errors = BTreeMap::from([
            ("beta".to_string(), "Required argument".to_string()),
            ("alpha".to_string(), "Required argument".to_string()),
        ]);

        let invalid = first_invalid_visible_field(&args, &field_errors).expect("invalid field");

        assert_eq!(invalid.arg.id, "alpha");
    }
}