flux-tui 0.5.0

Fast and lightweight Terminal UI drawing library
Documentation
use crate::common::*;
use crate::window::{Window, WindowFactory, WindowRef};
use crate::{framebuffer::*, window::handler::WindowHandler};
use crossterm::*;
use std::backtrace::{Backtrace, BacktraceStatus};
use std::io::{self, BufWriter, Stdout, Write, stdout};
use std::time::Duration;
use vector2d::Vector2D;


/// Main struct handling terminal rendering and events
pub struct Tui {
	stdout: BufWriter<Stdout>,
	framebuffer: Framebuffer,
	window_handler: WindowHandler,
}

impl Tui {
	/// Creates an instance of [Tui] which handles all windows/events
	/// The generic parameter corresponds to the main window which will be created by calling their default constructor
	/// This also sets a custom panic hook
	pub fn new(main_window: WindowRef) -> Result<Self, io::Error> {
		let mut out = BufWriter::new(stdout());
		crossterm::terminal::enable_raw_mode()?;
		out.execute(cursor::Hide)?;
		out.execute(terminal::EnterAlternateScreen)?;
		std::panic::set_hook(Box::new(|e| {
			let mut out = stdout();
			Self::reset_term(&mut out);
			eprintln!("{e}");
			let bt = Backtrace::capture();
			if bt.status() == BacktraceStatus::Captured {
				eprintln!("{bt}");
			}
		}));

		let size = terminal::window_size().map(|s| Vector2D::new(s.columns, s.rows))?;
		let window_handler = WindowHandler::new(main_window, size);
		Ok(Self {
			stdout: out,
			framebuffer: Framebuffer::new(size),
			window_handler,
		})
	}

	/// Same as [Tui::new] but with the default initialization of `W`
	pub fn new_default<W: Window + Default>() -> Result<Self, io::Error> {
		Self::new(WindowFactory::create::<W>())
	}

	/// Handles all incoming Terminal events and delegates them to the corresponding window(s)
	pub fn handle_events(&mut self) -> Result<(), io::Error> {
		while event::poll(Duration::from_micros(16))? {
			self.delegate_event(event::read()?);
		}
		Ok(())
	}

	/// Handles only the next incoming event and blocks until done
	/// Otherwise it works like Self::handle_events
	pub fn handle_next_event(&mut self) -> Result<(), io::Error> {
		self.delegate_event(event::read()?);
		Ok(())
	}

	/// Tries to poll for an event, if successfull handles it
	/// Otherwise it just returns Ok(false)
	/// Returns Err(io::Error) when an IO-Error occurs
	pub fn handle_next_event_poll(&mut self, timeout: Duration) -> Result<bool, io::Error> {
		if event::poll(timeout)? {
			self.delegate_event(event::read()?);
			Ok(true)
		}
		else {
			Ok(false)
		}
	}

	/// Renders all content from the shown window and their children to the framebuffer which is then drawn to the terminal
	pub fn render(&mut self) -> Result<(), io::Error> {
		self.window_handler.render(&mut self.framebuffer);
		let mut iter = self.framebuffer.get_line_iterator();
		while let Some(next) = iter.next() {
			if let LineContent::Line { index, glyphs } = next {
				self.stdout.queue(cursor::MoveTo(0, index))?;
				let (mut col_fg, mut col_bg) = (None, None);
				for visual in glyphs.iter().filter(|v| !v.is_null()) {
					if visual.style == Style::None {
						Self::draw_visual(&mut self.stdout, visual, &mut col_fg, &mut col_bg)?;
					}
					else {
						Style::apply_pre_styles(&mut self.stdout, visual.style)?;
						Self::draw_visual(&mut self.stdout, visual, &mut col_fg, &mut col_bg)?;
						Style::apply_post_styles(&mut self.stdout, visual.style)?;
					}
				}
			}
		}
		self.stdout.flush()?;

		Ok(())
	}

	fn delegate_event(&mut self, event: Event) {
		match event {
			Event::Resize(width, height) => {
				let size = Vector2D::new(width, height);
				self.window_handler.broadcast_resize(size);
				self.framebuffer.resize(size);
			}
			e => self.window_handler.handle_event(e),
		}
	}

	#[inline(always)]
	fn draw_visual(
		stdout: &mut BufWriter<Stdout>,
		visual: &Glyph,
		cur_fg: &mut Option<Color>,
		cur_bg: &mut Option<Color>,
	) -> Result<(), io::Error> {
		if visual.fg != *cur_fg {
			stdout.queue(style::SetForegroundColor(Color::Reset))?;
			if let Some(fg) = visual.fg {
				stdout.queue(style::SetForegroundColor(fg))?;
			}
			*cur_fg = visual.fg;
		}
		if visual.bg != *cur_bg {
			stdout.queue(style::SetBackgroundColor(Color::Reset))?;
			if let Some(bg) = visual.bg {
				stdout.queue(style::SetBackgroundColor(bg))?;
			}
			*cur_bg = visual.bg;
		}
		stdout.queue(style::Print(&visual.grapheme))?;
		Ok(())
	}

	fn reset_term(stdout: &mut Stdout) {
		stdout.execute(terminal::LeaveAlternateScreen).ok();
		stdout.execute(cursor::Show).ok();
	}
}

impl Drop for Tui {
	fn drop(&mut self) {
		Self::reset_term(self.stdout.get_mut());
	}
}