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}