rm-lisa 0.3.2

A logging library for rem-verse, with support for inputs, tasks, and more.
Documentation
//! A plaintext simple line based feed ideal for braille displays, and log
//! files.

use crate::{
	display::{
		renderers::{ConsoleOutputFeatures, ConsoleRenderer, get_ansi_escape_code_regex},
		tracing::SuperConsoleLogMessage,
	},
	errors::LisaError,
	input::{InputProvider, TerminalInputEvent},
	tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
};
use chrono::prelude::*;
use fnv::FnvHashMap;
use parking_lot::RwLock;
use regex::Regex;
use std::{
	borrow::Cow,
	env::var as env_var,
	fmt::Write,
	sync::atomic::{AtomicBool, Ordering},
};

/// A simple text-based console renderer.
#[derive(Debug)]
pub struct TextConsoleRenderer {
	ansi_escapes: Regex,
	force_pause: AtomicBool,
	ps1: RwLock<String>,
}

impl TextConsoleRenderer {
	/// Create a new simple text line-based console rendererr.
	#[must_use]
	pub fn new() -> Self {
		Self {
			ansi_escapes: get_ansi_escape_code_regex(),
			force_pause: AtomicBool::new(false),
			ps1: RwLock::new("> ".to_owned()),
		}
	}
}

impl Default for TextConsoleRenderer {
	fn default() -> Self {
		Self::new()
	}
}

impl ConsoleRenderer for TextConsoleRenderer {
	fn should_use_renderer(
		&self,
		_features: &dyn ConsoleOutputFeatures,
		environment_prefix: &str,
	) -> bool {
		// If someone has explicitly specificed a log format, ignore all else.
		if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
			return explicit_renderer.trim().eq_ignore_ascii_case("text");
		}

		// Now check for `NO_COLOR`/`NOCOLOR`/`CLICOLOR`
		for no_color_var in ["NO_COLOR", "NOCOLOR"] {
			if env_var(no_color_var).as_deref() == Ok("1") {
				return true;
			}
		}
		for color_var in ["CLICOLOR", "CLI_COLOR", "CLICOLOR_FORCE"] {
			let env = env_var(color_var);
			if env.as_deref() == Ok("0") {
				return true;
			}
			if env.as_deref() == Ok("1") {
				return false;
			}
		}

