1use crate::event::*;
2use crate::{InputCursor, Prompt, PromptInput, PromptState, RenderPayload, Validator};
3
4pub trait PasswordFormatter {
24 fn err_required(&self) -> String {
26 "This field is required.".into()
27 }
28}
29
30pub 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
42pub 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 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 pub fn with_formatter(&mut self, formatter: impl PasswordFormatter + 'static) -> &mut Self {
85 self.formatter = Box::new(formatter);
86 self
87 }
88
89 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 pub fn with_required(&mut self, required: bool) -> &mut Self {
97 self.required = required;
98 self
99 }
100
101 pub fn with_mask(&mut self, mask: char) -> &mut Self {
103 self.mask = mask;
104 self
105 }
106
107 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}