falcon_cli/help.rs
1// Copyright 2025 Aquila Labs of Alberta, Canada <matt@cicero.sh>
2// Licensed under either the Apache License, Version 2.0 OR the MIT License, at your option.
3// You may not use this file except in compliance with one of the Licenses.
4// Apache License text: https://www.apache.org/licenses/LICENSE-2.0
5// MIT License text: https://opensource.org/licenses/MIT
6use crate::CliCommand;
7use crate::router::CliRouter;
8use crate::*;
9use indexmap::{IndexMap, indexmap};
10
11/// Structure representing a help screen for a CLI command.
12///
13/// This struct contains all the information needed to render a complete help screen,
14/// including title, usage, description, parameters, flags, and examples.
15pub struct CliHelpScreen {
16 /// The title displayed at the top of the help screen.
17 pub title: String,
18 /// Usage string showing how to invoke the command.
19 pub usage: String,
20 /// Detailed description of what the command does.
21 pub description: String,
22 /// Map of parameter names to their descriptions.
23 pub params: IndexMap<String, String>,
24 /// Map of flag names to their descriptions.
25 pub flags: IndexMap<String, String>,
26 /// List of example command invocations.
27 pub examples: Vec<String>,
28}
29
30impl CliHelpScreen {
31 /// Creates a new help screen with the specified title, usage, and description.
32 ///
33 /// # Arguments
34 ///
35 /// * `title` - The title displayed at the top of the help screen
36 /// * `usage` - Usage string showing how to invoke the command
37 /// * `description` - Detailed description of what the command does
38 ///
39 /// # Example
40 ///
41 /// ```
42 /// use falcon_cli::CliHelpScreen;
43 ///
44 /// let help = CliHelpScreen::new(
45 /// "My Command",
46 /// "myapp command [OPTIONS]",
47 /// "This command does something useful"
48 /// );
49 /// ```
50 pub fn new(title: &str, usage: &str, description: &str) -> Self {
51 Self {
52 title: title.to_string(),
53 usage: usage.to_string(),
54 description: description.to_string(),
55 params: indexmap![],
56 flags: indexmap![],
57 examples: Vec::new(),
58 }
59 }
60
61 /// Adds a parameter to the list displayed in the help screen.
62 ///
63 /// # Arguments
64 ///
65 /// * `param` - The parameter name
66 /// * `description` - Description of what the parameter does
67 ///
68 /// # Example
69 ///
70 /// ```
71 /// # use falcon_cli::CliHelpScreen;
72 /// let mut help = CliHelpScreen::new("Title", "usage", "desc");
73 /// help.add_param("filename", "The name of the file to process");
74 /// ```
75 pub fn add_param(&mut self, param: &str, description: &str) {
76 self.params.insert(param.to_string(), description.to_string());
77 }
78
79 /// Adds a flag to the list displayed in the help screen.
80 ///
81 /// # Arguments
82 ///
83 /// * `flag` - The flag name (e.g., "--verbose" or "-v")
84 /// * `description` - Description of what the flag does
85 ///
86 /// # Example
87 ///
88 /// ```
89 /// # use falcon_cli::CliHelpScreen;
90 /// let mut help = CliHelpScreen::new("Title", "usage", "desc");
91 /// help.add_flag("--verbose|-v", "Enable verbose output");
92 /// ```
93 pub fn add_flag(&mut self, flag: &str, description: &str) {
94 self.flags.insert(flag.to_string(), description.to_string());
95 }
96
97 /// Adds an example to the list displayed in the help screen.
98 ///
99 /// # Arguments
100 ///
101 /// * `example` - An example command invocation
102 ///
103 /// # Example
104 ///
105 /// ```
106 /// # use falcon_cli::CliHelpScreen;
107 /// let mut help = CliHelpScreen::new("Title", "usage", "desc");
108 /// help.add_example("myapp command --verbose input.txt");
109 /// ```
110 pub fn add_example(&mut self, example: &str) {
111 self.examples.push(example.to_string());
112 }
113
114 /// Renders and displays the help screen for a specific CLI command.
115 ///
116 /// This method is automatically executed when the first argument passed via the command line
117 /// is 'help' or '-h'. It should not typically be called manually.
118 ///
119 /// # Arguments
120 ///
121 /// * `cmd` - The CLI command to display help for
122 /// * `cmd_alias` - The primary alias/name of the command
123 /// * `shortcuts` - List of shortcut aliases for the command
124 pub fn render(cmd: &Box<dyn CliCommand>, cmd_alias: &String, shortcuts: &Vec<String>) {
125 // Get help screen
126 let help = cmd.help();
127
128 // Display basics
129 cli_header(help.title.as_str());
130 cli_sendln!("USAGE\n");
131 cli_sendln!(format!(" {}\n", help.usage).as_str());
132
133 // Display shortcuts
134 for shortcut in shortcuts {
135 let tmp_usage = help.usage.replace(cmd_alias, shortcut.as_str());
136 cli_sendln!(format!(" {}", tmp_usage).as_str());
137 }
138 //cli_sendln!("");
139
140 // Description
141 if !help.description.is_empty() {
142 let options =
143 textwrap::Options::new(75).initial_indent(" ").subsequent_indent(" ");
144 let desc = textwrap::fill(help.description.as_str(), &options);
145
146 cli_sendln!("DESCRIPTION:\n");
147 cli_sendln!(desc.as_str());
148 cli_sendln!("");
149 }
150
151 // Parameters
152 if !help.params.is_empty() {
153 cli_sendln!("PARAMETERS\n");
154 cli_display_array(&help.params);
155 }
156
157 // Flags
158 if !help.flags.is_empty() {
159 cli_sendln!("FLAGS\n");
160 cli_display_array(&help.flags);
161 }
162
163 // Examples
164 if !help.examples.is_empty() {
165 cli_sendln!("EXAMPLES\n");
166 for example in help.examples {
167 println!(" {}\n", example);
168 }
169 }
170
171 // End
172 cli_sendln!("-- END --\n");
173 }
174
175 /// Renders and displays the main help index for the application.
176 ///
177 /// This method is automatically executed when the first and only argument passed via the
178 /// command line is 'help' or '-h'. It displays either all available categories or CLI commands
179 /// depending on whether categories have been added to the router.
180 ///
181 /// # Arguments
182 ///
183 /// * `router` - The CLI router containing all registered commands and categories
184 pub fn render_index(router: &CliRouter) {
185 // Header
186 if router.app_name.is_empty() {
187 cli_header("Help");
188 } else {
189 cli_header(&router.app_name);
190 }
191
192 // Globa flags, if we have them
193 if !router.global_flags.is_empty() {
194 cli_sendln!("GLOBAL FLAGS\n");
195 let mut global_arr = IndexMap::new();
196 for gf in router.global_flags.iter() {
197 let mut key = format!("{}|{}", gf.short, gf.long);
198 if gf.short.is_empty() {
199 key = gf.long.to_string();
200 }
201 if gf.long.is_empty() {
202 key = gf.short.to_string();
203 }
204 global_arr.insert(key, gf.desc.to_string());
205 }
206 cli_display_array(&global_arr);
207 }
208
209 cli_sendln!("AVAILABLE COMMANDS\n");
210 cli_sendln!("Run any of the commands with 'help' as the first argument for details\n");
211
212 // Display as needed
213 let mut table: IndexMap<String, String> = indexmap![];
214 if !router.categories.is_empty() {
215 // Sort keys
216 let mut keys: Vec<String> = router.categories.keys().map(|k| k.to_string()).collect();
217 keys.sort();
218
219 // Create array to render
220 for cat_alias in keys {
221 let cat = router.categories.get(&cat_alias).unwrap();
222 table.insert(cat.alias.to_string(), cat.description.to_string());
223 }
224
225 // Render array
226 cli_display_array(&table);
227
228 // No categories, display individual commands
229 } else {
230 // Sort keys
231 let mut keys: Vec<String> = router.commands.keys().cloned().collect();
232 keys.sort();
233
234 // Go through keys
235 for alias in keys {
236 let cmd = router.commands.get(&alias).unwrap();
237 let cmd_help = cmd.help();
238
239 table.insert(alias.to_string(), cmd_help.description);
240 }
241
242 // Display commands
243 cli_display_array(&table);
244 }
245
246 // Exit
247 cli_sendln!("-- END --\r\n");
248 exit(0);
249 }
250
251 /// Renders and displays help for a specific category.
252 ///
253 /// This method is only applicable when using multiple categories to organize groups of CLI commands.
254 /// It is automatically executed when the first argument via command line is either 'help' or '-h',
255 /// and the second argument is the name of a category. It displays all CLI commands available within that category.
256 ///
257 /// # Arguments
258 ///
259 /// * `router` - The CLI router containing all registered commands and categories
260 /// * `cat_alias` - The alias/name of the category to display
261 pub fn render_category(router: &CliRouter, cat_alias: &String) {
262 // GEt category
263 let cat = router.categories.get(&cat_alias.to_string()).unwrap();
264 cli_header(&cat.title);
265
266 // Description
267 if !cat.description.is_empty() {
268 let options =
269 textwrap::Options::new(75).initial_indent(" ").subsequent_indent(" ");
270 let desc = textwrap::fill(cat.description.as_str(), &options);
271
272 cli_sendln!("DESCRIPTION:\n");
273 cli_sendln!(desc.as_str());
274 }
275
276 // Sub categories
277 let chk = format!("{} ", cat_alias);
278 let mut sub_categories: Vec<String> =
279 router.categories.keys().filter(|&k| k.starts_with(&chk)).cloned().collect();
280 sub_categories.sort();
281
282 // Go through sub-categories
283 let mut table: IndexMap<String, String> = indexmap![];
284 for full_alias in sub_categories {
285 let alias = full_alias.trim_start_matches(&chk).to_string();
286 if alias.contains(" ") {
287 continue;
288 }
289 let desc = router.categories.get(&full_alias).unwrap().description.to_string();
290
291 table.insert(alias, desc.clone());
292 }
293
294 // Get commands to display
295 let mut keys: Vec<String> =
296 router.commands.keys().filter(|&k| k.starts_with(&chk)).cloned().collect();
297 keys.sort();
298
299 // GO through commands
300 for full_alias in keys {
301 let alias = full_alias.trim_start_matches(&chk).to_string();
302 if alias.contains(" ") {
303 continue;
304 }
305 let cmd = router.commands.get(&full_alias).unwrap();
306 let cmd_help = cmd.help();
307 table.insert(alias, cmd_help.description);
308 }
309
310 // Display commands
311 cli_sendln!("AVAILABLE COMMANDS\n");
312 cli_display_array(&table);
313 cli_sendln!("-- END --\n");
314 std::process::exit(0);
315 }
316}