conciliator 0.3.10

[WIP] Library for interactive CLI programs
Documentation
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::{
	AtomicBool,
	Ordering
};
use std::time::Duration;

use ansi_escapes::{
	CursorHide,
	CursorShow,
	CursorBackward,
	EraseLine,
	EraseStartLine,
	EraseEndLine
};

use crate::{
	Buffer,
	Claw,
	Paint,
	Color
};
use crate::data::spin;

/// 3-wide Braille character animation, two opposing groups of dots looping
pub const CHASE: Animation = Animation::new(90, &spin::CHASE);

/// Narrow spinner animation using Braille characters
///
/// The animation for this was taken from <https://github.com/FGRibreau/spinners>
pub const DOT: Animation = Animation::new(60, &spin::DOTS);

/// Spinner animation using Braille characters, wider version of [`DOT`]
pub const LOOP: Animation = Animation::new(90, &spin::LOOP);
/// Same as [`LOOP`], but even wider, Braille dots go all the way to the edge
pub const LOOP_WIDE: Animation = Animation::new(90, &spin::LOOP_WIDE);

/// Narrow spinner animation using half-filled square characters
pub const SQUARE: Animation = Animation::new(60, &spin::SQUARE);

/// Narrow spinner animation using triangle characters
///
/// The animation for this was taken from <https://github.com/FGRibreau/spinners>
pub const TRIANGLE: Animation = Animation::new(45, &spin::TRIANGLE);


/// Define a custom animation
///
/// This uses a separate mutable `State` to allow defining animations as constants.
///
/// Make sure that every "frame" or step of the animation has the same width!  
/// Otherwise, the animation will overlap with the message or previous frames will not be overwritten completely.
/// Note that the width is not strictly just the amount of characters, because the color codes and such are also emitted as characters -- because the terminal doesn't print these, you don't have to make sure you emit the same amount of color codes in every frame.
/// Just make sure the amount of characters that actually get printed stays the same.
pub trait Animate {
	/// Mutable state passed to the [`step`](Self::step) function
	///
	/// For [`Animation`] this is just a [`usize`] index into the array of frames.
	type State: Default + Send;
	/// Write the next frame of the animation into an empty [`Buffer`]
	///
	/// This method gets called once when the [`Spinner`](super::Spinner) is created, and then again and again after each returned [`Duration`] has elapsed, until the [`Spinner`](super::Spinner) is stopped.
	fn step(&self, state: &mut Self::State, buf: &mut Buffer) -> Duration;
	/// Write the final frame into an empty [`Buffer`]
	///
	/// Because this is the last frame, written as the [`Spinner`](super::Spinner) is stopped, it won't get overwritten later and will stick around in the terminal scrollback buffer.
	///
	/// This "frame" does *not* need to be the same width as the other frames.
	fn finish(&self, state: Self::State, buf: &mut Buffer);
}

/// A simple animation that cycles through a series of frames
#[derive(Clone, Copy)]
pub struct Animation {
	length: usize,
	time_per_frame: Duration,
	frames: &'static [&'static str]
}

pub(super) struct Animator<A: Animate> {
	claw: Claw,
	/// `\r[`
	open: Buffer,
	/// Animation provided by the [`Stepper`]
	anim: Buffer,
	/// `] `
	close: Buffer,
	/// `{EraseLine}\r`
	erase: Buffer,
	/// `{CursorHide}` at the start and `{CursorShow}\n` at the end
	ctrl_buf: Buffer,
	/// holds the animation & state
	stepper: Option<Stepper<A>>,
	cleared: Arc<AtomicBool>
}

/// Helper struct for the [`Animator`]
struct Stepper<A: Animate> {
	animation: A,
	state: A::State
}

/*
 *	ANIMATION
 */

impl Animation {
	/// Make sure that all frames have the same length (width)
	pub const fn new(ms_per_frame: u64, data: &'static [&'static str]) -> Self {
		Self {
			length: data.len(),
			time_per_frame: Duration::from_millis(ms_per_frame),
			frames: data
		}
	}
}

impl Animate for Animation {
	/// Index into the array of frames
	type State = usize;
	/// Writes the frame into the [`Buffer`] with [`Color::Alpha`], increments the index and returns the (constant) frame time.
	fn step(&self, state: &mut Self::State, buf: &mut Buffer) -> Duration {
		buf.push_with_color(Color::Alpha, self.frames[*state]);
		*state += 1;
		*state %= self.length;
		self.time_per_frame
	}
	/// Writes a `✔` checkmark character into the [`Buffer`] with [`Color::Alpha`]
	fn finish(&self, _: Self::State, buf: &mut Buffer) {
		buf.push_with_color(Color::Alpha, " ✔ ");
	}
}


/*
 *	ANIMATOR
 */

