clapcmd 0.3.3

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

use ansi_term::Color;
use rustyline::Editor;

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

use clap::Command;

use crate::async_stdout::AsyncStdout;
use crate::builder::ClapCmdBuilder;
use crate::errors::ExitError;
use crate::group::{HandlerGroup, HandlerGroupMeta};
use crate::handler::{Callback, ClapCmdResult, Handler};
use crate::helper::ClapCmdHelper;
use crate::shell_parser::{split, unescape_word, ShlexRunType};

/// An interactive CLI interface, holding state and responsible for acquiring user input and
/// assigning tasks to callback functions.
pub struct ClapCmd<State = (), History = rustyline::history::MemHistory>
where
    State: Clone,
    History: rustyline::history::History,
{
    pub(crate) editor: Editor<ClapCmdHelper<State>, History>,
    pub(crate) prompt: String,
    pub(crate) continuation_prompt: String,
    pub(crate) about: String,
    #[cfg(feature = "test-runner")]
    /// When `test-runner` feature is active, contains the `output` data from the previous command
    pub output: String,
    #[cfg(feature = "test-runner")]
    /// When `test-runner` feature is active, contains the `info` data from the previous command
    pub info: String,
    #[cfg(feature = "test-runner")]
    /// When `test-runner` feature is active, contains the `warn` data from the previous command
    pub warn: String,
    #[cfg(feature = "test-runner")]
    /// When `test-runner` feature is active, contains the `error` data from the previous command
    pub error: String,
    #[cfg(feature = "test-runner")]
    /// When `test-runner` feature is active, contains the `success` data from the previous command
    pub success: String,
    #[cfg(feature = "test-runner")]
    /// When `test-runner` feature is active, contains the `async_output` data from the previous command
    pub async_output: Arc<Mutex<String>>,
}

impl<State> Default for ClapCmd<State>
where
    State: Clone + Default + Send + Sync + 'static,
{
    fn default() -> Self {
        let builder = ClapCmdBuilder::<State>::default();
        builder.build()
    }
}