		// Wasn't explicitly blocked, so okay to load...
		// This makes it the default for things that aren't a tty.
		true
	}

	fn render_message(
		&self,
		app_name: &'static str,
		log: SuperConsoleLogMessage,
		_term_width: u16,
	) -> Result<String, LisaError> {
		let mut line = String::new();

		if log.should_decorate() {
			write!(
				&mut line,
				"{}/{}|",
				log.subsytem().unwrap_or(app_name),
				log.level(),
			)?;
		}

		if let Some(msg) = log.message() {
			line += &msg.replace('\n', "  ").replace('\r', "");
		} else {
			line += "<no message>";
		}
		if log.should_decorate() {
			write!(&mut line, "|")?;
		}
		if !log.metadata().is_empty() && !log.should_hide_fields_for_humans() {
			let mut has_written = false;
			for (key, value) in log.metadata() {
				if has_written {
					line.push(',');
				}
				write!(&mut line, "{key}={value}")?;
				has_written = true;
			}
		}
		write!(
			&mut line,
			"|{:04}/{:02}/{:02} {:02}:{:02}:{:02}.{:04}",
			log.at().year(),
			log.at().month0(),
			log.at().day0(),
			log.at().hour(),
			log.at().minute(),
			log.at().second(),
			log.at().timestamp_subsec_millis(),
		)?;
		writeln!(&mut line)?;

		Ok(match self.ansi_escapes.replace_all(&line, "") {
			Cow::Borrowed(_) => line,
			Cow::Owned(owned) => owned,
		})
	}

	fn default_ps1(&self) -> String {
		"> ".to_owned()
	}

	fn update_ps1(&self, new_ps1: String) {
		let mut guarded = self.ps1.write();
		*guarded = new_ps1;
	}

	fn supports_ansi(&self) -> bool {
		false
	}

	/// We actually pause rendering so we don't need to do any 'clear'-ing
	fn clear_input(&self, _term_width: u16) -> String {
		String::with_capacity(0)
	}

	/// Render a dynamic input line.
	///
	/// We actually don't render a dynamic input, as that'd require erasing
	/// things, and that can't be done in a TEXT mode, or supported in a braille
	/// display.
	///
	/// We actually only render when [`Self::on_input`] is called. As inputs are paused
	/// anyway clear/render is never called.
	///
	/// ## Errors
	///
	/// This function will never error.
	fn render_input(
		&self,
		_app_name: &'static str,
		_provider: &dyn InputProvider,
		_term_width: u16,
	) -> Result<String, LisaError> {
		Ok(String::with_capacity(0))
	}

	/// Clear the task list, we don't have any task list.
	fn clear_task_list(&self, _task_list_size: usize) -> String {
		String::with_capacity(0)
	}

	/// Re-render all the tasks as new events come in.
	///
	/// For this will just render any new events that come in between the last
	/// time we called it.
	fn rerender_tasks(
		&self,
		new_task_events: &[TaskEvent],
		_current_task_states: &FnvHashMap<
			GloballyUniqueTaskId,
			(DateTime<Utc>, String, LisaTaskStatus),
		>,
		_running_since: Option<DateTime<Utc>>,
		_term_height: u16,
	) -> Result<String, LisaError> {
		let mut result = String::new();

		for event in new_task_events {
			match event {
				TaskEvent::TaskStart(thread, task, name, status) => {
					write!(
						&mut result,
						"{thread}/{task}|task started with name: [{name}]|status={status}",
					)?;
				}
				TaskEvent::TaskStatusUpdate(thread, task, new_status) => {
					write!(
						&mut result,
						"{thread}/{task}|task has a new status|status={new_status}",
					)?;
				}
				TaskEvent::TaskEnd(thread, task) => {
					write!(&mut result, "{thread}/{task}|task ended")?;
				}
			}
			result.push('\n');
		}

		Ok(result)
	}

	/// Handle a user typing into the terminal.
	///
	/// This will simply render the actual input line, and characters as users
	/// type.
	///
	/// ## Errors
	///
	/// Never.
	fn on_input(
		&self,
		event: TerminalInputEvent,
		provider: &dyn InputProvider,
	) -> Result<String, LisaError> {
		match event {
			TerminalInputEvent::InputStarted => {
				let ps1_read = self.ps1.read();
				Ok(ps1_read.clone())
			}
			TerminalInputEvent::InputFinished => Ok("\n".to_owned()),
			TerminalInputEvent::InputAppend(character) => {
				let mut new = String::with_capacity(1);
				new.push(character);
				Ok(new)
			}
			TerminalInputEvent::InputMassAppend(data) => Ok(data),
			TerminalInputEvent::InputChanged(_) => {
				let ps1_read = self.ps1.read();
				let mut data = String::with_capacity(1 + ps1_read.len());
				data.push('\n');
				data.push_str(ps1_read.as_str());
				data.push_str(&provider.current_input());
				Ok(data)
			}
			TerminalInputEvent::InputCancelled => Ok("<CANCELLED>\n".to_owned()),
			TerminalInputEvent::ClearScreen => Ok(String::with_capacity(0)),
			TerminalInputEvent::CursorMoveLeft(_) | TerminalInputEvent::CursorMoveRight(_) => {
				Ok(String::with_capacity(0))
			}
			TerminalInputEvent::ToggleOutputPause => {
				self.force_pause.fetch_not(Ordering::Release);
				Ok(String::with_capacity(0))
			}
		}
	}

	fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool {
		provider.input_in_progress() || self.force_pause.load(Ordering::Acquire)
	}
}