impl<A: Animate> Animator<A> {
	pub fn new(claw: Claw, animation: A, cleared: Arc<AtomicBool>) -> Self {
		let mut open = claw.err.buffer();
		open
			.push(CursorHide) //TODO: could optimize by tracking input state
			.push("\r")
			.tag_open();

		let mut close = claw.err.buffer();
		close.tag_close();

		let mut erase = claw.err.buffer();
		write!(erase, "{EraseLine}\r").unwrap();

		let mut ctrl_buf = claw.err.buffer();
		write!(ctrl_buf, "{CursorHide}").unwrap();

		let stepper = Some(Stepper::new(animation));
		let anim = claw.err.buffer();

		Self {
			claw,
			open,
			anim,
			close,
			erase,
			ctrl_buf,
			stepper,
			cleared
		}
	}
	pub fn init(&mut self, msg_buf: &Buffer) -> Duration {
		self.claw.err.print_buffer(&self.ctrl_buf).unwrap();
		self.ctrl_buf.clear();
		write!(self.ctrl_buf, "{EraseEndLine}{CursorShow}").unwrap();
		let next_frame_in = self.stepper
			.as_mut()
			.unwrap()
			.step(&mut self.anim);
		self.write_message_line(msg_buf, None);
		next_frame_in
	}

	pub fn step(&mut self, msg: &Buffer, input: Option<&[u8]>) -> Duration {
		self.anim.clear();
		let next_frame_in = self.stepper
			.as_mut()
			.unwrap()
			.step(&mut self.anim);
		self.write_message_line(msg, input);
		next_frame_in
	}

	pub fn clear_line(&self) {
		self.claw.err.print_buffer(&self.erase).unwrap();
	}

	pub fn print_buffer(&self, buf: Buffer) {
		self.claw.out.print_buffer(&buf).unwrap();
	}

	pub fn write_message_line(&self, msg: &Buffer, input: Option<&[u8]>) {
		// print tag & msg buffers
		self.claw.err.print_buffer(&self.open).unwrap();
		self.claw.err.print_buffer(&self.anim).unwrap();
		self.claw.err.print_buffer(&self.close).unwrap();
		self.claw.err.print_buffer(msg).unwrap();
		if let Some(input) = input {
			self.claw.err.print_bytes(input).unwrap();
			self.claw.err.print_buffer(&self.ctrl_buf).unwrap();
		}
		self.claw.err.flush().unwrap();
	}

	pub fn accept_input(&self, msg: &Buffer, final_bytes: &[u8]) {
		if !final_bytes.is_empty() {
			let mut buf = self.claw.err.buffer();
			buf.write_all(final_bytes).unwrap();
			self.claw.err.print_buffer(&buf).unwrap();
		}
		self.claw.err.print_buffer(&self.open).unwrap();
		self.claw.err.print_buffer(&self.anim).unwrap();
		self.claw.err.print_buffer(&self.close).unwrap();
		self.claw.err.line().push(CursorBackward(1)).push(EraseStartLine);
		self.write_message_line(msg, None);
	}

	pub fn cleanup(mut self, msg_buf: &Buffer) {
		self.clear_line();
		self.anim.clear();
		self.stepper
			.take()
			.unwrap()
			.finish(&mut self.anim);

		//self.write_message_line();
		self.claw.err.print_buffer(&self.open).unwrap();
		self.claw.err.print_buffer(&self.anim).unwrap();
		self.claw.err.print_buffer(&self.close).unwrap();
		self.claw.err.print_buffer(msg_buf).unwrap();

		self.ctrl_buf.clear();
		// note the newline
		writeln!(self.ctrl_buf, "{CursorShow}").unwrap();
		self.claw.err.print_buffer_and_flush(&self.ctrl_buf).unwrap();

		self.cleared.store(true, Ordering::Release);
	}
}

/// If the Animator is dropped without running cleanup(), then atleast the cursor should get unhidden
/// This might happen because of a panic or if the task gets aborted.
impl<A: Animate> Drop for Animator<A> {
	fn drop(&mut self) {
		if !self.cleared.swap(true, Ordering::Acquire) {
			//self.clear_line();
			self.ctrl_buf.clear();
			write!(self.ctrl_buf, "{CursorShow}{EraseLine}\r").unwrap();
			self.claw.err.print_buffer_and_flush(&self.ctrl_buf).unwrap();
		}
	}
}
#[cfg(feature = "tokio")]
impl Claw {
	pub(super) fn clear_spinner(&self) {
		let mut clear = self.err.buffer();
		write!(clear, "{CursorShow}{EraseLine}\r").unwrap();
		self.err.print_buffer_and_flush(&clear).unwrap();
	}
}


/*
 *	STEPPER
 */

impl<A: Animate> Stepper<A> {
	fn new(animation: A) -> Self {Self {animation, state: Default::default()}}
	fn step(&mut self, buffer: &mut Buffer) -> Duration {
		self.animation.step(&mut self.state, buffer)
	}
	fn finish(self, buffer: &mut Buffer) {
		self.animation.finish(self.state, buffer);
	}
}