rm_lisa/display/renderers/
text.rs

1//! A plaintext simple line based feed ideal for braille displays, and log
2//! files.
3
4use crate::{
5	display::{
6		renderers::{ConsoleOutputFeatures, ConsoleRenderer, get_ansi_escape_code_regex},
7		tracing::SuperConsoleLogMessage,
8	},
9	errors::LisaError,
10	input::{InputProvider, TerminalInputEvent},
11	tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
12};
13use chrono::prelude::*;
14use fnv::FnvHashMap;
15use parking_lot::RwLock;
16use regex::Regex;
17use std::{
18	borrow::Cow,
19	env::var as env_var,
20	fmt::Write,
21	sync::atomic::{AtomicBool, Ordering},
22};
23
24/// A simple text-based console renderer.
25#[derive(Debug)]
26pub struct TextConsoleRenderer {
27	ansi_escapes: Regex,
28	force_pause: AtomicBool,
29	ps1: RwLock<String>,
30}
31
32impl TextConsoleRenderer {
33	/// Create a new simple text line-based console rendererr.
34	#[must_use]
35	pub fn new() -> Self {
36		Self {
37			ansi_escapes: get_ansi_escape_code_regex(),
38			force_pause: AtomicBool::new(false),
39			ps1: RwLock::new("> ".to_owned()),
40		}
41	}
42}
43
44impl Default for TextConsoleRenderer {
45	fn default() -> Self {
46		Self::new()
47	}
48}
49
50impl ConsoleRenderer for TextConsoleRenderer {
51	fn should_use_renderer(
52		&self,
53		_features: &dyn ConsoleOutputFeatures,
54		environment_prefix: &str,
55	) -> bool {
56		// If someone has explicitly specificed a log format, ignore all else.
57		if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
58			return explicit_renderer.trim().eq_ignore_ascii_case("text");
59		}
60
61		// Now check for `NO_COLOR`/`NOCOLOR`/`CLICOLOR`
62		for no_color_var in ["NO_COLOR", "NOCOLOR"] {
63			if env_var(no_color_var).as_deref() == Ok("1") {
64				return true;
65			}
66		}
67		for color_var in ["CLICOLOR", "CLI_COLOR", "CLICOLOR_FORCE"] {
68			let env = env_var(color_var);
69			if env.as_deref() == Ok("0") {
70				return true;
71			}
72			if env.as_deref() == Ok("1") {
73				return false;
74			}
75		}
76
77		// Wasn't explicitly blocked, so okay to load...
78		// This makes it the default for things that aren't a tty.
79		true
80	}
81
82	fn render_message(
83		&self,
84		app_name: &'static str,
85		log: SuperConsoleLogMessage,
86		_term_width: u16,
87	) -> Result<String, LisaError> {
88		let mut line = String::new();
89
90		if log.should_decorate() {
91			write!(
92				&mut line,
93				"{}/{}|",
94				log.subsytem().unwrap_or(app_name),
95				log.level(),
96			)?;
97		}
98
99		if let Some(msg) = log.message() {
100			line += &msg.replace('\n', "  ").replace('\r', "");
101		} else {
102			line += "<no message>";
103		}
104		if log.should_decorate() {
105			write!(&mut line, "|")?;
106		}
107		if !log.metadata().is_empty() && !log.should_hide_fields_for_humans() {
108			let mut has_written = false;
109			for (key, value) in log.metadata() {
110				if has_written {
111					line.push(',');
112				}
113				write!(&mut line, "{key}={value}")?;
114				has_written = true;
115			}
116		}
117		write!(
118			&mut line,
119			"|{:04}/{:02}/{:02} {:02}:{:02}:{:02}.{:04}",
120			log.at().year(),
121			log.at().month0(),
122			log.at().day0(),
123			log.at().hour(),
124			log.at().minute(),
125			log.at().second(),
126			log.at().timestamp_subsec_millis(),
127		)?;
128		writeln!(&mut line)?;
129
130		Ok(match self.ansi_escapes.replace_all(&line, "") {
131			Cow::Borrowed(_) => line,
132			Cow::Owned(owned) => owned,
133		})
134	}
135
136	fn default_ps1(&self) -> String {
137		"> ".to_owned()
138	}
139
140	fn update_ps1(&self, new_ps1: String) {
141		let mut guarded = self.ps1.write();
142		*guarded = new_ps1;
143	}
144
145	fn supports_ansi(&self) -> bool {
146		false
147	}
148
149	/// We actually pause rendering so we don't need to do any 'clear'-ing
150	fn clear_input(&self, _term_width: u16) -> String {
151		String::with_capacity(0)
152	}
153
154	/// Render a dynamic input line.
155	///
156	/// We actually don't render a dynamic input, as that'd require erasing
157	/// things, and that can't be done in a TEXT mode, or supported in a braille
158	/// display.
159	///
160	/// We actually only render when [`Self::on_input`] is called. As inputs are paused
161	/// anyway clear/render is never called.
162	///
163	/// ## Errors
164	///
165	/// This function will never error.
166	fn render_input(
167		&self,
168		_app_name: &'static str,
169		_provider: &dyn InputProvider,
170		_term_width: u16,
171	) -> Result<String, LisaError> {
172		Ok(String::with_capacity(0))
173	}
174
175	/// Clear the task list, we don't have any task list.
176	fn clear_task_list(&self, _task_list_size: usize) -> String {
177		String::with_capacity(0)
178	}
179
180	/// Re-render all the tasks as new events come in.
181	///
182	/// For this will just render any new events that come in between the last
183	/// time we called it.
184	fn rerender_tasks(
185		&self,
186		new_task_events: &[TaskEvent],
187		_current_task_states: &FnvHashMap<
188			GloballyUniqueTaskId,
189			(DateTime<Utc>, String, LisaTaskStatus),
190		>,
191		_running_since: Option<DateTime<Utc>>,
192		_term_height: u16,
193	) -> Result<String, LisaError> {
194		let mut result = String::new();
195
196		for event in new_task_events {
197			match event {
198				TaskEvent::TaskStart(thread, task, name, status) => {
199					write!(
200						&mut result,
201						"{thread}/{task}|task started with name: [{name}]|status={status}",
202					)?;
203				}
204				TaskEvent::TaskStatusUpdate(thread, task, new_status) => {
205					write!(
206						&mut result,
207						"{thread}/{task}|task has a new status|status={new_status}",
208					)?;
209				}
210				TaskEvent::TaskEnd(thread, task) => {
211					write!(&mut result, "{thread}/{task}|task ended")?;
212				}
213			}
214			result.push('\n');
215		}
216
217		Ok(result)
218	}
219
220	/// Handle a user typing into the terminal.
221	///
222	/// This will simply render the actual input line, and characters as users
223	/// type.
224	///
225	/// ## Errors
226	///
227	/// Never.
228	fn on_input(
229		&self,
230		event: TerminalInputEvent,
231		provider: &dyn InputProvider,
232	) -> Result<String, LisaError> {
233		match event {
234			TerminalInputEvent::InputStarted => {
235				let ps1_read = self.ps1.read();
236				Ok(ps1_read.clone())
237			}
238			TerminalInputEvent::InputFinished => Ok("\n".to_owned()),
239			TerminalInputEvent::InputAppend(character) => {
240				let mut new = String::with_capacity(1);
241				new.push(character);
242				Ok(new)
243			}
244			TerminalInputEvent::InputMassAppend(data) => Ok(data),
245			TerminalInputEvent::InputChanged(_) => {
246				let ps1_read = self.ps1.read();
247				let mut data = String::with_capacity(1 + ps1_read.len());
248				data.push('\n');
249				data.push_str(ps1_read.as_str());
250				data.push_str(&provider.current_input());
251				Ok(data)
252			}
253			TerminalInputEvent::InputCancelled => Ok("<CANCELLED>\n".to_owned()),
254			TerminalInputEvent::ClearScreen => Ok(String::with_capacity(0)),
255			TerminalInputEvent::CursorMoveLeft(_) | TerminalInputEvent::CursorMoveRight(_) => {
256				Ok(String::with_capacity(0))
257			}
258			TerminalInputEvent::ToggleOutputPause => {
259				self.force_pause.fetch_not(Ordering::Release);
260				Ok(String::with_capacity(0))
261			}
262		}
263	}
264
265	fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool {
266		provider.input_in_progress() || self.force_pause.load(Ordering::Acquire)
267	}
268}