Skip to main content

prompts/
confirm.rs

1//! Interactive prompt where the user can choose yes or no
2
3use crate::{
4    utils::{is_abort_event, print_input_icon, print_state_icon, PromptState},
5    Prompt,
6};
7use async_trait::async_trait;
8use crossterm::{
9    cursor,
10    event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
11    queue,
12    style::{style, Attribute, Color, Print, PrintStyledContent},
13    terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
14};
15use futures::StreamExt;
16use std::fmt;
17use std::io::{stdout, Write};
18
19/// Interactive prompt where the user can choose yes or no
20///
21/// Use <kbd>y</kbd>/<kbd>n</kbd> to answer the prompt.
22/// If default/initial is set <kbd>enter</kbd> will submit that value.
23///
24/// # Examples
25///
26/// ```
27/// use prompt::{confirm::ConfirmPrompt, Prompt};
28/// let mut prompt = ConfirmPrompt::new("Are you sure?");
29///
30/// match prompt.run().await {
31///     Ok(Some(true)) => println!("You were sure!"),
32///     Ok(Some(false)) => println!("You were not sure!"),
33///     Ok(None) => println!("Prompt was aborted!"),
34///     Err(e) => println!("Some kind of crossterm error happened: {:?}", e),
35/// }
36/// ```
37#[derive(Default)]
38pub struct ConfirmPrompt {
39    message: String,
40    state: PromptState,
41    answer: bool,
42    initial: Option<bool>,
43}
44impl fmt::Debug for ConfirmPrompt {
45    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
46        fmt.debug_struct("ConfirmPrompt")
47            .field("message", &self.message)
48            .field("initial", &self.initial)
49            .finish()
50    }
51}
52impl ConfirmPrompt {
53    /// Returns a ConfirmPrompt ready to be run
54    ///
55    /// # Arguments
56    ///
57    /// * `message` - The message to display to the user before the prompt
58    pub fn new<S>(message: S) -> ConfirmPrompt
59    where
60        S: Into<String>,
61    {
62        ConfirmPrompt {
63            message: message.into(),
64            ..Default::default()
65        }
66    }
67
68    /// Set default/initial answer
69    pub fn set_initial(mut self, initial: bool) -> ConfirmPrompt {
70        self.initial = Some(initial);
71        self
72    }
73}
74#[async_trait]
75impl Prompt<bool> for ConfirmPrompt {
76    /// Runs the prompt
77    ///
78    /// Stops either when the user selects an option, an error occurs,
79    /// or the prompt is aborted by the user using CTRL+c, CTRL+z or ESC.
80    async fn run(&mut self) -> std::result::Result<Option<bool>, crossterm::ErrorKind> {
81        enable_raw_mode()?;
82        let mut reader = EventStream::new();
83
84        self.display()?;
85
86        loop {
87            match reader.next().await {
88                Some(Ok(Event::Key(event))) => self.handle_key_event(event),
89                Some(Err(e)) => {
90                    disable_raw_mode()?;
91                    return Err(e);
92                }
93                _ => {}
94            }
95
96            self.display()?;
97
98            match self.state {
99                PromptState::Aborted => {
100                    disable_raw_mode()?;
101                    return Ok(None);
102                }
103                PromptState::Success => {
104                    disable_raw_mode()?;
105                    return Ok(Some(self.answer));
106                }
107                _ => (),
108            }
109        }
110    }
111    fn display(&mut self) -> crossterm::Result<()> {
112        let mut stdout = stdout();
113
114        queue!(
115            stdout,
116            cursor::MoveToColumn(0),
117            Clear(ClearType::FromCursorDown),
118            print_state_icon(&self.state),
119            Print(" "),
120            PrintStyledContent(style(&self.message).attribute(Attribute::Bold)),
121            Print(" "),
122            print_input_icon(&self.state),
123        )?;
124        if !self.state.is_done() {
125            queue!(
126                stdout,
127                PrintStyledContent(
128                    style(match self.initial {
129                        Some(true) => "(Y/n)",
130                        Some(false) => "(y/N)",
131                        None => "(y/n)",
132                    })
133                    .with(Color::DarkGrey)
134                )
135            )?;
136        }
137        if self.state == PromptState::Success {
138            queue!(stdout, Print(if self.answer { "yes" } else { "no" }))?;
139        }
140        if self.state.is_done() {
141            queue!(stdout, Print("\n\r"), cursor::Show)?;
142        }
143        stdout.flush()?;
144        crossterm::Result::Ok(())
145    }
146    fn handle_key_event(&mut self, event: KeyEvent) {
147        if is_abort_event(event) {
148            self.state = PromptState::Aborted;
149            return;
150        }
151        if event.modifiers == KeyModifiers::empty() {
152            match event.code {
153                KeyCode::Enter => {
154                    if let Some(initial) = self.initial {
155                        self.answer = initial;
156                        self.state = PromptState::Success;
157                    }
158                }
159                KeyCode::Char('y') => {
160                    self.answer = true;
161                    self.state = PromptState::Success;
162                }
163                KeyCode::Char('n') => {
164                    self.answer = false;
165                    self.state = PromptState::Success;
166                }
167                _ => {}
168            }
169        }
170    }
171}