clapcmd 0.3.3

A readline wrapper that allows for creating custom interactive shells, similar to python's cmd module
Documentation
use ansi_term::Style;

use rustyline::{Config, Editor};

#[cfg(feature = "test-runner")]
use std::sync::{Arc, Mutex};

use crate::group::HandlerGroupMeta;
use crate::{errors::ExitError, ArgMatches, ClapCmd, ClapCmdResult, Command};

use crate::helper::ClapCmdHelper;

impl std::fmt::Display for ExitError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "exit command received")
    }
}

impl std::error::Error for ExitError {}

/// The ClapCmdBuilder is mostly a thin wrapper around the rustyline builder that allows
/// you to specify how to interact with the readline interface. The main difference is that
/// the default options for the ClapCmdBuilder includes `auto_add_history`
#[derive(Clone)]
pub struct ClapCmdBuilder<State = ()> {
    config: Config,
    prompt: String,
    continuation_prompt: String,
    about: String,
    with_help: bool,
    with_exit: bool,
    state: Option<State>,
}

impl<State> ClapCmdBuilder<State> {
    /// Used to complete the build a return a `ClapCmd` struct
    #[must_use]
    pub fn build(&self) -> ClapCmd<State>
    where
        State: Clone + Send + Sync + 'static,
    {
        let mut editor =
            Editor::with_history(self.config, rustyline::history::MemHistory::new()).unwrap();
        editor.set_helper(Some(ClapCmdHelper::new(self.state.clone())));

        let mut cmd = ClapCmd {
            editor,
            prompt: self.prompt.clone(),
            continuation_prompt: self.continuation_prompt.clone(),
            about: self.about.clone(),
            #[cfg(feature = "test-runner")]
            output: String::new(),
            #[cfg(feature = "test-runner")]
            info: String::new(),
            #[cfg(feature = "test-runner")]
            warn: String::new(),
            #[cfg(feature = "test-runner")]
            error: String::new(),
            #[cfg(feature = "test-runner")]
            success: String::new(),
            #[cfg(feature = "test-runner")]
            async_output: Arc::new(Mutex::new(String::new())),
        };

        if self.with_help {
            cmd.add_command(
                Box::new(Self::display_help),
                Command::new("help").about("display the list of available commands"),
            );
        }
        if self.with_exit {
            cmd.add_command(
                Box::new(Self::exit),
                Command::new("exit").about("exit the shell"),
            );
        }

        cmd
    }

    /// Specify the starting state for the builder
    pub fn state(mut self, state: Option<State>) -> Self {
        self.state = state;
        self
    }

    /// Specify the "about" string displayed in the help menu
    pub fn about(mut self, about: &str) -> Self {
        self.about = about.to_owned();
        self
    }

    /// Specify the default prompt, this can later be altered using the `set_prompt()` method
    pub fn prompt(mut self, prompt: &str) -> Self {
        self.prompt = prompt.to_owned();
        self
    }

    /// Specify the default continuation prompt (for multiline input), this can later be altered using the `set_continuation_prompt()` method
    pub fn continuation_prompt(mut self, continuation_prompt: &str) -> Self {
        self.continuation_prompt = continuation_prompt.to_owned();
        self
    }

    /// Do not add the default 'help' command to the repl
    pub fn without_help(mut self) -> Self {
        self.with_help = false;
        self
    }

    /// Do not add the default 'exit' command to the repl
    pub fn without_exit(mut self) -> Self {
        self.with_exit = false;
        self
    }

    fn display_help(cmd: &mut ClapCmd<State>, _: ArgMatches) -> ClapCmdResult
    where
        State: Clone,
    {
        if !cmd.about.as_str().trim().is_empty() {
            println!("{}\n", cmd.about);
        }
        let helper = cmd.editor.helper().unwrap();
        let longest = helper
            .dispatcher
            .iter()
            .map(|handler| handler.command.get_name().len())
            .max()
            .unwrap();
        let padding = 4;
        let longest = longest + padding;
        let mut groupless: Vec<String> = vec![];
        let mut groups: Vec<(HandlerGroupMeta, Vec<String>)> = vec![];
        for handler in &helper.dispatcher {
            let command = &handler.command;
            let about = command.get_about().unwrap_or_default();
            let format = format!("{:<longest$}{}", command.to_string(), about,);
            if handler.group.as_ref().map_or(true, |g| !g.visible) {
                groupless.push(format);
            } else {
                let group = handler.group.clone().unwrap();
                let outputs = groups.iter_mut().find(|g| g.0 == group);
                if let Some(outputs) = outputs {
                    outputs.1.push(format);
                } else {
                    groups.push((group, vec![format]))
                }
            }
        }
        for line in groupless {
            println!("{line}");
        }
        for (group, lines) in groups {
            println!("\n{}", Style::new().underline().paint(group.name));
            if !group.description.as_str().trim().is_empty() {
                println!("{}\n", group.description);
            }
            for line in lines {
                println!("{line}");
            }
        }
        Ok(())
    }

    fn exit(_: &mut ClapCmd<State>, _: ArgMatches) -> ClapCmdResult
    where
        State: Clone,
    {
        Err(Box::new(ExitError {}))
    }
}

impl<State> Default for ClapCmdBuilder<State> {
    fn default() -> Self {
        let config = Config::builder().auto_add_history(true).build();
        Self {
            config,
            state: None,
            prompt: "> ".to_owned(),
            continuation_prompt: "  ".to_owned(),
            about: "".to_owned(),
            with_help: true,
            with_exit: true,
        }
    }
}

impl<State> rustyline::config::Configurer for ClapCmdBuilder<State> {
    fn config_mut(&mut self) -> &mut Config {
        &mut self.config
    }
}