diff options
Diffstat (limited to 'vendor/dialoguer/src')
-rw-r--r-- | vendor/dialoguer/src/completion.rs | 4 | ||||
-rw-r--r-- | vendor/dialoguer/src/edit.rs | 131 | ||||
-rw-r--r-- | vendor/dialoguer/src/history.rs | 15 | ||||
-rw-r--r-- | vendor/dialoguer/src/lib.rs | 62 | ||||
-rw-r--r-- | vendor/dialoguer/src/paging.rs | 118 | ||||
-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 | ||||
-rw-r--r-- | vendor/dialoguer/src/theme.rs | 976 | ||||
-rw-r--r-- | vendor/dialoguer/src/validate.rs | 49 |
15 files changed, 3989 insertions, 0 deletions
diff --git a/vendor/dialoguer/src/completion.rs b/vendor/dialoguer/src/completion.rs new file mode 100644 index 0000000..98c2a9c --- /dev/null +++ b/vendor/dialoguer/src/completion.rs @@ -0,0 +1,4 @@ +/// Trait for completion handling. +pub trait Completion { + fn get(&self, input: &str) -> Option<String>; +} diff --git a/vendor/dialoguer/src/edit.rs b/vendor/dialoguer/src/edit.rs new file mode 100644 index 0000000..f69844c --- /dev/null +++ b/vendor/dialoguer/src/edit.rs @@ -0,0 +1,131 @@ +use std::{ + env, + ffi::{OsStr, OsString}, + fs, io, + io::{Read, Write}, + process, +}; + +/// Launches the default editor to edit a string. +/// +/// ## Example +/// +/// ```rust,no_run +/// use dialoguer::Editor; +/// +/// if let Some(rv) = Editor::new().edit("Enter a commit message").unwrap() { +/// println!("Your message:"); +/// println!("{}", rv); +/// } else { +/// println!("Abort!"); +/// } +/// ``` +pub struct Editor { + editor: OsString, + extension: String, + require_save: bool, + trim_newlines: bool, +} + +fn get_default_editor() -> OsString { + if let Some(prog) = env::var_os("VISUAL") { + return prog; + } + if let Some(prog) = env::var_os("EDITOR") { + return prog; + } + if cfg!(windows) { + "notepad.exe".into() + } else { + "vi".into() + } +} + +impl Default for Editor { + fn default() -> Self { + Self::new() + } +} + +impl Editor { + /// Creates a new editor. + pub fn new() -> Self { + Self { + editor: get_default_editor(), + extension: ".txt".into(), + require_save: true, + trim_newlines: true, + } + } + + /// Sets a specific editor executable. + pub fn executable<S: AsRef<OsStr>>(&mut self, val: S) -> &mut Self { + self.editor = val.as_ref().into(); + self + } + + /// Sets a specific extension + pub fn extension(&mut self, val: &str) -> &mut Self { + self.extension = val.into(); + self + } + + /// Enables or disables the save requirement. + pub fn require_save(&mut self, val: bool) -> &mut Self { + self.require_save = val; + self + } + + /// Enables or disables trailing newline stripping. + /// + /// This is on by default. + pub fn trim_newlines(&mut self, val: bool) -> &mut Self { + self.trim_newlines = val; + self + } + + /// Launches the editor to edit a string. + /// + /// Returns `None` if the file was not saved or otherwise the + /// entered text. + pub fn edit(&self, s: &str) -> io::Result<Option<String>> { + let mut f = tempfile::Builder::new() + .prefix("edit-") + .suffix(&self.extension) + .rand_bytes(12) + .tempfile()?; + f.write_all(s.as_bytes())?; + f.flush()?; + let ts = fs::metadata(f.path())?.modified()?; + + let s: String = self.editor.clone().into_string().unwrap(); + let (cmd, args) = match shell_words::split(&s) { + Ok(mut parts) => { + let cmd = parts.remove(0); + (cmd, parts) + } + Err(_) => (s, vec![]), + }; + + let rv = process::Command::new(cmd) + .args(args) + .arg(f.path()) + .spawn()? + .wait()?; + + if rv.success() && self.require_save && ts >= fs::metadata(f.path())?.modified()? { + return Ok(None); + } + + let mut new_f = fs::File::open(f.path())?; + let mut rv = String::new(); + new_f.read_to_string(&mut rv)?; + + if self.trim_newlines { + let len = rv.trim_end_matches(&['\n', '\r'][..]).len(); + rv.truncate(len); + } + + Ok(Some(rv)) + } +} diff --git a/vendor/dialoguer/src/history.rs b/vendor/dialoguer/src/history.rs new file mode 100644 index 0000000..d0818cc --- /dev/null +++ b/vendor/dialoguer/src/history.rs @@ -0,0 +1,15 @@ +/// Trait for history handling. +pub trait History<T> { + /// This is called with the current position that should + /// be read from history. The `pos` represents the number + /// of times the `Up`/`Down` arrow key has been pressed. + /// This would normally be used as an index to some sort + /// of vector. If the `pos` does not have an entry, [`None`](Option::None) + /// should be returned. + fn read(&self, pos: usize) -> Option<String>; + + /// This is called with the next value you should store + /// in history at the first location. Normally history + /// is implemented as a FIFO queue. + fn write(&mut self, val: &T); +} diff --git a/vendor/dialoguer/src/lib.rs b/vendor/dialoguer/src/lib.rs new file mode 100644 index 0000000..cd8e307 --- /dev/null +++ b/vendor/dialoguer/src/lib.rs @@ -0,0 +1,62 @@ +//! dialoguer is a library for Rust that helps you build useful small +//! interactive user inputs for the command line. It provides utilities +//! to render various simple dialogs like confirmation prompts, text +//! inputs and more. +//! +//! Best paired with other libraries in the family: +//! +//! * [indicatif](https://docs.rs/indicatif) +//! * [console](https://docs.rs/console) +//! +//! # Crate Contents +//! +//! * Confirmation prompts +//! * Input prompts (regular and password) +//! * Input validation +//! * Selections prompts (single and multi) +//! * Fuzzy select prompt +//! * Other kind of prompts +//! * Editor launching +//! +//! # Crate Features +//! +//! The following crate features are available: +//! * `editor`: enables bindings to launch editor to edit strings +//! * `fuzzy-select`: enables fuzzy select prompt +//! * `history`: enables input prompts to be able to track history of inputs +//! * `password`: enables password input prompt +//! * `completion`: enables ability to implement custom tab-completion for input prompts +//! +//! By default `editor` and `password` are enabled. + +#![deny(clippy::all)] + +#[cfg(feature = "completion")] +pub use completion::Completion; +pub use console; +#[cfg(feature = "editor")] +pub use edit::Editor; +#[cfg(feature = "history")] +pub use history::History; +use paging::Paging; +pub use prompts::{ + confirm::Confirm, input::Input, multi_select::MultiSelect, select::Select, sort::Sort, +}; +pub use validate::Validator; + +#[cfg(feature = "fuzzy-select")] +pub use prompts::fuzzy_select::FuzzySelect; + +#[cfg(feature = "password")] +pub use prompts::password::Password; + +#[cfg(feature = "completion")] +mod completion; +#[cfg(feature = "editor")] +mod edit; +#[cfg(feature = "history")] +mod history; +mod paging; +mod prompts; +pub mod theme; +mod validate; diff --git a/vendor/dialoguer/src/paging.rs b/vendor/dialoguer/src/paging.rs new file mode 100644 index 0000000..f85d9b3 --- /dev/null +++ b/vendor/dialoguer/src/paging.rs @@ -0,0 +1,118 @@ +use std::io; + +use console::Term; + +/// Creates a paging module +/// +/// The paging module serves as tracking structure to allow paged views +/// and automatically (de-)activates paging depending on the current terminal size. +pub struct Paging<'a> { + pub pages: usize, + pub current_page: usize, + pub capacity: usize, + pub active: bool, + pub max_capacity: Option<usize>, + term: &'a Term, + current_term_size: (u16, u16), + items_len: usize, + activity_transition: bool, +} + +impl<'a> Paging<'a> { + pub fn new(term: &'a Term, items_len: usize, max_capacity: Option<usize>) -> Paging<'a> { + let term_size = term.size(); + // Subtract -2 because we need space to render the prompt, if paging is active + let capacity = max_capacity + .unwrap_or(std::usize::MAX) + .clamp(3, term_size.0 as usize) + - 2; + let pages = (items_len as f64 / capacity as f64).ceil() as usize; + + Paging { + pages, + current_page: 0, + capacity, + active: pages > 1, + term, + current_term_size: term_size, + items_len, + max_capacity, + // Set transition initially to true to trigger prompt rendering for inactive paging on start + activity_transition: true, + } + } + + /// Updates all internal based on the current terminal size and cursor position + pub fn update(&mut self, cursor_pos: usize) -> io::Result<()> { + let new_term_size = self.term.size(); + + if self.current_term_size != new_term_size { + self.current_term_size = new_term_size; + self.capacity = self + .max_capacity + .unwrap_or(std::usize::MAX) + .clamp(3, self.current_term_size.0 as usize) + - 2; + self.pages = (self.items_len as f64 / self.capacity as f64).ceil() as usize; + } + + if self.active == (self.pages > 1) { + self.activity_transition = false; + } else { + self.active = self.pages > 1; + self.activity_transition = true; + // Clear everything to prevent "ghost" lines in terminal when a resize happened + self.term.clear_last_lines(self.capacity)?; + } + + if cursor_pos != !0 + && (cursor_pos < self.current_page * self.capacity + || cursor_pos >= (self.current_page + 1) * self.capacity) + { + self.current_page = cursor_pos / self.capacity; + } + + Ok(()) + } + + /// Renders a prompt when the following conditions are met: + /// * Paging is active + /// * Transition of the paging activity happened (active -> inactive / inactive -> active) + pub fn render_prompt<F>(&mut self, mut render_prompt: F) -> io::Result<()> + where + F: FnMut(Option<(usize, usize)>) -> io::Result<()>, + { + if self.active { + let paging_info = Some((self.current_page + 1, self.pages)); + render_prompt(paging_info)?; + } else if self.activity_transition { + render_prompt(None)?; + } + + self.term.flush()?; + + Ok(()) + } + + /// Navigates to the next page + pub fn next_page(&mut self) -> usize { + if self.current_page == self.pages - 1 { + self.current_page = 0; + } else { + self.current_page += 1; + } + + self.current_page * self.capacity + } + + /// Navigates to the previous page + pub fn previous_page(&mut self) -> usize { + if self.current_page == 0 { + self.current_page = self.pages - 1; + } else { + self.current_page -= 1; + } + + self.current_page * self.capacity + } +} 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, + } + } +} diff --git a/vendor/dialoguer/src/theme.rs b/vendor/dialoguer/src/theme.rs new file mode 100644 index 0000000..1fbde92 --- /dev/null +++ b/vendor/dialoguer/src/theme.rs @@ -0,0 +1,976 @@ +//! Customizes the rendering of the elements. +use std::{fmt, io}; + +use console::{measure_text_width, style, Style, StyledObject, Term}; +#[cfg(feature = "fuzzy-select")] +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; + +/// Implements a theme for dialoguer. +pub trait Theme { + /// Formats a prompt. + #[inline] + fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + write!(f, "{}:", prompt) + } + + /// Formats out an error. + #[inline] + fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { + write!(f, "error: {}", err) + } + + /// Formats a confirm prompt. + fn format_confirm_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + default: Option<bool>, + ) -> fmt::Result { + if !prompt.is_empty() { + write!(f, "{} ", &prompt)?; + } + match default { + None => write!(f, "[y/n] ")?, + Some(true) => write!(f, "[Y/n] ")?, + Some(false) => write!(f, "[y/N] ")?, + } + Ok(()) + } + + /// Formats a confirm prompt after selection. + fn format_confirm_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + selection: Option<bool>, + ) -> fmt::Result { + let selection = selection.map(|b| if b { "yes" } else { "no" }); + + match selection { + Some(selection) if prompt.is_empty() => { + write!(f, "{}", selection) + } + Some(selection) => { + write!(f, "{} {}", &prompt, selection) + } + None if prompt.is_empty() => Ok(()), + None => { + write!(f, "{}", &prompt) + } + } + } + + /// Formats an input prompt. + fn format_input_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + default: Option<&str>, + ) -> fmt::Result { + match default { + Some(default) if prompt.is_empty() => write!(f, "[{}]: ", default), + Some(default) => write!(f, "{} [{}]: ", prompt, default), + None => write!(f, "{}: ", prompt), + } + } + + /// Formats an input prompt after selection. + #[inline] + fn format_input_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + sel: &str, + ) -> fmt::Result { + write!(f, "{}: {}", prompt, sel) + } + + /// Formats a password prompt. + #[inline] + #[cfg(feature = "password")] + fn format_password_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + self.format_input_prompt(f, prompt, None) + } + + /// Formats a password prompt after selection. + #[inline] + #[cfg(feature = "password")] + fn format_password_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + ) -> fmt::Result { + self.format_input_prompt_selection(f, prompt, "[hidden]") + } + + /// Formats a select prompt. + #[inline] + fn format_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + self.format_prompt(f, prompt) + } + + /// Formats a select prompt after selection. + #[inline] + fn format_select_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + sel: &str, + ) -> fmt::Result { + self.format_input_prompt_selection(f, prompt, sel) + } + + /// Formats a multi select prompt. + #[inline] + fn format_multi_select_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + self.format_prompt(f, prompt) + } + + /// Formats a sort prompt. + #[inline] + fn format_sort_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + self.format_prompt(f, prompt) + } + + /// Formats a multi_select prompt after selection. + fn format_multi_select_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + selections: &[&str], + ) -> fmt::Result { + write!(f, "{}: ", prompt)?; + for (idx, sel) in selections.iter().enumerate() { + write!(f, "{}{}", if idx == 0 { "" } else { ", " }, sel)?; + } + Ok(()) + } + + /// Formats a sort prompt after selection. + #[inline] + fn format_sort_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + selections: &[&str], + ) -> fmt::Result { + self.format_multi_select_prompt_selection(f, prompt, selections) + } + + /// Formats a select prompt item. + fn format_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + active: bool, + ) -> fmt::Result { + write!(f, "{} {}", if active { ">" } else { " " }, text) + } + + /// Formats a multi select prompt item. + fn format_multi_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + checked: bool, + active: bool, + ) -> fmt::Result { + write!( + f, + "{} {}", + match (checked, active) { + (true, true) => "> [x]", + (true, false) => " [x]", + (false, true) => "> [ ]", + (false, false) => " [ ]", + }, + text + ) + } + + /// Formats a sort prompt item. + fn format_sort_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + picked: bool, + active: bool, + ) -> fmt::Result { + write!( + f, + "{} {}", + match (picked, active) { + (true, true) => "> [x]", + (false, true) => "> [ ]", + (_, false) => " [ ]", + }, + text + ) + } + + /// Formats a fuzzy select prompt item. + #[cfg(feature = "fuzzy-select")] + fn format_fuzzy_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + active: bool, + highlight_matches: bool, + matcher: &SkimMatcherV2, + search_term: &str, + ) -> fmt::Result { + write!(f, "{} ", if active { ">" } else { " " })?; + + if highlight_matches { + if let Some((_score, indices)) = matcher.fuzzy_indices(text, &search_term) { + for (idx, c) in text.chars().into_iter().enumerate() { + if indices.contains(&idx) { + write!(f, "{}", style(c).for_stderr().bold())?; + } else { + write!(f, "{}", c)?; + } + } + + return Ok(()); + } + } + + write!(f, "{}", text) + } + + /// Formats a fuzzy select prompt. + #[cfg(feature = "fuzzy-select")] + fn format_fuzzy_select_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + search_term: &str, + cursor_pos: usize, + ) -> fmt::Result { + if !prompt.is_empty() { + write!(f, "{} ", prompt,)?; + } + + if cursor_pos < search_term.len() { + let st_head = search_term[0..cursor_pos].to_string(); + let st_tail = search_term[cursor_pos..search_term.len()].to_string(); + let st_cursor = "|".to_string(); + write!(f, "{}{}{}", st_head, st_cursor, st_tail) + } else { + let cursor = "|".to_string(); + write!(f, "{}{}", search_term.to_string(), cursor) + } + } +} + +/// The default theme. +pub struct SimpleTheme; + +impl Theme for SimpleTheme {} + +/// A colorful theme +pub struct ColorfulTheme { + /// The style for default values + pub defaults_style: Style, + /// The style for prompt + pub prompt_style: Style, + /// Prompt prefix value and style + pub prompt_prefix: StyledObject<String>, + /// Prompt suffix value and style + pub prompt_suffix: StyledObject<String>, + /// Prompt on success prefix value and style + pub success_prefix: StyledObject<String>, + /// Prompt on success suffix value and style + pub success_suffix: StyledObject<String>, + /// Error prefix value and style + pub error_prefix: StyledObject<String>, + /// The style for error message + pub error_style: Style, + /// The style for hints + pub hint_style: Style, + /// The style for values on prompt success + pub values_style: Style, + /// The style for active items + pub active_item_style: Style, + /// The style for inactive items + pub inactive_item_style: Style, + /// Active item in select prefix value and style + pub active_item_prefix: StyledObject<String>, + /// Inctive item in select prefix value and style + pub inactive_item_prefix: StyledObject<String>, + /// Checked item in multi select prefix value and style + pub checked_item_prefix: StyledObject<String>, + /// Unchecked item in multi select prefix value and style + pub unchecked_item_prefix: StyledObject<String>, + /// Picked item in sort prefix value and style + pub picked_item_prefix: StyledObject<String>, + /// Unpicked item in sort prefix value and style + pub unpicked_item_prefix: StyledObject<String>, + /// Formats the cursor for a fuzzy select prompt + #[cfg(feature = "fuzzy-select")] + pub fuzzy_cursor_style: Style, + // Formats the highlighting if matched characters + #[cfg(feature = "fuzzy-select")] + pub fuzzy_match_highlight_style: Style, + /// Show the selections from certain prompts inline + pub inline_selections: bool, +} + +impl Default for ColorfulTheme { + fn default() -> ColorfulTheme { + ColorfulTheme { + defaults_style: Style::new().for_stderr().cyan(), + prompt_style: Style::new().for_stderr().bold(), + prompt_prefix: style("?".to_string()).for_stderr().yellow(), + prompt_suffix: style("›".to_string()).for_stderr().black().bright(), + success_prefix: style("✔".to_string()).for_stderr().green(), + success_suffix: style("·".to_string()).for_stderr().black().bright(), + error_prefix: style("✘".to_string()).for_stderr().red(), + error_style: Style::new().for_stderr().red(), + hint_style: Style::new().for_stderr().black().bright(), + values_style: Style::new().for_stderr().green(), + active_item_style: Style::new().for_stderr().cyan(), + inactive_item_style: Style::new().for_stderr(), + active_item_prefix: style("❯".to_string()).for_stderr().green(), + inactive_item_prefix: style(" ".to_string()).for_stderr(), + checked_item_prefix: style("✔".to_string()).for_stderr().green(), + unchecked_item_prefix: style("✔".to_string()).for_stderr().black(), + picked_item_prefix: style("❯".to_string()).for_stderr().green(), + unpicked_item_prefix: style(" ".to_string()).for_stderr(), + #[cfg(feature = "fuzzy-select")] + fuzzy_cursor_style: Style::new().for_stderr().black().on_white(), + #[cfg(feature = "fuzzy-select")] + fuzzy_match_highlight_style: Style::new().for_stderr().bold(), + inline_selections: true, + } + } +} + +impl Theme for ColorfulTheme { + /// Formats a prompt. + fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + write!(f, "{}", &self.prompt_suffix) + } + + /// Formats an error + fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { + write!( + f, + "{} {}", + &self.error_prefix, + self.error_style.apply_to(err) + ) + } + + /// Formats an input prompt. + fn format_input_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + default: Option<&str>, + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + match default { + Some(default) => write!( + f, + "{} {} ", + self.hint_style.apply_to(&format!("({})", default)), + &self.prompt_suffix + ), + None => write!(f, "{} ", &self.prompt_suffix), + } + } + + /// Formats a confirm prompt. + fn format_confirm_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + default: Option<bool>, + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + match default { + None => write!( + f, + "{} {}", + self.hint_style.apply_to("(y/n)"), + &self.prompt_suffix + ), + Some(true) => write!( + f, + "{} {} {}", + self.hint_style.apply_to("(y/n)"), + &self.prompt_suffix, + self.defaults_style.apply_to("yes") + ), + Some(false) => write!( + f, + "{} {} {}", + self.hint_style.apply_to("(y/n)"), + &self.prompt_suffix, + self.defaults_style.apply_to("no") + ), + } + } + + /// Formats a confirm prompt after selection. + fn format_confirm_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + selection: Option<bool>, + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.success_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + let selection = selection.map(|b| if b { "yes" } else { "no" }); + + match selection { + Some(selection) => { + write!( + f, + "{} {}", + &self.success_suffix, + self.values_style.apply_to(selection) + ) + } + None => { + write!(f, "{}", &self.success_suffix) + } + } + } + + /// Formats an input prompt after selection. + fn format_input_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + sel: &str, + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.success_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + write!( + f, + "{} {}", + &self.success_suffix, + self.values_style.apply_to(sel) + ) + } + + /// Formats a password prompt after selection. + #[cfg(feature = "password")] + fn format_password_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + ) -> fmt::Result { + self.format_input_prompt_selection(f, prompt, "********") + } + + /// Formats a multi select prompt after selection. + fn format_multi_select_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + selections: &[&str], + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.success_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + write!(f, "{} ", &self.success_suffix)?; + + if self.inline_selections { + for (idx, sel) in selections.iter().enumerate() { + write!( + f, + "{}{}", + if idx == 0 { "" } else { ", " }, + self.values_style.apply_to(sel) + )?; + } + } + + Ok(()) + } + + /// Formats a select prompt item. + fn format_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + active: bool, + ) -> fmt::Result { + let details = if active { + ( + &self.active_item_prefix, + self.active_item_style.apply_to(text), + ) + } else { + ( + &self.inactive_item_prefix, + self.inactive_item_style.apply_to(text), + ) + }; + + write!(f, "{} {}", details.0, details.1) + } + + /// Formats a multi select prompt item. + fn format_multi_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + checked: bool, + active: bool, + ) -> fmt::Result { + let details = match (checked, active) { + (true, true) => ( + &self.checked_item_prefix, + self.active_item_style.apply_to(text), + ), + (true, false) => ( + &self.checked_item_prefix, + self.inactive_item_style.apply_to(text), + ), + (false, true) => ( + &self.unchecked_item_prefix, + self.active_item_style.apply_to(text), + ), + (false, false) => ( + &self.unchecked_item_prefix, + self.inactive_item_style.apply_to(text), + ), + }; + + write!(f, "{} {}", details.0, details.1) + } + + /// Formats a sort prompt item. + fn format_sort_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + picked: bool, + active: bool, + ) -> fmt::Result { + let details = match (picked, active) { + (true, true) => ( + &self.picked_item_prefix, + self.active_item_style.apply_to(text), + ), + (false, true) => ( + &self.unpicked_item_prefix, + self.active_item_style.apply_to(text), + ), + (_, false) => ( + &self.unpicked_item_prefix, + self.inactive_item_style.apply_to(text), + ), + }; + + write!(f, "{} {}", details.0, details.1) + } + + /// Formats a fuzzy select prompt item. + #[cfg(feature = "fuzzy-select")] + fn format_fuzzy_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + active: bool, + highlight_matches: bool, + matcher: &SkimMatcherV2, + search_term: &str, + ) -> fmt::Result { + write!( + f, + "{} ", + if active { + &self.active_item_prefix + } else { + &self.inactive_item_prefix + } + )?; + + if highlight_matches { + if let Some((_score, indices)) = matcher.fuzzy_indices(text, &search_term) { + for (idx, c) in text.chars().into_iter().enumerate() { + if indices.contains(&idx) { + if active { + write!( + f, + "{}", + self.active_item_style + .apply_to(self.fuzzy_match_highlight_style.apply_to(c)) + )?; + } else { + write!(f, "{}", self.fuzzy_match_highlight_style.apply_to(c))?; + } + } else { + if active { + write!(f, "{}", self.active_item_style.apply_to(c))?; + } else { + write!(f, "{}", c)?; + } + } + } + + return Ok(()); + } + } + + write!(f, "{}", text) + } + + /// Formats a fuzzy-selectprompt after selection. + #[cfg(feature = "fuzzy-select")] + fn format_fuzzy_select_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + search_term: &str, + cursor_pos: usize, + ) -> fmt::Result { + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + if cursor_pos < search_term.len() { + let st_head = search_term[0..cursor_pos].to_string(); + let st_tail = search_term[cursor_pos + 1..search_term.len()].to_string(); + let st_cursor = self + .fuzzy_cursor_style + .apply_to(search_term.to_string().chars().nth(cursor_pos).unwrap()); + write!( + f, + "{} {}{}{}", + &self.prompt_suffix, st_head, st_cursor, st_tail + ) + } else { + let cursor = self.fuzzy_cursor_style.apply_to(" "); + write!( + f, + "{} {}{}", + &self.prompt_suffix, + search_term.to_string(), + cursor + ) + } + } +} + +/// Helper struct to conveniently render a theme of a term. +pub(crate) struct TermThemeRenderer<'a> { + term: &'a Term, + theme: &'a dyn Theme, + height: usize, + prompt_height: usize, + prompts_reset_height: bool, +} + +impl<'a> TermThemeRenderer<'a> { + pub fn new(term: &'a Term, theme: &'a dyn Theme) -> TermThemeRenderer<'a> { + TermThemeRenderer { + term, + theme, + height: 0, + prompt_height: 0, + prompts_reset_height: true, + } + } + + #[cfg(feature = "password")] + pub fn set_prompts_reset_height(&mut self, val: bool) { + self.prompts_reset_height = val; + } + + #[cfg(feature = "password")] + pub fn term(&self) -> &Term { + self.term + } + + pub fn add_line(&mut self) { + self.height += 1; + } + + fn write_formatted_str< + F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, + >( + &mut self, + f: F, + ) -> io::Result<usize> { + let mut buf = String::new(); + f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + self.height += buf.chars().filter(|&x| x == '\n').count(); + self.term.write_str(&buf)?; + Ok(measure_text_width(&buf)) + } + + fn write_formatted_line< + F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, + >( + &mut self, + f: F, + ) -> io::Result<()> { + let mut buf = String::new(); + f(self, &mut buf).map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + self.height += buf.chars().filter(|&x| x == '\n').count() + 1; + self.term.write_line(&buf) + } + + fn write_formatted_prompt< + F: FnOnce(&mut TermThemeRenderer, &mut dyn fmt::Write) -> fmt::Result, + >( + &mut self, + f: F, + ) -> io::Result<()> { + self.write_formatted_line(f)?; + if self.prompts_reset_height { + self.prompt_height = self.height; + self.height = 0; + } + Ok(()) + } + + fn write_paging_info(buf: &mut dyn fmt::Write, paging_info: (usize, usize)) -> fmt::Result { + write!(buf, " [Page {}/{}] ", paging_info.0, paging_info.1) + } + + pub fn error(&mut self, err: &str) -> io::Result<()> { + self.write_formatted_line(|this, buf| this.theme.format_error(buf, err)) + } + + pub fn confirm_prompt(&mut self, prompt: &str, default: Option<bool>) -> io::Result<usize> { + self.write_formatted_str(|this, buf| this.theme.format_confirm_prompt(buf, prompt, default)) + } + + pub fn confirm_prompt_selection(&mut self, prompt: &str, sel: Option<bool>) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_confirm_prompt_selection(buf, prompt, sel) + }) + } + + #[cfg(feature = "fuzzy-select")] + pub fn fuzzy_select_prompt( + &mut self, + prompt: &str, + search_term: &str, + cursor_pos: usize, + ) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme + .format_fuzzy_select_prompt(buf, prompt, search_term, cursor_pos) + }) + } + + pub fn input_prompt(&mut self, prompt: &str, default: Option<&str>) -> io::Result<usize> { + self.write_formatted_str(|this, buf| this.theme.format_input_prompt(buf, prompt, default)) + } + + pub fn input_prompt_selection(&mut self, prompt: &str, sel: &str) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_input_prompt_selection(buf, prompt, sel) + }) + } + + #[cfg(feature = "password")] + pub fn password_prompt(&mut self, prompt: &str) -> io::Result<usize> { + self.write_formatted_str(|this, buf| { + write!(buf, "\r")?; + this.theme.format_password_prompt(buf, prompt) + }) + } + + #[cfg(feature = "password")] + pub fn password_prompt_selection(&mut self, prompt: &str) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_password_prompt_selection(buf, prompt) + }) + } + + pub fn select_prompt( + &mut self, + prompt: &str, + paging_info: Option<(usize, usize)>, + ) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_select_prompt(buf, prompt)?; + + if let Some(paging_info) = paging_info { + TermThemeRenderer::write_paging_info(buf, paging_info)?; + } + + Ok(()) + }) + } + + pub fn select_prompt_selection(&mut self, prompt: &str, sel: &str) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_select_prompt_selection(buf, prompt, sel) + }) + } + + pub fn select_prompt_item(&mut self, text: &str, active: bool) -> io::Result<()> { + self.write_formatted_line(|this, buf| { + this.theme.format_select_prompt_item(buf, text, active) + }) + } + + #[cfg(feature = "fuzzy-select")] + pub fn fuzzy_select_prompt_item( + &mut self, + text: &str, + active: bool, + highlight: bool, + matcher: &SkimMatcherV2, + search_term: &str, + ) -> io::Result<()> { + self.write_formatted_line(|this, buf| { + this.theme.format_fuzzy_select_prompt_item( + buf, + text, + active, + highlight, + matcher, + search_term, + ) + }) + } + + pub fn multi_select_prompt( + &mut self, + prompt: &str, + paging_info: Option<(usize, usize)>, + ) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_multi_select_prompt(buf, prompt)?; + + if let Some(paging_info) = paging_info { + TermThemeRenderer::write_paging_info(buf, paging_info)?; + } + + Ok(()) + }) + } + + pub fn multi_select_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme + .format_multi_select_prompt_selection(buf, prompt, sel) + }) + } + + pub fn multi_select_prompt_item( + &mut self, + text: &str, + checked: bool, + active: bool, + ) -> io::Result<()> { + self.write_formatted_line(|this, buf| { + this.theme + .format_multi_select_prompt_item(buf, text, checked, active) + }) + } + + pub fn sort_prompt( + &mut self, + prompt: &str, + paging_info: Option<(usize, usize)>, + ) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_sort_prompt(buf, prompt)?; + + if let Some(paging_info) = paging_info { + TermThemeRenderer::write_paging_info(buf, paging_info)?; + } + + Ok(()) + }) + } + + pub fn sort_prompt_selection(&mut self, prompt: &str, sel: &[&str]) -> io::Result<()> { + self.write_formatted_prompt(|this, buf| { + this.theme.format_sort_prompt_selection(buf, prompt, sel) + }) + } + + pub fn sort_prompt_item(&mut self, text: &str, picked: bool, active: bool) -> io::Result<()> { + self.write_formatted_line(|this, buf| { + this.theme + .format_sort_prompt_item(buf, text, picked, active) + }) + } + + pub fn clear(&mut self) -> io::Result<()> { + self.term + .clear_last_lines(self.height + self.prompt_height)?; + self.height = 0; + self.prompt_height = 0; + Ok(()) + } + + pub fn clear_preserve_prompt(&mut self, size_vec: &[usize]) -> io::Result<()> { + let mut new_height = self.height; + let prefix_width = 2; + //Check each item size, increment on finding an overflow + for size in size_vec { + if *size > self.term.size().1 as usize { + new_height += (((*size as f64 + prefix_width as f64) / self.term.size().1 as f64) + .ceil()) as usize + - 1; + } + } + + self.term.clear_last_lines(new_height)?; + self.height = 0; + Ok(()) + } +} diff --git a/vendor/dialoguer/src/validate.rs b/vendor/dialoguer/src/validate.rs new file mode 100644 index 0000000..addc9b4 --- /dev/null +++ b/vendor/dialoguer/src/validate.rs @@ -0,0 +1,49 @@ +//! Provides validation for text inputs + +/// Trait for input validators. +/// +/// A generic implementation for `Fn(&str) -> Result<(), E>` is provided +/// to facilitate development. +pub trait Validator<T> { + type Err; + + /// Invoked with the value to validate. + /// + /// If this produces `Ok(())` then the value is used and parsed, if + /// an error is returned validation fails with that error. + fn validate(&mut self, input: &T) -> Result<(), Self::Err>; +} + +impl<T, F, E> Validator<T> for F +where + F: FnMut(&T) -> Result<(), E>, +{ + type Err = E; + + fn validate(&mut self, input: &T) -> Result<(), Self::Err> { + self(input) + } +} + +/// Trait for password validators. +#[allow(clippy::ptr_arg)] +pub trait PasswordValidator { + type Err; + + /// Invoked with the value to validate. + /// + /// If this produces `Ok(())` then the value is used and parsed, if + /// an error is returned validation fails with that error. + fn validate(&self, input: &String) -> Result<(), Self::Err>; +} + +impl<F, E> PasswordValidator for F +where + F: Fn(&String) -> Result<(), E>, +{ + type Err = E; + + fn validate(&self, input: &String) -> Result<(), Self::Err> { + self(input) + } +} |