fast_rich/
prompt.rs

1//! Interactive prompt module.
2
3use crate::console::Console;
4use std::fmt::Display;
5use std::io::{self, Write};
6use std::str::FromStr;
7
8/// Interactive prompt to ask for user input.
9pub struct Prompt<T> {
10    prompt: String,
11    default: Option<T>,
12    password: bool,
13    choices: Option<Vec<T>>,
14    show_default: bool,
15    show_choices: bool,
16    _case_sensitive: bool,
17}
18
19impl<T> Prompt<T>
20where
21    T: FromStr + Display + Clone + PartialEq,
22    T::Err: Display,
23{
24    /// Create a new prompt with the given query.
25    pub fn new(prompt: &str) -> Self {
26        Self {
27            prompt: prompt.to_string(),
28            default: None,
29            password: false,
30            choices: None,
31            show_default: true,
32            show_choices: true,
33            _case_sensitive: true,
34        }
35    }
36
37    /// Set a default value.
38    pub fn default(mut self, default: T) -> Self {
39        self.default = Some(default);
40        self
41    }
42
43    /// Hide the input (for passwords).
44    pub fn secret(mut self) -> Self {
45        self.password = true;
46        self
47    }
48
49    /// Set valid choices.
50    pub fn choices(mut self, choices: &[T]) -> Self {
51        self.choices = Some(choices.to_vec());
52        self
53    }
54
55    /// Show or hide the default value in the prompt.
56    pub fn show_default(mut self, show: bool) -> Self {
57        self.show_default = show;
58        self
59    }
60
61    /// Show or hide the choices in the prompt.
62    pub fn show_choices(mut self, show: bool) -> Self {
63        self.show_choices = show;
64        self
65    }
66
67    /// Ask the question and return the result.
68    pub fn ask(&self) -> T {
69        let console = Console::new();
70
71        loop {
72            self.render_prompt(&console);
73            let _ = io::stdout().flush();
74
75            let mut input = String::new();
76            if self.password {
77                // For MVP, just read line. Ideally use crossterm to hide input.
78                // crossterm::terminal::enable_raw_mode() ...
79                // But keeping it simple for now to ensure stability.
80                io::stdin().read_line(&mut input).unwrap_or_default();
81            } else {
82                io::stdin().read_line(&mut input).unwrap_or_default();
83            }
84
85            let trimmed = input.trim();
86
87            if trimmed.is_empty() {
88                if let Some(ref def) = self.default {
89                    return def.clone();
90                }
91            }
92
93            match T::from_str(trimmed) {
94                Ok(val) => {
95                    if let Some(ref choices) = self.choices {
96                        if !choices.contains(&val) {
97                            console
98                                .print("[bold red]Please select one of the available options[/]");
99                            continue;
100                        }
101                    }
102                    return val;
103                }
104                Err(_) => {
105                    console.print("[bold red]Please enter a valid value[/]");
106                }
107            }
108        }
109    }
110
111    fn render_prompt(&self, console: &Console) {
112        let mut text = String::new();
113        text.push_str(&self.prompt);
114
115        if self.show_choices {
116            if let Some(ref choices) = self.choices {
117                let choices_str: Vec<String> = choices.iter().map(|c| c.to_string()).collect();
118                text.push_str(&format!(" [bold magenta][{}][/]", choices_str.join("/")));
119            }
120        }
121
122        if self.show_default {
123            if let Some(ref def) = self.default {
124                if !self.password {
125                    text.push_str(&format!(" [bold cyan]({:?})[/]", def.to_string()));
126                }
127            }
128        }
129
130        text.push_str(": ");
131        console.print(&text);
132    }
133}
134
135/// Prompt for integers.
136pub type IntPrompt = Prompt<i64>;
137
138/// Prompt for floats.
139pub type FloatPrompt = Prompt<f64>;
140
141/// Confirmation prompt (yes/no).
142pub struct Confirm;
143
144impl Confirm {
145    /// Ask a yes/no question.
146    pub fn ask(prompt: &str, default: Option<bool>) -> bool {
147        let def_str = default
148            .map(|v| if v { "y" } else { "n" })
149            .unwrap_or("y")
150            .to_string();
151
152        let p = Prompt::<String>::new(prompt)
153            .choices(&[
154                "y".to_string(),
155                "n".to_string(),
156                "yes".to_string(),
157                "no".to_string(),
158            ])
159            .default(def_str);
160
161        let answer = p.ask();
162        answer.to_lowercase().starts_with('y')
163    }
164}