fatui 0.1.0

a silly little framework for terminal user interfaces
Documentation
//! the bridge to the actual os input/output
//!
//! backends are the bridge between the ui framework and the system.
//! through the `Backend` trait and associated types,
//! they offer a consistent view

use std::io::{self, IsTerminal, stdout};

use crate::{Event, Frame, pos::Size};

#[cfg(feature = "backend-ansi")]
pub mod ansi;

/// renders character grids and provides inputs
///
/// backends are largely responsible for tracking their own state,
/// e.g. turning raw keypresses into a keyboard state.
///
/// backends are encouraged to implement a few optimizations:
/// - detecting when there are multiple pending inputs
///   and returning a [`Frame::empty`] for all but the last
/// - perform the actual rendering in a background thread to avoid blocking inputs
pub trait Backend {
	/// render the old `Frame`, then return the new one
	fn step(&mut self) -> io::Result<Frame<'_, Event>>;
	/// set the title of the window
	fn title(&mut self, title: &str) -> io::Result<()>;
}

/// get the "right" backend, based on
/// the enabled features and runtime environment
///
/// more specifically,
/// this tries to open a terminal backend first,
/// if the process is connected to a terminal.
/// otherwise, it tries to start a graphical backend.
/// if they all fail, this returns **the last error**.
///
/// if no runtimes are configured, this will instead panic, since that's a logic error.
///
/// which exact ones are picked are deliberately not specified --
/// if you need that level of control,
/// just instantiate the ones you want directly.
pub fn open() -> io::Result<Box<dyn Backend>> {
	#[allow(unused_assignments)]
	let mut error = None;
	macro_rules! attempt {
		($($feat:literal),* => $ex:expr) => {
			#[cfg(all($(feature = $feat)*))]
			{
				match $ex {
					Ok(b) => return Ok(Box::new(b)),
					Err(e) => error = Some(e),
				}
			}
		}
	}
	attempt!("backend-ansi" => if stdout().is_terminal() {
		ansi::Term::excl()
	} else {
		Err(io::Error::other("won't default to ansi terminal in non-TTY"))
	});

	match error {
		None => unreachable!("attempted to `open` with no configured backends"),
		Some(e) => Err(e),
	}
}

/// if you ever need a no-op [`Backend`], just use `()`:
/// - it will always instantly return [`Event::Close`]
/// -
impl Backend for () {
	fn step(&mut self) -> io::Result<Frame<'_, Event>> {
		Ok(Frame::null(Event::Close, Size::ZERO))
	}

	fn title(&mut self, _: &str) -> io::Result<()> {
		Ok(())
	}
}