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;
}
}
}
}
}