dialoguer/prompts/
confirm.rs

1use std::io;
2
3use console::{Key, Term};
4
5use crate::{
6    theme::{render::TermThemeRenderer, SimpleTheme, Theme},
7    Result,
8};
9
10/// Renders a confirm prompt.
11///
12/// ## Example
13///
14/// ```rust,no_run
15/// use dialoguer::Confirm;
16///
17/// fn main() {
18///     let confirmation = Confirm::new()
19///         .with_prompt("Do you want to continue?")
20///         .interact()
21///         .unwrap();
22///
23///     if confirmation {
24///         println!("Looks like you want to continue");
25///     } else {
26///         println!("nevermind then :(");
27///     }
28/// }
29/// ```
30#[derive(Clone)]
31pub struct Confirm<'a> {
32    prompt: String,
33    report: bool,
34    default: Option<bool>,
35    show_default: bool,
36    wait_for_newline: bool,
37    theme: &'a dyn Theme,
38}
39
40impl Default for Confirm<'static> {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl Confirm<'static> {
47    /// Creates a confirm prompt with default theme.
48    pub fn new() -> Self {
49        Self::with_theme(&SimpleTheme)
50    }
51}
52
53impl Confirm<'_> {
54    /// Sets the confirm prompt.
55    pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
56        self.prompt = prompt.into();
57        self
58    }
59
60    /// Indicates whether or not to report the chosen selection after interaction.
61    ///
62    /// The default is to report the chosen selection.
63    pub fn report(mut self, val: bool) -> Self {
64        self.report = val;
65        self
66    }
67
68    /// Sets when to react to user input.
69    ///
70    /// When `false` (default), we check on each user keystroke immediately as
71    /// it is typed. Valid inputs can be one of 'y', 'n', or a newline to accept
72    /// the default.
73    ///
74    /// When `true`, the user must type their choice and hit the Enter key before
75    /// proceeding. Valid inputs can be "yes", "no", "y", "n", or an empty string
76    /// to accept the default.
77    pub fn wait_for_newline(mut self, wait: bool) -> Self {
78        self.wait_for_newline = wait;
79        self
80    }
81
82    /// Sets a default.
83    ///
84    /// Out of the box the prompt does not have a default and will continue
85    /// to display until the user inputs something and hits enter. If a default is set the user
86    /// can instead accept the default with enter.
87    pub fn default(mut self, val: bool) -> Self {
88        self.default = Some(val);
89        self
90    }
91
92    /// Disables or enables the default value display.
93    ///
94    /// The default is to append the default value to the prompt to tell the user.
95    pub fn show_default(mut self, val: bool) -> Self {
96        self.show_default = val;
97        self
98    }
99
100    /// Enables user interaction and returns the result.
101    ///
102    /// The dialog is rendered on stderr.
103    ///
104    /// Result contains `bool` if user answered "yes" or "no" or `default` (configured in [`default`](Self::default) if pushes enter.
105    /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'.
106    #[inline]
107    pub fn interact(self) -> Result<bool> {
108        self.interact_on(&Term::stderr())
109    }
110
111    /// Enables user interaction and returns the result.
112    ///
113    /// The dialog is rendered on stderr.
114    ///
115    /// Result contains `Some(bool)` if user answered "yes" or "no" or `Some(default)` (configured in [`default`](Self::default)) if pushes enter,
116    /// or `None` if user cancelled with 'Esc' or 'q'.
117    ///
118    /// ## Example
119    ///
120    /// ```rust,no_run
121    /// use dialoguer::Confirm;
122    ///
123    /// fn main() {
124    ///     let confirmation = Confirm::new()
125    ///         .interact_opt()
126    ///         .unwrap();
127    ///
128    ///     match confirmation {
129    ///         Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }),
130    ///         None => println!("User did not answer")
131    ///     }
132    /// }
133    /// ```
134    #[inline]
135    pub fn interact_opt(self) -> Result<Option<bool>> {
136        self.interact_on_opt(&Term::stderr())
137    }
138
139    /// Like [`interact`](Self::interact) but allows a specific terminal to be set.
140    #[inline]
141    pub fn interact_on(self, term: &Term) -> Result<bool> {
142        Ok(self
143            ._interact_on(term, false)?
144            .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case"))?)
145    }
146
147    /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set.
148    #[inline]
149    pub fn interact_on_opt(self, term: &Term) -> Result<Option<bool>> {
150        self._interact_on(term, true)
151    }
152
153    fn _interact_on(self, term: &Term, allow_quit: bool) -> Result<Option<bool>> {
154        if !term.is_term() {
155            return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
156        }
157
158        let mut render = TermThemeRenderer::new(term, self.theme);
159
160        let default_if_show = if self.show_default {
161            self.default
162        } else {
163            None
164        };
165
166        render.confirm_prompt(&self.prompt, default_if_show)?;
167
168        term.hide_cursor()?;
169        term.flush()?;
170
171        let rv;
172
173        if self.wait_for_newline {
174            // Waits for user input and for the user to hit the Enter key
175            // before validation.
176            let mut value = default_if_show;
177
178            loop {
179                let input = term.read_key()?;
180
181                match input {
182                    Key::Char('y') | Key::Char('Y') => {
183                        value = Some(true);
184                    }
185                    Key::Char('n') | Key::Char('N') => {
186                        value = Some(false);
187                    }
188                    Key::Enter => {
189                        if !allow_quit {
190                            value = value.or(self.default);
191                        }
192
193                        if value.is_some() || allow_quit {
194                            rv = value;
195                            break;
196                        }
197                        continue;
198                    }
199                    Key::Escape | Key::Char('q') if allow_quit => {
200                        value = None;
201                    }
202                    _ => {
203                        continue;
204                    }
205                };
206
207                term.clear_line()?;
208                render.confirm_prompt(&self.prompt, value)?;
209            }
210        } else {
211            // Default behavior: matches continuously on every keystroke,
212            // and does not wait for user to hit the Enter key.
213            loop {
214                let input = term.read_key()?;
215                let value = match input {
216                    Key::Char('y') | Key::Char('Y') => Some(true),
217                    Key::Char('n') | Key::Char('N') => Some(false),
218                    Key::Enter if self.default.is_some() => Some(self.default.unwrap()),
219                    Key::Escape | Key::Char('q') if allow_quit => None,
220                    _ => {
221                        continue;
222                    }
223                };
224
225                rv = value;
226                break;
227            }
228        }
229
230        term.clear_line()?;
231        if self.report {
232            render.confirm_prompt_selection(&self.prompt, rv)?;
233        }
234        term.show_cursor()?;
235        term.flush()?;
236
237        Ok(rv)
238    }
239}
240
241impl<'a> Confirm<'a> {
242    /// Creates a confirm prompt with a specific theme.
243    ///
244    /// ## Example
245    ///
246    /// ```rust,no_run
247    /// use dialoguer::{theme::ColorfulTheme, Confirm};
248    ///
249    /// fn main() {
250    ///     let confirmation = Confirm::with_theme(&ColorfulTheme::default())
251    ///         .interact()
252    ///         .unwrap();
253    /// }
254    /// ```
255    pub fn with_theme(theme: &'a dyn Theme) -> Self {
256        Self {
257            prompt: "".into(),
258            report: true,
259            default: None,
260            show_default: true,
261            wait_for_newline: false,
262            theme,
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_clone() {
273        let confirm = Confirm::new().with_prompt("Do you want to continue?");
274
275        let _ = confirm.clone();
276    }
277}