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, skippable strings.
5
6use std::{
7    io,
8    str::FromStr,
9    thread,
10    time::{Duration, Instant},
11};
12
13use crate::{
14    control::{clear_line, flush, move_cursor_down, move_cursor_up, Visibility},
15    read::{key_pressed_within, read_key, Key},
16    styled::{Color, StyledText},
17};
18
19/// A Wrapper for allowing empty inputs which then return `None`.
20#[derive(Clone, Copy, Debug, Default)]
21pub struct Empty<T>(pub Option<T>);
22
23impl<T> FromStr for Empty<T>
24where
25    T: FromStr,
26    T::Err: std::fmt::Debug,
27{
28    type Err = T::Err;
29
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        if s.trim().is_empty() {
32            Ok(Empty(None))
33        } else {
34            s.trim().parse::<T>().map(|v| Empty(Some(v)))
35        }
36    }
37}
38
39/// Reads user input from the console.
40///
41/// This function prompts the user with a message (`before`) and reads a line of input from the
42/// console. The input can be empty.
43///
44/// # Arguments
45///
46/// * `before` - The text to display before prompting for input. Add here `\n` for a new line.
47///
48/// # Returns
49///
50/// Returns an `T` containing the user's input converted to the specified type.
51pub fn input<T>(before: &str) -> T
52where
53    T: std::str::FromStr,
54    T::Err: std::fmt::Debug,
55{
56    loop {
57        let quest = StyledText::new("?").fg(Color::Red);
58        let caret = StyledText::new("›").fg(Color::BrightBlack);
59        print!("{quest} {before} {caret} ");
60        flush();
61
62        let mut cli = String::new();
63        io::stdin().read_line(&mut cli).unwrap();
64
65        match cli.parse() {
66            Ok(value) => return value,
67            Err(_) => {
68                let x = StyledText::new("X").fg(Color::Red);
69                println!("\n{x} Invalid Input Type\n")
70            }
71        }
72    }
73}
74
75/// Allows the user to select one option from a list using the console.
76///
77/// This function displays a list of options. The user can navigate through the
78/// options using arrow keys or 'w' and 's' keys. If the user presses Enter, the
79/// function returns the selected option.
80///
81/// # Arguments
82///
83/// * `before` - The text to display before the list of options.
84/// * `options` - A vector of strings representing the available options.
85///
86/// # Returns
87///
88/// Returns an `usize` as an index of the inputted array `options`
89pub fn select<'a>(before: &'a str, options: &'a [&'a str]) -> usize {
90    let mut i = 0;
91
92    // print everything
93    let quest = StyledText::new("?").fg(Color::Red);
94    let caret = StyledText::new("›").fg(Color::BrightBlack);
95    println!("{quest} {before} {caret} ");
96
97    populate(options, None, 0);
98
99    // hide cursor
100    let vis = Visibility::new();
101    vis.hide_cursor();
102
103    loop {
104        if let Ok(character) = read_key() {
105            match character {
106                Key::ArrowUp | Key::Char('w') | Key::Char('W') => {
107                    if i > 0 {
108                        i -= 1;
109                        populate(options, None, i);
110                    }
111                }
112                Key::ArrowDown | Key::Char('s') | Key::Char('S') => {
113                    if i < options.len() - 1 {
114                        i += 1;
115                        populate(options, None, i);
116                    }
117                }
118                Key::Enter => {
119                    break;
120                }
121                _ => {}
122            }
123        }
124    }
125
126    // reset cursor
127    move_cursor_down(options.len());
128
129    i
130}
131
132/// Allows the user to select multiple options from a list using the console.
133///
134/// This function displays a list of options with checkboxes. The user can navigate through the
135/// options using arrow keys or 'w' and 's' keys. Pressing the spacebar toggles the selection of
136/// the current option. If the user presses Enter, the function returns a vector of booleans
137/// indicating which options were selected.
138///
139/// # Arguments
140///
141/// * `before` - The text to display before the list of options.
142/// * `options` - A vector of strings representing the available options.
143///
144/// # Returns
145///
146/// Returns an `Vec<bool>` containing a vector of booleans indicating which options were
147/// selected.
148pub fn multiselect(before: &str, options: &[&str]) -> Vec<bool> {
149    let mut matrix: Vec<bool> = vec![false; options.len()];
150    let mut i = 0;
151
152    // print everything
153    let quest = StyledText::new("?").fg(Color::Red);
154    let caret = StyledText::new("›").fg(Color::BrightBlack);
155    println!("{quest} {before} {caret} ");
156
157    populate(options, Some(&matrix), 0);
158
159    // hide cursor
160    let vis = Visibility::new();
161    vis.hide_cursor();
162
163    loop {
164        if let Ok(character) = read_key() {
165            match character {
166                Key::ArrowUp | Key::Char('w') | Key::Char('W') => {
167                    if i > 0 {
168                        i -= 1;
169                        populate(options, Some(&matrix), i);
170                    }
171                }
172                Key::ArrowDown | Key::Char('s') | Key::Char('S') => {
173                    if i < options.len() - 1 {
174                        i += 1;
175                        populate(options, Some(&matrix), i);
176                    }
177                }
178                Key::Char(' ') => {
179                    move_cursor_down(i);
180                    clear_line();
181                    matrix[i] = !matrix[i];
182                    flush();
183                    move_cursor_up(i);
184                    populate(options, Some(&matrix), i);
185                }
186                Key::Enter => {
187                    break;
188                }
189                _ => {}
190            }
191        }
192    }
193
194    // reset cursor
195    move_cursor_down(options.len());
196
197    matrix
198}
199
200/// Populate function for select/multiselect
201fn populate(options: &[&str], matrix: Option<&[bool]>, cursor: usize) {
202    for (i, option) in options.iter().enumerate() {
203        clear_line();
204        if i == cursor {
205            let caret = StyledText::new("›").fg(Color::Green);
206            let option = if matrix.is_some() && matrix.unwrap()[i] {
207                StyledText::new(option).fg(Color::Green)
208            } else {
209                StyledText::new(option).fg(Color::Cyan)
210            };
211            println!(" {caret} {option}");
212        } else if matrix.is_some() && matrix.unwrap()[i] {
213            let option = StyledText::new(option).fg(Color::Green);
214            println!("   {}", option);
215        } else {
216            println!("   {}", option);
217        }
218    }
219    move_cursor_up(options.len());
220}
221
222/// Enumeration representing different types of spinners.
223#[derive(Debug, Clone)]
224pub enum SpinnerType {
225    /// Spinner with characters `/` `-` `\` `|`.
226    Standard,
227    /// Spinner with dots `.` `..` `...` `.....`.
228    Dots,
229    /// Spinner with box characters `▌` `▀` `▐` `▄`.
230    Box,
231    /// Spinner with flip characters `_` `_` `_` `-` `\` `'` `´` `-` `_` `_` `_`.
232    Flip,
233    /// Custom spinner with user-defined frames.
234    Custom(&'static [&'static str]),
235}
236
237impl SpinnerType {
238    /// Returns the frames of the spinner type.
239    pub fn frames(&self) -> &'static [&'static str] {
240        match self {
241            SpinnerType::Standard => &["/", "-", "\\", "|"],
242            SpinnerType::Dots => &[".", "..", "...", "....", "...", ".."],
243            SpinnerType::Box => &["▌", "▀", "▐", "▄"],
244            SpinnerType::Flip => &["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"],
245            SpinnerType::Custom(frames) => frames,
246        }
247    }
248}
249
250/// Displays a console-based spinner animation.
251///
252/// A spinner is a visual indicator of a long-running process. It consists of a set of frames
253/// that are displayed sequentially to create the appearance of motion.
254///
255/// # Parameters
256///
257/// - `time`: A floating-point number representing the duration of the spinner animation in seconds.
258/// - `spinner_type`: The type of spinner to display.
259pub fn spinner(mut time: f64, spinner_type: SpinnerType) {
260    let frames = spinner_type.frames();
261    let mut i = 0;
262
263    while time > 0.0 {
264        clear_line();
265        print!("{}", frames[i]);
266        flush();
267        thread::sleep(Duration::from_secs_f64(0.075));
268        time -= 0.075;
269        if i < frames.len() - 1 {
270            i += 1
271        } else {
272            i = 0
273        }
274    }
275
276    clear_line();
277}
278
279const FAST_GRACE_MS: u64 = 120;
280const FAST_GRACE: Duration = Duration::from_millis(FAST_GRACE_MS);
281
282/// Reveals a string gradually, printing one character at a time with a specified time interval.
283///
284/// Useful for typing effects or slow reveals. Can be sped up with the optional skip key (≈ time_between / 50).
285///
286/// # Arguments
287///
288/// - `str` - The string to reveal gradually. Include `\n` for new lines.
289/// - `time_between` - The time interval (in seconds) between each revealed character.
290/// - `skip_key` - If `Some(key)`, pressing this key will temporarily speed up
291///   the reveal rate. The speed-up lasts briefly after the last press (a grace period of 120
292///   milliseconds) before returning to the normal pace. Holding or repeatedly pressing the key will
293///   extend the fast-forward window. If `None`, the reveal speed cannot be changed.
294pub fn reveal(str: &str, time_between: f64, skip_key: Option<Key>) {
295    // Sanitize input
296    let tb = if time_between.is_finite() && time_between >= 0.0 {
297        time_between
298    } else {
299        0.0
300    };
301
302    let normal_delay = Duration::from_secs_f64(tb);
303
304    // throttle while fast-forwarding: time_between / 50 (clamped to >= 1ms)
305    let fast_delay = {
306        let d = tb / 50.0;
307        if d < 0.001 {
308            Duration::from_millis(1)
309        } else {
310            Duration::from_secs_f64(d)
311        }
312    };
313
314    // If Some(t), we are in fast mode until `t`
315    let mut fast_until: Option<Instant> = None;
316
317    for ch in str.chars() {
318        print!("{ch}");
319        flush();
320
321        // Decide current delay based on whether fast window is active
322        let now = Instant::now();
323        let fast_active = fast_until.map_or(false, |t| now < t);
324        let delay = if fast_active {
325            fast_delay
326        } else {
327            normal_delay
328        };
329
330        if skip_key.is_none() {
331            std::thread::sleep(delay);
332            continue;
333        }
334
335        // Wait up to `delay`, reacting to Tab to (re)enter/extend fast mode
336        match key_pressed_within(delay) {
337            Ok(Some(k)) if Some(k.clone()) == skip_key => {
338                // Enter/extend fast mode
339                fast_until = Some(Instant::now() + FAST_GRACE);
340
341                // Drain immediate Tabs (zero wait) to keep extending the window
342                while let Ok(Some(k2)) = key_pressed_within(Duration::from_millis(0)) {
343                    if Some(k2) == skip_key {
344                        fast_until = Some(Instant::now() + FAST_GRACE);
345                    }
346                }
347            }
348            _ => {
349                // Timer expired; if we were in fast mode and grace elapsed, leave it
350                if let Some(t) = fast_until {
351                    if Instant::now() >= t {
352                        fast_until = None;
353                    }
354                }
355            }
356        }
357    }
358}