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}