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#[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 #[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 pub fn state(mut self, state: Option<State>) -> Self {
83 self.state = state;
84 self
85 }
86
87 pub fn about(mut self, about: &str) -> Self {
89 self.about = about.to_owned();
90 self
91 }
92
93 pub fn prompt(mut self, prompt: &str) -> Self {
95 self.prompt = prompt.to_owned();
96 self
97 }
98
99 pub fn continuation_prompt(mut self, continuation_prompt: &str) -> Self {
101 self.continuation_prompt = continuation_prompt.to_owned();
102 self
103 }
104
105 pub fn without_help(mut self) -> Self {
107 self.with_help = false;
108 self
109 }
110
111 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}