conciliator 0.3.10

[WIP] Library for interactive CLI programs
Documentation
/*! WIP library for writing interactive CLI programs.

Currently very unstable with a somewhat unusual API.
Subject to major changes as I use it and implement missing functionality.
Intended to implement the things that the Python `click` library provides which are missing from `clap`.
Using that basis, provide useful abstractions for interacting with the CLI.


Features:
- Pretty [`status`](Conciliator::status), [`info`](Conciliator::info), [`warn`](Conciliator::warn) and [`error`](Conciliator::error) messages
- Convenient [traits](Inline) and [methods](Paint) for printing color escape codes [when available](init)
- [Print](crate::print) [`List`]s and [`Tree`]s
- Simplify dealing with [terminal colors](term) using color [`Palette`]s
- Request and validate user [input]
- Launch `$EDITOR` to [edit] plain text and typed data
- [Animated spinners](spin) (using `tokio` for `async` projects and [`std::thread`] otherwise)


Out of scope:
- Non-UNIX platforms
- Config file handling
- Argument parsing (intended to be used alongside `clap`)
- Progress bars (for now)
- `curses`-style *text user interface* (TUI) painting to the terminal as a canvas


Initially, this library was written as an extension of [termcolor](https://crates.io/crates/termcolor), but now the functionality that was required has been absorbed / copied / rewritten.
As such, some of the ideas and design are taken from there and credit is due.


## Capture output during `cargo test`

As explained in [`core::Stream`], there is an issue with `cargo test` only capturing output that uses the [`print!`], [`println!`], [`eprint!`], and [`eprintln!`] macros (see [rust#12309] and [rust#90785]).  
As a workaround, this crate provides the `test_capture` feature flag which makes all output go through those macros. This is worse (see [`core::Stream`] on how), but it also makes `cargo test` properly capture the output.

Luckily, there is a way to have this feature only enabled when it is needed: simply add this crate as a [development dependency] *as well*, with the `test_capture` feature enabled **only** on that entry and **not** on the regular dependency, like so:

```toml
[dependencies]
conciliator = "…"

[dev-dependencies.conciliator]
version = "…"
features = ["test_capture"]
```

This way, the `test_capture` feature will only be enabled for compiling "tests, examples, and benchmarks" (see [cargo reference]) and not for building binaries.

[rust#12309]: https://github.com/rust-lang/rust/issues/12309
[rust#90785]: https://github.com/rust-lang/rust/issues/90785
[development dependency]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#development-dependencies
[cargo reference]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#development-dependencies

*/

#![cfg_attr(docsrs, feature(doc_auto_cfg))]


pub mod core;
pub use crate::core::{
	Buffer,
	Line,
	Stream,
	GetLine
};
mod data;
//#[macro_use]
pub mod edit;
pub mod input;
pub use input::Input;
pub mod spin;
pub use spin::Spinner;
mod push;
pub use push::{
	Inline,
	Pushable,
	Tag,
	Wrap,
	WrapBold
};
#[doc(hidden)]
pub use push::LambdaFmt;

pub mod print;
pub use print::{
	Print,
	List,
	Tree
};
pub mod style;
pub use style::{
	Paint,
	Palette,
	Color,
	Tags
};
pub mod term;

use std::ops::DerefMut;

use crate::core::InitialContent;
use crate::edit::{
	Editable,
	Edited
};
use crate::input::Confirm;
use crate::style::TAGS;

