simple_cli/
lib.rs

1#![feature(int_roundings)]
2
3use std::{any::type_name, fmt::Display, io, str::FromStr};
4
5fn print_prompt(prompt: Option<&str>) -> bool {
6    match prompt {
7        Some(input_prompt) => {
8            println!("{}", input_prompt);
9            return true;
10        }
11        None => {
12            return false;
13        }
14    }
15}
16
17fn check_length(length: &usize, max_length: Option<i32>) -> bool {
18    match max_length {
19        Some(max) => {
20            let input_length = *length as i32;
21            if input_length > max {
22                println!(
23                    "Your input is {} characters higher than the {} character limit. Please try again.",
24                    input_length - max,
25                    length
26                );
27                return false;
28            } else {
29                return true;
30            }
31        }
32        None => return true,
33    }
34}
35
36fn check_empty(length: &usize, can_be_empty: bool) -> bool {
37    let input_length = *length as i32;
38    if input_length <= 0 && !can_be_empty {
39        println!("Your input cannot be empty.");
40        return false;
41    } else {
42        return true;
43    }
44}
45
46fn check_min_max<T: PartialOrd + Display>(
47    number: T,
48    min_value: Option<T>,
49    max_value: Option<T>,
50) -> bool {
51    match min_value {
52        Some(min) => {
53            if number < min {
54                println!(
55                    "Your input ({}) is lower than the minimum allowed value of {}.",
56                    number, min
57                );
58                return false;
59            }
60        }
61        None => {}
62    }
63    match max_value {
64        Some(max) => {
65            if number > max {
66                println!(
67                    "Your input ({}) is larger than the maximum allowed value of {}.",
68                    number, max
69                );
70                return false;
71            }
72        }
73        None => {}
74    }
75    return true;
76}
77
78fn check_number_is_a_choice<T: PartialOrd + Display>(
79    number: &T,
80    choices: &Vec<T>,
81    show_choices_on_failure: bool,
82) -> bool {
83    for choice in choices.iter() {
84        if number == choice {
85            return true;
86        }
87    }
88    if show_choices_on_failure {
89        print!("Your input ({}) is not an option of the choices: ", number);
90        for choice in choices.iter() {
91            print!("{}, ", choice);
92        }
93        print!("\n");
94    } else {
95        println!("Your input ({}) is not a valid choice.", number);
96    }
97
98    return false;
99}
100
101fn check_string_is_a_choice(
102    input: &String,
103    choices: &Vec<&str>,
104    case_sensitive: bool,
105    show_choices_on_failure: bool,
106) -> bool {
107    for choice in choices.iter() {
108        if input == choice {
109            return true;
110        } else if input.to_lowercase() == choice.to_lowercase() && !case_sensitive {
111            return true;
112        }
113    }
114    if show_choices_on_failure {
115        print!("Your input ({}) is not an option of the choices: ", input);
116        for choice in choices.iter() {
117            print!("{}, ", choice);
118        }
119        print!("\n");
120    } else {
121        print!("Your input ({}) is not a valid choice. ", input);
122    }
123    print!("(Case Sensitive: {})\n", case_sensitive);
124    return false;
125}
126
127/// Displays a list of items.
128///
129/// # Arguments
130///
131/// * `header_message` - An option that can contain a string slice which holds a header message for the paginated list.
132/// * `items` - An array of items of a type with 'Display' trait
133///
134/// # Example
135///
136/// ```
137/// use simple_cli::*;
138/// let items = vec!["Moe", "Larry", "Curly"];
139/// print_list(Some("My list:"), &items);
140///
141/// ```
142pub fn print_list<T: Display>(header_message: Option<&str>, items: &[T]) {
143    print_prompt(header_message);
144    for i in 0..items.len() {
145        println!("{}", items[i])
146    }
147}
148
149/// Clears all printed lines from the terminal.
150///
151/// # Example
152///
153/// ```no_run
154/// use simple_cli::*;
155/// clear_terminal();
156/// ```
157pub fn clear_terminal() {
158    print!("{esc}c", esc = 27 as char);
159}
160
161/// Displays a paginated list of items.
162///
163/// # Arguments
164///
165/// * `header_message` - An option that can contain a string slice which holds a header message for the paginated list.
166/// * `items` - An array of items of a type with 'Display' trait
167/// * `items_per_page` - The number of items that will be displayed per page.
168/// * `clear_on_update` - A boolean which denotes whether the terminal should clear each time the user navigates to a new page. This is helpful when making command-line apps that "re-render" a single display.
169///
170/// # Examples
171///
172/// ```no_run
173/// use simple_cli::*;
174/// let items = vec!["Moe", "Larry", "Curly"];
175/// paginated_list(Some("Here is my paginated list:"), &items, 2, true);
176///
177/// use simple_cli::*;
178/// let items = vec![1, 2, 3, 4, 5, 6, 7, 8, 9];
179/// paginated_list::<i8>(Some("Here is my paginated list:"), &items, 2, true);
180/// ```
181pub fn paginated_list<T: Display>(
182    header_message: Option<&str>,
183    items: &[T],
184    items_per_page: i32,
185    clear_on_update: bool,
186) {
187    if items_per_page <= 0 {
188        panic!("Items per page must be greater than zero.");
189    }
190    let mut quit = false;
191    let number_of_items = items.len() as i32;
192    let mut current_page: i32 = 1;
193    let mut number_of_pages: i32 = number_of_items.div_ceil(items_per_page);
194    if number_of_pages == 0 {
195        number_of_pages = 1;
196    }
197    while !quit {
198        print_prompt(header_message);
199        let end_index: i32;
200        if current_page == number_of_pages {
201            end_index = number_of_items;
202        } else {
203            end_index = current_page * items_per_page;
204        }
205        if number_of_items > 0 {
206            for i in ((current_page - 1) * items_per_page)..end_index {
207                println!("{}", items[i as usize]);
208            }
209        }
210        println!("(Page {} of {})", current_page, number_of_pages);
211        let user_input = select_string_from_choices(
212            Some("Press N to view the next page, P for previous, S for a specific page, or E to Exit."),
213            Some("Press N to view the next page, P for previous, S for a specific page, or E to Exit."),
214            vec!["N", "P", "S", "E"],
215            false,
216            true
217        );
218        match user_input.to_lowercase().as_str() {
219            "n" => {
220                if current_page < number_of_pages {
221                    current_page += 1;
222                }
223            }
224            "p" => {
225                if current_page > 1 {
226                    current_page -= 1;
227                }
228            }
229            "s" => {
230                current_page = select_number_from_choices(
231                    Some("Enter the page you would like to view."),
232                    Some("Enter the page you would like to view."),
233                    (1..(number_of_pages + 1)).collect(),
234                    false,
235                );
236            }
237            "e" => {
238                quit = true;
239            }
240            _ => {}
241        }
242        if clear_on_update {
243            clear_terminal();
244        }
245    }
246}
247
248/// Prompts the user for a string input and returns it.
249///
250/// # Arguments
251///
252/// * `prompt` - An option that can contain a string slice which holds the prompt to present the user with.
253/// * `repeat_message` - An option that can contain a string slice which holds a repeat message which will be displayed if the user enters invalid input
254/// * `max_length` - An option that can contain a integer which specifies the maximum length the user's input can reach.
255/// * `can_be_empty` - A boolean which denotes whether the user's input can be an empty string.
256///
257/// # Example
258///
259/// ```
260/// use simple_cli::*;
261/// let input = get_string(Some("Enter your name:"), Some("Enter your name:"), Some(25), false);
262/// ```
263pub fn get_string(
264    prompt: Option<&str>,
265    repeat_message: Option<&str>,
266    max_length: Option<i32>,
267    can_be_empty: bool,
268) -> String {
269    print_prompt(prompt);
270    let mut input = String::new();
271    loop {
272        match io::stdin().read_line(&mut input) {
273            Ok(_n) => {
274                let trimmed_input = input.trim();
275                let length = trimmed_input.len();
276                if check_length(&length, max_length) && check_empty(&length, can_be_empty) {
277                    return trimmed_input.to_string();
278                }
279            }
280            Err(error) => panic!("Unexpected stdin error while reading input: {}", error),
281        }
282        input.clear();
283        print_prompt(repeat_message);
284    }
285}
286
287/// Prompts the user for a number input and returns it.
288///
289/// # Arguments
290///
291/// * `prompt` - An option that can contain a string slice which holds the prompt to present the user with.
292/// * `repeat_message` - An option that can contain a string slice which holds a repeat message which will be displayed if the user enters invalid input
293/// * `min_value` - An option that can contain a number of type T which specifies the minimum value the user can input.
294/// * `max_value` - An option that can contain a number of type T which specifies the maximum value the user can input.
295///
296/// # Example
297///
298/// ```
299/// use simple_cli::*;
300/// let input = get_number::<i8>(Some("Enter an integer from 0 to 10:"), None, Some(0), Some(10));
301///
302/// let float_input = get_number::<f32>(Some("Enter a float from 0 to 10:"), None, Some(0.0), Some(10.0));
303/// ```
304pub fn get_number<T: PartialOrd + Display + FromStr + Copy>(
305    prompt: Option<&str>,
306    repeat_message: Option<&str>,
307    min_value: Option<T>,
308    max_value: Option<T>,
309) -> T {
310    print_prompt(prompt);
311    let mut input = String::new();
312    loop {
313        match io::stdin().read_line(&mut input) {
314            Ok(_n) => match input.trim().parse::<T>() {
315                Ok(number) => {
316                    if check_min_max(number, min_value, max_value) {
317                        return number;
318                    }
319                }
320                Err(_e) => {
321                    println!("Please enter a valid {} value.", type_name::<T>());
322                }
323            },
324            Err(error) => panic!("Unexpected stdin error while reading input: {}", error),
325        }
326        input.clear();
327        print_prompt(repeat_message);
328    }
329}
330
331/// Prompts the user to input a number from a selection of number choices, and returns the number the user selected. Panics if there are no numbers in the vector passed into the function.
332///
333/// # Arguments
334///
335/// * `prompt` - An option that can contain a string slice which holds the prompt to present the user with.
336/// * `repeat_message` - An option that can contain a string slice which holds a repeat message which will be displayed if the user enters invalid input
337/// * `choices` - A vector of numbers of type T which make up the choices the user can select from.
338/// * `show_choices_on_failure` - Whether or not to show the available choices after invalid input.
339///
340/// # Example
341///
342/// ```
343/// use simple_cli::*;
344/// let choices: Vec<i8> = vec![1,2,3];
345/// let choice = select_number_from_choices::<i8>(Some("Enter 1, 2 or 3"), None, choices, true);
346///
347/// ```
348pub fn select_number_from_choices<T: PartialOrd + Display + FromStr + Copy>(
349    prompt: Option<&str>,
350    repeat_message: Option<&str>,
351    choices: Vec<T>,
352    show_choices_on_failure: bool,
353) -> T {
354    if choices.len() == 0 {
355        panic!("You have not supplied a vector of at least one integer choices.")
356    }
357
358    print_prompt(prompt);
359    let mut input = String::new();
360    loop {
361        match io::stdin().read_line(&mut input) {
362            Ok(_n) => match input.trim().parse::<T>() {
363                Ok(number) => {
364                    if check_number_is_a_choice(&number, &choices, show_choices_on_failure) {
365                        return number;
366                    }
367                }
368                Err(_e) => {
369                    println!("Please enter a valid {} value.", type_name::<T>());
370                }
371            },
372            Err(error) => panic!("Unexpected stdin error while reading input: {}", error),
373        }
374        input.clear();
375        print_prompt(repeat_message);
376    }
377}
378
379/// Prompts the user to input a string from a selection of string choices, and returns the string the user selected. Panics if there are no strings in the choices vector passed into the function.
380///
381/// # Arguments
382///
383/// * `prompt` - An option that can contain a string slice which holds the prompt to present the user with.
384/// * `repeat_message` - An option that can contain a string slice which holds a repeat message which will be displayed if the user enters invalid input
385/// * `choices` - A vector of string slices which make up the choices the user can select from.
386/// * `case_sensitive` - A boolean which represents whether the user's input is case-sensitive.
387/// * `show_choices_on_failure` - Whether or not to show the available choices after invalid input.
388///
389/// # Example
390///
391/// ```
392/// use simple_cli::*;
393/// let choices = vec!["Moe", "Larry", "Curly"];
394/// let choice = select_string_from_choices(Some("Select Moe, Larry, or Curly"), None, choices, false, true);
395///
396/// ```
397pub fn select_string_from_choices(
398    prompt: Option<&str>,
399    repeat_message: Option<&str>,
400    choices: Vec<&str>,
401    case_sensitive: bool,
402    show_choices_on_failure: bool,
403) -> String {
404    if choices.len() == 0 {
405        panic!("You have not supplied a vector of at least one string choices.")
406    }
407    print_prompt(prompt);
408    let mut input = String::new();
409    loop {
410        match io::stdin().read_line(&mut input) {
411            Ok(_n) => {
412                let trimmed = input.trim().to_string();
413                if check_string_is_a_choice(
414                    &trimmed,
415                    &choices,
416                    case_sensitive,
417                    show_choices_on_failure,
418                ) {
419                    return trimmed;
420                }
421            }
422            Err(error) => panic!("Unexpected stdin error while reading input: {}", error),
423        }
424        input.clear();
425        print_prompt(repeat_message);
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_print_prompt() {
435        let no_prompt: Option<&str> = None;
436        let some_prompt: Option<&str> = Some("Test Message.");
437        assert_eq!(print_prompt(no_prompt), false);
438        assert_eq!(print_prompt(some_prompt), true);
439    }
440
441    #[test]
442    fn test_check_length() {
443        let no_max_length: Option<i32> = None;
444        let yes_max_length: Option<i32> = Some(10);
445        let small_string = "hi";
446        let big_string = "abcuiwehfuewnfiuewnf";
447        assert_eq!(check_length(&small_string.len(), no_max_length), true);
448        assert_eq!(check_length(&small_string.len(), yes_max_length), true);
449        assert_eq!(check_length(&big_string.len(), yes_max_length), false);
450    }
451
452    #[test]
453    fn test_check_empty() {
454        let empty_string = "";
455        let non_empty_string = "Hello!";
456        assert_eq!(check_empty(&empty_string.len(), true), true);
457        assert_eq!(check_empty(&empty_string.len(), false), false);
458        assert_eq!(check_empty(&non_empty_string.len(), false), true);
459        assert_eq!(check_empty(&non_empty_string.len(), true), true);
460    }
461
462    #[test]
463    fn test_min_max() {
464        let no_min: Option<i32> = None;
465        let min: Option<i32> = Some(1);
466        let no_max: Option<i32> = None;
467        let max: Option<i32> = Some(3);
468        let no_min_2: Option<f32> = None;
469        let min_2: Option<f32> = Some(1.5);
470        let no_max_2: Option<f32> = None;
471        let max_2: Option<f32> = Some(3.5);
472        assert_eq!(check_min_max(5, no_min, no_max), true);
473        assert_eq!(check_min_max(-5, min, no_max), false);
474        assert_eq!(check_min_max(-5, no_min, max), true);
475        assert_eq!(check_min_max(5, no_min, max), false);
476        assert_eq!(check_min_max(5, min, max), false);
477        assert_eq!(check_min_max(2, min, max), true);
478        assert_eq!(check_min_max(5.0, no_min_2, no_max_2), true);
479        assert_eq!(check_min_max(-5.0, min_2, no_max_2), false);
480        assert_eq!(check_min_max(-5.0, no_min_2, max_2), true);
481        assert_eq!(check_min_max(5.0, no_min_2, max_2), false);
482        assert_eq!(check_min_max(5.0, min_2, max_2), false);
483        assert_eq!(check_min_max(2.0, min_2, max_2), true);
484    }
485
486    #[test]
487    fn test_check_string_is_choice() {
488        let choices = vec!["Earl", "Roger", "Mark"];
489        let bob = String::from("Bob");
490        let earl_uppercase = String::from("EARL");
491        let mark = String::from("Mark");
492        assert_eq!(check_string_is_a_choice(&bob, &choices, false, true), false);
493        assert_eq!(check_string_is_a_choice(&bob, &choices, true, true), false);
494        assert_eq!(
495            check_string_is_a_choice(&earl_uppercase, &choices, false, true),
496            true
497        );
498        assert_eq!(
499            check_string_is_a_choice(&earl_uppercase, &choices, true, false),
500            false
501        );
502        assert_eq!(check_string_is_a_choice(&mark, &choices, true, false), true);
503    }
504
505    #[test]
506    fn test_check_num_is_choice() {
507        let choices = vec![1, 5, 10, 15];
508        let choices_float = vec![0.5, 1.5, 2.0, 3.35];
509        assert_eq!(check_number_is_a_choice(&1, &choices, true), true);
510        assert_eq!(check_number_is_a_choice(&5, &choices, false), true);
511        assert_eq!(check_number_is_a_choice(&10, &choices, true), true);
512        assert_eq!(check_number_is_a_choice(&15, &choices, false), true);
513        assert_eq!(check_number_is_a_choice(&-50, &choices, true), false);
514        assert_eq!(check_number_is_a_choice(&0.5, &choices_float, false), true);
515        assert_eq!(check_number_is_a_choice(&1.5, &choices_float, true), true);
516        assert_eq!(check_number_is_a_choice(&2.0, &choices_float, false), true);
517        assert_eq!(check_number_is_a_choice(&3.35, &choices_float, true), true);
518        assert_eq!(
519            check_number_is_a_choice(&-5.5, &choices_float, false),
520            false
521        );
522    }
523}