From 5bd07195e7897092ab811f51d23c1263f103d3c1 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:09:13 -0600 Subject: [PATCH] added confirm --- examples/confirm.rs | 9 ++ src/confirm.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/theme.rs | 15 ++++ 4 files changed, 229 insertions(+) create mode 100644 examples/confirm.rs create mode 100644 src/confirm.rs diff --git a/examples/confirm.rs b/examples/confirm.rs new file mode 100644 index 0000000..0a4f7f5 --- /dev/null +++ b/examples/confirm.rs @@ -0,0 +1,9 @@ +use demand::Confirm; + +fn main() { + let ms = Confirm::new("Are you sure?") + .affirmative("Yes!") + .negative("No."); + let yes = ms.run().expect("error running confirm"); + println!("yes: {}", yes); +} diff --git a/src/confirm.rs b/src/confirm.rs new file mode 100644 index 0000000..3e0fcea --- /dev/null +++ b/src/confirm.rs @@ -0,0 +1,203 @@ +use crate::theme::Theme; + +use console::{Key, Term}; + + +use std::io; +use std::io::Write; +use termcolor::{Buffer, WriteColor}; + +/// Select multiple options from a list +/// +/// # Example +/// ```rust +/// use demand::Confirm; +/// +/// let ms = Confirm::new("Are you sure?") +/// .affirmative("Yes!") +/// .negative("No."); +/// let yes = ms.run().expect("error running confirm"); +/// println!("yes: {}", yes); +/// ``` +pub struct Confirm { + /// The title of the selector + pub title: String, + /// The colors/style of the selector + pub theme: Theme, + /// A description to display above the selector + pub description: String, + /// The text to display for the affirmative option + pub affirmative: String, + /// The text to display for the negative option + pub negative: String, + /// If true, the affirmative option is selected by default + pub selected: bool, + term: Term, + height: usize, +} + +impl Confirm { + /// Create a new multi select with the given title + pub fn new>(title: S) -> Self { + Self { + title: title.into(), + description: String::new(), + theme: Theme::default(), + term: Term::stderr(), + affirmative: "Yes".to_string(), + negative: "No".to_string(), + selected: true, + height: 0, + } + } + + /// Set the description of the selector + pub fn description(mut self, description: &str) -> Self { + self.description = description.to_string(); + self + } + + /// Set the label of the affirmative option + pub fn affirmative>(mut self, affirmative: S) -> Self { + self.affirmative = affirmative.into(); + self + } + + /// Set the label of the negative option + pub fn negative>(mut self, negative: S) -> Self { + self.negative = negative.into(); + self + } + + /// Set whether the affirmative option is selected by default + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + + /// Set the theme of the dialog + pub fn theme(mut self, theme: Theme) -> Self { + self.theme = theme; + self + } + + /// Displays the dialog to the user and returns their response + pub fn run(mut self) -> io::Result { + let affirmative_char = self.affirmative.to_lowercase().chars().next().unwrap(); + let negative_char = self.negative.to_lowercase().chars().next().unwrap(); + loop { + self.clear()?; + let output = self.render()?; + self.height = output.lines().count() - 1; + self.term.write_all(output.as_bytes())?; + self.term.flush()?; + match self.term.read_key()? { + Key::ArrowLeft | Key::Char('h') => self.handle_left(), + Key::ArrowRight | Key::Char('l') => self.handle_right(), + Key::Char(c) if c == affirmative_char => { + self.selected = true; + return self.handle_submit(); + } + Key::Char(c) if c == negative_char => { + self.selected = false; + return self.handle_submit(); + } + Key::Enter => { + return self.handle_submit(); + } + _ => {} + } + } + } + + fn handle_submit(mut self) -> io::Result { + self.clear()?; + let output = self.render_success()?; + self.term.write_all(output.as_bytes())?; + Ok(self.selected) + } + + fn handle_left(&mut self) { + if !self.selected { + self.selected = true; + } + } + + fn handle_right(&mut self) { + if self.selected { + self.selected = false; + } + } + + fn render(&self) -> io::Result { + let mut out = Buffer::ansi(); + + out.set_color(&self.theme.title)?; + write!(out, " {}", self.title)?; + + if !self.description.is_empty() { + out.set_color(&self.theme.description)?; + write!(out, " {}", self.description)?; + writeln!(out)?; + } + writeln!(out, "\n")?; + + write!(out, " ")?; + if self.selected { + out.set_color(&self.theme.focused_button)?; + } else { + out.set_color(&self.theme.blurred_button)?; + } + write!(out, " {} ", self.affirmative)?; + out.reset()?; + write!(out, " ")?; + if self.selected { + out.set_color(&self.theme.blurred_button)?; + } else { + out.set_color(&self.theme.focused_button)?; + } + write!(out, " {} ", self.negative)?; + out.reset()?; + writeln!(out, "\n")?; + + let mut help_keys = vec![("←/→", "toggle")]; + let affirmative_char = self.affirmative.to_lowercase().chars().next().unwrap(); + let negative_char = self.negative.to_lowercase().chars().next().unwrap(); + let submit_keys = format!("{affirmative_char}/{negative_char}/enter"); + help_keys.push((&submit_keys, "submit")); + for (i, (key, desc)) in help_keys.iter().enumerate() { + if i > 0 { + out.set_color(&self.theme.help_sep)?; + write!(out, " • ")?; + } + out.set_color(&self.theme.help_key)?; + write!(out, "{}", key)?; + out.set_color(&self.theme.help_desc)?; + write!(out, " {}", desc)?; + } + + out.reset()?; + Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string()) + } + + fn render_success(&self) -> io::Result { + let mut out = Buffer::ansi(); + out.set_color(&self.theme.title)?; + write!(out, " {}", self.title)?; + out.set_color(&self.theme.selected_option)?; + if self.selected { + writeln!(out, " {}", self.affirmative)?; + } else { + writeln!(out, " {}", self.negative)?; + } + out.reset()?; + Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string()) + } + + fn clear(&mut self) -> io::Result<()> { + self.term.clear_to_end_of_screen()?; + self.term.clear_last_lines(self.height)?; + self.height = 0; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index ded1025..732b5fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ //! A prompt library for Rust. Based on [huh? for Go](https://github.com/charmbracelet/huh). +pub use confirm::Confirm; pub use multiselect::MultiSelect; pub use option::DemandOption; pub use theme::Theme; +mod confirm; mod multiselect; mod option; mod theme; diff --git a/src/theme.rs b/src/theme.rs index 33cb855..596bb1b 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -19,6 +19,9 @@ pub struct Theme { pub help_key: ColorSpec, pub help_desc: ColorSpec, pub help_sep: ColorSpec, + + pub focused_button: ColorSpec, + pub blurred_button: ColorSpec, } impl Theme { @@ -39,6 +42,8 @@ impl Theme { help_key: ColorSpec::new(), help_desc: ColorSpec::new(), help_sep: ColorSpec::new(), + focused_button: ColorSpec::new(), + blurred_button: ColorSpec::new(), } } @@ -49,10 +54,17 @@ impl Theme { let red = Color::Rgb(255, 70, 114); let fuchsia = Color::Rgb(247, 128, 226); let green = Color::Rgb(2, 191, 135); + let cream = Color::Rgb(255, 253, 245); let mut title = make_color(indigo); title.set_bold(true); + let mut focused_button = make_color(cream); + focused_button.set_bg(Some(fuchsia)); + + let mut blurred_button = make_color(normal); + blurred_button.set_bg(Some(Color::Ansi256(238))); + Self { title, error_indicator: make_color(red), @@ -71,6 +83,9 @@ impl Theme { help_key: make_color(Color::Rgb(98, 98, 98)), help_desc: make_color(Color::Rgb(74, 74, 74)), help_sep: make_color(Color::Rgb(60, 60, 60)), + + focused_button, + blurred_button, } } }