Skip to main content

rich_rs/
prompt.rs

1//! Interactive prompts for user input.
2//!
3//! This module provides prompt types for getting user input with validation,
4//! choices, and default values. It matches the Python Rich `prompt.py` module.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use rich_rs::prompt::{Prompt, IntPrompt, Confirm};
10//!
11//! // Simple string prompt
12//! let name = Prompt::ask("Enter your name")?;
13//!
14//! // Prompt with default value
15//! let name = Prompt::new("Enter your name")
16//!     .with_default("Anonymous")
17//!     .ask()?;
18//!
19//! // Integer prompt
20//! let count: i32 = IntPrompt::ask("How many?")?;
21//!
22//! // Confirmation prompt
23//! if Confirm::ask("Continue?")? {
24//!     println!("Continuing...");
25//! }
26//! ```
27
28use std::io::{self, BufRead};
29
30use crate::Console;
31use crate::text::Text;
32
33// ============================================================================
34// Errors
35// ============================================================================
36
37/// Errors that can occur during prompting.
38#[derive(Debug, Clone)]
39pub enum PromptError {
40    /// An I/O error occurred.
41    Io(String),
42    /// The user provided an invalid response.
43    InvalidResponse(InvalidResponse),
44    /// Input was interrupted (e.g., Ctrl+C or EOF).
45    Interrupted,
46}
47
48impl std::fmt::Display for PromptError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            PromptError::Io(msg) => write!(f, "I/O error: {}", msg),
52            PromptError::InvalidResponse(err) => write!(f, "{}", err.message),
53            PromptError::Interrupted => write!(f, "Input interrupted"),
54        }
55    }
56}
57
58impl std::error::Error for PromptError {}
59
60impl From<io::Error> for PromptError {
61    fn from(err: io::Error) -> Self {
62        match err.kind() {
63            io::ErrorKind::UnexpectedEof | io::ErrorKind::Interrupted => PromptError::Interrupted,
64            _ => PromptError::Io(err.to_string()),
65        }
66    }
67}
68
69impl From<InvalidResponse> for PromptError {
70    fn from(err: InvalidResponse) -> Self {
71        PromptError::InvalidResponse(err)
72    }
73}
74
75/// Indicates that a response was invalid.
76///
77/// Raise this within processing logic to indicate an error and provide an error message.
78#[derive(Debug, Clone)]
79pub struct InvalidResponse {
80    /// The error message to display.
81    pub message: String,
82}
83
84impl InvalidResponse {
85    /// Create a new invalid response error.
86    pub fn new(message: impl Into<String>) -> Self {
87        InvalidResponse {
88            message: message.into(),
89        }
90    }
91}
92
93impl std::fmt::Display for InvalidResponse {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "{}", self.message)
96    }
97}
98
99impl std::error::Error for InvalidResponse {}
100
101/// Result type for prompt operations.
102pub type Result<T> = std::result::Result<T, PromptError>;
103
104// ============================================================================
105// PromptBase - Common prompt functionality
106// ============================================================================
107
108/// Base trait for prompt implementations.
109pub trait PromptBase<T> {
110    /// The error message for invalid values.
111    const VALIDATE_ERROR_MESSAGE: &'static str = "[prompt.invalid]Please enter a valid value";
112
113    /// The error message for invalid choices.
114    const ILLEGAL_CHOICE_MESSAGE: &'static str =
115        "[prompt.invalid.choice]Please select one of the available options";
116
117    /// The prompt suffix.
118    const PROMPT_SUFFIX: &'static str = ": ";
119
120    /// Process the raw string response into the target type.
121    fn process_response(&self, value: &str) -> std::result::Result<T, InvalidResponse>;
122
123    /// Render the default value for display.
124    fn render_default(&self, default: &T) -> String;
125}
126
127// ============================================================================
128// Prompt - String prompts
129// ============================================================================
130
131/// A prompt that returns a string.
132///
133/// # Example
134///
135/// ```ignore
136/// use rich_rs::prompt::Prompt;
137///
138/// // Simple usage
139/// let name = Prompt::ask("Enter your name")?;
140///
141/// // With options
142/// let name = Prompt::new("Enter your name")
143///     .with_default("Anonymous")
144///     .with_choices(&["Alice", "Bob", "Charlie"])
145///     .ask()?;
146/// ```
147pub struct Prompt {
148    /// The prompt text.
149    prompt: String,
150    /// Optional default value.
151    default: Option<String>,
152    /// Optional list of valid choices.
153    choices: Option<Vec<String>>,
154    /// Whether choice matching is case-sensitive.
155    case_sensitive: bool,
156    /// Whether to show the default value in the prompt.
157    show_default: bool,
158    /// Whether to show the choices in the prompt.
159    show_choices: bool,
160    /// Whether this is a password prompt (input will be masked).
161    password: bool,
162    /// Optional input stream (for testing; reads from this instead of stdin).
163    stream: Option<Box<dyn BufRead + Send>>,
164    /// Optional callback called just before displaying the prompt.
165    pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
166}
167
168impl Default for Prompt {
169    fn default() -> Self {
170        Self::new("")
171    }
172}
173
174impl Prompt {
175    /// Create a new prompt with the given text.
176    pub fn new(prompt: impl Into<String>) -> Self {
177        Prompt {
178            prompt: prompt.into(),
179            default: None,
180            choices: None,
181            case_sensitive: true,
182            show_default: true,
183            show_choices: true,
184            password: false,
185            stream: None,
186            pre_prompt: None,
187        }
188    }
189
190    /// Set the default value.
191    pub fn with_default(mut self, default: impl Into<String>) -> Self {
192        self.default = Some(default.into());
193        self
194    }
195
196    /// Set the list of valid choices.
197    pub fn with_choices(mut self, choices: &[&str]) -> Self {
198        self.choices = Some(choices.iter().map(|s| s.to_string()).collect());
199        self
200    }
201
202    /// Set whether choice matching is case-sensitive (default: true).
203    pub fn case_sensitive(mut self, sensitive: bool) -> Self {
204        self.case_sensitive = sensitive;
205        self
206    }
207
208    /// Set whether to show the default value in the prompt (default: true).
209    pub fn show_default(mut self, show: bool) -> Self {
210        self.show_default = show;
211        self
212    }
213
214    /// Set whether to show the choices in the prompt (default: true).
215    pub fn show_choices(mut self, show: bool) -> Self {
216        self.show_choices = show;
217        self
218    }
219
220    /// Set whether this is a password prompt (default: false).
221    pub fn password(mut self, is_password: bool) -> Self {
222        self.password = is_password;
223        self
224    }
225
226    /// Set an input stream to read from instead of stdin.
227    ///
228    /// This enables testing prompts without actual stdin interaction.
229    pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
230        self.stream = Some(Box::new(stream));
231        self
232    }
233
234    /// Set a callback called just before displaying the prompt.
235    pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
236        self.pre_prompt = Some(Box::new(f));
237        self
238    }
239
240    /// Shortcut to create and run a prompt, returning the result.
241    ///
242    /// # Example
243    ///
244    /// ```ignore
245    /// let name = Prompt::ask("Enter your name")?;
246    /// ```
247    pub fn ask(prompt: impl Into<String>) -> Result<String> {
248        Prompt::new(prompt).run()
249    }
250
251    /// Debug representation (stream/pre_prompt are not Debug).
252    pub fn has_stream(&self) -> bool {
253        self.stream.is_some()
254    }
255
256    /// Run the prompt loop and return the result.
257    pub fn run(&mut self) -> Result<String> {
258        let mut console = Console::new();
259        self.run_with_console(&mut console)
260    }
261
262    /// Run the prompt loop with a specific console.
263    pub fn run_with_console(&mut self, console: &mut Console) -> Result<String> {
264        loop {
265            if let Some(ref pre_prompt) = self.pre_prompt {
266                pre_prompt();
267            }
268
269            let prompt_text = self.make_prompt();
270
271            let value = if let Some(ref mut stream) = self.stream {
272                // Print prompt but read from the provided stream.
273                let _ = console.print(&prompt_text, None, None, None, false, "");
274                let mut line = String::new();
275                stream.read_line(&mut line).map_err(PromptError::from)?;
276                if line.is_empty() {
277                    return Err(PromptError::Interrupted);
278                }
279                line.trim_end_matches('\n')
280                    .trim_end_matches('\r')
281                    .to_string()
282            } else {
283                console.input(&prompt_text, self.password)?
284            };
285
286            // Use default if input is empty
287            if value.is_empty() {
288                if let Some(ref default) = self.default {
289                    return Ok(default.clone());
290                }
291            }
292
293            // Process the response
294            match self.process_response(&value) {
295                Ok(result) => return Ok(result),
296                Err(err) => {
297                    // Display error and continue loop
298                    let error_text = Text::from_markup(&err.message, false)
299                        .unwrap_or_else(|_| Text::plain(&err.message));
300                    let _ = console.print(&error_text, None, None, None, false, "\n");
301                }
302            }
303        }
304    }
305
306    /// Build the prompt text including choices and default.
307    fn make_prompt(&self) -> Text {
308        let mut parts = vec![self.prompt.clone()];
309
310        // Add choices
311        if self.show_choices {
312            if let Some(ref choices) = self.choices {
313                let choices_str = choices.join("/");
314                parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str));
315            }
316        }
317
318        // Add default
319        if self.show_default {
320            if let Some(ref default) = self.default {
321                parts.push(format!(" [prompt.default]({})[/]", default));
322            }
323        }
324
325        // Add suffix
326        parts.push(": ".to_string());
327
328        let markup = parts.join("");
329        Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
330    }
331
332    /// Check if the value is in the list of valid choices.
333    fn check_choice(&self, value: &str) -> bool {
334        if let Some(ref choices) = self.choices {
335            if self.case_sensitive {
336                choices.iter().any(|c| c == value)
337            } else {
338                let value_lower = value.to_lowercase();
339                choices.iter().any(|c| c.to_lowercase() == value_lower)
340            }
341        } else {
342            true
343        }
344    }
345
346    /// Get the original choice (for case-insensitive matching).
347    fn get_original_choice(&self, value: &str) -> String {
348        if let Some(ref choices) = self.choices {
349            if !self.case_sensitive {
350                let value_lower = value.to_lowercase();
351                for choice in choices {
352                    if choice.to_lowercase() == value_lower {
353                        return choice.clone();
354                    }
355                }
356            }
357        }
358        value.to_string()
359    }
360
361    /// Process the response.
362    fn process_response(&self, value: &str) -> std::result::Result<String, InvalidResponse> {
363        let value = value.trim();
364
365        // Check choice
366        if self.choices.is_some() && !self.check_choice(value) {
367            return Err(InvalidResponse::new(
368                "[prompt.invalid.choice]Please select one of the available options",
369            ));
370        }
371
372        // Return original choice for case-insensitive matching
373        if !self.case_sensitive && self.choices.is_some() {
374            Ok(self.get_original_choice(value))
375        } else {
376            Ok(value.to_string())
377        }
378    }
379}
380
381// ============================================================================
382// IntPrompt - Integer prompts
383// ============================================================================
384
385/// A prompt that returns an integer.
386///
387/// # Example
388///
389/// ```ignore
390/// use rich_rs::prompt::IntPrompt;
391///
392/// let count: i32 = IntPrompt::ask("How many?")?;
393///
394/// // With default
395/// let count = IntPrompt::new("How many?")
396///     .with_default(5)
397///     .ask()?;
398/// ```
399pub struct IntPrompt {
400    /// The prompt text.
401    prompt: String,
402    /// Optional default value.
403    default: Option<i64>,
404    /// Whether to show the default value in the prompt.
405    show_default: bool,
406    /// Optional list of valid choices.
407    choices: Option<Vec<i64>>,
408    /// Optional input stream (for testing).
409    stream: Option<Box<dyn BufRead + Send>>,
410    /// Optional callback called just before displaying the prompt.
411    pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
412}
413
414impl Default for IntPrompt {
415    fn default() -> Self {
416        Self::new("")
417    }
418}
419
420impl IntPrompt {
421    /// Create a new integer prompt with the given text.
422    pub fn new(prompt: impl Into<String>) -> Self {
423        IntPrompt {
424            prompt: prompt.into(),
425            default: None,
426            show_default: true,
427            choices: None,
428            stream: None,
429            pre_prompt: None,
430        }
431    }
432
433    /// Set the default value.
434    pub fn with_default(mut self, default: i64) -> Self {
435        self.default = Some(default);
436        self
437    }
438
439    /// Set whether to show the default value in the prompt (default: true).
440    pub fn show_default(mut self, show: bool) -> Self {
441        self.show_default = show;
442        self
443    }
444
445    /// Set valid choices for the prompt.
446    pub fn with_choices(mut self, choices: Vec<i64>) -> Self {
447        self.choices = Some(choices);
448        self
449    }
450
451    /// Set an input stream to read from instead of stdin.
452    pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
453        self.stream = Some(Box::new(stream));
454        self
455    }
456
457    /// Set a callback called just before displaying the prompt.
458    pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
459        self.pre_prompt = Some(Box::new(f));
460        self
461    }
462
463    /// Shortcut to create and run a prompt, returning the result.
464    ///
465    /// # Example
466    ///
467    /// ```ignore
468    /// let count: i64 = IntPrompt::ask("How many?")?;
469    /// ```
470    pub fn ask(prompt: impl Into<String>) -> Result<i64> {
471        IntPrompt::new(prompt).run()
472    }
473
474    /// Run the prompt loop and return the result.
475    pub fn run(&mut self) -> Result<i64> {
476        let mut console = Console::new();
477        self.run_with_console(&mut console)
478    }
479
480    /// Run the prompt loop with a specific console.
481    pub fn run_with_console(&mut self, console: &mut Console) -> Result<i64> {
482        loop {
483            if let Some(ref pre_prompt) = self.pre_prompt {
484                pre_prompt();
485            }
486
487            let prompt_text = self.make_prompt();
488
489            let value = if let Some(ref mut stream) = self.stream {
490                let _ = console.print(&prompt_text, None, None, None, false, "");
491                let mut line = String::new();
492                stream.read_line(&mut line).map_err(PromptError::from)?;
493                if line.is_empty() {
494                    return Err(PromptError::Interrupted);
495                }
496                line.trim_end_matches('\n')
497                    .trim_end_matches('\r')
498                    .to_string()
499            } else {
500                console.input(&prompt_text, false)?
501            };
502
503            // Use default if input is empty
504            if value.is_empty() {
505                if let Some(default) = self.default {
506                    return Ok(default);
507                }
508            }
509
510            // Process the response
511            match self.process_response(&value) {
512                Ok(result) => return Ok(result),
513                Err(err) => {
514                    // Display error and continue loop
515                    let error_text = Text::from_markup(&err.message, false)
516                        .unwrap_or_else(|_| Text::plain(&err.message));
517                    let _ = console.print(&error_text, None, None, None, false, "\n");
518                }
519            }
520        }
521    }
522
523    /// Build the prompt text including choices and default.
524    fn make_prompt(&self) -> Text {
525        let mut parts = vec![self.prompt.clone()];
526
527        // Add choices
528        if let Some(ref choices) = self.choices {
529            let choices_str: Vec<String> = choices.iter().map(|c| c.to_string()).collect();
530            parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str.join("/")));
531        }
532
533        // Add default
534        if self.show_default {
535            if let Some(default) = self.default {
536                parts.push(format!(" [prompt.default]({})[/]", default));
537            }
538        }
539
540        // Add suffix
541        parts.push(": ".to_string());
542
543        let markup = parts.join("");
544        Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
545    }
546
547    /// Process the response.
548    fn process_response(&self, value: &str) -> std::result::Result<i64, InvalidResponse> {
549        let value = value.trim();
550        let parsed = value.parse::<i64>().map_err(|_| {
551            InvalidResponse::new("[prompt.invalid]Please enter a valid integer number")
552        })?;
553        if let Some(ref choices) = self.choices {
554            if !choices.contains(&parsed) {
555                return Err(InvalidResponse::new(
556                    "[prompt.invalid.choice]Please select one of the available options",
557                ));
558            }
559        }
560        Ok(parsed)
561    }
562}
563
564// ============================================================================
565// FloatPrompt - Float prompts
566// ============================================================================
567
568/// A prompt that returns a floating-point number.
569///
570/// # Example
571///
572/// ```ignore
573/// use rich_rs::prompt::FloatPrompt;
574///
575/// let temperature: f64 = FloatPrompt::ask("Enter temperature")?;
576///
577/// // With default
578/// let temp = FloatPrompt::new("Enter temperature")
579///     .with_default(98.6)
580///     .ask()?;
581/// ```
582pub struct FloatPrompt {
583    /// The prompt text.
584    prompt: String,
585    /// Optional default value.
586    default: Option<f64>,
587    /// Whether to show the default value in the prompt.
588    show_default: bool,
589    /// Optional list of valid choices.
590    choices: Option<Vec<f64>>,
591    /// Optional input stream (for testing).
592    stream: Option<Box<dyn BufRead + Send>>,
593    /// Optional callback called just before displaying the prompt.
594    pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
595}
596
597impl Default for FloatPrompt {
598    fn default() -> Self {
599        Self::new("")
600    }
601}
602
603impl FloatPrompt {
604    /// Create a new float prompt with the given text.
605    pub fn new(prompt: impl Into<String>) -> Self {
606        FloatPrompt {
607            prompt: prompt.into(),
608            default: None,
609            show_default: true,
610            choices: None,
611            stream: None,
612            pre_prompt: None,
613        }
614    }
615
616    /// Set the default value.
617    pub fn with_default(mut self, default: f64) -> Self {
618        self.default = Some(default);
619        self
620    }
621
622    /// Set whether to show the default value in the prompt (default: true).
623    pub fn show_default(mut self, show: bool) -> Self {
624        self.show_default = show;
625        self
626    }
627
628    /// Set valid choices for the prompt.
629    pub fn with_choices(mut self, choices: Vec<f64>) -> Self {
630        self.choices = Some(choices);
631        self
632    }
633
634    /// Set an input stream to read from instead of stdin.
635    pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
636        self.stream = Some(Box::new(stream));
637        self
638    }
639
640    /// Set a callback called just before displaying the prompt.
641    pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
642        self.pre_prompt = Some(Box::new(f));
643        self
644    }
645
646    /// Shortcut to create and run a prompt, returning the result.
647    ///
648    /// # Example
649    ///
650    /// ```ignore
651    /// let temp: f64 = FloatPrompt::ask("Enter temperature")?;
652    /// ```
653    pub fn ask(prompt: impl Into<String>) -> Result<f64> {
654        FloatPrompt::new(prompt).run()
655    }
656
657    /// Run the prompt loop and return the result.
658    pub fn run(&mut self) -> Result<f64> {
659        let mut console = Console::new();
660        self.run_with_console(&mut console)
661    }
662
663    /// Run the prompt loop with a specific console.
664    pub fn run_with_console(&mut self, console: &mut Console) -> Result<f64> {
665        loop {
666            if let Some(ref pre_prompt) = self.pre_prompt {
667                pre_prompt();
668            }
669
670            let prompt_text = self.make_prompt();
671
672            let value = if let Some(ref mut stream) = self.stream {
673                let _ = console.print(&prompt_text, None, None, None, false, "");
674                let mut line = String::new();
675                stream.read_line(&mut line).map_err(PromptError::from)?;
676                if line.is_empty() {
677                    return Err(PromptError::Interrupted);
678                }
679                line.trim_end_matches('\n')
680                    .trim_end_matches('\r')
681                    .to_string()
682            } else {
683                console.input(&prompt_text, false)?
684            };
685
686            // Use default if input is empty
687            if value.is_empty() {
688                if let Some(default) = self.default {
689                    return Ok(default);
690                }
691            }
692
693            // Process the response
694            match self.process_response(&value) {
695                Ok(result) => return Ok(result),
696                Err(err) => {
697                    // Display error and continue loop
698                    let error_text = Text::from_markup(&err.message, false)
699                        .unwrap_or_else(|_| Text::plain(&err.message));
700                    let _ = console.print(&error_text, None, None, None, false, "\n");
701                }
702            }
703        }
704    }
705
706    /// Build the prompt text including choices and default.
707    fn make_prompt(&self) -> Text {
708        let mut parts = vec![self.prompt.clone()];
709
710        // Add choices
711        if let Some(ref choices) = self.choices {
712            let choices_str: Vec<String> = choices.iter().map(|c| c.to_string()).collect();
713            parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str.join("/")));
714        }
715
716        // Add default
717        if self.show_default {
718            if let Some(default) = self.default {
719                parts.push(format!(" [prompt.default]({})[/]", default));
720            }
721        }
722
723        // Add suffix
724        parts.push(": ".to_string());
725
726        let markup = parts.join("");
727        Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
728    }
729
730    /// Process the response.
731    fn process_response(&self, value: &str) -> std::result::Result<f64, InvalidResponse> {
732        let value = value.trim();
733        let parsed = value
734            .parse::<f64>()
735            .map_err(|_| InvalidResponse::new("[prompt.invalid]Please enter a number"))?;
736        if let Some(ref choices) = self.choices {
737            if !choices.iter().any(|c| (*c - parsed).abs() < f64::EPSILON) {
738                return Err(InvalidResponse::new(
739                    "[prompt.invalid.choice]Please select one of the available options",
740                ));
741            }
742        }
743        Ok(parsed)
744    }
745}
746
747// ============================================================================
748// Confirm - Yes/No prompts
749// ============================================================================
750
751/// A yes/no confirmation prompt.
752///
753/// # Example
754///
755/// ```ignore
756/// use rich_rs::prompt::Confirm;
757///
758/// if Confirm::ask("Continue?")? {
759///     println!("Continuing...");
760/// }
761///
762/// // With default
763/// if Confirm::new("Continue?").with_default(true).ask()? {
764///     println!("Continuing...");
765/// }
766/// ```
767pub struct Confirm {
768    /// The prompt text.
769    prompt: String,
770    /// Optional default value.
771    default: Option<bool>,
772    /// Whether to show the default value in the prompt.
773    show_default: bool,
774    /// The yes choice string.
775    yes_choice: String,
776    /// The no choice string.
777    no_choice: String,
778    /// Optional input stream (for testing).
779    stream: Option<Box<dyn BufRead + Send>>,
780    /// Optional callback called just before displaying the prompt.
781    pre_prompt: Option<Box<dyn Fn() + Send + Sync>>,
782}
783
784impl Default for Confirm {
785    fn default() -> Self {
786        Self::new("")
787    }
788}
789
790impl Confirm {
791    /// Create a new confirmation prompt with the given text.
792    pub fn new(prompt: impl Into<String>) -> Self {
793        Confirm {
794            prompt: prompt.into(),
795            default: None,
796            show_default: true,
797            yes_choice: "y".to_string(),
798            no_choice: "n".to_string(),
799            stream: None,
800            pre_prompt: None,
801        }
802    }
803
804    /// Set the default value.
805    pub fn with_default(mut self, default: bool) -> Self {
806        self.default = Some(default);
807        self
808    }
809
810    /// Set whether to show the default value in the prompt (default: true).
811    pub fn show_default(mut self, show: bool) -> Self {
812        self.show_default = show;
813        self
814    }
815
816    /// Set custom yes/no choices (default: "y"/"n").
817    pub fn with_choices(mut self, yes: impl Into<String>, no: impl Into<String>) -> Self {
818        self.yes_choice = yes.into();
819        self.no_choice = no.into();
820        self
821    }
822
823    /// Set an input stream to read from instead of stdin.
824    pub fn with_stream(mut self, stream: impl BufRead + Send + 'static) -> Self {
825        self.stream = Some(Box::new(stream));
826        self
827    }
828
829    /// Set a callback called just before displaying the prompt.
830    pub fn with_pre_prompt(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
831        self.pre_prompt = Some(Box::new(f));
832        self
833    }
834
835    /// Shortcut to create and run a prompt, returning the result.
836    ///
837    /// # Example
838    ///
839    /// ```ignore
840    /// if Confirm::ask("Continue?")? {
841    ///     println!("Continuing...");
842    /// }
843    /// ```
844    pub fn ask(prompt: impl Into<String>) -> Result<bool> {
845        Confirm::new(prompt).run()
846    }
847
848    /// Run the prompt loop and return the result.
849    pub fn run(&mut self) -> Result<bool> {
850        let mut console = Console::new();
851        self.run_with_console(&mut console)
852    }
853
854    /// Run the prompt loop with a specific console.
855    pub fn run_with_console(&mut self, console: &mut Console) -> Result<bool> {
856        loop {
857            if let Some(ref pre_prompt) = self.pre_prompt {
858                pre_prompt();
859            }
860
861            let prompt_text = self.make_prompt();
862
863            let value = if let Some(ref mut stream) = self.stream {
864                let _ = console.print(&prompt_text, None, None, None, false, "");
865                let mut line = String::new();
866                stream.read_line(&mut line).map_err(PromptError::from)?;
867                if line.is_empty() {
868                    return Err(PromptError::Interrupted);
869                }
870                line.trim_end_matches('\n')
871                    .trim_end_matches('\r')
872                    .to_string()
873            } else {
874                console.input(&prompt_text, false)?
875            };
876
877            // Use default if input is empty
878            if value.is_empty() {
879                if let Some(default) = self.default {
880                    return Ok(default);
881                }
882            }
883
884            // Process the response
885            match self.process_response(&value) {
886                Ok(result) => return Ok(result),
887                Err(err) => {
888                    // Display error and continue loop
889                    let error_text = Text::from_markup(&err.message, false)
890                        .unwrap_or_else(|_| Text::plain(&err.message));
891                    let _ = console.print(&error_text, None, None, None, false, "\n");
892                }
893            }
894        }
895    }
896
897    /// Build the prompt text including choices and default.
898    fn make_prompt(&self) -> Text {
899        let mut parts = vec![self.prompt.clone()];
900
901        // Add choices
902        let choices_str = format!("{}/{}", self.yes_choice, self.no_choice);
903        parts.push(format!(" [prompt.choices]\\[{}][/]", choices_str));
904
905        // Add default as y/n
906        if self.show_default {
907            if let Some(default) = self.default {
908                let default_str = if default {
909                    &self.yes_choice
910                } else {
911                    &self.no_choice
912                };
913                parts.push(format!(" [prompt.default]({})[/]", default_str));
914            }
915        }
916
917        // Add suffix
918        parts.push(": ".to_string());
919
920        let markup = parts.join("");
921        Text::from_markup(&markup, false).unwrap_or_else(|_| Text::plain(&markup))
922    }
923
924    /// Process the response.
925    fn process_response(&self, value: &str) -> std::result::Result<bool, InvalidResponse> {
926        let value = value.trim().to_lowercase();
927        let yes_lower = self.yes_choice.to_lowercase();
928        let no_lower = self.no_choice.to_lowercase();
929
930        if value == yes_lower {
931            Ok(true)
932        } else if value == no_lower {
933            Ok(false)
934        } else {
935            Err(InvalidResponse::new("[prompt.invalid]Please enter Y or N"))
936        }
937    }
938}
939
940// ============================================================================
941// Tests
942// ============================================================================
943
944#[cfg(test)]
945mod tests {
946    use super::*;
947
948    #[test]
949    fn test_prompt_new() {
950        let prompt = Prompt::new("Enter name");
951        assert_eq!(prompt.prompt, "Enter name");
952        assert!(prompt.default.is_none());
953        assert!(prompt.choices.is_none());
954        assert!(prompt.case_sensitive);
955        assert!(prompt.show_default);
956        assert!(prompt.show_choices);
957        assert!(!prompt.password);
958        assert!(!prompt.has_stream());
959    }
960
961    #[test]
962    fn test_prompt_with_options() {
963        let prompt = Prompt::new("Choose")
964            .with_default("Alice")
965            .with_choices(&["Alice", "Bob", "Charlie"])
966            .case_sensitive(false)
967            .password(true);
968
969        assert_eq!(prompt.default, Some("Alice".to_string()));
970        assert_eq!(
971            prompt.choices,
972            Some(vec![
973                "Alice".to_string(),
974                "Bob".to_string(),
975                "Charlie".to_string()
976            ])
977        );
978        assert!(!prompt.case_sensitive);
979        assert!(prompt.password);
980    }
981
982    #[test]
983    fn test_prompt_check_choice_case_sensitive() {
984        let prompt = Prompt::new("Choose")
985            .with_choices(&["Alice", "Bob"])
986            .case_sensitive(true);
987
988        assert!(prompt.check_choice("Alice"));
989        assert!(prompt.check_choice("Bob"));
990        assert!(!prompt.check_choice("alice"));
991        assert!(!prompt.check_choice("Charlie"));
992    }
993
994    #[test]
995    fn test_prompt_check_choice_case_insensitive() {
996        let prompt = Prompt::new("Choose")
997            .with_choices(&["Alice", "Bob"])
998            .case_sensitive(false);
999
1000        assert!(prompt.check_choice("Alice"));
1001        assert!(prompt.check_choice("alice"));
1002        assert!(prompt.check_choice("ALICE"));
1003        assert!(!prompt.check_choice("Charlie"));
1004    }
1005
1006    #[test]
1007    fn test_prompt_get_original_choice() {
1008        let prompt = Prompt::new("Choose")
1009            .with_choices(&["Alice", "Bob"])
1010            .case_sensitive(false);
1011
1012        assert_eq!(prompt.get_original_choice("alice"), "Alice");
1013        assert_eq!(prompt.get_original_choice("ALICE"), "Alice");
1014        assert_eq!(prompt.get_original_choice("bob"), "Bob");
1015    }
1016
1017    #[test]
1018    fn test_prompt_process_response_valid() {
1019        let prompt = Prompt::new("Enter name");
1020        let result = prompt.process_response("  John  ");
1021        assert_eq!(result.unwrap(), "John");
1022    }
1023
1024    #[test]
1025    fn test_prompt_process_response_invalid_choice() {
1026        let prompt = Prompt::new("Choose").with_choices(&["Alice", "Bob"]);
1027        let result = prompt.process_response("Charlie");
1028        assert!(result.is_err());
1029    }
1030
1031    #[test]
1032    fn test_int_prompt_new() {
1033        let prompt = IntPrompt::new("Enter number");
1034        assert_eq!(prompt.prompt, "Enter number");
1035        assert!(prompt.default.is_none());
1036        assert!(prompt.show_default);
1037        assert!(prompt.choices.is_none());
1038    }
1039
1040    #[test]
1041    fn test_int_prompt_with_default() {
1042        let prompt = IntPrompt::new("Enter number").with_default(42);
1043        assert_eq!(prompt.default, Some(42));
1044    }
1045
1046    #[test]
1047    fn test_int_prompt_with_choices() {
1048        let prompt = IntPrompt::new("Pick").with_choices(vec![1, 2, 3]);
1049        assert_eq!(prompt.choices, Some(vec![1, 2, 3]));
1050        // Valid choice
1051        assert!(prompt.process_response("2").is_ok());
1052        // Invalid choice
1053        assert!(prompt.process_response("5").is_err());
1054    }
1055
1056    #[test]
1057    fn test_int_prompt_process_response_valid() {
1058        let prompt = IntPrompt::new("Enter number");
1059        assert_eq!(prompt.process_response("42").unwrap(), 42);
1060        assert_eq!(prompt.process_response("  -10  ").unwrap(), -10);
1061    }
1062
1063    #[test]
1064    fn test_int_prompt_process_response_invalid() {
1065        let prompt = IntPrompt::new("Enter number");
1066        assert!(prompt.process_response("abc").is_err());
1067        assert!(prompt.process_response("3.14").is_err());
1068    }
1069
1070    #[test]
1071    fn test_float_prompt_new() {
1072        let prompt = FloatPrompt::new("Enter number");
1073        assert_eq!(prompt.prompt, "Enter number");
1074        assert!(prompt.default.is_none());
1075        assert!(prompt.choices.is_none());
1076    }
1077
1078    #[test]
1079    fn test_float_prompt_with_default() {
1080        let prompt = FloatPrompt::new("Enter number").with_default(3.125);
1081        assert_eq!(prompt.default, Some(3.125));
1082    }
1083
1084    #[test]
1085    fn test_float_prompt_with_choices() {
1086        let prompt = FloatPrompt::new("Pick").with_choices(vec![1.0, 2.5, 3.125]);
1087        // Valid choice
1088        assert!(prompt.process_response("2.5").is_ok());
1089        // Invalid choice
1090        assert!(prompt.process_response("9.9").is_err());
1091    }
1092
1093    #[test]
1094    fn test_float_prompt_process_response_valid() {
1095        let prompt = FloatPrompt::new("Enter number");
1096        assert!((prompt.process_response("3.125").unwrap() - 3.125).abs() < f64::EPSILON);
1097        assert!((prompt.process_response("  -2.5  ").unwrap() - (-2.5)).abs() < f64::EPSILON);
1098        assert!((prompt.process_response("42").unwrap() - 42.0).abs() < f64::EPSILON);
1099    }
1100
1101    #[test]
1102    fn test_float_prompt_process_response_invalid() {
1103        let prompt = FloatPrompt::new("Enter number");
1104        assert!(prompt.process_response("abc").is_err());
1105    }
1106
1107    #[test]
1108    fn test_confirm_new() {
1109        let confirm = Confirm::new("Continue?");
1110        assert_eq!(confirm.prompt, "Continue?");
1111        assert!(confirm.default.is_none());
1112        assert_eq!(confirm.yes_choice, "y");
1113        assert_eq!(confirm.no_choice, "n");
1114        assert!(confirm.stream.is_none());
1115    }
1116
1117    #[test]
1118    fn test_confirm_with_default() {
1119        let confirm = Confirm::new("Continue?").with_default(true);
1120        assert_eq!(confirm.default, Some(true));
1121    }
1122
1123    #[test]
1124    fn test_confirm_with_choices() {
1125        let confirm = Confirm::new("Continue?").with_choices("yes", "no");
1126        assert_eq!(confirm.yes_choice, "yes");
1127        assert_eq!(confirm.no_choice, "no");
1128    }
1129
1130    #[test]
1131    fn test_confirm_process_response_yes() {
1132        let confirm = Confirm::new("Continue?");
1133        assert!(confirm.process_response("y").unwrap());
1134        assert!(confirm.process_response("Y").unwrap());
1135    }
1136
1137    #[test]
1138    fn test_confirm_process_response_no() {
1139        let confirm = Confirm::new("Continue?");
1140        assert!(!confirm.process_response("n").unwrap());
1141        assert!(!confirm.process_response("N").unwrap());
1142    }
1143
1144    #[test]
1145    fn test_confirm_process_response_invalid() {
1146        let confirm = Confirm::new("Continue?");
1147        assert!(confirm.process_response("x").is_err());
1148        assert!(confirm.process_response("yes").is_err()); // Default choices are y/n
1149    }
1150
1151    #[test]
1152    fn test_confirm_custom_choices() {
1153        let confirm = Confirm::new("Continue?").with_choices("yes", "no");
1154        assert!(confirm.process_response("yes").unwrap());
1155        assert!(!confirm.process_response("no").unwrap());
1156        assert!(confirm.process_response("y").is_err()); // Custom choices don't include y
1157    }
1158
1159    #[test]
1160    fn test_invalid_response() {
1161        let err = InvalidResponse::new("Test error");
1162        assert_eq!(err.message, "Test error");
1163        assert_eq!(format!("{}", err), "Test error");
1164    }
1165
1166    #[test]
1167    fn test_prompt_error_display() {
1168        let io_err = PromptError::Io("test error".to_string());
1169        assert!(format!("{}", io_err).contains("test error"));
1170
1171        let invalid_err = PromptError::InvalidResponse(InvalidResponse::new("invalid"));
1172        assert!(format!("{}", invalid_err).contains("invalid"));
1173
1174        let interrupted = PromptError::Interrupted;
1175        assert!(format!("{}", interrupted).contains("interrupted"));
1176    }
1177
1178    #[test]
1179    fn test_prompt_with_stream() {
1180        let input = std::io::Cursor::new(b"Alice\n");
1181        let mut prompt = Prompt::new("Name")
1182            .with_stream(input)
1183            .with_choices(&["Alice", "Bob"]);
1184        let mut console = Console::new();
1185        let result = prompt.run_with_console(&mut console).unwrap();
1186        assert_eq!(result, "Alice");
1187    }
1188
1189    #[test]
1190    fn test_prompt_make_prompt() {
1191        let prompt = Prompt::new("Enter name")
1192            .with_default("John")
1193            .with_choices(&["John", "Jane"]);
1194
1195        let text = prompt.make_prompt();
1196        let plain = text.plain_text();
1197        assert!(plain.contains("Enter name"));
1198        assert!(plain.contains(":")); // suffix
1199    }
1200}