rm-lisa 0.3.2

A logging library for rem-verse, with support for inputs, tasks, and more.
Documentation
//! All the logic for the 'render state' of super console.
//!
//! This type is wrapped in a mutex, and is where most of the actual calls to print happen.

use crate::{
	display::{
		SuperConsole,
		renderers::{ConsoleOutputFeatures, ConsoleRenderer},
		tracing::SuperConsoleLogMessage,
	},
	errors::LisaError,
	input::{InputProvider, TerminalInputEvent},
	tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent, TaskEventLogProvider},
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use std::{env::var as env_var, hash::BuildHasherDefault, io::Write as IoWrite};
use tokio::{
	runtime::Handle,
	sync::mpsc::{Receiver as BoundedReceiver, Sender as BoundedSender},
};
use tracing::warn;

/// A task that is being tracked, is three components:
///
/// 1. The datetime the task started according to super console.
/// 2. The name of the task.
/// 3. The current status of the task.
type TrackedTask = (DateTime<Utc>, String, LisaTaskStatus);

/// The underlying renderer stat that is mutable, and is locked to get acted upon.
pub struct UnlockedRendererState<
	StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
	StderrTy: ConsoleOutputFeatures + IoWrite + Send,
> {
	/// The underlying application name.
	app_name: &'static str,
	/// Any log events waiting to be rendered.
	cached_log_events: Vec<SuperConsoleLogMessage>,
	/// A channel to send all completed inputs on.
	completed_input_senders: Option<BoundedSender<String>>,
	/// If the app is configured to force a specific terminal height.
	force_term_height: Option<u16>,
	/// If the app is configured to force a specific terminal width.
	force_term_width: Option<u16>,
	/// If we've warned that our terminal size is a bit too small.
	has_warned_about_terminal_size: bool,
	/// The currently active input provider.
	input_provider: Option<Box<dyn InputProvider>>,
	/// Any log messages that are pending to be flushed.
	log_messages: BoundedReceiver<SuperConsoleLogMessage>,
	/// The STDERR output stream.
	stderr: StderrTy,
	/// The renderer capable of rendering messages on STDERR.
	stderr_renderer: Box<dyn ConsoleRenderer>,
	/// The STDOUT ouptut stream.
	stdout: StdoutTy,
	/// The renderer capable of rendering messages on STDOUT.
	stdout_renderer: Box<dyn ConsoleRenderer>,
	/// The task provider that will provide us updates.
	task_provider: Option<Box<dyn TaskEventLogProvider>>,
	/// The statuses of any tasks that are actively running.
	task_statuses: FnvHashMap<GloballyUniqueTaskId, TrackedTask>,
	/// When the tasks have started running since.
	tasks_running_since: Option<DateTime<Utc>>,
}

impl<
	StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
	StderrTy: ConsoleOutputFeatures + IoWrite + Send,
