diff options
Diffstat (limited to 'vendor/dialoguer/src/prompts/input.rs')
-rw-r--r-- | vendor/dialoguer/src/prompts/input.rs | 691 |
1 files changed, 691 insertions, 0 deletions
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; + } + } + } + } +} |