/// **`[ > ]`** Decorate user-facing messages with tags
///
/// This trait extends [`GetLine`] implementers to decorate messages with a [tag](crate::style::Tags) like **`[ > ]`**.
/// There are four different tags and they can be used to categorize output messages similar to a log level.
/// In this regard, they can be used to highlight warning and error messages visually, but there is no filtering / severity threshold (or any of the functionality which a logging library might provide): the tags are entirely decorative.
///
/// It could be argued that there is some *utility* in visually highlighting warning and error messages, but the main motivation is aesthetic -- to get a slightly more polished and intentional look than what you'd get using bare [`println!`]s.
/// It adds a little bit of "structure" to the output & makes certain messages stand out without having to resort to ALL CAPS or drawing boxes with `@@@@@@@@@@` and `=============`.
///
/// Whether it achieves that is obviously a matter of personal taste, and to some extent this really is just needless ceremony.
/// With that said, using the [`Conciliator`] over [`println!`] does encourage a certain degree of discipline around writing to stdout, which is a globally shared resource.
/// Instead of having access to it directly from anywhere in the program, a struct (i.e. the [`Claw`]) is instantiated once and then passed around explicitly.
///
/// Additionally, it offers the user an opportunity to customize the appearance of the application, as described in the [`style`] module-level docs.
pub trait Conciliator: for<'l> GetLine<'l> {
	/// Get a plain buffer without a tag
	fn line<I: InitialContent>(&self, init: I) -> <Self as GetLine>::Line {
		let mut line = self.get_line();
		init.init_buffer(line.deref_mut());
		line
	}
	/// Get a line buffer with the [status](style::Tags::status) tag
	fn status<I: InitialContent>(&self, init: I) -> <Self as GetLine>::Line {
		let mut line = self.get_line();
		line.tag(Color::Alpha, TAGS.status);
		init.init_buffer(line.deref_mut());
		line
	}
	/// Get a line buffer with the [info](style::Tags::info) tag
	fn info<I: InitialContent>(&self, init: I) -> <Self as GetLine>::Line {
		let mut line = self.get_line();
		line.tag(Color::Beta, TAGS.info);
		init.init_buffer(line.deref_mut());
		line
	}
	/// Get a line buffer with the [warn](style::Tags::warn) tag
	fn warn<I: InitialContent>(&self, init: I) -> <Self as GetLine>::Line {
		let mut line = self.get_line();
		line.tag(Color::Gamma, TAGS.warn);
		init.init_buffer(line.deref_mut());
		line
	}
	/// Get a line buffer with the [error](style::Tags::error) tag
	fn error<I: InitialContent>(&self, init: I) -> <Self as GetLine>::Line {
		let mut line = self.get_line();
		line.tag(Color::Delta, TAGS.error);
		init.init_buffer(line.deref_mut());
		line
	}
	/// [`Print`] a multi-line text segment
	fn print<T: Print>(&self, thing: T) {
		thing.print(self);
	}
}

/// The main [`Conciliator`] implementor
///
/// Offers [`Conciliator`] methods for printing to stdout and basic methods for stderr.
///
/// Constructed using [`conciliator::init`](crate::init).
pub struct Claw {
	out: Stream,
	err: Stream
}

/// Initialize the main [`Conciliator`] implementor, [`Claw`]
///
/// This is provided so that [`Claw`] doesn't need to be imported:
/// ```rust,no_run
/// use conciliator::Conciliator;
/// let con = conciliator::init();
/// con.info("Test123 :^)");
/// ```
///
/// For both the stdout & stderr [`Stream`], colors will only be enabled if:
///  - the stream is a TTY, **and**
///  - the `TERM` environment variable is set to something other than `dumb`, **and**
///  - the environment variable `NO_COLOR` is *not* set.
///
/// This is the algorithm as implemented by [termcolor](https://crates.io/crates/termcolor).
pub fn init() -> Claw {
	Claw {
		out: Stream::stdout(None),
		err: Stream::stderr(None)
	}
}

