diff options
Diffstat (limited to 'vendor/dialoguer/src/prompts')
-rw-r--r-- | vendor/dialoguer/src/prompts/confirm.rs | 287 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/fuzzy_select.rs | 326 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/input.rs | 691 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/mod.rs | 13 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/multi_select.rs | 356 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/password.rs | 194 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/select.rs | 419 | ||||
-rw-r--r-- | vendor/dialoguer/src/prompts/sort.rs | 348 |
8 files changed, 2634 insertions, 0 deletions
diff --git a/vendor/dialoguer/src/prompts/confirm.rs b/vendor/dialoguer/src/prompts/confirm.rs new file mode 100644 index 0000000..24bcc4c --- /dev/null +++ b/vendor/dialoguer/src/prompts/confirm.rs @@ -0,0 +1,287 @@ +use std::io; + +use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; + +use console::{Key, Term}; + +/// Renders a confirm prompt. +/// +/// ## Example usage +/// +/// ```rust,no_run +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// use dialoguer::Confirm; +/// +/// if Confirm::new().with_prompt("Do you want to continue?").interact()? { +/// println!("Looks like you want to continue"); +/// } else { +/// println!("nevermind then :("); +/// } +/// # Ok(()) } fn main() { test().unwrap(); } +/// ``` +pub struct Confirm<'a> { + prompt: String, + report: bool, + default: Option<bool>, + show_default: bool, + wait_for_newline: bool, + theme: &'a dyn Theme, +} + +impl Default for Confirm<'static> { + fn default() -> Self { + Self::new() + } +} + +impl Confirm<'static> { + /// Creates a confirm prompt. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } +} + +impl Confirm<'_> { + /// Sets the confirm prompt. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = prompt.into(); + self + } + + /// Indicates whether or not to report the chosen selection after interaction. + /// + /// The default is to report the chosen selection. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + #[deprecated(note = "Use with_prompt() instead", since = "0.6.0")] + #[inline] + pub fn with_text(&mut self, text: &str) -> &mut Self { + self.with_prompt(text) + } + + /// Sets when to react to user input. + /// + /// When `false` (default), we check on each user keystroke immediately as + /// it is typed. Valid inputs can be one of 'y', 'n', or a newline to accept + /// the default. + /// + /// When `true`, the user must type their choice and hit the Enter key before + /// proceeding. Valid inputs can be "yes", "no", "y", "n", or an empty string + /// to accept the default. + pub fn wait_for_newline(&mut self, wait: bool) -> &mut Self { + self.wait_for_newline = wait; + self + } + + /// Sets a default. + /// + /// Out of the box the prompt does not have a default and will continue + /// to display until the user inputs something and hits enter. If a default is set the user + /// can instead accept the default with enter. + pub fn default(&mut self, val: bool) -> &mut Self { + self.default = Some(val); + self + } + + /// Disables or enables the default value display. + /// + /// The default is to append the default value to the prompt to tell the user. + pub fn show_default(&mut self, val: bool) -> &mut Self { + self.show_default = val; + self + } + + /// Enables user interaction and returns the result. + /// + /// The dialog is rendered on stderr. + /// + /// Result contains `bool` if user answered "yes" or "no" or `default` (configured in [`default`](Self::default) if pushes enter. + /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. + #[inline] + pub fn interact(&self) -> io::Result<bool> { + self.interact_on(&Term::stderr()) + } + + /// Enables user interaction and returns the result. + /// + /// The dialog is rendered on stderr. + /// + /// Result contains `Some(bool)` if user answered "yes" or "no" or `Some(default)` (configured in [`default`](Self::default)) if pushes enter, + /// or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<bool>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + /// + /// ```rust,no_run + /// use dialoguer::Confirm; + /// use console::Term; + /// + /// # fn main() -> std::io::Result<()> { + /// let proceed = Confirm::new() + /// .with_prompt("Do you wish to continue?") + /// .interact_on(&Term::stderr())?; + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<bool> { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Confirm; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let confirmation = Confirm::new() + /// .interact_on_opt(&Term::stdout())?; + /// + /// match confirmation { + /// Some(answer) => println!("User answered {}", if answer { "yes" } else { "no " }), + /// None => println!("User did not answer") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<bool>> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<bool>> { + let mut render = TermThemeRenderer::new(term, self.theme); + + let default_if_show = if self.show_default { + self.default + } else { + None + }; + + render.confirm_prompt(&self.prompt, default_if_show)?; + + term.hide_cursor()?; + term.flush()?; + + let rv; + + if self.wait_for_newline { + // Waits for user input and for the user to hit the Enter key + // before validation. + let mut value = default_if_show; + + loop { + let input = term.read_key()?; + + match input { + Key::Char('y') | Key::Char('Y') => { + value = Some(true); + } + Key::Char('n') | Key::Char('N') => { + value = Some(false); + } + Key::Enter => { + if !allow_quit { + value = value.or(self.default); + } + + if value.is_some() || allow_quit { + rv = value; + break; + } + continue; + } + Key::Escape | Key::Char('q') if allow_quit => { + value = None; + } + Key::Unknown => { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "Not a terminal", + )) + } + _ => { + continue; + } + }; + + term.clear_line()?; + render.confirm_prompt(&self.prompt, value)?; + } + } else { + // Default behavior: matches continuously on every keystroke, + // and does not wait for user to hit the Enter key. + loop { + let input = term.read_key()?; + let value = match input { + Key::Char('y') | Key::Char('Y') => Some(true), + Key::Char('n') | Key::Char('N') => Some(false), + Key::Enter if self.default.is_some() => Some(self.default.unwrap()), + Key::Escape | Key::Char('q') if allow_quit => None, + Key::Unknown => { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "Not a terminal", + )) + } + _ => { + continue; + } + }; + + rv = value; + break; + } + } + + term.clear_line()?; + if self.report { + render.confirm_prompt_selection(&self.prompt, rv)?; + } + term.show_cursor()?; + term.flush()?; + + Ok(rv) + } +} + +impl<'a> Confirm<'a> { + /// Creates a confirm prompt with a specific theme. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::{ + /// Confirm, + /// theme::ColorfulTheme + /// }; + /// + /// # fn main() -> std::io::Result<()> { + /// let proceed = Confirm::with_theme(&ColorfulTheme::default()) + /// .with_prompt("Do you wish to continue?") + /// .interact()?; + /// # Ok(()) + /// # } + /// ``` + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + prompt: "".into(), + report: true, + default: None, + show_default: true, + wait_for_newline: false, + theme, + } + } +} diff --git a/vendor/dialoguer/src/prompts/fuzzy_select.rs b/vendor/dialoguer/src/prompts/fuzzy_select.rs new file mode 100644 index 0000000..9b7f992 --- /dev/null +++ b/vendor/dialoguer/src/prompts/fuzzy_select.rs @@ -0,0 +1,326 @@ +use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; +use console::{Key, Term}; +use fuzzy_matcher::FuzzyMatcher; +use std::{io, ops::Rem}; + +/// Renders a selection menu that user can fuzzy match to reduce set. +/// +/// User can use fuzzy search to limit selectable items. +/// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice. +/// +/// ## Examples +/// +/// ```rust,no_run +/// use dialoguer::{ +/// FuzzySelect, +/// theme::ColorfulTheme +/// }; +/// use console::Term; +/// +/// fn main() -> std::io::Result<()> { +/// let items = vec!["Item 1", "item 2"]; +/// let selection = FuzzySelect::with_theme(&ColorfulTheme::default()) +/// .items(&items) +/// .default(0) +/// .interact_on_opt(&Term::stderr())?; +/// +/// match selection { +/// Some(index) => println!("User selected item : {}", items[index]), +/// None => println!("User did not select anything") +/// } +/// +/// Ok(()) +/// } +/// ``` + +pub struct FuzzySelect<'a> { + default: Option<usize>, + items: Vec<String>, + prompt: String, + report: bool, + clear: bool, + highlight_matches: bool, + max_length: Option<usize>, + theme: &'a dyn Theme, + /// Search string that a fuzzy search with start with. + /// Defaults to an empty string. + initial_text: String, +} + +impl Default for FuzzySelect<'static> { + fn default() -> Self { + Self::new() + } +} + +impl FuzzySelect<'static> { + /// Creates the prompt with a specific text. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } +} + +impl FuzzySelect<'_> { + /// Sets the clear behavior of the menu. + /// + /// The default is to clear the menu. + pub fn clear(&mut self, val: bool) -> &mut Self { + self.clear = val; + self + } + + /// Sets a default for the menu + pub fn default(&mut self, val: usize) -> &mut Self { + self.default = Some(val); + self + } + + /// Add a single item to the fuzzy selector. + pub fn item<T: ToString>(&mut self, item: T) -> &mut Self { + self.items.push(item.to_string()); + self + } + + /// Adds multiple items to the fuzzy selector. + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Self { + for item in items { + self.items.push(item.to_string()); + } + self + } + + /// Sets the search text that a fuzzy search starts with. + pub fn with_initial_text<S: Into<String>>(&mut self, initial_text: S) -> &mut Self { + self.initial_text = initial_text.into(); + self + } + + /// Prefaces the menu with a prompt. + /// + /// When a prompt is set the system also prints out a confirmation after + /// the fuzzy selection. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = prompt.into(); + self + } + + /// Indicates whether to report the selected value after interaction. + /// + /// The default is to report the selection. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + /// Indicates whether to highlight matched indices + /// + /// The default is to highlight the indices + pub fn highlight_matches(&mut self, val: bool) -> &mut Self { + self.highlight_matches = val; + self + } + + /// Sets the maximum number of visible options. + /// + /// The default is the height of the terminal minus 2. + pub fn max_length(&mut self, rows: usize) -> &mut Self { + self.max_length = Some(rows); + self + } + + /// Enables user interaction and returns the result. + /// + /// The user can select the items using 'Enter' and the index of selected item will be returned. + /// The dialog is rendered on stderr. + /// Result contains `index` of selected item if user hit 'Enter'. + /// This unlike [interact_opt](#method.interact_opt) does not allow to quit with 'Esc' or 'q'. + #[inline] + pub fn interact(&self) -> io::Result<usize> { + self.interact_on(&Term::stderr()) + } + + /// Enables user interaction and returns the result. + /// + /// The user can select the items using 'Enter' and the index of selected item will be returned. + /// The dialog is rendered on stderr. + /// Result contains `Some(index)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<usize>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like `interact` but allows a specific terminal to be set. + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<usize> { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like `interact` but allows a specific terminal to be set. + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<usize>> { + self._interact_on(term, true) + } + + /// Like `interact` but allows a specific terminal to be set. + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<usize>> { + // Place cursor at the end of the search term + let mut position = self.initial_text.len(); + let mut search_term = self.initial_text.to_owned(); + + let mut render = TermThemeRenderer::new(term, self.theme); + let mut sel = self.default; + + let mut size_vec = Vec::new(); + for items in self.items.iter().as_slice() { + let size = &items.len(); + size_vec.push(*size); + } + + // Fuzzy matcher + let matcher = fuzzy_matcher::skim::SkimMatcherV2::default(); + + // Subtract -2 because we need space to render the prompt. + let visible_term_rows = (term.size().0 as usize).max(3) - 2; + let visible_term_rows = self + .max_length + .unwrap_or(visible_term_rows) + .min(visible_term_rows); + // Variable used to determine if we need to scroll through the list. + let mut starting_row = 0; + + term.hide_cursor()?; + + loop { + render.clear()?; + render.fuzzy_select_prompt(self.prompt.as_str(), &search_term, position)?; + + // Maps all items to a tuple of item and its match score. + let mut filtered_list = self + .items + .iter() + .map(|item| (item, matcher.fuzzy_match(item, &search_term))) + .filter_map(|(item, score)| score.map(|s| (item, s))) + .collect::<Vec<_>>(); + + // Renders all matching items, from best match to worst. + filtered_list.sort_unstable_by(|(_, s1), (_, s2)| s2.cmp(s1)); + + for (idx, (item, _)) in filtered_list + .iter() + .enumerate() + .skip(starting_row) + .take(visible_term_rows) + { + render.fuzzy_select_prompt_item( + item, + Some(idx) == sel, + self.highlight_matches, + &matcher, + &search_term, + )?; + } + term.flush()?; + + match (term.read_key()?, sel) { + (Key::Escape, _) if allow_quit => { + if self.clear { + render.clear()?; + term.flush()?; + } + term.show_cursor()?; + return Ok(None); + } + (Key::ArrowUp | Key::BackTab, _) if !filtered_list.is_empty() => { + if sel == Some(0) { + starting_row = + filtered_list.len().max(visible_term_rows) - visible_term_rows; + } else if sel == Some(starting_row) { + starting_row -= 1; + } + sel = match sel { + None => Some(filtered_list.len() - 1), + Some(sel) => Some( + ((sel as i64 - 1 + filtered_list.len() as i64) + % (filtered_list.len() as i64)) + as usize, + ), + }; + term.flush()?; + } + (Key::ArrowDown | Key::Tab, _) if !filtered_list.is_empty() => { + sel = match sel { + None => Some(0), + Some(sel) => { + Some((sel as u64 + 1).rem(filtered_list.len() as u64) as usize) + } + }; + if sel == Some(visible_term_rows + starting_row) { + starting_row += 1; + } else if sel == Some(0) { + starting_row = 0; + } + term.flush()?; + } + (Key::ArrowLeft, _) if position > 0 => { + position -= 1; + term.flush()?; + } + (Key::ArrowRight, _) if position < search_term.len() => { + position += 1; + term.flush()?; + } + (Key::Enter, Some(sel)) if !filtered_list.is_empty() => { + if self.clear { + render.clear()?; + } + + if self.report { + render + .input_prompt_selection(self.prompt.as_str(), filtered_list[sel].0)?; + } + + let sel_string = filtered_list[sel].0; + let sel_string_pos_in_items = + self.items.iter().position(|item| item.eq(sel_string)); + + term.show_cursor()?; + return Ok(sel_string_pos_in_items); + } + (Key::Backspace, _) if position > 0 => { + position -= 1; + search_term.remove(position); + term.flush()?; + } + (Key::Char(chr), _) if !chr.is_ascii_control() => { + search_term.insert(position, chr); + position += 1; + term.flush()?; + sel = Some(0); + starting_row = 0; + } + + _ => {} + } + + render.clear_preserve_prompt(&size_vec)?; + } + } +} + +impl<'a> FuzzySelect<'a> { + /// Same as `new` but with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + default: None, + items: vec![], + prompt: "".into(), + report: true, + clear: true, + highlight_matches: true, + max_length: None, + theme, + initial_text: "".into(), + } + } +} diff --git a/vendor/dialoguer/src/prompts/input.rs b/vendor/dialoguer/src/prompts/input.rs new file mode 100644 index 0000000..b7cd829 --- /dev/null +++ b/vendor/dialoguer/src/prompts/input.rs @@ -0,0 +1,691 @@ +use std::{cmp::Ordering, fmt::Debug, io, iter, str::FromStr}; + +#[cfg(feature = "completion")] +use crate::completion::Completion; +#[cfg(feature = "history")] +use crate::history::History; +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + validate::Validator, +}; + +use console::{Key, Term}; + +type ValidatorCallback<'a, T> = Box<dyn FnMut(&T) -> Option<String> + 'a>; + +/// Renders an input prompt. +/// +/// ## Example usage +/// +/// ```rust,no_run +/// use dialoguer::Input; +/// +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// let input : String = Input::new() +/// .with_prompt("Tea or coffee?") +/// .with_initial_text("Yes") +/// .default("No".into()) +/// .interact_text()?; +/// # Ok(()) +/// # } +/// ``` +/// It can also be used with turbofish notation: +/// +/// ```rust,no_run +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// # use dialoguer::Input; +/// let input = Input::<String>::new() +/// .interact_text()?; +/// # Ok(()) +/// # } +/// ``` +pub struct Input<'a, T> { + prompt: String, + post_completion_text: Option<String>, + report: bool, + default: Option<T>, + show_default: bool, + initial_text: Option<String>, + theme: &'a dyn Theme, + permit_empty: bool, + validator: Option<ValidatorCallback<'a, T>>, + #[cfg(feature = "history")] + history: Option<&'a mut dyn History<T>>, + #[cfg(feature = "completion")] + completion: Option<&'a dyn Completion>, +} + +impl<T> Default for Input<'static, T> { + fn default() -> Self { + Self::new() + } +} + +impl<T> Input<'_, T> { + /// Creates an input prompt. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } + + /// Sets the input prompt. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = prompt.into(); + self + } + + /// Changes the prompt text to the post completion text after input is complete + pub fn with_post_completion_text<S: Into<String>>( + &mut self, + post_completion_text: S, + ) -> &mut Self { + self.post_completion_text = Some(post_completion_text.into()); + self + } + + /// Indicates whether to report the input value after interaction. + /// + /// The default is to report the input value. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + /// Sets initial text that user can accept or erase. + pub fn with_initial_text<S: Into<String>>(&mut self, val: S) -> &mut Self { + self.initial_text = Some(val.into()); + self + } + + /// Sets a default. + /// + /// Out of the box the prompt does not have a default and will continue + /// to display until the user inputs something and hits enter. If a default is set the user + /// can instead accept the default with enter. + pub fn default(&mut self, value: T) -> &mut Self { + self.default = Some(value); + self + } + + /// Enables or disables an empty input + /// + /// By default, if there is no default value set for the input, the user must input a non-empty string. + pub fn allow_empty(&mut self, val: bool) -> &mut Self { + self.permit_empty = val; + self + } + + /// Disables or enables the default value display. + /// + /// The default behaviour is to append [`default`](#method.default) to the prompt to tell the + /// user what is the default value. + /// + /// This method does not affect existence of default value, only its display in the prompt! + pub fn show_default(&mut self, val: bool) -> &mut Self { + self.show_default = val; + self + } +} + +impl<'a, T> Input<'a, T> { + /// Creates an input prompt with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + prompt: "".into(), + post_completion_text: None, + report: true, + default: None, + show_default: true, + initial_text: None, + theme, + permit_empty: false, + validator: None, + #[cfg(feature = "history")] + history: None, + #[cfg(feature = "completion")] + completion: None, + } + } + + /// Enable history processing + /// + /// # Example + /// + /// ```no_run + /// # use dialoguer::{History, Input}; + /// # use std::{collections::VecDeque, fmt::Display}; + /// let mut history = MyHistory::default(); + /// loop { + /// if let Ok(input) = Input::<String>::new() + /// .with_prompt("hist") + /// .history_with(&mut history) + /// .interact_text() + /// { + /// // Do something with the input + /// } + /// } + /// # struct MyHistory { + /// # history: VecDeque<String>, + /// # } + /// # + /// # impl Default for MyHistory { + /// # fn default() -> Self { + /// # MyHistory { + /// # history: VecDeque::new(), + /// # } + /// # } + /// # } + /// # + /// # impl<T: ToString> History<T> for MyHistory { + /// # fn read(&self, pos: usize) -> Option<String> { + /// # self.history.get(pos).cloned() + /// # } + /// # + /// # fn write(&mut self, val: &T) + /// # where + /// # { + /// # self.history.push_front(val.to_string()); + /// # } + /// # } + /// ``` + #[cfg(feature = "history")] + pub fn history_with<H>(&mut self, history: &'a mut H) -> &mut Self + where + H: History<T>, + { + self.history = Some(history); + self + } + + /// Enable completion + #[cfg(feature = "completion")] + pub fn completion_with<C>(&mut self, completion: &'a C) -> &mut Self + where + C: Completion, + { + self.completion = Some(completion); + self + } +} + +impl<'a, T> Input<'a, T> +where + T: 'a, +{ + /// Registers a validator. + /// + /// # Example + /// + /// ```no_run + /// # use dialoguer::Input; + /// let mail: String = Input::new() + /// .with_prompt("Enter email") + /// .validate_with(|input: &String| -> Result<(), &str> { + /// if input.contains('@') { + /// Ok(()) + /// } else { + /// Err("This is not a mail address") + /// } + /// }) + /// .interact() + /// .unwrap(); + /// ``` + pub fn validate_with<V>(&mut self, mut validator: V) -> &mut Self + where + V: Validator<T> + 'a, + V::Err: ToString, + { + let mut old_validator_func = self.validator.take(); + + self.validator = Some(Box::new(move |value: &T| -> Option<String> { + if let Some(old) = old_validator_func.as_mut() { + if let Some(err) = old(value) { + return Some(err); + } + } + + match validator.validate(value) { + Ok(()) => None, + Err(err) => Some(err.to_string()), + } + })); + + self + } +} + +impl<T> Input<'_, T> +where + T: Clone + ToString + FromStr, + <T as FromStr>::Err: Debug + ToString, +{ + /// Enables the user to enter a printable ascii sequence and returns the result. + /// + /// Its difference from [`interact`](#method.interact) is that it only allows ascii characters for string, + /// while [`interact`](#method.interact) allows virtually any character to be used e.g arrow keys. + /// + /// The dialog is rendered on stderr. + pub fn interact_text(&mut self) -> io::Result<T> { + self.interact_text_on(&Term::stderr()) + } + + /// Like [`interact_text`](#method.interact_text) but allows a specific terminal to be set. + pub fn interact_text_on(&mut self, term: &Term) -> io::Result<T> { + let mut render = TermThemeRenderer::new(term, self.theme); + + loop { + let default_string = self.default.as_ref().map(ToString::to_string); + + let prompt_len = render.input_prompt( + &self.prompt, + if self.show_default { + default_string.as_deref() + } else { + None + }, + )?; + + // Read input by keystroke so that we can suppress ascii control characters + if !term.features().is_attended() { + return Ok("".to_owned().parse::<T>().unwrap()); + } + + let mut chars: Vec<char> = Vec::new(); + let mut position = 0; + #[cfg(feature = "history")] + let mut hist_pos = 0; + + if let Some(initial) = self.initial_text.as_ref() { + term.write_str(initial)?; + chars = initial.chars().collect(); + position = chars.len(); + } + term.flush()?; + + loop { + match term.read_key()? { + Key::Backspace if position > 0 => { + position -= 1; + chars.remove(position); + let line_size = term.size().1 as usize; + // Case we want to delete last char of a line so the cursor is at the beginning of the next line + if (position + prompt_len) % (line_size - 1) == 0 { + term.clear_line()?; + term.move_cursor_up(1)?; + term.move_cursor_right(line_size + 1)?; + } else { + term.clear_chars(1)?; + } + + let tail: String = chars[position..].iter().collect(); + + if !tail.is_empty() { + term.write_str(&tail)?; + + let total = position + prompt_len + tail.len(); + let total_line = total / line_size; + let line_cursor = (position + prompt_len) / line_size; + term.move_cursor_up(total_line - line_cursor)?; + + term.move_cursor_left(line_size)?; + term.move_cursor_right((position + prompt_len) % line_size)?; + } + + term.flush()?; + } + Key::Char(chr) if !chr.is_ascii_control() => { + chars.insert(position, chr); + position += 1; + let tail: String = + iter::once(&chr).chain(chars[position..].iter()).collect(); + term.write_str(&tail)?; + term.move_cursor_left(tail.len() - 1)?; + term.flush()?; + } + Key::ArrowLeft if position > 0 => { + if (position + prompt_len) % term.size().1 as usize == 0 { + term.move_cursor_up(1)?; + term.move_cursor_right(term.size().1 as usize)?; + } else { + term.move_cursor_left(1)?; + } + position -= 1; + term.flush()?; + } + Key::ArrowRight if position < chars.len() => { + if (position + prompt_len) % (term.size().1 as usize - 1) == 0 { + term.move_cursor_down(1)?; + term.move_cursor_left(term.size().1 as usize)?; + } else { + term.move_cursor_right(1)?; + } + position += 1; + term.flush()?; + } + Key::UnknownEscSeq(seq) if seq == vec!['b'] => { + let line_size = term.size().1 as usize; + let nb_space = chars[..position] + .iter() + .rev() + .take_while(|c| c.is_whitespace()) + .count(); + let find_last_space = chars[..position - nb_space] + .iter() + .rposition(|c| c.is_whitespace()); + + // If we find a space we set the cursor to the next char else we set it to the beginning of the input + if let Some(mut last_space) = find_last_space { + if last_space < position { + last_space += 1; + let new_line = (prompt_len + last_space) / line_size; + let old_line = (prompt_len + position) / line_size; + let diff_line = old_line - new_line; + if diff_line != 0 { + term.move_cursor_up(old_line - new_line)?; + } + + let new_pos_x = (prompt_len + last_space) % line_size; + let old_pos_x = (prompt_len + position) % line_size; + let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; + //println!("new_pos_x = {}, old_pos_x = {}, diff = {}", new_pos_x, old_pos_x, diff_pos_x); + if diff_pos_x < 0 { + term.move_cursor_left(-diff_pos_x as usize)?; + } else { + term.move_cursor_right((diff_pos_x) as usize)?; + } + position = last_space; + } + } else { + term.move_cursor_left(position)?; + position = 0; + } + + term.flush()?; + } + Key::UnknownEscSeq(seq) if seq == vec!['f'] => { + let line_size = term.size().1 as usize; + let find_next_space = + chars[position..].iter().position(|c| c.is_whitespace()); + + // If we find a space we set the cursor to the next char else we set it to the beginning of the input + if let Some(mut next_space) = find_next_space { + let nb_space = chars[position + next_space..] + .iter() + .take_while(|c| c.is_whitespace()) + .count(); + next_space += nb_space; + let new_line = (prompt_len + position + next_space) / line_size; + let old_line = (prompt_len + position) / line_size; + term.move_cursor_down(new_line - old_line)?; + + let new_pos_x = (prompt_len + position + next_space) % line_size; + let old_pos_x = (prompt_len + position) % line_size; + let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; + if diff_pos_x < 0 { + term.move_cursor_left(-diff_pos_x as usize)?; + } else { + term.move_cursor_right((diff_pos_x) as usize)?; + } + position += next_space; + } else { + let new_line = (prompt_len + chars.len()) / line_size; + let old_line = (prompt_len + position) / line_size; + term.move_cursor_down(new_line - old_line)?; + + let new_pos_x = (prompt_len + chars.len()) % line_size; + let old_pos_x = (prompt_len + position) % line_size; + let diff_pos_x = new_pos_x as i64 - old_pos_x as i64; + match diff_pos_x.cmp(&0) { + Ordering::Less => { + term.move_cursor_left((-diff_pos_x - 1) as usize)?; + } + Ordering::Equal => {} + Ordering::Greater => { + term.move_cursor_right((diff_pos_x) as usize)?; + } + } + position = chars.len(); + } + + term.flush()?; + } + #[cfg(feature = "completion")] + Key::ArrowRight | Key::Tab => { + if let Some(completion) = &self.completion { + let input: String = chars.clone().into_iter().collect(); + if let Some(x) = completion.get(&input) { + term.clear_chars(chars.len())?; + chars.clear(); + position = 0; + for ch in x.chars() { + chars.insert(position, ch); + position += 1; + } + term.write_str(&x)?; + term.flush()?; + } + } + } + #[cfg(feature = "history")] + Key::ArrowUp => { + let line_size = term.size().1 as usize; + if let Some(history) = &self.history { + if let Some(previous) = history.read(hist_pos) { + hist_pos += 1; + let mut chars_len = chars.len(); + while ((prompt_len + chars_len) / line_size) > 0 { + term.clear_chars(chars_len)?; + if (prompt_len + chars_len) % line_size == 0 { + chars_len -= std::cmp::min(chars_len, line_size); + } else { + chars_len -= std::cmp::min( + chars_len, + (prompt_len + chars_len + 1) % line_size, + ); + } + if chars_len > 0 { + term.move_cursor_up(1)?; + term.move_cursor_right(line_size)?; + } + } + term.clear_chars(chars_len)?; + chars.clear(); + position = 0; + for ch in previous.chars() { + chars.insert(position, ch); + position += 1; + } + term.write_str(&previous)?; + term.flush()?; + } + } + } + #[cfg(feature = "history")] + Key::ArrowDown => { + let line_size = term.size().1 as usize; + if let Some(history) = &self.history { + let mut chars_len = chars.len(); + while ((prompt_len + chars_len) / line_size) > 0 { + term.clear_chars(chars_len)?; + if (prompt_len + chars_len) % line_size == 0 { + chars_len -= std::cmp::min(chars_len, line_size); + } else { + chars_len -= std::cmp::min( + chars_len, + (prompt_len + chars_len + 1) % line_size, + ); + } + if chars_len > 0 { + term.move_cursor_up(1)?; + term.move_cursor_right(line_size)?; + } + } + term.clear_chars(chars_len)?; + chars.clear(); + position = 0; + // Move the history position back one in case we have up arrowed into it + // and the position is sitting on the next to read + if let Some(pos) = hist_pos.checked_sub(1) { + hist_pos = pos; + // Move it back again to get the previous history entry + if let Some(pos) = pos.checked_sub(1) { + if let Some(previous) = history.read(pos) { + for ch in previous.chars() { + chars.insert(position, ch); + position += 1; + } + term.write_str(&previous)?; + } + } + } + term.flush()?; + } + } + Key::Enter => break, + _ => (), + } + } + let input = chars.iter().collect::<String>(); + + term.clear_line()?; + render.clear()?; + + if chars.is_empty() { + if let Some(ref default) = self.default { + if let Some(ref mut validator) = self.validator { + if let Some(err) = validator(default) { + render.error(&err)?; + continue; + } + } + + if self.report { + render.input_prompt_selection(&self.prompt, &default.to_string())?; + } + term.flush()?; + return Ok(default.clone()); + } else if !self.permit_empty { + continue; + } + } + + match input.parse::<T>() { + Ok(value) => { + if let Some(ref mut validator) = self.validator { + if let Some(err) = validator(&value) { + render.error(&err)?; + continue; + } + } + + #[cfg(feature = "history")] + if let Some(history) = &mut self.history { + history.write(&value); + } + + if self.report { + if let Some(post_completion_text) = &self.post_completion_text { + render.input_prompt_selection(post_completion_text, &input)?; + } else { + render.input_prompt_selection(&self.prompt, &input)?; + } + } + term.flush()?; + + return Ok(value); + } + Err(err) => { + render.error(&err.to_string())?; + continue; + } + } + } + } +} + +impl<T> Input<'_, T> +where + T: Clone + ToString + FromStr, + <T as FromStr>::Err: ToString, +{ + /// Enables user interaction and returns the result. + /// + /// Allows any characters as input, including e.g arrow keys. + /// Some of the keys might have undesired behavior. + /// For more limited version, see [`interact_text`](#method.interact_text). + /// + /// If the user confirms the result is `true`, `false` otherwise. + /// The dialog is rendered on stderr. + pub fn interact(&mut self) -> io::Result<T> { + self.interact_on(&Term::stderr()) + } + + /// Like [`interact`](#method.interact) but allows a specific terminal to be set. + pub fn interact_on(&mut self, term: &Term) -> io::Result<T> { + let mut render = TermThemeRenderer::new(term, self.theme); + + loop { + let default_string = self.default.as_ref().map(ToString::to_string); + + render.input_prompt( + &self.prompt, + if self.show_default { + default_string.as_deref() + } else { + None + }, + )?; + term.flush()?; + + let input = if let Some(initial_text) = self.initial_text.as_ref() { + term.read_line_initial_text(initial_text)? + } else { + term.read_line()? + }; + + render.add_line(); + term.clear_line()?; + render.clear()?; + + if input.is_empty() { + if let Some(ref default) = self.default { + if let Some(ref mut validator) = self.validator { + if let Some(err) = validator(default) { + render.error(&err)?; + continue; + } + } + + if self.report { + render.input_prompt_selection(&self.prompt, &default.to_string())?; + } + term.flush()?; + return Ok(default.clone()); + } else if !self.permit_empty { + continue; + } + } + + match input.parse::<T>() { + Ok(value) => { + if let Some(ref mut validator) = self.validator { + if let Some(err) = validator(&value) { + render.error(&err)?; + continue; + } + } + + if self.report { + render.input_prompt_selection(&self.prompt, &input)?; + } + term.flush()?; + + return Ok(value); + } + Err(err) => { + render.error(&err.to_string())?; + continue; + } + } + } + } +} diff --git a/vendor/dialoguer/src/prompts/mod.rs b/vendor/dialoguer/src/prompts/mod.rs new file mode 100644 index 0000000..1c13185 --- /dev/null +++ b/vendor/dialoguer/src/prompts/mod.rs @@ -0,0 +1,13 @@ +#![allow(clippy::needless_doctest_main)] + +pub mod confirm; +pub mod input; +pub mod multi_select; +pub mod select; +pub mod sort; + +#[cfg(feature = "fuzzy-select")] +pub mod fuzzy_select; + +#[cfg(feature = "password")] +pub mod password; diff --git a/vendor/dialoguer/src/prompts/multi_select.rs b/vendor/dialoguer/src/prompts/multi_select.rs new file mode 100644 index 0000000..eed55a1 --- /dev/null +++ b/vendor/dialoguer/src/prompts/multi_select.rs @@ -0,0 +1,356 @@ +use std::{io, iter::repeat, ops::Rem}; + +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + Paging, +}; + +use console::{Key, Term}; + +/// Renders a multi select prompt. +/// +/// ## Example usage +/// ```rust,no_run +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// use dialoguer::MultiSelect; +/// +/// let items = vec!["Option 1", "Option 2"]; +/// let chosen : Vec<usize> = MultiSelect::new() +/// .items(&items) +/// .interact()?; +/// # Ok(()) +/// # } +/// ``` +pub struct MultiSelect<'a> { + defaults: Vec<bool>, + items: Vec<String>, + prompt: Option<String>, + report: bool, + clear: bool, + max_length: Option<usize>, + theme: &'a dyn Theme, +} + +impl Default for MultiSelect<'static> { + fn default() -> Self { + Self::new() + } +} + +impl MultiSelect<'static> { + /// Creates a multi select prompt. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } +} + +impl MultiSelect<'_> { + /// Sets the clear behavior of the menu. + /// + /// The default is to clear the menu. + pub fn clear(&mut self, val: bool) -> &mut Self { + self.clear = val; + self + } + + /// Sets a defaults for the menu. + pub fn defaults(&mut self, val: &[bool]) -> &mut Self { + self.defaults = val + .to_vec() + .iter() + .copied() + .chain(repeat(false)) + .take(self.items.len()) + .collect(); + self + } + + /// Sets an optional max length for a page + /// + /// Max length is disabled by None + pub fn max_length(&mut self, val: usize) -> &mut Self { + // Paging subtracts two from the capacity, paging does this to + // make an offset for the page indicator. So to make sure that + // we can show the intended amount of items we need to add two + // to our value. + self.max_length = Some(val + 2); + self + } + + /// Add a single item to the selector. + #[inline] + pub fn item<T: ToString>(&mut self, item: T) -> &mut Self { + self.item_checked(item, false) + } + + /// Add a single item to the selector with a default checked state. + pub fn item_checked<T: ToString>(&mut self, item: T, checked: bool) -> &mut Self { + self.items.push(item.to_string()); + self.defaults.push(checked); + self + } + + /// Adds multiple items to the selector. + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Self { + for item in items { + self.items.push(item.to_string()); + self.defaults.push(false); + } + self + } + + /// Adds multiple items to the selector with checked state + pub fn items_checked<T: ToString>(&mut self, items: &[(T, bool)]) -> &mut Self { + for &(ref item, checked) in items { + self.items.push(item.to_string()); + self.defaults.push(checked); + } + self + } + + /// Prefaces the menu with a prompt. + /// + /// By default, when a prompt is set the system also prints out a confirmation after + /// the selection. You can opt-out of this with [`report`](#method.report). + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = Some(prompt.into()); + self + } + + /// Indicates whether to report the selected values after interaction. + /// + /// The default is to report the selections. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + /// Enables user interaction and returns the result. + /// + /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned. + /// The dialog is rendered on stderr. + /// Result contains `Vec<index>` if user hit 'Enter'. + /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. + #[inline] + pub fn interact(&self) -> io::Result<Vec<usize>> { + self.interact_on(&Term::stderr()) + } + + /// Enables user interaction and returns the result. + /// + /// The user can select the items with the 'Space' bar and on 'Enter' the indices of selected items will be returned. + /// The dialog is rendered on stderr. + /// Result contains `Some(Vec<index>)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<Vec<usize>>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + ///```rust,no_run + /// use dialoguer::MultiSelect; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = MultiSelect::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on(&Term::stderr())?; + /// + /// println!("User selected options at indices {:?}", selections); + /// + /// Ok(()) + /// } + ///``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<Vec<usize>> { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::MultiSelect; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = MultiSelect::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on_opt(&Term::stdout())?; + /// + /// match selections { + /// Some(positions) => println!("User selected options at indices {:?}", positions), + /// None => println!("User exited using Esc or q") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<Vec<usize>>> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<Vec<usize>>> { + if self.items.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Empty list of items given to `MultiSelect`", + )); + } + + let mut paging = Paging::new(term, self.items.len(), self.max_length); + let mut render = TermThemeRenderer::new(term, self.theme); + let mut sel = 0; + + let mut size_vec = Vec::new(); + + for items in self + .items + .iter() + .flat_map(|i| i.split('\n')) + .collect::<Vec<_>>() + { + let size = &items.len(); + size_vec.push(*size); + } + + let mut checked: Vec<bool> = self.defaults.clone(); + + term.hide_cursor()?; + + loop { + if let Some(ref prompt) = self.prompt { + paging + .render_prompt(|paging_info| render.multi_select_prompt(prompt, paging_info))?; + } + + for (idx, item) in self + .items + .iter() + .enumerate() + .skip(paging.current_page * paging.capacity) + .take(paging.capacity) + { + render.multi_select_prompt_item(item, checked[idx], sel == idx)?; + } + + term.flush()?; + + match term.read_key()? { + Key::ArrowDown | Key::Tab | Key::Char('j') => { + if sel == !0 { + sel = 0; + } else { + sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; + } + } + Key::ArrowUp | Key::BackTab | Key::Char('k') => { + if sel == !0 { + sel = self.items.len() - 1; + } else { + sel = ((sel as i64 - 1 + self.items.len() as i64) + % (self.items.len() as i64)) as usize; + } + } + Key::ArrowLeft | Key::Char('h') => { + if paging.active { + sel = paging.previous_page(); + } + } + Key::ArrowRight | Key::Char('l') => { + if paging.active { + sel = paging.next_page(); + } + } + Key::Char(' ') => { + checked[sel] = !checked[sel]; + } + Key::Char('a') => { + if checked.iter().all(|&item_checked| item_checked) { + checked.fill(false); + } else { + checked.fill(true); + } + } + Key::Escape | Key::Char('q') => { + if allow_quit { + if self.clear { + render.clear()?; + } else { + term.clear_last_lines(paging.capacity)?; + } + + term.show_cursor()?; + term.flush()?; + + return Ok(None); + } + } + Key::Enter => { + if self.clear { + render.clear()?; + } + + if let Some(ref prompt) = self.prompt { + if self.report { + let selections: Vec<_> = checked + .iter() + .enumerate() + .filter_map(|(idx, &checked)| { + if checked { + Some(self.items[idx].as_str()) + } else { + None + } + }) + .collect(); + + render.multi_select_prompt_selection(prompt, &selections[..])?; + } + } + + term.show_cursor()?; + term.flush()?; + + return Ok(Some( + checked + .into_iter() + .enumerate() + .filter_map(|(idx, checked)| if checked { Some(idx) } else { None }) + .collect(), + )); + } + _ => {} + } + + paging.update(sel)?; + + if paging.active { + render.clear()?; + } else { + render.clear_preserve_prompt(&size_vec)?; + } + } + } +} + +impl<'a> MultiSelect<'a> { + /// Creates a multi select prompt with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + items: vec![], + defaults: vec![], + clear: true, + prompt: None, + report: true, + max_length: None, + theme, + } + } +} diff --git a/vendor/dialoguer/src/prompts/password.rs b/vendor/dialoguer/src/prompts/password.rs new file mode 100644 index 0000000..7327605 --- /dev/null +++ b/vendor/dialoguer/src/prompts/password.rs @@ -0,0 +1,194 @@ +use std::io; + +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + validate::PasswordValidator, +}; + +use console::Term; +use zeroize::Zeroizing; + +type PasswordValidatorCallback<'a> = Box<dyn Fn(&String) -> Option<String> + 'a>; + +/// Renders a password input prompt. +/// +/// ## Example usage +/// +/// ```rust,no_run +/// # fn test() -> Result<(), Box<std::error::Error>> { +/// use dialoguer::Password; +/// +/// let password = Password::new().with_prompt("New Password") +/// .with_confirmation("Confirm password", "Passwords mismatching") +/// .interact()?; +/// println!("Length of the password is: {}", password.len()); +/// # Ok(()) } fn main() { test().unwrap(); } +/// ``` +pub struct Password<'a> { + prompt: String, + report: bool, + theme: &'a dyn Theme, + allow_empty_password: bool, + confirmation_prompt: Option<(String, String)>, + validator: Option<PasswordValidatorCallback<'a>>, +} + +impl Default for Password<'static> { + fn default() -> Password<'static> { + Self::new() + } +} + +impl Password<'static> { + /// Creates a password input prompt. + pub fn new() -> Password<'static> { + Self::with_theme(&SimpleTheme) + } +} + +impl<'a> Password<'a> { + /// Sets the password input prompt. + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = prompt.into(); + self + } + + /// Indicates whether to report confirmation after interaction. + /// + /// The default is to report. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + /// Enables confirmation prompting. + pub fn with_confirmation<A, B>(&mut self, prompt: A, mismatch_err: B) -> &mut Self + where + A: Into<String>, + B: Into<String>, + { + self.confirmation_prompt = Some((prompt.into(), mismatch_err.into())); + self + } + + /// Allows/Disables empty password. + /// + /// By default this setting is set to false (i.e. password is not empty). + pub fn allow_empty_password(&mut self, allow_empty_password: bool) -> &mut Self { + self.allow_empty_password = allow_empty_password; + self + } + + /// Registers a validator. + /// + /// # Example + /// + /// ```no_run + /// # use dialoguer::Password; + /// let password: String = Password::new() + /// .with_prompt("Enter password") + /// .validate_with(|input: &String| -> Result<(), &str> { + /// if input.len() > 8 { + /// Ok(()) + /// } else { + /// Err("Password must be longer than 8") + /// } + /// }) + /// .interact() + /// .unwrap(); + /// ``` + pub fn validate_with<V>(&mut self, validator: V) -> &mut Self + where + V: PasswordValidator + 'a, + V::Err: ToString, + { + let old_validator_func = self.validator.take(); + + self.validator = Some(Box::new(move |value: &String| -> Option<String> { + if let Some(old) = &old_validator_func { + if let Some(err) = old(value) { + return Some(err); + } + } + + match validator.validate(value) { + Ok(()) => None, + Err(err) => Some(err.to_string()), + } + })); + + self + } + + /// Enables user interaction and returns the result. + /// + /// If the user confirms the result is `true`, `false` otherwise. + /// The dialog is rendered on stderr. + pub fn interact(&self) -> io::Result<String> { + self.interact_on(&Term::stderr()) + } + + /// Like `interact` but allows a specific terminal to be set. + pub fn interact_on(&self, term: &Term) -> io::Result<String> { + let mut render = TermThemeRenderer::new(term, self.theme); + render.set_prompts_reset_height(false); + + loop { + let password = Zeroizing::new(self.prompt_password(&mut render, &self.prompt)?); + + if let Some(ref validator) = self.validator { + if let Some(err) = validator(&password) { + render.error(&err)?; + continue; + } + } + + if let Some((ref prompt, ref err)) = self.confirmation_prompt { + let pw2 = Zeroizing::new(self.prompt_password(&mut render, prompt)?); + + if *password != *pw2 { + render.error(err)?; + continue; + } + } + + render.clear()?; + + if self.report { + render.password_prompt_selection(&self.prompt)?; + } + term.flush()?; + + return Ok((*password).clone()); + } + } + + fn prompt_password(&self, render: &mut TermThemeRenderer, prompt: &str) -> io::Result<String> { + loop { + render.password_prompt(prompt)?; + render.term().flush()?; + + let input = render.term().read_secure_line()?; + + render.add_line(); + + if !input.is_empty() || self.allow_empty_password { + return Ok(input); + } + } + } +} + +impl<'a> Password<'a> { + /// Creates a password input prompt with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + prompt: "".into(), + report: true, + theme, + allow_empty_password: false, + confirmation_prompt: None, + validator: None, + } + } +} diff --git a/vendor/dialoguer/src/prompts/select.rs b/vendor/dialoguer/src/prompts/select.rs new file mode 100644 index 0000000..d080abd --- /dev/null +++ b/vendor/dialoguer/src/prompts/select.rs @@ -0,0 +1,419 @@ +use std::{io, ops::Rem}; + +use crate::paging::Paging; +use crate::theme::{SimpleTheme, TermThemeRenderer, Theme}; + +use console::{Key, Term}; + +/// Renders a select prompt. +/// +/// User can select from one or more options. +/// Interaction returns index of an item selected in the order they appear in `item` invocation or `items` slice. +/// +/// ## Examples +/// +/// ```rust,no_run +/// use dialoguer::{console::Term, theme::ColorfulTheme, Select}; +/// +/// fn main() -> std::io::Result<()> { +/// let items = vec!["Item 1", "item 2"]; +/// let selection = Select::with_theme(&ColorfulTheme::default()) +/// .items(&items) +/// .default(0) +/// .interact_on_opt(&Term::stderr())?; +/// +/// match selection { +/// Some(index) => println!("User selected item : {}", items[index]), +/// None => println!("User did not select anything") +/// } +/// +/// Ok(()) +/// } +/// ``` +pub struct Select<'a> { + default: usize, + items: Vec<String>, + prompt: Option<String>, + report: bool, + clear: bool, + theme: &'a dyn Theme, + max_length: Option<usize>, +} + +impl Default for Select<'static> { + fn default() -> Self { + Self::new() + } +} + +impl Select<'static> { + /// Creates a select prompt builder with default theme. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } +} + +impl Select<'_> { + /// Indicates whether select menu should be erased from the screen after interaction. + /// + /// The default is to clear the menu. + pub fn clear(&mut self, val: bool) -> &mut Self { + self.clear = val; + self + } + + /// Sets initial selected element when select menu is rendered + /// + /// Element is indicated by the index at which it appears in `item` method invocation or `items` slice. + pub fn default(&mut self, val: usize) -> &mut Self { + self.default = val; + self + } + + /// Sets an optional max length for a page. + /// + /// Max length is disabled by None + pub fn max_length(&mut self, val: usize) -> &mut Self { + // Paging subtracts two from the capacity, paging does this to + // make an offset for the page indicator. So to make sure that + // we can show the intended amount of items we need to add two + // to our value. + self.max_length = Some(val + 2); + self + } + + /// Add a single item to the selector. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Select; + /// + /// fn main() -> std::io::Result<()> { + /// let selection: usize = Select::new() + /// .item("Item 1") + /// .item("Item 2") + /// .interact()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn item<T: ToString>(&mut self, item: T) -> &mut Self { + self.items.push(item.to_string()); + self + } + + /// Adds multiple items to the selector. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Select; + /// + /// fn main() -> std::io::Result<()> { + /// let items = vec!["Item 1", "Item 2"]; + /// let selection: usize = Select::new() + /// .items(&items) + /// .interact()?; + /// + /// println!("{}", items[selection]); + /// + /// Ok(()) + /// } + /// ``` + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Self { + for item in items { + self.items.push(item.to_string()); + } + self + } + + /// Sets the select prompt. + /// + /// By default, when a prompt is set the system also prints out a confirmation after + /// the selection. You can opt-out of this with [`report`](#method.report). + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Select; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::new() + /// .with_prompt("Which option do you prefer?") + /// .item("Option A") + /// .item("Option B") + /// .interact()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = Some(prompt.into()); + self.report = true; + self + } + + /// Indicates whether to report the selected value after interaction. + /// + /// The default is to report the selection. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + /// Enables user interaction and returns the result. + /// + /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned. + /// The dialog is rendered on stderr. + /// Result contains `index` if user selected one of items using 'Enter'. + /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. + #[inline] + pub fn interact(&self) -> io::Result<usize> { + self.interact_on(&Term::stderr()) + } + + /// Enables user interaction and returns the result. + /// + /// The user can select the items with the 'Space' bar or 'Enter' and the index of selected item will be returned. + /// The dialog is rendered on stderr. + /// Result contains `Some(index)` if user selected one of items using 'Enter' or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<usize>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + ///```rust,no_run + /// use dialoguer::{console::Term, Select}; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on(&Term::stderr())?; + /// + /// println!("User selected option at index {}", selection); + /// + /// Ok(()) + /// } + ///``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<usize> { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::{console::Term, Select}; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on_opt(&Term::stdout())?; + /// + /// match selection { + /// Some(position) => println!("User selected option at index {}", position), + /// None => println!("User did not select anything or exited using Esc or q") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<usize>> { + self._interact_on(term, true) + } + + /// Like `interact` but allows a specific terminal to be set. + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<usize>> { + if self.items.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Empty list of items given to `Select`", + )); + } + + let mut paging = Paging::new(term, self.items.len(), self.max_length); + let mut render = TermThemeRenderer::new(term, self.theme); + let mut sel = self.default; + + let mut size_vec = Vec::new(); + + for items in self + .items + .iter() + .flat_map(|i| i.split('\n')) + .collect::<Vec<_>>() + { + let size = &items.len(); + size_vec.push(*size); + } + + term.hide_cursor()?; + + loop { + if let Some(ref prompt) = self.prompt { + paging.render_prompt(|paging_info| render.select_prompt(prompt, paging_info))?; + } + + for (idx, item) in self + .items + .iter() + .enumerate() + .skip(paging.current_page * paging.capacity) + .take(paging.capacity) + { + render.select_prompt_item(item, sel == idx)?; + } + + term.flush()?; + + match term.read_key()? { + Key::ArrowDown | Key::Tab | Key::Char('j') => { + if sel == !0 { + sel = 0; + } else { + sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; + } + } + Key::Escape | Key::Char('q') => { + if allow_quit { + if self.clear { + render.clear()?; + } else { + term.clear_last_lines(paging.capacity)?; + } + + term.show_cursor()?; + term.flush()?; + + return Ok(None); + } + } + Key::ArrowUp | Key::BackTab | Key::Char('k') => { + if sel == !0 { + sel = self.items.len() - 1; + } else { + sel = ((sel as i64 - 1 + self.items.len() as i64) + % (self.items.len() as i64)) as usize; + } + } + Key::ArrowLeft | Key::Char('h') => { + if paging.active { + sel = paging.previous_page(); + } + } + Key::ArrowRight | Key::Char('l') => { + if paging.active { + sel = paging.next_page(); + } + } + + Key::Enter | Key::Char(' ') if sel != !0 => { + if self.clear { + render.clear()?; + } + + if let Some(ref prompt) = self.prompt { + if self.report { + render.select_prompt_selection(prompt, &self.items[sel])?; + } + } + + term.show_cursor()?; + term.flush()?; + + return Ok(Some(sel)); + } + _ => {} + } + + paging.update(sel)?; + + if paging.active { + render.clear()?; + } else { + render.clear_preserve_prompt(&size_vec)?; + } + } + } +} + +impl<'a> Select<'a> { + /// Creates a select prompt builder with a specific theme. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::{ + /// Select, + /// theme::ColorfulTheme + /// }; + /// + /// fn main() -> std::io::Result<()> { + /// let selection = Select::with_theme(&ColorfulTheme::default()) + /// .item("Option A") + /// .item("Option B") + /// .interact()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + default: !0, + items: vec![], + prompt: None, + report: false, + clear: true, + max_length: None, + theme, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_str() { + let selections = &[ + "Ice Cream", + "Vanilla Cupcake", + "Chocolate Muffin", + "A Pile of sweet, sweet mustard", + ]; + + assert_eq!( + Select::new().default(0).items(&selections[..]).items, + selections + ); + } + + #[test] + fn test_string() { + let selections = vec!["a".to_string(), "b".to_string()]; + + assert_eq!( + Select::new().default(0).items(&selections[..]).items, + selections + ); + } + + #[test] + fn test_ref_str() { + let a = "a"; + let b = "b"; + + let selections = &[a, b]; + + assert_eq!( + Select::new().default(0).items(&selections[..]).items, + selections + ); + } +} diff --git a/vendor/dialoguer/src/prompts/sort.rs b/vendor/dialoguer/src/prompts/sort.rs new file mode 100644 index 0000000..03152bf --- /dev/null +++ b/vendor/dialoguer/src/prompts/sort.rs @@ -0,0 +1,348 @@ +use std::{io, ops::Rem}; + +use crate::{ + theme::{SimpleTheme, TermThemeRenderer, Theme}, + Paging, +}; + +use console::{Key, Term}; + +/// Renders a sort prompt. +/// +/// Returns list of indices in original items list sorted according to user input. +/// +/// ## Example usage +/// ```rust,no_run +/// use dialoguer::Sort; +/// +/// # fn test() -> Result<(), Box<dyn std::error::Error>> { +/// let items_to_order = vec!["Item 1", "Item 2", "Item 3"]; +/// let ordered = Sort::new() +/// .with_prompt("Order the items") +/// .items(&items_to_order) +/// .interact()?; +/// # Ok(()) +/// # } +/// ``` +pub struct Sort<'a> { + items: Vec<String>, + prompt: Option<String>, + report: bool, + clear: bool, + max_length: Option<usize>, + theme: &'a dyn Theme, +} + +impl Default for Sort<'static> { + fn default() -> Self { + Self::new() + } +} + +impl Sort<'static> { + /// Creates a sort prompt. + pub fn new() -> Self { + Self::with_theme(&SimpleTheme) + } +} + +impl Sort<'_> { + /// Sets the clear behavior of the menu. + /// + /// The default is to clear the menu after user interaction. + pub fn clear(&mut self, val: bool) -> &mut Self { + self.clear = val; + self + } + + /// Sets an optional max length for a page + /// + /// Max length is disabled by None + pub fn max_length(&mut self, val: usize) -> &mut Self { + // Paging subtracts two from the capacity, paging does this to + // make an offset for the page indicator. So to make sure that + // we can show the intended amount of items we need to add two + // to our value. + self.max_length = Some(val + 2); + self + } + + /// Add a single item to the selector. + pub fn item<T: ToString>(&mut self, item: T) -> &mut Self { + self.items.push(item.to_string()); + self + } + + /// Adds multiple items to the selector. + pub fn items<T: ToString>(&mut self, items: &[T]) -> &mut Self { + for item in items { + self.items.push(item.to_string()); + } + self + } + + /// Prefaces the menu with a prompt. + /// + /// By default, when a prompt is set the system also prints out a confirmation after + /// the selection. You can opt-out of this with [`report`](#method.report). + pub fn with_prompt<S: Into<String>>(&mut self, prompt: S) -> &mut Self { + self.prompt = Some(prompt.into()); + self + } + + /// Indicates whether to report the selected order after interaction. + /// + /// The default is to report the selected order. + pub fn report(&mut self, val: bool) -> &mut Self { + self.report = val; + self + } + + /// Enables user interaction and returns the result. + /// + /// The user can order the items with the 'Space' bar and the arrows. On 'Enter' ordered list of the incides of items will be returned. + /// The dialog is rendered on stderr. + /// Result contains `Vec<index>` if user hit 'Enter'. + /// This unlike [`interact_opt`](Self::interact_opt) does not allow to quit with 'Esc' or 'q'. + #[inline] + pub fn interact(&self) -> io::Result<Vec<usize>> { + self.interact_on(&Term::stderr()) + } + + /// Enables user interaction and returns the result. + /// + /// The user can order the items with the 'Space' bar and the arrows. On 'Enter' ordered list of the incides of items will be returned. + /// The dialog is rendered on stderr. + /// Result contains `Some(Vec<index>)` if user hit 'Enter' or `None` if user cancelled with 'Esc' or 'q'. + #[inline] + pub fn interact_opt(&self) -> io::Result<Option<Vec<usize>>> { + self.interact_on_opt(&Term::stderr()) + } + + /// Like [interact](#method.interact) but allows a specific terminal to be set. + /// + /// ## Examples + ///```rust,no_run + /// use dialoguer::Sort; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = Sort::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on(&Term::stderr())?; + /// + /// println!("User sorted options as indices {:?}", selections); + /// + /// Ok(()) + /// } + ///``` + #[inline] + pub fn interact_on(&self, term: &Term) -> io::Result<Vec<usize>> { + self._interact_on(term, false)? + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Quit not allowed in this case")) + } + + /// Like [`interact_opt`](Self::interact_opt) but allows a specific terminal to be set. + /// + /// ## Examples + /// ```rust,no_run + /// use dialoguer::Sort; + /// use console::Term; + /// + /// fn main() -> std::io::Result<()> { + /// let selections = Sort::new() + /// .item("Option A") + /// .item("Option B") + /// .interact_on_opt(&Term::stdout())?; + /// + /// match selections { + /// Some(positions) => println!("User sorted options as indices {:?}", positions), + /// None => println!("User exited using Esc or q") + /// } + /// + /// Ok(()) + /// } + /// ``` + #[inline] + pub fn interact_on_opt(&self, term: &Term) -> io::Result<Option<Vec<usize>>> { + self._interact_on(term, true) + } + + fn _interact_on(&self, term: &Term, allow_quit: bool) -> io::Result<Option<Vec<usize>>> { + if self.items.is_empty() { + return Err(io::Error::new( + io::ErrorKind::Other, + "Empty list of items given to `Sort`", + )); + } + + let mut paging = Paging::new(term, self.items.len(), self.max_length); + let mut render = TermThemeRenderer::new(term, self.theme); + let mut sel = 0; + + let mut size_vec = Vec::new(); + + for items in self.items.iter().as_slice() { + let size = &items.len(); + size_vec.push(*size); + } + + let mut order: Vec<_> = (0..self.items.len()).collect(); + let mut checked: bool = false; + + term.hide_cursor()?; + + loop { + if let Some(ref prompt) = self.prompt { + paging.render_prompt(|paging_info| render.sort_prompt(prompt, paging_info))?; + } + + for (idx, item) in order + .iter() + .enumerate() + .skip(paging.current_page * paging.capacity) + .take(paging.capacity) + { + render.sort_prompt_item(&self.items[*item], checked, sel == idx)?; + } + + term.flush()?; + + match term.read_key()? { + Key::ArrowDown | Key::Tab | Key::Char('j') => { + let old_sel = sel; + + if sel == !0 { + sel = 0; + } else { + sel = (sel as u64 + 1).rem(self.items.len() as u64) as usize; + } + + if checked && old_sel != sel { + order.swap(old_sel, sel); + } + } + Key::ArrowUp | Key::BackTab | Key::Char('k') => { + let old_sel = sel; + + if sel == !0 { + sel = self.items.len() - 1; + } else { + sel = ((sel as i64 - 1 + self.items.len() as i64) + % (self.items.len() as i64)) as usize; + } + + if checked && old_sel != sel { + order.swap(old_sel, sel); + } + } + Key::ArrowLeft | Key::Char('h') => { + if paging.active { + let old_sel = sel; + let old_page = paging.current_page; + + sel = paging.previous_page(); + + if checked { + let indexes: Vec<_> = if old_page == 0 { + let indexes1: Vec<_> = (0..=old_sel).rev().collect(); + let indexes2: Vec<_> = (sel..self.items.len()).rev().collect(); + [indexes1, indexes2].concat() + } else { + (sel..=old_sel).rev().collect() + }; + + for index in 0..(indexes.len() - 1) { + order.swap(indexes[index], indexes[index + 1]); + } + } + } + } + Key::ArrowRight | Key::Char('l') => { + if paging.active { + let old_sel = sel; + let old_page = paging.current_page; + + sel = paging.next_page(); + + if checked { + let indexes: Vec<_> = if old_page == paging.pages - 1 { + let indexes1: Vec<_> = (old_sel..self.items.len()).collect(); + let indexes2: Vec<_> = vec![0]; + [indexes1, indexes2].concat() + } else { + (old_sel..=sel).collect() + }; + + for index in 0..(indexes.len() - 1) { + order.swap(indexes[index], indexes[index + 1]); + } + } + } + } + Key::Char(' ') => { + checked = !checked; + } + Key::Escape | Key::Char('q') => { + if allow_quit { + if self.clear { + render.clear()?; + } else { + term.clear_last_lines(paging.capacity)?; + } + + term.show_cursor()?; + term.flush()?; + + return Ok(None); + } + } + Key::Enter => { + if self.clear { + render.clear()?; + } + + if let Some(ref prompt) = self.prompt { + if self.report { + let list: Vec<_> = order + .iter() + .enumerate() + .map(|(_, item)| self.items[*item].as_str()) + .collect(); + render.sort_prompt_selection(prompt, &list[..])?; + } + } + + term.show_cursor()?; + term.flush()?; + + return Ok(Some(order)); + } + _ => {} + } + + paging.update(sel)?; + + if paging.active { + render.clear()?; + } else { + render.clear_preserve_prompt(&size_vec)?; + } + } + } +} + +impl<'a> Sort<'a> { + /// Creates a sort prompt with a specific theme. + pub fn with_theme(theme: &'a dyn Theme) -> Self { + Self { + items: vec![], + clear: true, + prompt: None, + report: true, + max_length: None, + theme, + } + } +} |