impl<State> ClapCmd<State>
where
    State: Clone,
{
    /// Generate a default `ClapCmd` instance with the specified initial `State`
    pub fn with_state(state: State) -> Self
    where
        State: Send + Sync + 'static,
    {
        ClapCmdBuilder::default().state(Some(state)).build()
    }

    /// Generate a `ClapCmdBuilder` object used mainly to expose rustyline settings
    pub fn builder() -> ClapCmdBuilder {
        ClapCmdBuilder::default()
    }

    /// Set the internal state that is provided to callbacks
    pub fn set_state(&mut self, state: State) {
        self.editor
            .helper_mut()
            .expect("helper is always provided")
            .set_state(Some(state));
    }

    /// Delete the internal state
    pub fn unset_state(&mut self) {
        self.editor
            .helper_mut()
            .expect("helper is always provided")
            .set_state(None);
    }

    /// Retrieve the current state from a callback
    pub fn get_state(&self) -> Option<&State> {
        self.editor
            .helper()
            .expect("helper is always provided")
            .get_state()
    }

    /// Retrieve a mutable reference to the current state from a callback
    pub fn get_state_mut(&mut self) -> Option<&mut State> {
        self.editor
            .helper_mut()
            .expect("helper is always provided")
            .get_state_mut()
    }

    /// Set the current prompt
    pub fn set_prompt(&mut self, prompt: &str) {
        self.prompt = prompt.to_owned();
    }

    /// Set the continuation prompt
    pub fn set_continuation_prompt(&mut self, continuation_prompt: &str) {
        self.continuation_prompt = continuation_prompt.to_owned();
    }

    /// Checks if a given &str is connected to a command
    pub fn has_command(&self, command: &str) -> bool {
        self.editor
            .helper()
            .expect("helper is always provided")
            .dispatcher
            .iter()
            .any(|c| c.command.get_name() == command)
    }

    /// Adds the specified command to the default group
    pub fn add_command(&mut self, callback: impl Callback<State>, command: Command) {
        self.add_command_from_handler(Handler {
            command,
            group: None,
            callback: callback.clone_box(),
        })
    }

    fn add_command_from_handler(&mut self, handler: Handler<State>) {
        if self.has_command(handler.command.get_name()) {
            return;
        }
        self.editor
            .helper_mut()
            .expect("helper is always provided")
            .dispatcher
            .push(handler);
    }

    /// Removes the command distinguished by the string `command`
    pub fn remove_command(&mut self, command: &str) {
        self.editor
            .helper_mut()
            .expect("helper is always provided")
            .dispatcher
            .retain(|c| c.command.get_name() != command);
    }

    /// Creates a new group that can be loaded an unloaded
    pub fn group(name: &str) -> HandlerGroup<State> {
        HandlerGroup {
            group: HandlerGroupMeta {
                name: name.to_owned(),
                description: "".to_owned(),
                visible: true,
            },
            groups: vec![],
        }
    }

    /// Creates a new unnamed group that can be loaded an unloaded
    pub fn unnamed_group() -> HandlerGroup<State> {
        HandlerGroup {
            group: HandlerGroupMeta {
                name: "".to_owned(),
                description: "".to_owned(),
                visible: false,
            },
            groups: vec![],
        }
    }

    /// Adds all the commands in a group
    pub fn add_group(&mut self, groups: &HandlerGroup<State>) {
        for handler in &groups.groups {
            self.add_command_from_handler(handler.clone());
        }
    }

    /// Removes all the commands in a group
    pub fn remove_group(&mut self, groups: &HandlerGroup<State>) {
        for command in &groups.groups {
            if self
                .editor
                .helper()
                .expect("helper is always provided")
                .dispatcher
                .iter()
                .any(|h| {
                    h.command.get_name() == command.command.get_name()
                        && h.group
                            .as_ref()
                            .map_or(false, |g| g.name == groups.group.name)
                })
            {
                self.remove_command(command.command.get_name());
            }
        }
    }

    /// Returns a thread-safe `Write` object that can be used to asyncronously write output to stdout
    /// without interferring with the promptline
    pub fn get_async_writer(&mut self) -> Result<impl std::io::Write, Box<dyn Error>> {
        let printer = self.editor.create_external_printer()?;
        Ok(AsyncStdout {
            printer,
            #[cfg(feature = "test-runner")]
            output: self.async_output.clone(),
        })
    }

    /// Print a standard message to stdout
    pub fn output(&mut self, output: impl Into<String>) {
        let output = output.into();
        #[cfg(feature = "test-runner")]
        self.output.push_str(&output);
        println!("{}", output);
    }

    /// Print a warning to stdout
    pub fn warn(&mut self, output: impl Into<String>) {
        let output = output.into();
        #[cfg(feature = "test-runner")]
        self.warn.push_str(&output);
        let output = format!("[-] {}", output);
        println!("{}", Color::Yellow.bold().paint(output));
    }

    /// Print an error to stdout
    pub fn error(&mut self, output: impl Into<String>) {
        let output = output.into();
        #[cfg(feature = "test-runner")]
        self.error.push_str(&output);
        let output = format!("[!] {}", output);
        println!("{}", Color::Red.bold().paint(output));
    }

    /// Print a success message to stdout
    pub fn success(&mut self, output: impl Into<String>) {
        let output = output.into();
        #[cfg(feature = "test-runner")]
        self.success.push_str(&output);
        let output = format!("[+] {}", output);
        println!("{}", Color::Green.bold().paint(output));
    }

    /// Print an informational message to stdout
    pub fn info(&mut self, output: impl Into<String>) {
        let output = output.into();
        #[cfg(feature = "test-runner")]
        self.info.push_str(&output);
        let output = format!("[*] {}", output);
        println!("{}", Color::Cyan.bold().paint(output));
    }

    #[cfg(feature = "test-runner")]
    fn clear_all(&mut self) {
        self.output.clear();
        self.info.clear();
        self.warn.clear();
        self.success.clear();
        self.error.clear();
    }

    /// Creates a prompt and reads a single line from the user
    pub fn read_line(&mut self, prompt: &str) -> Option<String> {
        let line = self.editor.readline(prompt).ok()?;
        if !line.ends_with('\\') {
            return Some(line);
        }
        let line = &line[..line.len() - 1];
        let continuation = self.read_line(&self.continuation_prompt.clone());
        let line = format!("{}\n{}", line, continuation.unwrap_or_default());
        Some(line)
    }

    /// Executes the appropriate command(s) specified by the line read from the user
    pub fn one_cmd(&mut self, line: &str) -> ClapCmdResult {
        #[cfg(feature = "test-runner")]
        self.clear_all();
        let statements = split(line);
        let mut last_result = Ok(());
        for statement in statements {
            match statement.run_type {
                ShlexRunType::Unconditional => {}
                ShlexRunType::ConditionalAnd => {
                    if last_result.is_err() {
                        break;
                    }
                }
                ShlexRunType::ConditionalOr => {
                    if last_result.is_ok() {
                        break;
                    }
                }
            }
            let mut argv = vec![];
            for word in statement.iter() {
                argv.push(unescape_word(line, word));
            }
            let argv: Vec<String> = argv
                .into_iter()
                .skip_while(|word| word.is_empty())
                .collect();
            if argv.is_empty() {
                continue;
            }
            let command_to_run = &argv[0].to_owned();
            let helper = self.editor.helper().expect("helper is always provided");
            let handler = helper
                .dispatcher
                .iter()
                .find(|h| h.command.get_name() == command_to_run);

            if handler.is_none() {
                self.error(format!("unknown command: '{command_to_run}'"));
                continue;
            }

            let handler = handler.expect("some type can always be unwrapped");
            let matches = handler.command.clone().try_get_matches_from_mut(argv);
            match matches {
                Ok(matches) => {
                    last_result = handler.clone().callback.call(self, matches);
                }
                Err(err) => {
                    self.output(format!("{}", err.render()));
                }
            }
        }
        last_result
    }

    /// Run the command loop until a callback returns `Err(Box::new(clapcmd::errors::ExitError))`
    pub fn run_loop(&mut self) {
        loop {
            let prompt = self.prompt.clone();
            let prompt = prompt.as_str();
            let input = self.read_line(prompt);
            if input.is_none() {
                break;
            }
            let input = input.expect("input needs to be utf-8");
            if let Err(err) = self.one_cmd(&input) {
                if !err.is::<ExitError>() {
                    self.error(format!(
                        "Error: {}",
                        err.source().expect("error source unknown")
                    ));
                    continue;
                }
                break;
            }
        }
    }
}