console-rs / dialoguer

Rust utility library for nice command line prompts and similar things
MIT License
1.33k stars 142 forks source link

Feature request: alert message with pressing enter as only option #287

Open sluedecke opened 1 year ago

sluedecke commented 1 year ago

In case of an unexpected events, I would love to display a warning to the user, which simply needs to be "confirmed" for the programm to move on.

The workaround I use is a selection with "OK" as single option, but this is a bit hacky.

I kindly request to add alert as a new interaction option to dialoguer. One can use the patch I attached, but I probably messed the return types up ...

diff --git a/examples/alert.rs b/examples/alert.rs
new file mode 100644
index 0000000..b8a9188
--- /dev/null
+++ b/examples/alert.rs
@@ -0,0 +1,16 @@
+use dialoguer::{theme::ColorfulTheme, Alert};
+
+fn main() {
+    let _ = Alert::with_theme(&ColorfulTheme::default())
+        .with_prompt("Something went wrong!  Press enter to continue.")
+        .interact();
+
+    let _ = Alert::with_theme(&ColorfulTheme::default())
+        .with_alert_text("This is an alert, press enter to continue.")
+        .interact();
+
+    let _ = Alert::with_theme(&ColorfulTheme::default())
+        .with_alert_text("Strange things happened: <spooky error message>.")
+        .with_prompt("Press enter to continue.")
+        .interact();
+}
diff --git a/src/lib.rs b/src/lib.rs
index 46bf30c..a76967d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -48,7 +48,8 @@ pub use prompts::fuzzy_select::FuzzySelect;
 #[cfg(feature = "password")]
 pub use prompts::password::Password;
 pub use prompts::{
-    confirm::Confirm, input::Input, multi_select::MultiSelect, select::Select, sort::Sort,
+    alert::Alert, confirm::Confirm, input::Input, multi_select::MultiSelect, select::Select,
+    sort::Sort,
 };

 #[cfg(feature = "completion")]
