cli_prompts/prompts/
input.rs

1use super::Prompt;
2use crate::{
3    engine::CommandBuffer,
4    input::Key,
5    prompts::{AbortReason, EventOutcome},
6    style::InputStyle,
7};
8
9/// This is a normal text input prompt with the following features:
10/// - Custom label
11/// - Validation of the input with error reporting
12/// - Transformation of the text input to arbitrary Rust type
13/// - Optional default value
14/// - Optional help message
15/// - Customizable colors and formatting
16///
17/// ```rust
18/// use cli_prompts::{
19///     prompts::{Input, AbortReason::{self, Error}},
20///     style::{InputStyle, Formatting},
21///     DisplayPrompt
22/// };
23///
24/// fn validate_and_transform(input: &str) -> Result<u16, String> {
25///     input
26///       .parse::<u16>()
27///       .map_err(|e| "Provided input is not a valid time of the day".into())
28///       .and_then(|n| if n <= 24 { Ok(n) } else { Err("Provided input is not a valid time of the day".into()) })
29/// }
30///
31/// fn main() {
32///     let input = Input::new("At what time do you usually eat lunch?", validate_and_transform)
33///         .default_value("12")
34///         .help_message("Enter an hour of the day as a number from 0 to 24")
35///         .style(InputStyle::default()
36///                   .default_value_formatting(Formatting::default().bold())
37///          );
38///
39///     let lunch_time : Result<u16, AbortReason> = input.display();
40///     match lunch_time {
41///         Ok(time) => println!("You eat lunch at {} o'clock", time),
42///         Err(abort_reason) => match abort_reason {
43///             Interrupt => println!("The prompt was interrupted by pressing the ESC key"),
44///             Error(err) => println!("I/O error has occured: {:?}", err),
45///         }
46///     }
47///     
48/// }
49/// ```
50pub struct Input<F> {
51    label: String,
52    input: String,
53    help_message: Option<String>,
54    is_first_input: bool,
55    is_submitted: bool,
56    error: Option<String>,
57    validation: F,
58    style: InputStyle,
59}
60
61impl<F, T> Input<F>
62where
63    F: Fn(&str) -> Result<T, String>,
64{
65    /// Constructs an input prompt with a given label and a validation function
66    /// The function serves both as validator and transformer: it should return `Ok`
67    /// of the arbitrary type `T` if validation passed and `Err(String)` if it failed.
68    /// The containing String will be displayed as an error message and the prompt will continue
69    /// until this function returns Ok
70    pub fn new(label: impl Into<String>, validation: F) -> Self {
71        Self {
72            label: label.into(),
73            input: String::new(),
74            help_message: None,
75            is_first_input: true,
76            is_submitted: false,
77            error: None,
78            validation,
79            style: InputStyle::default(),
80        }
81    }
82
83    /// Sets a help message which will be displayed after the input string
84    /// until the prompt is completed
85    pub fn help_message<S: Into<String>>(mut self, message: S) -> Self {
86        self.help_message = Some(message.into());
87        self
88    }
89
90    /// Sets the default value for the prompt. It is cleared once any key
91    /// is pressed other than Enter.
92    pub fn default_value<S: Into<String>>(mut self, val: S) -> Self {
93        self.input = val.into();
94        self
95    }
96
97    /// Sets the style for the prompt
98    pub fn style(mut self, style: InputStyle) -> Self {
99        self.style = style;
100        self
101    }
102}
103
104impl<T, F> Prompt<T> for Input<F>
105where
106    F: Fn(&str) -> Result<T, String>,
107{
108    fn draw(&self, commands: &mut impl CommandBuffer) {
109        self.style.label_style.print(&self.label, commands);
110
111        if let Some(error) = self.error.as_ref() {
112            self.style
113                .error_formatting
114                .print(format!("[{}]", error), commands);
115        } else if self.is_submitted {
116            self.style.submitted_formatting.print(&self.input, commands);
117        } else if self.is_first_input && self.input.len() > 0 {
118            self.style
119                .default_value_formatting
120                .print(format!("[{}]", self.input), commands);
121        } else if !self.is_first_input {
122            self.style.input_formatting.print(&self.input, commands);
123        }
124
125        if let Some(help_message) = self.help_message.as_ref() {
126            self.style
127                .help_message_formatting
128                .print(format!("[{}]", help_message), commands);
129        }
130    }
131
132    fn on_key_pressed(&mut self, key: Key) -> EventOutcome<T> {
133        let is_first_input = self.is_first_input;
134        self.is_first_input = false;
135        match key {
136            Key::Char(c) => {
137                if is_first_input {
138                    self.input.clear();
139                }
140                self.error = None;
141                self.input.push(c);
142                EventOutcome::Continue
143            }
144            Key::Backspace => {
145                if is_first_input {
146                    self.input.clear();
147                }
148                self.error = None;
149                self.input.pop();
150                EventOutcome::Continue
151            }
152            Key::Enter => {
153                self.error = (self.validation)(&self.input).err();
154                match self.error {
155                    Some(_) => {
156                        self.input.clear();
157                        EventOutcome::Continue
158                    }
159                    None => {
160                        self.is_submitted = true;
161                        EventOutcome::Done((self.validation)(&self.input).unwrap())
162                    }
163                }
164            }
165            Key::Esc => EventOutcome::Abort(AbortReason::Interrupt),
166            _ => EventOutcome::Continue,
167        }
168    }
169}