falcon_cli/
lib.rs

1#![crate_type = "lib"]
2//#[allow(unused_assignments)]
3#![allow(clippy::borrowed_box)]
4//
5// Easily and efficiently develop high quality and fully featured CLI apps.  Supports the following:
6//
7// * Excellent and easily accessible functions to send wordwrapped text, send a header, get user input, get password, get new password (ie. enter twice to confirm), get y/n confirmation, display table layout ala mySQL prompt, display two column borderless table, and more.
8// * Easily add routes to new CLI commands.  Simply define the impl of the CLI command, the command alias, shortcuts, and any flags that will contain values.
9// * Built-in help screens for every CLI command
10// * Utilizes the levenshtein algorithm to automatically identify and correct typos within command names.
11// * Categorize commands to one or more levels for better organization.
12//
13//  For full usage details and code examples, please visit the [Readme file](https://github.com/mdizak/rust-aalcon-cli/).
14//
15
16use help::CliHelpScreen;
17pub use indexmap::{indexmap, IndexMap};
18pub use textwrap::Options as Textwrap_Options;
19pub use textwrap::fill as Textwrap_Fill;
20use router::CliRouter;
21use rpassword::read_password;
22use std::collections::HashMap;
23pub use std::io;
24pub use std::io::Write;
25use zxcvbn::zxcvbn;
26
27pub mod help;
28pub mod router;
29
30pub trait CliCommand {
31    fn process(&self, args: Vec<String>, flags: Vec<String>, value_flags: HashMap<String, String>);
32    fn help(&self) -> CliHelpScreen;
33}
34
35// Execute the necessary CLI command based on arguments passed.  This function should be executed once all necessary outes have been defined.
36pub fn cli_run(router: &CliRouter) {
37    // Lookup route
38    let (cmd, req) = router.lookup();
39
40    // Process as needed
41    if req.is_help {
42        CliHelpScreen::render(cmd, &req.cmd_alias, &req.shortcuts);
43    } else {
44        cmd.process(req.args, req.flags, req.value_flags);
45    }
46}
47
48// Display header.  Outputs given text with 30 line of dashes at the top and bottom to signify a header.
49pub fn cli_header(text: &str) {
50    println!("------------------------------");
51    println!("-- {}", text);
52    println!("------------------------------\n");
53}
54
55/// utput text wordwrapped to 70 characters per-line.
56#[macro_export]
57macro_rules! cli_send {
58    ($text:expr) => {
59        let wrapped_text = Textwrap_Fill($text, Textwrap_Options::new(75));
60        print!("{}", wrapped_text);
61        io::stdout().flush().unwrap();
62    };
63    ($text:expr, $( $arg:expr ),*) => {
64
65        // Gather args
66        let mut args = vec![];
67        $( args.push($arg.to_string()); )*
68
69        // Replace placeholders
70        let mut text: String = $text.to_string();
71        for arg in args {
72            text = text.replacen("{}", arg.to_string().as_str(), 1);
73        }
74
75        // DIsplay text
76        let wrapped_text = Textwrap_Fill(text.as_str(), Textwrap_Options::new(75));
77        print!("{}", wrapped_text);
78        io::stdout().flush().unwrap();
79    };
80}
81
82
83// Pass an IndexMap (similar to HashMap but remains ordered) and will return the option the user selects.  Will not return until user submits valid option value.
84pub fn cli_get_option(question: &str, options: &IndexMap<String, String>) -> String {
85    let message = format!("{}\r\n\r\n", question);
86    cli_send!(&message);
87    for (key, value) in options.iter() {
88        let line = format!("    [{}] {}\r\n", key, value);
89        cli_send!(&line);
90    }
91    cli_send!("\r\nSelect One: ");
92
93    // Get user input
94    let mut input: String;
95    loop {
96        input = String::new();
97
98        io::stdin()
99            .read_line(&mut input)
100            .expect("Failed to read line");
101        let input = input.trim();
102        if !options.contains_key(input) {
103            print!("\r\nInvalid option, try again: ");
104            io::stdout().flush().unwrap();
105        } else {
106            break;
107        }
108    }
109
110    input.trim().to_string()
111}
112
113// Get input from the user
114pub fn cli_get_input(message: &str, default_value: &str) -> String {
115    // Display message
116    cli_send!(message);
117    io::stdout().flush().unwrap();
118
119    // Get user input
120    let mut input = String::new();
121    io::stdin()
122        .read_line(&mut input)
123        .expect("Failed to read line");
124    let mut input = input.trim();
125
126    // Default value, if needed
127    if input.trim().is_empty() {
128        input = default_value;
129    }
130
131    String::from(input)
132}
133
134// Request confirmation from the user
135pub fn cli_confirm(message: &str) -> bool {
136    // Send message
137    let confirm_message = format!("{} (y/n): ", message);
138    cli_send!(&confirm_message);
139
140    // Get user input
141    let mut _input = "".to_string();
142    loop {
143        _input = String::new();
144
145        io::stdin()
146            .read_line(&mut _input)
147            .expect("Failed to read line");
148        let _input = _input.trim().to_lowercase();
149
150        if _input != "y" && _input != "n" {
151            cli_send!("Invalid option, please try again.  Enter (y/n): ");
152        } else {
153            break;
154        }
155    }
156
157    // Return
158    let res_char = _input.chars().next().unwrap();
159
160    res_char == 'y'
161}
162
163// Get a single password without the user's input being output to the terminal.
164pub fn cli_get_password(message: &str) -> String {
165    // Get message
166    let password_message = if message.is_empty() {
167        "Password: "
168    } else {
169        message
170    };
171
172    // Get password
173    let mut _password = String::new();
174    loop {
175        cli_send!(password_message);
176        _password = read_password().unwrap();
177
178        if _password.is_empty() {
179            cli_send!("You did not specify a password");
180        } else {
181            break;
182        }
183    }
184
185    _password
186}
187
188// Get a new password that does both, ensures the user types it in twice to confirm and also checks for desired security strength.  The 'strength' parameter can be 0 - 4.
189pub fn cli_get_new_password(req_strength: u8) -> String {
190    // Initialize
191    let mut _password = String::new();
192    let mut _confirm_password = String::new();
193
194    // Get new password
195    loop {
196        cli_send!("Desired Password: ");
197        _password = read_password().unwrap();
198
199        if _password.is_empty() {
200            cli_send!("You did not specify a password");
201            continue;
202        }
203
204        // Check strength
205        let strength = zxcvbn(&_password, &[]).unwrap();
206        if strength.score() < req_strength {
207            cli_send!("Password is not strong enough.  Please try again.\n\n");
208            continue;
209        }
210
211        // Confirm password
212        cli_send!("Confirm Password: ");
213        _confirm_password = read_password().unwrap();
214        if _password != _confirm_password {
215            cli_send!("Passwords do not match, please try again.\n\n");
216            continue;
217        }
218        break;
219    }
220
221    _password
222}
223
224// Display a tabular output similar to any SQL database prompt's output
225pub fn cli_display_table(columns: Vec<&str>, rows: Vec<Vec<&str>>) {
226    // Return if no rows
227    if rows.is_empty() {
228        cli_send!("No rows to display.\n\n");
229        return;
230    }
231
232    // nitialize sizes
233    let mut sizes: HashMap<&str, usize> = HashMap::new();
234    for col in columns.as_slice() {
235        sizes.insert(col, 0);
236    }
237
238    // Get sizes of columns
239    for _row in rows.clone() {
240        for col in columns.as_slice() {
241            let num: usize = col.len();
242            if num > sizes[col] {
243                sizes.insert(col, num + 3);
244            }
245        }
246    }
247
248    // Initialize header variables
249    let mut header = String::from("+");
250    let mut col_header = String::from("|");
251
252    // Print column headers
253    for col in columns.clone() {
254        let padded_col = format!("{}{}", col, " ".repeat(sizes[col] - col.len()));
255        header = header + "-".repeat(sizes[col] + 1).as_str() + "+";
256        col_header += format!(" {}|", padded_col).as_str();
257    }
258    println!("{}\n{}\n{}", header, col_header, header);
259
260    // Display the rows
261    for row in rows {
262        // Go through values
263        let mut line = String::from("|");
264        for (i, val) in row.into_iter().enumerate() {
265            let padded_val = format!(" {}{}", val, " ".repeat(sizes[columns[i]] - val.len()));
266            line += format!("{}|", padded_val).as_str();
267        }
268        println!("{}", line);
269    }
270    println!("{}\n", header);
271}
272
273// Display a two column array with proper spacing.  Mainly used for the help() function to display available parameters and flags.
274pub fn cli_display_array(rows: &IndexMap<String, String>) {
275    // Get max left column size
276    let mut size = 0;
277    for key in rows.keys() {
278        if key.len() + 8 > size {
279            size = key.len() + 8;
280        }
281    }
282    let indent = " ".repeat(size);
283    let indent_size = size - 4;
284
285    // Go through rows
286    for (key, value) in rows {
287        let left_col = format!("    {}{}", key, " ".repeat(indent_size - key.len()));
288        let options = textwrap::Options::new(75)
289            .initial_indent(&left_col)
290            .subsequent_indent(&indent);
291        let line = textwrap::fill(value, &options);
292        println!("{}", line);
293    }
294    cli_send!("\r\n");
295}
296
297// Give an error message, followed by exiting with status of 1.
298#[macro_export]
299macro_rules! cli_error {
300    ($text:expr) => {
301        let wrapped_text = Textwrap_Fill(format!("ERROR: {}", $text).as_str(), Textwrap_Options::new(75));
302        print!("{}\r\n", wrapped_text);
303        io::stdout().flush().unwrap();
304    };
305    ($text:expr, $( $arg:expr ),*) => {
306
307        // Gather args
308        let mut args = vec![];
309        $( args.push($arg.to_string()); )*
310
311        // Replace placeholders
312        let mut text: String = $text.to_string();
313        for arg in args {
314            text = text.replacen("{}", arg.to_string().as_str(), 1);
315        }
316
317        // DIsplay text
318        let wrapped_text = Textwrap_Fill(format!("ERROR: {}", text).as_str(), Textwrap_Options::new(75));
319        print!("{}\r\n", wrapped_text);
320        io::stdout().flush().unwrap();
321    };
322}
323
324
325// Output success message that displays a vector of filenames or anything else indented below the message.
326pub fn cli_success (message: &str, indented_lines: Vec<&str>) {
327    cli_send!(&message);
328    cli_send!("\r\n");
329    for line in indented_lines {
330        println!("    {}", line);
331    }
332    cli_send!("\r\n");
333}
334
335
336// Clear all lines within terminal and revert to blank terminal screen.
337pub fn cli_clear_screen() {
338    print!("\x1B[2J");
339}