impl Claw {
	pub(crate) fn clone(&self) -> Self {
		Self {
			out: self.out.clone(),
			err: self.err.clone()
		}
	}
	/// Get a [`Line`] buffer to print to stderr instead of stdout
	pub fn stderr_line<I: InitialContent>(&self, init: I) -> Line<'_> {
		let mut line = self.err.line();
		init.init_buffer(&mut line);
		line
	}
	/// [`Print`] a multi-line text segment to stderr instead of stdout
	pub fn stderr_print<T: Print>(&self, thing: T) {
		thing.print(&self.err);
	}
	/// Ask a yes or no question using [`Confirm`]
	#[must_use]
	pub fn confirm<Q, M>(&self, default: bool, question: Q) -> bool
		where Confirm<Q, M>: Input<T = bool>
	{
		self.input(Confirm::new(default, question))
	}
	/// Request arbitrary user [`Input`]
	///
	/// ```
	/// use conciliator::Conciliator;
	/// let con = conciliator::init();
	/// let name = con.input("Please enter your name");
	/// con.status(format_args!("Hello {name}!"));
	/// ```
	pub fn input<I: Input>(&self, mut request: I) -> I::T {

		// Print the request
		request.print(self);

		let mut input_buf = String::new();

		loop {
			// prompt
			request.prompt(&mut self.line(..).no_newline());

			// read line
			std::io::stdin().read_line(&mut input_buf).unwrap();

			// validate
			if let Some(thing) = request.validate(input_buf.trim()) {
				return thing
			}
			else {input_buf.clear();}
		}

	}
	/// Ask the user to pick an element from a [`Vec`]
	///
	/// Returns `None` **only** if the [`Vec`] is empty.
	///
	/// If there is only 1 item, it is returned without asking.
	///
	/// Otherwise it will ask (and keep asking) until the user has made a selection.
	///
	/// For example:
	/// ```rust,no_run
	/// use conciliator::{Conciliator, Wrap, Paint};
	/// let con = conciliator::init();
	/// let list = vec![
	///     Wrap::Iota("62bfe952-c9b3-4d32-a2fb-eba5a9b77136"),
	///     Wrap::Iota("3ba08f2d-6b37-42d6-a12b-b22e0b5ad01f"),
	///     Wrap::Iota("0730fbdf-9703-4ffb-9186-d93e412cec63"),
	///     Wrap::Iota("36f295d9-1a94-42f4-a4f2-f2c298cf7716"),
	///     Wrap::Iota("879738e8-82e5-418f-8557-d7b95c6be06a"),
	///     Wrap::Iota("2d5f0e37-f578-4764-9ffe-9542c99ef761")
	/// ];
	/// let x = con.select(list, "UUIDs", "Select UUID").unwrap();
	/// con.info("Selected ").push(&x).push_plain("!");
	/// ```
	/// Produces:
	/// ```text
	/// [ > ] 6 UUIDs:
	///         [1] - 62bfe952-c9b3-4d32-a2fb-eba5a9b77136
	///         [2] - 3ba08f2d-6b37-42d6-a12b-b22e0b5ad01f
	///         [3] - 0730fbdf-9703-4ffb-9186-d93e412cec63
	///         [4] - 36f295d9-1a94-42f4-a4f2-f2c298cf7716
	///         [5] - 879738e8-82e5-418f-8557-d7b95c6be06a
	///         [6] - 2d5f0e37-f578-4764-9ffe-9542c99ef761
	/// Select UUID [1 - 6]: 2
	/// [ + ] Selected 3ba08f2d-6b37-42d6-a12b-b22e0b5ad01f!
	/// ```
	pub fn select<T: Inline>(
		&self,
		mut things: Vec<T>,
		description: &str,
		question: &str)
		-> Option<T>
	{
		match things.len() {
			0 => None,
			1 => things.pop(),
			_ => {
				let list = List::indexed(things.iter()).with_count(description);
				let select = input::Select::new(list, question);
				let index = self.input(select);
				Some(things.swap_remove(index))
			}
		}
	}

	/// Invoke the text editor
	///
	/// For further information, see the [`edit`] module.
	/// ```
	/// use conciliator::{Conciliator, edit::Edited};
	/// let con = conciliator::init();
	/// match con.edit("Test123 :^)") {
	///     Edited::Ok(s) => con.status(format_args!("Edited:\n{s}")),
	///     Edited::Cancelled => con.info("Edit aborted!"),
	///     Edited::Err(e) => con.error(format_args!("{e:?}"))
	/// };
	/// ```
	pub fn edit<E: Editable>(&self, to_edit: E) -> Edited<E::E> {
		edit::edit(self, to_edit.into_edit())
	}

	/// Start a [`Spinner`] with the [`CHASE`](spin::CHASE) animation
	///
	/// Takes `&mut self` to prevent accidentally messing up the [`Spinner`] output -- use its [`Conciliator`] methods instead!
	///
	/// You can choose a different animation with [`Spinner::new`].
	/// See the [`spin`] module for more information.
	/// ```
	/// use conciliator::Conciliator;
	#[cfg_attr(
		feature = "tokio",
		doc = "# #[tokio::main] async fn main() {")
	]
	/// // `mut` so that the Spinner can get an exclusive (mutable) reference
	/// let mut con = conciliator::init();
	/// con.status("Starting process");
	/// let sp = con.spin("Downloading thing...");
	/// // < download the thing >
	/// sp.status("Download complete");
	/// sp.message("Unpacking thing...");
	/// // < unpack the thing >
	/// sp.status("Thing unpacked");
	/// sp.message("Process finished");
	/// sp.finish();
	/// // you can only use the Claw after the Spinner is finished
	/// con.info("Doing other thing");
	#[cfg_attr(feature = "tokio", doc = "# }")]
	/// ```
	#[must_use = "Spinner spins until dropped"]
	pub fn spin<I: InitialContent>(&mut self, message: I) -> Spinner {
		Spinner::new(self, spin::CHASE, message)
	}

	/// Run a [`Spinner`] with the [`CHASE`](spin::CHASE) animation while waiting for a future to complete
	///
	/// Needs `&mut self` to prevent accidentally messing up the [`Spinner`] output.
	///
	/// See the [`spin`] module for more information.
	/// ```
	/// use conciliator::Conciliator;
	/// async fn download_thing() { /* ... */ }
	///
	/// # #[tokio::main] async fn main() {
	/// // `mut` so that the Spinner can get an exclusive (mutable) reference
	/// let mut con = conciliator::init();
	/// con.spin_while("Downloading thing...", download_thing()).await;
	/// # }
	/// ```
	#[cfg(feature = "tokio")]
	pub async fn spin_while<I, F, T>(&mut self, message: I, future: F) -> T
		where I: InitialContent,
			F: std::future::Future<Output = T>
	{
		let sp = Spinner::new(self, spin::CHASE, message);
		let thing = future.await;
		sp.clear().await;
		thing
	}
}

impl<'l> GetLine<'l> for Claw {
	type Line = Line<'l>;
	fn get_line(&'l self) -> Self::Line {
		self.out.line()
	}
}
impl Conciliator for Claw {}