dialoguer/prompts/
password.rs

1use std::{io, sync::Arc};
2
3use console::Term;
4use zeroize::Zeroizing;
5
6use crate::{
7    theme::{render::TermThemeRenderer, SimpleTheme, Theme},
8    validate::PasswordValidator,
9    Result,
10};
11
12type PasswordValidatorCallback<'a> = Arc<dyn Fn(&String) -> Option<String> + 'a>;
13
14/// Renders a password input prompt.
15///
16/// ## Example
17///
18/// ```rust,no_run
19/// use dialoguer::Password;
20///
21/// fn main() {
22///     let password = Password::new()
23///         .with_prompt("New Password")
24///         .with_confirmation("Confirm password", "Passwords mismatching")
25///         .interact()
26///         .unwrap();
27///
28///     println!("Your password length is: {}", password.len());
29/// }
30/// ```
31#[derive(Clone)]
32pub struct Password<'a> {
33    prompt: String,
34    report: bool,
35    theme: &'a dyn Theme,
36    allow_empty_password: bool,
37    confirmation_prompt: Option<(String, String)>,
38    validator: Option<PasswordValidatorCallback<'a>>,
39}
40
41impl Default for Password<'static> {
42    fn default() -> Password<'static> {
43        Self::new()
44    }
45}
46
47impl Password<'static> {
48    /// Creates a password input prompt with default theme.
49    pub fn new() -> Password<'static> {
50        Self::with_theme(&SimpleTheme)
51    }
52}
53
54impl Password<'_> {
55    /// Sets the password input prompt.
56    pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
57        self.prompt = prompt.into();
58        self
59    }
60
61    /// Indicates whether to report confirmation after interaction.
62    ///
63    /// The default is to report.
64    pub fn report(mut self, val: bool) -> Self {
65        self.report = val;
66        self
67    }
68
69    /// Enables confirmation prompting.
70    pub fn with_confirmation<A, B>(mut self, prompt: A, mismatch_err: B) -> Self
71    where
72        A: Into<String>,
73        B: Into<String>,
74    {
75        self.confirmation_prompt = Some((prompt.into(), mismatch_err.into()));
76        self
77    }
78
79    /// Allows/Disables empty password.
80    ///
81    /// By default this setting is set to false (i.e. password is not empty).
82    pub fn allow_empty_password(mut self, allow_empty_password: bool) -> Self {
83        self.allow_empty_password = allow_empty_password;
84        self
85    }
86
87    /// Enables user interaction and returns the result.
88    ///
89    /// If the user confirms the result is `Ok()`, `Err()` otherwise.
90    /// The dialog is rendered on stderr.
91    pub fn interact(self) -> Result<String> {
92        self.interact_on(&Term::stderr())
93    }
94
95    /// Like [`interact`](Self::interact) but allows a specific terminal to be set.
96    pub fn interact_on(self, term: &Term) -> Result<String> {
97        if !term.is_term() {
98            return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
99        }
100
101        let mut render = TermThemeRenderer::new(term, self.theme);
102        render.set_prompts_reset_height(false);
103
104        loop {
105            let password = Zeroizing::new(self.prompt_password(&mut render, &self.prompt)?);
106
107            if let Some(ref validator) = self.validator {
108                if let Some(err) = validator(&password) {
109                    render.error(&err)?;
110                    continue;
111                }
112            }
113
114            if let Some((ref prompt, ref err)) = self.confirmation_prompt {
115                let pw2 = Zeroizing::new(self.prompt_password(&mut render, prompt)?);
116
117                if *password != *pw2 {
118                    render.error(err)?;
119                    continue;
120                }
121            }
122
123            render.clear()?;
124
125            if self.report {
126                render.password_prompt_selection(&self.prompt)?;
127            }
128            term.flush()?;
129
130            return Ok((*password).clone());
131        }
132    }
133
134    fn prompt_password(&self, render: &mut TermThemeRenderer, prompt: &str) -> Result<String> {
135        loop {
136            render.password_prompt(prompt)?;
137            render.term().flush()?;
138
139            let input = render.term().read_secure_line()?;
140
141            render.add_line();
142
143            if !input.is_empty() || self.allow_empty_password {
144                return Ok(input);
145            }
146        }
147    }
148}
149
150impl<'a> Password<'a> {
151    /// Registers a validator.
152    ///
153    /// # Example
154    ///
155    /// ```rust,no_run
156    /// use dialoguer::Password;
157    ///
158    /// fn main() {
159    ///     let password: String = Password::new()
160    ///         .with_prompt("Enter password")
161    ///         .validate_with(|input: &String| -> Result<(), &str> {
162    ///             if input.chars().count() > 8 {
163    ///                 Ok(())
164    ///             } else {
165    ///                 Err("Password must be longer than 8")
166    ///             }
167    ///         })
168    ///         .interact()
169    ///         .unwrap();
170    /// }
171    /// ```
172    pub fn validate_with<V>(mut self, validator: V) -> Self
173    where
174        V: PasswordValidator + 'a,
175        V::Err: ToString,
176    {
177        let old_validator_func = self.validator.take();
178
179        self.validator = Some(Arc::new(move |value: &String| -> Option<String> {
180            if let Some(old) = &old_validator_func {
181                if let Some(err) = old(value) {
182                    return Some(err);
183                }
184            }
185
186            match validator.validate(value) {
187                Ok(()) => None,
188                Err(err) => Some(err.to_string()),
189            }
190        }));
191
192        self
193    }
194
195    /// Creates a password input prompt with a specific theme.
196    ///
197    /// ## Example
198    ///
199    /// ```rust,no_run
200    /// use dialoguer::{theme::ColorfulTheme, Password};
201    ///
202    /// fn main() {
203    ///     let password = Password::with_theme(&ColorfulTheme::default())
204    ///         .interact()
205    ///         .unwrap();
206    /// }
207    /// ```
208    pub fn with_theme(theme: &'a dyn Theme) -> Self {
209        Self {
210            prompt: "".into(),
211            report: true,
212            theme,
213            allow_empty_password: false,
214            confirmation_prompt: None,
215            validator: None,
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_clone() {
226        let password = Password::new().with_prompt("Enter password");
227
228        let _ = password.clone();
229    }
230}