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}