1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
#![crate_type = "lib"]
//#[allow(unused_assignments)]
#![allow(clippy::borrowed_box)]
//
// Easily and efficiently develop high quality and fully featured CLI apps.  Supports the following:
//
// * 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.
// * 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.
// * Built-in help screens for every CLI command
// * Utilizes the levenshtein algorithm to automatically identify and correct typos within command names.
// * Categorize commands to one or more levels for better organization.
//
//  For full usage details and code examples, please visit the [Readme file](https://github.com/mdizak/rust-aalcon-cli/).
//

use help::CliHelpScreen;
pub use indexmap::{indexmap, IndexMap};
pub use textwrap::Options as Textwrap_Options;
pub use textwrap::fill as Textwrap_Fill;
use router::CliRouter;
use rpassword::read_password;
use std::collections::HashMap;
pub use std::io;
pub use std::io::Write;
use zxcvbn::zxcvbn;

pub mod help;
pub mod router;

pub trait CliCommand {
    fn process(&self, args: Vec<String>, flags: Vec<String>, value_flags: HashMap<String, String>);
    fn help(&self) -> CliHelpScreen;
}

// Execute the necessary CLI command based on arguments passed.  This function should be executed once all necessary outes have been defined.
pub fn cli_run(router: &CliRouter) {
    // Lookup route
    let (cmd, req) = router.lookup();

    // Process as needed
    if req.is_help {
        CliHelpScreen::render(cmd, &req.cmd_alias, &req.shortcuts);
    } else {
        cmd.process(req.args, req.flags, req.value_flags);
    }
}

// Display header.  Outputs given text with 30 line of dashes at the top and bottom to signify a header.
pub fn cli_header(text: &str) {
    println!("------------------------------");
    println!("-- {}", text);
    println!("------------------------------\n");
}

/// utput text wordwrapped to 70 characters per-line.
#[macro_export]
macro_rules! cli_send {
    ($text:expr) => {
        let wrapped_text = Textwrap_Fill($text, Textwrap_Options::new(75));
        print!("{}", wrapped_text);
        io::stdout().flush().unwrap();
    };
    ($text:expr, $( $arg:expr ),*) => {

        // Gather args
        let mut args = vec![];
        $( args.push($arg.to_string()); )*

        // Replace placeholders
        let mut text: String = $text.to_string();
        for arg in args {
            text = text.replacen("{}", arg.to_string().as_str(), 1);
        }

        // DIsplay text
        let wrapped_text = Textwrap_Fill(text.as_str(), Textwrap_Options::new(75));
        print!("{}", wrapped_text);
        io::stdout().flush().unwrap();
    };
}


// 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.
pub fn cli_get_option(question: &str, options: &IndexMap<String, String>) -> String {
    let message = format!("{}\r\n\r\n", question);
    cli_send!(&message);
    for (key, value) in options.iter() {
        let line = format!("    [{}] {}\r\n", key, value);
        cli_send!(&line);
    }
    cli_send!("\r\nSelect One: ");

    // Get user input
    let mut input: String;
    loop {
        input = String::new();

        io::stdin()
            .read_line(&mut input)
            .expect("Failed to read line");
        let input = input.trim();
        if !options.contains_key(input) {
            print!("\r\nInvalid option, try again: ");
            io::stdout().flush().unwrap();
        } else {
            break;
        }
    }

    input
}

// Get input from the user
pub fn cli_get_input(message: &str, default_value: &str) -> String {
    // Display message
    cli_send!(message);
    io::stdout().flush().unwrap();

    // Get user input
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    let mut input = input.trim();

    // Default value, if needed
    if input.trim().is_empty() {
        input = default_value;
    }

    String::from(input)
}

// Request confirmation from the user
pub fn cli_confirm(message: &str) -> bool {
    // Send message
    let confirm_message = format!("{} (y/n): ", message);
    cli_send!(&confirm_message);

    // Get user input
    let mut _input = "".to_string();
    loop {
        _input = String::new();

        io::stdin()
            .read_line(&mut _input)
            .expect("Failed to read line");
        let _input = _input.trim().to_lowercase();

        if _input != "y" && _input != "n" {
            cli_send!("Invalid option, please try again.  Enter (y/n): ");
        } else {
            break;
        }
    }

    // Return
    let res_char = _input.chars().next().unwrap();

    res_char == 'y'
}

// Get a single password without the user's input being output to the terminal.
pub fn cli_get_password(message: &str) -> String {
    // Get message
    let password_message = if message.is_empty() {
        "Password: "
    } else {
        message
    };

    // Get password
    let mut _password = String::new();
    loop {
        cli_send!(password_message);
        _password = read_password().unwrap();

        if _password.is_empty() {
            cli_send!("You did not specify a password");
        } else {
            break;
        }
    }

    _password
}

