clapcmd/
builder.rs

1use ansi_term::Style;
2
3use rustyline::{Config, Editor};
4
5#[cfg(feature = "test-runner")]
6use std::sync::{Arc, Mutex};
7
8use crate::group::HandlerGroupMeta;
9use crate::{errors::ExitError, ArgMatches, ClapCmd, ClapCmdResult, Command};
10
11use crate::helper::ClapCmdHelper;
12
13impl std::fmt::Display for ExitError {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "exit command received")
16    }
17}
18
19impl std::error::Error for ExitError {}
20
21/// The ClapCmdBuilder is mostly a thin wrapper around the rustyline builder that allows
22/// you to specify how to interact with the readline interface. The main difference is that
23/// the default options for the ClapCmdBuilder includes `auto_add_history`
24#[derive(Clone)]
25pub struct ClapCmdBuilder<State = ()> {
26    config: Config,
27    prompt: String,
28    continuation_prompt: String,
29    about: String,
30    with_help: bool,
31    with_exit: bool,
32    state: Option<State>,
33}
34
35impl<State> ClapCmdBuilder<State> {
36    /// Used to complete the build a return a `ClapCmd` struct
37    #[must_use]
38    pub fn build(&self) -> ClapCmd<State>
39    where
40        State: Clone + Send + Sync + 'static,
41    {
42        let mut editor =
43            Editor::with_history(self.config, rustyline::history::MemHistory::new()).unwrap();
44        editor.set_helper(Some(ClapCmdHelper::new(self.state.clone())));
45
46        let mut cmd = ClapCmd {
47            editor,
48            prompt: self.prompt.clone(),
49            continuation_prompt: self.continuation_prompt.clone(),
50            about: self.about.clone(),
51            #[cfg(feature = "test-runner")]
52            output: String::new(),
53            #[cfg(feature = "test-runner")]
54            info: String::new(),
55            #[cfg(feature = "test-runner")]
56            warn: String::new(),
57            #[cfg(feature = "test-runner")]
58            error: String::new(),
59            #[cfg(feature = "test-runner")]
60            success: String::new(),
61            #[cfg(feature = "test-runner")]
62            async_output: Arc::new(Mutex::new(String::new())),
63        };
64
65        if self.with_help {
66            cmd.add_command(
67                Box::new(Self::display_help),
68                Command::new("help").about("display the list of available commands"),
69            );
70        }
71        if self.with_exit {
72            cmd.add_command(
73                Box::new(Self::exit),
74                Command::new("exit").about("exit the shell"),
75            );
76        }
77
78        cmd
79    }
80
81    /// Specify the starting state for the builder
82    pub fn state(mut self, state: Option<State>) -> Self {
83        self.state = state;
84        self
85    }
86
87    /// Specify the "about" string displayed in the help menu
88    pub fn about(mut self, about: &str) -> Self {
89        self.about = about.to_owned();
90        self
91    }
92
93    /// Specify the default prompt, this can later be altered using the `set_prompt()` method
94    pub fn prompt(mut self, prompt: &str) -> Self {
95        self.prompt = prompt.to_owned();
96        self
97    }
98
99    /// Specify the default continuation prompt (for multiline input), this can later be altered using the `set_continuation_prompt()` method
100    pub fn continuation_prompt(mut self, continuation_prompt: &str) -> Self {
101        self.continuation_prompt = continuation_prompt.to_owned();
102        self
103    }
104
105    /// Do not add the default 'help' command to the repl
106    pub fn without_help(mut self) -> Self {
107        self.with_help = false;
108        self
109    }
110
111    /// Do not add the default 'exit' command to the repl
112    pub fn without_exit(mut self) -> Self {
113        self.with_exit = false;
114        self
115    }
116
117    fn display_help(cmd: &mut ClapCmd<State>, _: ArgMatches) -> ClapCmdResult
118    where
119        State: Clone,
120    {
121        if !cmd.about.as_str().trim().is_empty() {
122            println!("{}\n", cmd.about);
123        }
124        let helper = cmd.editor.helper().unwrap();
125        let longest = helper
126            .dispatcher
127            .iter()
128            .map(|handler| handler.command.get_name().len())
129            .max()
130            .unwrap();
131        let padding = 4;
132        let longest = longest + padding;
133        let mut groupless: Vec<String> = vec![];
134        let mut groups: Vec<(HandlerGroupMeta, Vec<String>)> = vec![];
135        for handler in &helper.dispatcher {
136            let command = &handler.command;
137            let about = command.get_about().unwrap_or_default();
138            let format = format!("{:<longest$}{}", command.to_string(), about,);
139            if handler.group.as_ref().map_or(true, |g| !g.visible) {
140                groupless.push(format);
141            } else {
142                let group = handler.group.clone().unwrap();
143                let outputs = groups.iter_mut().find(|g| g.0 == group);
144                if let Some(outputs) = outputs {
145                    outputs.1.push(format);
146                } else {
147                    groups.push((group, vec![format]))
148                }
149            }
150        }
151        for line in groupless {
152            println!("{line}");
153        }
154        for (group, lines) in groups {
155            println!("\n{}", Style::new().underline().paint(group.name));
156            if !group.description.as_str().trim().is_empty() {
157                println!("{}\n", group.description);
158            }
159            for line in lines {
160                println!("{line}");
161            }
162        }
163        Ok(())
164    }
165
166    fn exit(_: &mut ClapCmd<State>, _: ArgMatches) -> ClapCmdResult
167    where
168        State: Clone,
169    {
170        Err(Box::new(ExitError {}))
171    }
172}
173
174impl<State> Default for ClapCmdBuilder<State> {
175    fn default() -> Self {
176        let config = Config::builder().auto_add_history(true).build();
177        Self {
178            config,
179            state: None,
180            prompt: "> ".to_owned(),
181            continuation_prompt: "  ".to_owned(),
182            about: "".to_owned(),
183            with_help: true,
184            with_exit: true,
185        }
186    }
187}
188
189impl<State> rustyline::config::Configurer for ClapCmdBuilder<State> {
190    fn config_mut(&mut self) -> &mut Config {
191        &mut self.config
192    }
193}