diff --git a/src/prompts/alert.rs b/src/prompts/alert.rs
new file mode 100644
index 0000000..ed45b98
--- /dev/null
+++ b/src/prompts/alert.rs
@@ -0,0 +1,148 @@
+use std::io;
+
+use console::{Key, Term};
+
+use crate::{
+    theme::{render::TermThemeRenderer, SimpleTheme, Theme},
+    Result,
+};
+
+/// Renders an alert prompt.
+///
+/// ## Example
+///
+/// ```rust,no_run
+/// use dialoguer::{theme::ColorfulTheme, Alert};
+///
+/// fn main() {
+///     let _ = Alert::with_theme(&ColorfulTheme::default())
+///         .with_prompt("Something went wrong!  Press enter to continue.")
+///         .interact();
+///
+///     let _ = Alert::with_theme(&ColorfulTheme::default())
+///         .with_alert_text("This is an alert, press enter to continue.")
+///         .interact();
+///
+///     let _ = Alert::with_theme(&ColorfulTheme::default())
+///         .with_alert_text("Strange things happened: <spooky error message>.")
+///         .with_prompt("Press enter to continue.")
+///         .interact();
+/// }
+/// ```
+#[derive(Clone)]
+pub struct Alert<'a> {
+    alert_text: String,
+    prompt: String,
+    theme: &'a dyn Theme,
+}
+
+impl Default for Alert<'static> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Alert<'static> {
+    /// Creates a alert prompt with default theme.
+    pub fn new() -> Self {
+        Self::with_theme(&SimpleTheme)
+    }
+}
+
+impl Alert<'_> {
+    /// Sets the alert content message.
+    pub fn with_alert_text<S: Into<String>>(mut self, alert_text: S) -> Self {
+        self.alert_text = alert_text.into();
+        self
+    }
+
+    /// Sets the alert prompt.
+    pub fn with_prompt<S: Into<String>>(mut self, prompt: S) -> Self {
+        self.prompt = prompt.into();
+        self
+    }
+
+    /// Enables user interaction.
+    ///
+    /// The dialog is rendered on stderr.
+    #[inline]
+    pub fn interact(self) -> Result<Option<()>> {
+        self.interact_on(&Term::stderr())
+    }
+
+    /// Like [`interact`](Self::interact) but allows a specific terminal to be set.
+    #[inline]
+    pub fn interact_on(self, term: &Term) -> Result<Option<()>> {
+        Ok(Some(self._interact_on(term)?.ok_or_else(|| {
+            io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")
+        })?))
+    }
+
+    fn _interact_on(self, term: &Term) -> Result<Option<()>> {
+        if !term.is_term() {
+            return Err(io::Error::new(io::ErrorKind::NotConnected, "not a terminal").into());
+        }
+
+        let mut render = TermThemeRenderer::new(term, self.theme);
+
+        render.alert_prompt(&self.alert_text, &self.prompt)?;
+
+        term.hide_cursor()?;
+        term.flush()?;
+
+        // Default behavior:  wait for user to hit the Enter key.
+        loop {
+            let input = term.read_key()?;
+            match input {
+                Key::Enter => (),
+                _ => {
+                    continue;
+                }
+            };
+
+            break;
+        }
+
+        term.write_line("")?;
+        term.show_cursor()?;
+        term.flush()?;
+
+        Ok(Some(()))
+    }
+}
+
+impl<'a> Alert<'a> {
+    /// Creates an alert prompt with a specific theme.
+    ///
+    /// ## Example
+    ///
+    /// ```rust,no_run
+    /// use dialoguer::{theme::ColorfulTheme, Alert};
+    ///
+    /// fn main() {
+    ///     let alert = Alert::with_theme(&ColorfulTheme::default())
+    ///         .interact();
+    /// }
+    /// ```
+    pub fn with_theme(theme: &'a dyn Theme) -> Self {
+        Self {
+            alert_text: "".into(),
+            prompt: "".into(),
+            theme,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_clone() {
+        let alert = Alert::new()
+            .with_alert_text("FYI: ground gets wet if it rains.")
+            .with_prompt("Press enter continue");
+
+        let _ = alert.clone();
+    }
+}
diff --git a/src/prompts/mod.rs b/src/prompts/mod.rs
index 1c13185..2842ce5 100644
--- a/src/prompts/mod.rs
+++ b/src/prompts/mod.rs
@@ -1,5 +1,6 @@
 #![allow(clippy::needless_doctest_main)]

+pub mod alert;
 pub mod confirm;
 pub mod input;
 pub mod multi_select;
diff --git a/src/theme/mod.rs b/src/theme/mod.rs
index d22001c..c1f462f 100644
--- a/src/theme/mod.rs
+++ b/src/theme/mod.rs
@@ -27,6 +27,25 @@ pub trait Theme {
         write!(f, "error: {}", err)
     }

+    /// Formats an alert prompt.
+    fn format_alert_prompt(
+        &self,
+        f: &mut dyn fmt::Write,
+        alert_text: &str,
+        prompt: &str,
+    ) -> fmt::Result {
+        if !alert_text.is_empty() {
+            write!(f, "⚠ {}", &alert_text)?;
+        }
+        if !prompt.is_empty() {
+            if !alert_text.is_empty() {
+                writeln!(f, "")?;
+            }
+            write!(f, "{}", &prompt)?;
+        }
+        Ok(())
+    }
+
     /// Formats a confirm prompt.
     fn format_confirm_prompt(
         &self,
diff --git a/src/theme/render.rs b/src/theme/render.rs
index e6f3add..06d076a 100644
--- a/src/theme/render.rs
+++ b/src/theme/render.rs
@@ -87,6 +87,12 @@ impl<'a> TermThemeRenderer<'a> {
         self.write_formatted_line(|this, buf| this.theme.format_error(buf, err))
     }

+    pub fn alert_prompt(&mut self, alert_text: &str, prompt: &str) -> Result<usize> {
+        self.write_formatted_str(|this, buf| {
+            this.theme.format_alert_prompt(buf, alert_text, prompt)
+        })
+    }
+
     pub fn confirm_prompt(&mut self, prompt: &str, default: Option<bool>) -> Result<usize> {
         self.write_formatted_str(|this, buf| this.theme.format_confirm_prompt(buf, prompt, default))
     }