promptuity/prompts/
password.rs

1use crate::event::*;
2use crate::{InputCursor, Prompt, PromptInput, PromptState, RenderPayload, Validator};
3
4/// A trait for formatting the [`Password`] prompt.
5///
6/// All methods have default implementations, allowing you to override only the specific formatting process you need.
7///
8/// # Examples
9///
10/// ```no_run
11/// use promptuity::prompts::{Password, PasswordFormatter};
12///
13/// struct CustomFormatter;
14///
15/// impl PasswordFormatter for CustomFormatter {
16///     fn err_required(&self) -> String {
17///         "REQUIRED!!".into()
18///     }
19/// }
20///
21/// let _ = Password::new("...").with_formatter(CustomFormatter);
22/// ```
23pub trait PasswordFormatter {
24    /// Formats the error message when the input is empty and required.
25    fn err_required(&self) -> String {
26        "This field is required.".into()
27    }
28}
29
30/// The default formatter for [`Password`].
31pub struct DefaultPasswordFormatter;
32
33impl DefaultPasswordFormatter {
34    #[allow(clippy::new_without_default)]
35    pub fn new() -> Self {
36        Self {}
37    }
38}
39
40impl PasswordFormatter for DefaultPasswordFormatter {}
41
42/// A text input prompt where the input is not displayed.
43///
44/// # Options
45///
46/// - **Formatter**: Customizes the prompt display. See [`PasswordFormatter`].
47/// - **Hint**: A message to assist with field input. Defaults to `None`.
48/// - **Required**: A flag indicating whether to allow no input.
49/// - **Mask**: A string used to mask the input value. Defaults to `*`.
50/// - **Validator**: A function to validate the value at the time of submission.
51///
52/// # Examples
53///
54/// ```no_run
55/// use promptuity::prompts::Password;
56///
57/// let _ = Password::new("What is your password?").with_required(false);
58/// ```
59pub struct Password {
60    formatter: Box<dyn PasswordFormatter>,
61    message: String,
62    hint: Option<String>,
63    required: bool,
64    mask: char,
65    validator: Option<Box<dyn Validator<String>>>,
66    input: InputCursor,
67}
68
69impl Password {
70    /// Creates a new [`Password`] prompt.
71    pub fn new(message: impl std::fmt::Display) -> Self {
72        Self {
73            formatter: Box::new(DefaultPasswordFormatter::new()),
74            message: message.to_string(),
75            hint: None,
76            required: true,
77            mask: '*',
78            validator: None,
79            input: InputCursor::new(String::new(), 0),
80        }
81    }
82
83    /// Sets the formatter for the prompt.
84    pub fn with_formatter(&mut self, formatter: impl PasswordFormatter + 'static) -> &mut Self {
85        self.formatter = Box::new(formatter);
86        self
87    }
88
89    /// Sets the hint message for the prompt.
90    pub fn with_hint(&mut self, hint: impl std::fmt::Display) -> &mut Self {
91        self.hint = Some(hint.to_string());
92        self
93    }
94
95    /// Sets the required flag for the prompt.
96    pub fn with_required(&mut self, required: bool) -> &mut Self {
97        self.required = required;
98        self
99    }
100
101    /// Sets the mask char for the prompt.
102    pub fn with_mask(&mut self, mask: char) -> &mut Self {
103        self.mask = mask;
104        self
105    }
106
107    /// Sets the validator for the prompt.
108    pub fn with_validator(&mut self, f: impl Validator<String> + 'static) -> &mut Self {
109        self.validator = Some(Box::new(move |value: &String| -> Result<(), String> {
110            f.validate(value).map_err(|err| err.to_string())
111        }));
112        self
113    }
114}
115
116impl AsMut<Password> for Password {
117    fn as_mut(&mut self) -> &mut Password {
118        self
119    }
120}
121
122impl Prompt for Password {
123    type Output = String;
124
125    fn handle(
126        &mut self,
127        code: crossterm::event::KeyCode,
128        modifiers: crossterm::event::KeyModifiers,
129    ) -> crate::PromptState {
130        match (code, modifiers) {
131            (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => PromptState::Cancel,
132            (KeyCode::Enter, _) => {
133                if self.input.is_empty() && self.required {
134                    PromptState::Error(self.formatter.err_required())
135                } else {
136                    PromptState::Submit
137                }
138            }
139            (KeyCode::Left, _) | (KeyCode::Char('b'), KeyModifiers::CONTROL) => {
140                self.input.move_left();
141                PromptState::Active
142            }
143            (KeyCode::Right, _) | (KeyCode::Char('f'), KeyModifiers::CONTROL) => {
144                self.input.move_right();
145                PromptState::Active
146            }
147            (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
148                self.input.move_home();
149                PromptState::Active
150            }
151            (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
152                self.input.move_end();
153                PromptState::Active
154            }
155            (KeyCode::Backspace, _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
156                self.input.delete_left_char();
157                PromptState::Active
158            }
159            (KeyCode::Char('w'), KeyModifiers::CONTROL) => {
160                self.input.delete_left_word();
161                PromptState::Active
162            }
163            (KeyCode::Delete, _) | (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
164                self.input.delete_right_char();
165                PromptState::Active
166            }
167            (KeyCode::Char('k'), KeyModifiers::CONTROL) => {
168                self.input.delete_rest_line();
169                PromptState::Active
170            }
171            (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
172                self.input.delete_line();
173                PromptState::Active
174            }
175            (KeyCode::Char(c), _) => {
176                self.input.insert(c);
177                PromptState::Active
178            }
179            _ => PromptState::Active,
180        }
181    }
182
183    fn submit(&mut self) -> Self::Output {
184        self.input.value()
185    }
186
187    fn render(&mut self, _: &crate::PromptState) -> Result<crate::RenderPayload, String> {
188        let input = InputCursor::new(
189            self.input.value().chars().map(|_| self.mask).collect(),
190            self.input.cursor(),
191        );
192
193        Ok(
194            RenderPayload::new(self.message.clone(), self.hint.clone(), None)
195                .input(PromptInput::Cursor(input)),
196        )
197    }
198
199    fn validate(&self) -> Result<(), String> {
200        self.validator
201            .as_ref()
202            .map_or(Ok(()), |validator| validator.validate(&self.input.value()))
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::test_prompt;
210
211    test_prompt!(
212        test_hint,
213        Password::new("test message").with_hint("hint message"),
214        vec![]
215    );
216
217    test_prompt!(
218        test_required_error,
219        Password::new("test message").with_required(true),
220        vec![(KeyCode::Enter, KeyModifiers::NONE)]
221    );
222
223    test_prompt!(
224        test_non_required_empty_submit,
225        Password::new("test message").with_required(false),
226        vec![(KeyCode::Enter, KeyModifiers::NONE)]
227    );
228
229    test_prompt!(
230        test_input,
231        Password::new("test message").as_mut(),
232        vec![
233            (KeyCode::Char('a'), KeyModifiers::NONE),
234            (KeyCode::Char('b'), KeyModifiers::NONE),
235            (KeyCode::Char('1'), KeyModifiers::NONE),
236            (KeyCode::Char('0'), KeyModifiers::NONE),
237            (KeyCode::Enter, KeyModifiers::NONE),
238        ]
239    );
240
241    test_prompt!(
242        test_editing,
243        Password::new("test message").as_mut(),
244        vec![
245            (KeyCode::Char('a'), KeyModifiers::NONE),
246            (KeyCode::Char('b'), KeyModifiers::NONE),
247            (KeyCode::Char('c'), KeyModifiers::NONE),
248            (KeyCode::Char('d'), KeyModifiers::NONE),
249            (KeyCode::Char('e'), KeyModifiers::NONE),
250            (KeyCode::Char('f'), KeyModifiers::NONE),
251            (KeyCode::Backspace, KeyModifiers::NONE),
252            (KeyCode::Char('h'), KeyModifiers::CONTROL),
253            (KeyCode::Char('h'), KeyModifiers::NONE),
254            (KeyCode::Home, KeyModifiers::NONE),
255            (KeyCode::Delete, KeyModifiers::NONE),
256            (KeyCode::Right, KeyModifiers::NONE),
257            (KeyCode::Char('d'), KeyModifiers::CONTROL),
258            (KeyCode::Char('k'), KeyModifiers::CONTROL),
259            (KeyCode::Char('a'), KeyModifiers::NONE),
260            (KeyCode::Char('r'), KeyModifiers::NONE),
261            (KeyCode::Char(' '), KeyModifiers::NONE),
262            (KeyCode::Char('b'), KeyModifiers::NONE),
263            (KeyCode::Char('a'), KeyModifiers::NONE),
264            (KeyCode::Char('z'), KeyModifiers::NONE),
265            (KeyCode::Char('w'), KeyModifiers::CONTROL),
266            (KeyCode::Char('w'), KeyModifiers::CONTROL),
267            (KeyCode::Char('a'), KeyModifiers::NONE),
268            (KeyCode::Char('b'), KeyModifiers::NONE),
269            (KeyCode::Char('c'), KeyModifiers::NONE),
270            (KeyCode::Left, KeyModifiers::NONE),
271            (KeyCode::Left, KeyModifiers::NONE),
272            (KeyCode::Char('u'), KeyModifiers::CONTROL),
273        ]
274    );
275
276    test_prompt!(
277        test_custom_mask,
278        Password::new("test message").with_mask('∙'),
279        vec![
280            (KeyCode::Char('a'), KeyModifiers::NONE),
281            (KeyCode::Char('b'), KeyModifiers::NONE),
282            (KeyCode::Char('c'), KeyModifiers::NONE),
283            (KeyCode::Enter, KeyModifiers::NONE),
284        ]
285    );
286}