conciliator 0.3.10

[WIP] Library for interactive CLI programs
Documentation
//! User input
use std::marker::PhantomData;
use crate::{
	Buffer,
	Paint,
	Conciliator,
	List,
	Print,
	Pushable
};

/// Requesting user input
///
/// Used by [`Claw::input`](crate::Claw::input), where:
/// - [`print`](Self::print) is called *once* at the start of the input procedure to establish the context of the input being requested,
/// - [`prompt`](Self::prompt) is called to prompt the user for input,
/// - then, a line is read from the standard input and [`trim`](str::trim)med before being passed to
/// - [`validate`](Self::validate), which (parses and) validates the input. If it returns
///     - `Some`, the input procedure ends successfully.
///     - `None`, the user will be [`prompt`](Self::prompt)ed again (and again) until [`validate`](Self::validate) returns [`Some`].
pub trait Input {
	/// Type being requested from the user (after parsing)
	type T;
	/// Make the request
	///
	/// Provide a concise question or request, ideally indicating the expected format.
	/// In general, do not add a trailing newline.
	///
	/// This will be called at least once, and then again for every time [`validate`](Self::validate) returns `None`.
	///
	/// For example: `Are you sure you want to continue? [y/N]: `
	fn prompt(&self, buffer: &mut Buffer);
	/// Parse & validate the user input
	///
	/// Returning `Some(T)` indicates success: the input procedure ends and `T` is returned.
	/// Otherwise, the user will be [`prompt`](Self::prompt)ed to try again.
	fn validate(&self, user_input: &str) -> Option<Self::T>;
	/// Establish the context of the request (optional, the provided method does nothing)
	///
	/// For example: to request a selection to be made from a [`List`] of items, this function is implemented and [`Print`]s the [`List`].
	/// Then [`prompt`](Self::prompt) only specifies the range of valid indices.
	///
	/// This differs from the signature of [`Print`] in that it takes `&mut self` instead `self` by value, because the [`Input`] implementor will still be needed to [`prompt`](Self::prompt) and [`validate`](Self::validate).
	/// Because of this, implementors of this that are wrappers around [`Print`] implementors may chose to use the `if let Some(p) = self.p.take() { p.print(con) }` pattern.
	fn print<C: Conciliator + ?Sized>(&mut self, con: &C) {
		let _ = con;
	}
}

/// Boolean [`Input`], e.g. `Are you sure? [y/N]`
pub struct Confirm<Q, M> {
	question: Q,
	marker: PhantomData<M>,
	default: bool
}

/// Ask the user to pick something from a list by index
pub struct Select<Q, M, L> {
	list: Option<L>,
	question: Q,
	marker: PhantomData<M>,
	len: usize
}


/// Ask whether to "Abort, Retry, or Continue"
///
/// Whichever variant is used with [`Claw::input`](crate::Claw::input) is the default (returned if the user presses enter without making an explicit selection).
///
/// There's no "behavior" inherent to the variants, it's up to you how to implement that.
///
/// For example:
/// ```
/// use conciliator::{Conciliator, input::AbortRetryContinue};
/// let con = conciliator::init();
/// loop {
///     match con.input(AbortRetryContinue::Retry) {
///         AbortRetryContinue::Abort => break Err("Aborted!"),
///         AbortRetryContinue::Retry => break Ok("Retry"),
///         AbortRetryContinue::Continue => continue
///     };
/// };
/// ```
/// Asks:
/// ```text
/// Abort, Retry, or Continue? [a/R/c]:
/// ```
#[derive(Clone, Copy)]
pub enum AbortRetryContinue {
	/// **A**bort
	Abort,
	/// **R**etry
	Retry,
	/// **C**ontinue
	Continue
}

/*
 *	ABORT RETRY CONTINUE
 */

impl Input for AbortRetryContinue {
	type T = Self;
	fn prompt(&self, buffer: &mut Buffer) {
		buffer
			.push_bold("A")
			.push_plain("bort, ")
			.push_bold("R")
			.push_plain("etry, or ")
			.push_bold("C")
			.push_plain("ontinue?");

		match *self {
			Self::Abort => buffer.push_plain(" [A/r/c]: "),
			Self::Retry => buffer.push_plain(" [a/R/c]: "),
			Self::Continue => buffer.push_plain(" [a/r/C]: ")
		};
	}
	fn validate(&self, user_input: &str) -> Option<Self::T> {
		match user_input {
			"" => Some(*self),
			"a" | "A" => Some(Self::Abort),
			"r" | "R" => Some(Self::Retry),
			"c" | "C" => Some(Self::Continue),
			_ => None
		}
	}
}


/*
 *	STRING INPUT
 */

/// Basic [`String`] input, no validation
impl<'s> Input for &'s str {
	type T = String;
	fn prompt(&self, buffer: &mut Buffer) {
		buffer
			.push_plain(self)
			.push_plain(": ");
	}
	fn validate(&self, user_input: &str) -> Option<String> {
		Some(user_input.to_owned())
	}
}

/*
 *	CONFIRM
 */

impl<Q, M> Confirm<Q, M> {
	/// Ask a yes or no question
	pub fn new(default: bool, question: Q) -> Self {
		Self {question, marker: PhantomData, default}
	}
	/// Return `false` for empty input `[y/N]`
	pub fn default_no(question: Q) -> Self {
		Self {question, marker: PhantomData, default: false}
	}
	/// Return `true` for empty input `[Y/n]`
	pub fn default_yes(question: Q) -> Self {
		Self {question, marker: PhantomData, default: true}
	}
}

impl<Q, M> Input for Confirm<Q, M>
	where for<'a> &'a Q: Pushable<M>
{
	type T = bool;
	fn prompt(&self, buffer: &mut Buffer) {
		buffer
			.push(&self.question)
			.push_plain(if self.default {" [Y/n]: "} else {" [y/N]: "});
	}
	fn validate(&self, user_input: &str) -> Option<bool> {
		match user_input {
			"" => Some(self.default),
			"y" | "Y" => Some(true),
			"n" | "N" => Some(false),
			_ => None
		}
	}
}

/*
 *	SELECT
 */

impl<Q, M, H, I, T, F, P> Select<Q, M, List<H, I, F, P>>
	where
		I: ExactSizeIterator<Item = T>,
		List<H, I, F, P>: Print
{
	/// Create a new [`Select`] from a [`List`]
	pub fn new(list: List<H, I, F, P>, question: Q) -> Self {
		Self {
			len: list.len(),
			list: Some(list),
			marker: PhantomData,
			question
		}
	}
}

impl<Q, M, H, I, F, P> Input for Select<Q, M, List<H, I, F, P>>
	where List<H, I, F, P>: Print,
		Q: Pushable<M>
{
	type T = usize;
	fn prompt(&self, buffer: &mut Buffer) {
		self.question.push_into(buffer);
		buffer
			.push_plain(" [1 - ")
			.push_plain(&self.len)
			.push_plain("]: ");
	}
	fn validate(&self, user_input: &str) -> Option<usize> {
		user_input
			.parse()
			.ok()
			.filter(|&i| i > 0)
			.map(|i: usize| i - 1)
			.filter(|i| (0..self.len).contains(i))
	}
	fn print<C: Conciliator + ?Sized>(&mut self, con: &C) {
		if let Some(list) = self.list.take() {
			list.print(con);
		}
	}
}