console_utils/
input.rs

1//! Input Utilities
2//!
3//! This module provides functions for handling user input in console applications, including reading user input,
4//! selecting options from a list, displaying spinners, and gradually revealing strings.
5use std::{io, str::FromStr, thread, time::Duration};
6
7use crate::{
8    control::{clear_line, flush, move_cursor_down, move_cursor_up, Visibility},
9    read::{read_key, Key},
10    styled::{Color, StyledText},
11};
12
13/// A Wrapper for empty inputs returning a None
14#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash, Default)]
15pub enum Empty<T> {
16    Some(T),
17    #[default]
18    None,
19}
20
21impl<T> FromStr for Empty<T>
22where
23    T: FromStr,
24    T::Err: std::fmt::Debug,
25{
26    type Err = T::Err;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        if s.trim().is_empty() {
30            Ok(Empty::None)
31        } else {
32            s.trim().parse::<T>().map(Empty::Some)
33        }
34    }
35}
36
37/// Reads user input from the console.
38///
39/// This function prompts the user with a message (`before`) and reads a line of input from the
40/// console. The input can be empty.
41///
42/// # Arguments
43///
44/// * `before` - The text to display before prompting for input. Add here `\n` for a new line.
45///
46/// # Returns
47///
48/// Returns an `T` containing the user's input converted to the specified type.
49pub fn input<T>(before: &str) -> T
50where
51    T: std::str::FromStr,
52    T::Err: std::fmt::Debug,
53{
54    loop {
55        let quest = StyledText::new("?").fg(Color::Red);
56        let caret = StyledText::new("›").fg(Color::BrightBlack);
57        print!("{quest} {before} {caret} ");
58        flush();
59
60        let mut cli = String::new();
61        io::stdin().read_line(&mut cli).unwrap();
62
63        match cli.parse() {
64            Ok(value) => return value,
65            Err(_) => {
66                let x = StyledText::new("X").fg(Color::Red);
67                println!("\n{x} Invalid Input Type\n")
68            }
69        }
70    }
71}
72
73/// Allows the user to select one option from a list using the console.
74///
75/// This function displays a list of options. The user can navigate through the
76/// options using arrow keys or 'w' and 's' keys. If the user presses Enter, the
77/// function returns the selected option.
78///
79/// # Arguments
80///
81/// * `before` - The text to display before the list of options.
82/// * `options` - A vector of strings representing the available options.
83///
84/// # Returns
85///
86/// Returns an `usize` as an index of the inputted array `options`
87pub fn select<'a>(before: &'a str, options: &'a [&'a str]) -> usize {
88    let mut i = 0;
89
90    // print everything
91    let quest = StyledText::new("?").fg(Color::Red);
92    let caret = StyledText::new("›").fg(Color::BrightBlack);
93    println!("{quest} {before} {caret} ");
94
95    populate(options, None, 0);
96
97    // hide cursor
98    let vis = Visibility::new();
99    vis.hide_cursor();
100
101    loop {
102        if let Ok(character) = read_key() {
103            match character {
104                Key::ArrowUp | Key::Char('w') | Key::Char('W') => {
105                    if i > 0 {
106                        i -= 1;
107                        populate(options, None, i);
108                    }
109                }
110                Key::ArrowDown | Key::Char('s') | Key::Char('S') => {
111                    if i < options.len() - 1 {
112                        i += 1;
113                        populate(options, None, i);
114                    }
115                }
116                Key::Enter => {
117                    break;
118                }
119                _ => {}
120            }
121        }
122    }
123
124    // reset cursor
125    move_cursor_down(options.len());
126
127    i
128}
129
130/// Allows the user to select multiple options from a list using the console.
131///
132/// This function displays a list of options with checkboxes. The user can navigate through the
133/// options using arrow keys or 'w' and 's' keys. Pressing the spacebar toggles the selection of
134/// the current option. If the user presses Enter, the function returns a vector of booleans
135/// indicating which options were selected.
136///
137/// # Arguments
138///
139/// * `before` - The text to display before the list of options.
140/// * `options` - A vector of strings representing the available options.
141///
142/// # Returns
143///
144/// Returns an `Vec<bool>` containing a vector of booleans indicating which options were
145/// selected.
146pub fn multiselect(before: &str, options: &[&str]) -> Vec<bool> {
147    let mut matrix: Vec<bool> = vec![false; options.len()];
148    let mut i = 0;
149
150    // print everything
151    let quest = StyledText::new("?").fg(Color::Red);
152    let caret = StyledText::new("›").fg(Color::BrightBlack);
153    println!("{quest} {before} {caret} ");
154
155    populate(options, Some(&matrix), 0);
156
157    // hide cursor
158    let vis = Visibility::new();
159    vis.hide_cursor();
160
161    loop {
162        if let Ok(character) = read_key() {
163            match character {
164                Key::ArrowUp | Key::Char('w') | Key::Char('W') => {
165                    if i > 0 {
166                        i -= 1;
167                        populate(options, Some(&matrix), i);
168                    }
169                }
170                Key::ArrowDown | Key::Char('s') | Key::Char('S') => {
171                    if i < options.len() - 1 {
172                        i += 1;
173                        populate(options, Some(&matrix), i);
174                    }
175                }
176                Key::Char(' ') => {
177                    move_cursor_down(i);
178                    clear_line();
179                    matrix[i] = !matrix[i];
180                    flush();
181                    move_cursor_up(i);
182                    populate(options, Some(&matrix), i);
183                }
184                Key::Enter => {
185                    break;
186                }
187                _ => {}
188            }
189        }
190    }
191
192    // reset cursor
193    move_cursor_down(options.len());
194
195    matrix
196}
197
198/// Populate function for select/multiselect
199fn populate(options: &[&str], matrix: Option<&[bool]>, cursor: usize) {
200    for (i, option) in options.iter().enumerate() {
201        clear_line();
202        if i == cursor {
203            let caret = StyledText::new("›").fg(Color::Green);
204            let option = if matrix.is_some() && matrix.unwrap()[i] {
205                StyledText::new(option).fg(Color::Green)
206            } else {
207                StyledText::new(option).fg(Color::Cyan)
208            };
209            println!(" {caret} {option}");
210        } else if matrix.is_some() && matrix.unwrap()[i] {
211            let option = StyledText::new(option).fg(Color::Green);
212            println!("   {}", option);
213        } else {
214            println!("   {}", option);
215        }
216    }
217    move_cursor_up(options.len());
218}
219
220/// Enumeration representing different types of spinners.
221#[derive(Debug, Clone)]
222pub enum SpinnerType {
223    /// Spinner with characters `/` `-` `\` `|`.
224    Standard,
225    /// Spinner with dots `.` `..` `...` `.....`.
226    Dots,
227    /// Spinner with box characters `▌` `▀` `▐` `▄`.
228    Box,
229    /// Spinner with flip characters `_` `_` `_` `-` `\` `'` `´` `-` `_` `_` `_`.
230    Flip,
231    /// Custom spinner with user-defined frames.
232    Custom(&'static [&'static str]),
233}
234
235impl SpinnerType {
236    /// Returns the frames of the spinner type.
237    pub fn frames(&self) -> &'static [&'static str] {
238        match self {
239            SpinnerType::Standard => &["/", "-", "\\", "|"],
240            SpinnerType::Dots => &[".", "..", "...", "....", "...", ".."],
241            SpinnerType::Box => &["▌", "▀", "▐", "▄"],
242            SpinnerType::Flip => &["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"],
243            SpinnerType::Custom(frames) => frames,
244        }
245    }
246}
247
248/// Displays a console-based spinner animation.
249///
250/// A spinner is a visual indicator of a long-running process. It consists of a set of frames
251/// that are displayed sequentially to create the appearance of motion.
252///
253/// # Parameters
254///
255/// - `time`: A floating-point number representing the duration of the spinner animation in seconds.
256/// - `spinner_type`: The type of spinner to display.
257pub fn spinner(mut time: f64, spinner_type: SpinnerType) {
258    let frames = spinner_type.frames();
259    let mut i = 0;
260
261    while time > 0.0 {
262        clear_line();
263        print!("{}", frames[i]);
264        flush();
265        thread::sleep(Duration::from_secs_f64(0.075));
266        time -= 0.075;
267        if i < frames.len() - 1 {
268            i += 1
269        } else {
270            i = 0
271        }
272    }
273
274    clear_line();
275}
276
277/// Reveals a string gradually, printing one character at a time with a specified time interval.
278///
279/// This function is useful for creating a typing effect or slowly displaying information to the user.
280///
281/// # Arguments
282///
283/// * `str` - The string to reveal gradually. Add here `\n` for a new line.
284/// * `time_between` - The time interval (in seconds) between each revealed character.
285pub fn reveal(str: &str, time_between: f64) {
286    for i in 0..str.len() {
287        print!("{}", str.chars().nth(i).unwrap_or(' '));
288        flush();
289        thread::sleep(Duration::from_secs_f64(time_between));
290    }
291}