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}