// 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.
pub fn cli_get_new_password(req_strength: u8) -> String {
    // Initialize
    let mut _password = String::new();
    let mut _confirm_password = String::new();

    // Get new password
    loop {
        cli_send!("Desired Password: ");
        _password = read_password().unwrap();

        if _password.is_empty() {
            cli_send!("You did not specify a password");
            continue;
        }

        // Check strength
        let strength = zxcvbn(&_password, &[]).unwrap();
        if strength.score() < req_strength {
            cli_send!("Password is not strong enough.  Please try again.\n\n");
            continue;
        }

        // Confirm password
        cli_send!("Confirm Password: ");
        _confirm_password = read_password().unwrap();
        if _password != _confirm_password {
            cli_send!("Passwords do not match, please try again.\n\n");
            continue;
        }
        break;
    }

    _password
}

// Display a tabular output similar to any SQL database prompt's output
pub fn cli_display_table(columns: Vec<&str>, rows: Vec<Vec<&str>>) {
    // Return if no rows
    if rows.is_empty() {
        cli_send!("No rows to display.\n\n");
        return;
    }

    // nitialize sizes
    let mut sizes: HashMap<&str, usize> = HashMap::new();
    for col in columns.as_slice() {
        sizes.insert(col, 0);
    }

    // Get sizes of columns
    for _row in rows.clone() {
        for col in columns.as_slice() {
            let num: usize = col.len();
            if num > sizes[col] {
                sizes.insert(col, num + 3);
            }
        }
    }

    // Initialize header variables
    let mut header = String::from("+");
    let mut col_header = String::from("|");

    // Print column headers
    for col in columns.clone() {
        let padded_col = format!("{}{}", col, " ".repeat(sizes[col] - col.len()));
        header = header + "-".repeat(sizes[col] + 1).as_str() + "+";
        col_header += format!(" {}|", padded_col).as_str();
    }
    println!("{}\n{}\n{}", header, col_header, header);

    // Display the rows
    for row in rows {
        // Go through values
        let mut line = String::from("|");
        for (i, val) in row.into_iter().enumerate() {
            let padded_val = format!(" {}{}", val, " ".repeat(sizes[columns[i]] - val.len()));
            line += format!("{}|", padded_val).as_str();
        }
        println!("{}", line);
    }
    println!("{}\n", header);
}

// Display a two column array with proper spacing.  Mainly used for the help() function to display available parameters and flags.
pub fn cli_display_array(rows: &IndexMap<String, String>) {
    // Get max left column size
    let mut size = 0;
    for key in rows.keys() {
        if key.len() + 8 > size {
            size = key.len() + 8;
        }
    }
    let indent = " ".repeat(size);
    let indent_size = size - 4;

    // Go through rows
    for (key, value) in rows {
        let left_col = format!("    {}{}", key, " ".repeat(indent_size - key.len()));
        let options = textwrap::Options::new(75)
            .initial_indent(&left_col)
            .subsequent_indent(&indent);
        let line = textwrap::fill(value, &options);
        println!("{}", line);
    }
    cli_send!("\r\n");
}

// Give an error message, followed by exiting with status of 1.
#[macro_export]
macro_rules! cli_error {
    ($text:expr) => {
        let wrapped_text = Textwrap_Fill(format!("ERROR: {}", $text).as_str(), Textwrap_Options::new(75));
        print!("{}\r\n", wrapped_text);
        io::stdout().flush().unwrap();
    };
    ($text:expr, $( $arg:expr ),*) => {

        // Gather args
        let mut args = vec![];
        $( args.push($arg.to_string()); )*

        // Replace placeholders
        let mut text: String = $text.to_string();
        for arg in args {
            text = text.replacen("{}", arg.to_string().as_str(), 1);
        }

        // DIsplay text
        let wrapped_text = Textwrap_Fill(format!("ERROR: {}", text).as_str(), Textwrap_Options::new(75));
        print!("{}\r\n", wrapped_text);
        io::stdout().flush().unwrap();
    };
}


// Output success message that displays a vector of filenames or anything else indented below the message.
pub fn cli_success (message: &str, indented_lines: Vec<&str>) {
    cli_send!(&message);
    cli_send!("\r\n");
    for line in indented_lines {
        println!("    {}", line);
    }
    cli_send!("\r\n");
}


// Clear all lines within terminal and revert to blank terminal screen.
pub fn cli_clear_screen() {
    print!("\x1B[2J");
}