Skip to main content

cli_ui/prompt/
confirm.rs

1//! Boolean confirmation prompt — built on the [`Prompt`] trait.
2
3use crate::styles::{paint, ACCENT, DIM, WHITE};
4use std::io::Write;
5
6use super::core::{Frame, Key, Prompt, RenderCtx, Step};
7use super::error::{PromptError, Result};
8use super::theme;
9
10/// Builder returned by [`confirm()`].
11pub struct ConfirmPrompt {
12    label: String,
13    default: Option<bool>,
14    value: bool,
15}
16
17/// Ask the user a yes/no question. Returns `true` for Yes.
18///
19/// ```no_run
20/// use cli_ui::prompt::confirm;
21///
22/// let init_git = confirm("Initialise git?").default(true).run()?;
23/// # Ok::<(), cli_ui::prompt::PromptError>(())
24/// ```
25pub fn confirm(label: impl Into<String>) -> ConfirmPrompt {
26    ConfirmPrompt {
27        label: label.into(),
28        default: None,
29        value: true,
30    }
31}
32
33impl ConfirmPrompt {
34    /// Initial selection — also accepted as the value when the user just
35    /// hits Enter without choosing.
36    pub fn default(mut self, v: bool) -> Self {
37        self.default = Some(v);
38        self.value = v;
39        self
40    }
41
42    /// Run the prompt to completion.
43    pub fn run(self) -> Result<bool> {
44        super::core::run(self)
45    }
46}
47
48impl Prompt for ConfirmPrompt {
49    type Output = bool;
50
51    fn handle(&mut self, key: Key) -> Step<bool> {
52        match key {
53            Key::Left | Key::Right => {
54                self.value = !self.value;
55                Step::Continue
56            }
57            Key::Char('y') | Key::Char('Y') => Step::Submit(true),
58            Key::Char('n') | Key::Char('N') => Step::Submit(false),
59            Key::Enter => Step::Submit(self.value),
60            Key::Escape | Key::Interrupt => Step::Cancel,
61            _ => Step::Continue,
62        }
63    }
64
65    fn render(&self, _ctx: RenderCtx) -> Frame {
66        vec![
67            theme::label(&self.label),
68            theme::confirm_line(self.value),
69            theme::frame_bot(None),
70        ]
71    }
72
73    fn render_answered(&self, value: &bool) -> Frame {
74        theme::answered(&self.label, if *value { "Yes" } else { "No" })
75            .split("\r\n")
76            .map(String::from)
77            .collect()
78    }
79
80    fn run_fallback(self) -> Result<bool> {
81        let hint = match self.default {
82            Some(true) => "Y/n",
83            Some(false) => "y/N",
84            None => "y/n",
85        };
86        eprint!(
87            "{}  {} [{}]: ",
88            paint(ACCENT, "◆"),
89            paint(WHITE, &self.label),
90            paint(DIM, hint)
91        );
92        std::io::stderr().flush().map_err(PromptError::Io)?;
93        loop {
94            let line = super::engine::fallback::read_line_raw()?;
95            match line.to_lowercase().as_str() {
96                "y" | "yes" => return Ok(true),
97                "n" | "no" => return Ok(false),
98                "" if self.default.is_some() => return Ok(self.default.unwrap()),
99                _ => {
100                    eprint!("  Please enter y or n: ");
101                    std::io::stderr().flush().map_err(PromptError::Io)?;
102                }
103            }
104        }
105    }
106}