promptt 1.1.0

Interactive CLI prompts library, lightweight and easy to use.
Documentation
//! Text prompt.

use crate::util::style;
use colour::write_bold;
use std::io::{self, BufRead, Write};

/// Text prompt options.
pub struct TextPromptOptions {
    pub message: String,
    pub initial: Option<String>,
}

impl Default for TextPromptOptions {
    fn default() -> Self {
        Self {
            message: String::new(),
            initial: None,
        }
    }
}

/// Runs text prompt. Returns input or initial when empty.
pub fn run_text<R: BufRead, W: Write>(
    opts: &TextPromptOptions,
    stdin: &mut R,
    stdout: &mut W,
) -> io::Result<String> {
    let initial = opts.initial.as_deref().unwrap_or("");
    let mut output = Vec::with_capacity(opts.message.len() + 32);
    write_bold!(&mut output, "{}", opts.message).ok();
    let msg_styled = String::from_utf8_lossy(&output).into_owned();
    let delim = style::delimiter(false);
    // Do not pre-display initial value: it is not editable. Use initial only when user submits empty.
    let prompt_line = format!("{} {} ", msg_styled, delim);
    write!(stdout, "{}", prompt_line)?;
    stdout.flush()?;
    let mut line = String::new();
    stdin.read_line(&mut line)?;
    let value = line.trim().to_string();
    let value = if value.is_empty() {
        initial.to_string()
    } else {
        value
    };
    stdout.flush()?;
    Ok(value)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn text_prompt_options_default() {
        let opts = TextPromptOptions::default();
        assert!(opts.message.is_empty());
        assert!(opts.initial.is_none());
    }

    #[test]
    fn run_text_returns_entered_value() {
        let opts = TextPromptOptions {
            message: "Name?".into(),
            initial: None,
        };
        let mut stdin = Cursor::new(b"Bob\n");
        let mut stdout = Vec::new();
        let r = run_text(&opts, &mut stdin, &mut stdout);
        assert!(r.is_ok());
        assert_eq!(r.unwrap(), "Bob");
    }

    #[test]
    fn run_text_empty_uses_initial() {
        let opts = TextPromptOptions {
            message: "Name?".into(),
            initial: Some("default".into()),
        };
        let mut stdin = Cursor::new(b"\n");
        let mut stdout = Vec::new();
        let r = run_text(&opts, &mut stdin, &mut stdout);
        assert!(r.is_ok());
        assert_eq!(r.unwrap(), "default");
    }

    #[test]
    fn run_text_trims_input() {
        let opts = TextPromptOptions {
            message: "X?".into(),
            initial: None,
        };
        let mut stdin = Cursor::new(b"  spaced  \n");
        let mut stdout = Vec::new();
        let r = run_text(&opts, &mut stdin, &mut stdout);
        assert!(r.is_ok());
        assert_eq!(r.unwrap(), "spaced");
    }

    #[test]
    fn run_text_empty_input_no_initial_returns_empty_string() {
        let opts = TextPromptOptions {
            message: "Name?".into(),
            initial: None,
        };
        let mut stdin = Cursor::new(b"\n");
        let mut stdout = Vec::new();
        let r = run_text(&opts, &mut stdin, &mut stdout);
        assert!(r.is_ok());
        assert_eq!(r.unwrap(), "");
    }

    #[test]
    fn run_text_whitespace_only_treated_as_empty_uses_initial() {
        let opts = TextPromptOptions {
            message: "X?".into(),
            initial: Some("default".into()),
        };
        let mut stdin = Cursor::new(b"   \n");
        let mut stdout = Vec::new();
        let r = run_text(&opts, &mut stdin, &mut stdout);
        assert!(r.is_ok());
        assert_eq!(r.unwrap(), "default");
    }
}