> UnlockedRendererState<StdoutTy, StderrTy>
{
	/// Create a new rendering state.
	#[must_use]
	pub fn new(
		app_name: &'static str,
		environment_prefix: &str,
		(stdout, stderr): (StdoutTy, StderrTy),
		(stdout_renderer, stderr_renderer): (Box<dyn ConsoleRenderer>, Box<dyn ConsoleRenderer>),
		log_messages: BoundedReceiver<SuperConsoleLogMessage>,
	) -> Self {
		Self {
			app_name,
			cached_log_events: Vec::with_capacity(0),
			completed_input_senders: None,
			force_term_height: env_var(format!("{environment_prefix}_FORCE_TERM_HEIGHT"))
				.ok()
				.and_then(|item| item.parse::<u16>().ok()),
			force_term_width: env_var(format!("{environment_prefix}_FORCE_TERM_WIDTH"))
				.ok()
				.and_then(|item| item.parse::<u16>().ok()),
			has_warned_about_terminal_size: false,
			input_provider: None,
			log_messages,
			stderr,
			stderr_renderer,
			stdout,
			stdout_renderer,
			task_provider: None,
			task_statuses: FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default()),
			tasks_running_since: None,
		}
	}

	/// Get the current input provider.
	#[must_use]
	pub fn get_input_provider(&mut self) -> &mut Option<Box<dyn InputProvider>> {
		&mut self.input_provider
	}

	pub fn set_input_provider(&mut self, iprovider: Box<dyn InputProvider>) {
		self.input_provider = Some(iprovider);
	}

	pub fn set_task_provider(&mut self, tprovider: Box<dyn TaskEventLogProvider>) {
		self.task_provider = Some(tprovider);
	}

	pub fn set_completed_input_channel(&mut self, channel: BoundedSender<String>) {
		self.completed_input_senders = Some(channel);
	}

	pub fn get_unprocessed_inputs(&mut self) -> Vec<String> {
		if let Some(iprovider) = self.input_provider.as_mut() {
			iprovider.inputs()
		} else {
			Vec::with_capacity(0)
		}
	}

	/// Try to mark our input as active.
	///
	/// ## Errors
	///
	/// If the input provider fails being marked as active.
	pub fn set_input_active(&mut self, active: bool) -> Result<(), LisaError> {
		if let Some(prov) = self.input_provider.as_mut() {
			prov.set_active(active)?;
		}

		Ok(())
	}

	/// Render if any new events or log messages have occured.
	///
	/// ## Errors
	///
	/// If we cannot write to our stderr channel, or cannot render a message.
	pub fn render_if_needed(&mut self) -> Result<(), LisaError> {
		let mut pause_log_events = false;
		if let Some(iprovider) = self.input_provider.as_ref()
			&& iprovider.is_active()
			&& iprovider.is_stdin()
			&& self.stderr_renderer.should_pause_log_events(&**iprovider)
		{
			pause_log_events = true;
		}

		let mut new_logs = self.cached_log_events.drain(..).collect::<Vec<_>>();
		while let Ok(value) = self.log_messages.try_recv() {
			new_logs.push(value);
		}
		let mut input_events = Vec::with_capacity(0);
		if let Some(iprovider) = self.input_provider.as_mut()
			&& iprovider.is_active()
			&& iprovider.is_stdin()
		{
			input_events = iprovider.poll_for_input(self.stderr_renderer.supports_ansi());
		}
		let mut task_events = Vec::with_capacity(0);
		if let Some(tprovider) = self.task_provider.as_ref() {
			task_events = tprovider.new_events();
		}

		if new_logs.is_empty() && input_events.is_empty() && task_events.is_empty() {
			return Ok(());
		}
		if pause_log_events && input_events.is_empty() {
			return Ok(());
		}
		self.render(pause_log_events, new_logs, input_events, &task_events)
	}

	/// Actually do a render.
	fn render(
		&mut self,
		pause_logs: bool,
		new_logs: Vec<SuperConsoleLogMessage>,
		new_input_events: Vec<TerminalInputEvent>,
		new_task_events: &[TaskEvent],
	) -> Result<(), LisaError> {
		let mut term_width = self
			.force_term_width
			.or_else(SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_width)
			.unwrap_or(40_u16);
		if term_width < 40 {
			if !self.has_warned_about_terminal_size {
				warn!(actual_terminal_width = %term_width, "Your terminal is a very small size! Please bump it up to at least 40 characters for the best experience!");
				self.has_warned_about_terminal_size = true;
			}
			term_width = 40;
		}

		let had_input_events = !new_input_events.is_empty();
		if let Some(iprovider) = self.input_provider.as_mut() {
			for ievent in new_input_events {
				let rendered = self.stderr_renderer.on_input(ievent, &**iprovider)?;
				if !rendered.is_empty() {
					self.stderr.write_all(rendered.as_bytes())?;
				}
			}

			if let Some(channel) = self.completed_input_senders.as_ref() {
				for input in iprovider.inputs() {
					// Are we in a tokio runtime? if so we need to block in place.
					if let Ok(h) = Handle::try_current() {
						tokio::task::block_in_place(move || {
							h.block_on(async {
								_ = channel.send(input).await;
							});
						});
					} else {
						_ = channel.blocking_send(input);
					}
				}
			}
		}
		// If this was just an input event we don't need to do a full clear...
		if had_input_events && (new_logs.is_empty() && new_task_events.is_empty()) {
			return Ok(());
		}

		self.clear_input(term_width)?;
		self.clear_tasks()?;

		if pause_logs {
			self.cached_log_events = new_logs;
		} else {
			for log_line in new_logs {
				if log_line.towards_stdout() {
					let rendered =
						self.stdout_renderer
							.render_message(self.app_name, log_line, term_width)?;

					self.stdout.write_all(rendered.as_bytes())?;
				} else {
					let rendered =
						self.stderr_renderer
							.render_message(self.app_name, log_line, term_width)?;

					if !rendered.is_empty() {
						self.stderr.write_all(rendered.as_bytes())?;
					}
				}
			}
		}

		self.render_tasks(new_task_events)?;
		self.render_input(term_width)?;

		Ok(())
	}

	/// Clear the input that was previously rendered.
	///
	/// ## Errors
	///
	/// If we cannot write the clear input lines to STDERR.
	fn clear_input(&mut self, term_width: u16) -> Result<(), LisaError> {
		if let Some(iprovider) = self.input_provider.as_ref()
			&& iprovider.is_stdin()
			&& iprovider.is_active()
		{
			let clear_line = self.stderr_renderer.clear_input(term_width);
			if !clear_line.is_empty() {
				self.stderr.write_all(clear_line.as_bytes())?;
			}
		}

		Ok(())
	}

	/// Clear all the tasks that were previously rendered.
	///
	/// ## Errors
	///
	/// If we cannot write the clear tasks lines to STDERR.
	fn clear_tasks(&mut self) -> Result<(), LisaError> {
		if self.task_provider.is_some() {
			let clear_line = self
				.stderr_renderer
				.clear_task_list(self.task_statuses.len());
			if !clear_line.is_empty() {
				self.stderr.write_all(clear_line.as_bytes())?;
			}
		}

		Ok(())
	}

	/// Render all the tasks and process any new task events.
	fn render_tasks(&mut self, new_task_events: &[TaskEvent]) -> Result<(), LisaError> {
		let my_date = Utc::now();
		let mut was_empty = self.task_statuses.is_empty();

		for task_event in new_task_events {
			match task_event {
				TaskEvent::TaskStart(thread_id, lisa_task_id, name, status) => {
					if was_empty {
						_ = self.tasks_running_since.insert(my_date);
						was_empty = false;
					}
					self.task_statuses.insert(
						(*thread_id, *lisa_task_id),
						(my_date, name.clone(), status.clone()),
					);
				}
				TaskEvent::TaskStatusUpdate(thread_id, lisa_task_id, new_status) => {
					if let Some(mutable_data) =
						self.task_statuses.get_mut(&(*thread_id, *lisa_task_id))
					{
						mutable_data.2 = new_status.clone();
					}
				}
				TaskEvent::TaskEnd(thread_id, lisa_task_id) => {
					self.task_statuses.remove(&(*thread_id, *lisa_task_id));
				}
			}
		}

		if !was_empty && self.task_statuses.is_empty() {
			self.tasks_running_since = None;
		}

		let rendered = self.stderr_renderer.rerender_tasks(
			new_task_events,
			&self.task_statuses,
			self.tasks_running_since,
			self.force_term_height
				.and_then(|_| SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_height())
				.unwrap_or_default(),
		)?;

		if !rendered.is_empty() {
			self.stderr.write_all(rendered.as_bytes())?;
		}

		Ok(())
	}

	fn render_input(&mut self, normalized_term_width: u16) -> Result<(), LisaError> {
		if let Some(iprovider) = self.input_provider.as_ref()
			&& iprovider.is_stdin()
			&& iprovider.is_active()
		{
			let input_renderer = self.stderr_renderer.render_input(
				self.app_name,
				&**iprovider,
				normalized_term_width,
			)?;
			if !input_renderer.is_empty() {
				self.stderr.write_all(input_renderer.as_bytes())?;
			}
		}

		Ok(())
	}
}