uquery 0.2.2

A simple library to display information to the user and to query them for information in a bright, colourful manner.
Documentation
#![doc = include_str!("../README.md")]
#![warn(clippy::pedantic)]
#![warn(missing_docs)]

use core::str::FromStr;
use std::fmt::Display;
use std::io;
use std::io::Write;

use yansi::Paint;

/// A type representing the error for a query.
pub type QueryResult<T> = io::Result<T>;

/// Asks the users input
fn query_input() -> QueryResult<String> {
    let stdin = io::stdin();

    // Read in the input
    let mut buffer = String::new();
    stdin.read_line(&mut buffer)?;
    Ok(buffer)
}

/// Writes a prompt out in the form of:
///
/// ```txt
/// {prefix} (Defaulting to: {def}) {text}
/// ```
fn write_prompt(
    text: &str,
    def: Option<&impl Display>,
    prefix: &impl Display,
) -> QueryResult<String> {
    let mut stdout = io::stdout();

    // Write out the prompt
    write!(stdout, "{} ", prefix)?;
    if let Some(ref def) = def {
        write!(
            stdout,
            "{} {}{} ",
            Paint::green("(Defaulting to:"),
            Paint::green(def),
            Paint::green(")"),
        )?;
    }
    write!(stdout, "{} ", text)?;
    stdout.flush()?;

    query_input()
}

/// Queries the user for string input.
///
/// # Errors
///
/// Fails on io errors with the stdin and stdout.
pub fn string(string: &str) -> io::Result<String> {
    string_with_default(string, Option::<String>::None)
}

/// Queries the user for string input with a default.
///
///  The user can unset the value by typing a space,
/// otherwise, if no string is specified and a deafult is given the
/// default will be used.
///
/// # Errors
///
/// Fails on io errors with the stdin and stdout.
pub fn string_with_default(
    text: &str,
    def: Option<impl Into<String> + Display>,
) -> QueryResult<String> {

    // Write out the prompt
    let buffer = write_prompt(text, def.as_ref(), &Paint::yellow("::").bold())?;

    match def {
        Some(def)
            if buffer.trim().is_empty() &&
                (buffer.len() < 2 || &buffer[..buffer.len() - 1] != " ") =>
        {
            Ok(def.into())
        }
        _ => Ok(buffer.trim().to_owned()),
    }
}

/// Queries the user for input of any time that implements `FromStr`
/// and `Display`
///
/// # Errors
///
/// Fails on io errors with the stdin and stdout.
pub fn parsable_with_default<T: FromStr + Display>(
    text: &str,
    def: T,
) -> QueryResult<T> {
    let buffer = write_prompt(text, Some(&def), &Paint::blue("::").bold())?;

    if buffer.trim().is_empty() {
        Ok(def)
    } else {
        match buffer.trim().parse() {
            Ok(o) => Ok(o),
            Err(_e) => {
                println!(
                    "{}",
                    Paint::red("Failed to be parse input! Retrying...")
                );
                parsable_with_default(text, def)
            }
        }
    }
}

/// Queries the user for input of any time that implements `FromStr`.
///
/// # Errors
///
/// Fails on io errors with the stdin and stdout.
pub fn parsable<T>(text: &str) -> QueryResult<T>
where
    T: FromStr,
    <T as FromStr>::Err: ToString,
{
    let buffer =
        write_prompt(text, Option::<&u8>::None, &Paint::blue("::").bold())?;

    match buffer.trim().parse() {
        Ok(o) => Ok(o),
        Err(e) => {
            println!();
            error(
                "Failed to be parse input! Retrying...",
                Some(&e.to_string()),
            );
            println!();
            parsable(text)
        }
    }
}

/// Queries the user for boolean input, with an optional default.
///
/// # Errors
///
/// Fails with io errors with the stdin or stdout.
pub fn boolean(text: &str, def: Option<bool>) -> QueryResult<bool> {
    let mut stdout = io::stdout();
    write!(stdout, "{} ", Paint::magenta("::").bold())?;
    write!(stdout, "{} ", text)?;
    write!(
        stdout,
        "{} ",
        Paint::green(match def {
            Some(true) => "[Y/n]",
            Some(false) => "[y/N]",
            None => "[y/n]",
        }),
    )?;
    stdout.flush()?;

    let buffer = query_input()?;

    match (
        def,
        buffer
            .trim()
            .chars()
            .next()
            .and_then(|x| x.to_lowercase().next())
    ) {
        (_, Some('y')) => Ok(true),
        (_, Some('n')) => Ok(false),
        (Some(def), None) => Ok(def),
        _ => {
            println!();
            error(
                "Failed to be parse input! Retrying...",
                Some("Use either `y` or `n` as your response"),
            );
            println!();
            boolean(text, def)
        }
    }
}

/// Displays an error to the user.
pub fn error(string: &str, suggestion: Option<&str>) {
    match suggestion {
        Some(suggestion) => {
            eprintln!("{} {}", Paint::red("Error:").bold(), string);
            eprintln!("  {} {}", Paint::cyan("Suggestion:"), suggestion);
        }
        None => {
            eprintln!("{} {}", Paint::red("Error:").bold(), string);
        }
    }
}

/// Displays a warning to the user.
pub fn warning(string: &str, suggestion: Option<&str>) {
    match suggestion {
        Some(suggestion) => {
            eprintln!("{} {}", Paint::yellow("Warning:").bold(), string);
            eprintln!("  {} {}", Paint::cyan("Suggestion:"), suggestion);
        }
        None => {
            eprintln!("{} {}", Paint::yellow("Warning:").bold(), string);
        }
    }
}