dialoguer 0.10.3

A command line prompting library.
Documentation
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;
                }
            }
        }
    }
}