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 {}
#[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> {
#[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
}
pub fn state(mut self, state: Option<State>) -> Self {
self.state = state;
self
}
pub fn about(mut self, about: &str) -> Self {
self.about = about.to_owned();
self
}
pub fn prompt(mut self, prompt: &str) -> Self {
self.prompt = prompt.to_owned();
self
}
pub fn continuation_prompt(mut self, continuation_prompt: &str) -> Self {
self.continuation_prompt = continuation_prompt.to_owned();
self
}
pub fn without_help(mut self) -> Self {
self.with_help = false;
self
}
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
}
}