rm_lisa/display/
mod.rs

1//! [`SuperConsole`]; The Structure that renders everything.
2//!
3//! This directory contains the base implementation for [`SuperConsole`] the
4//! main type that handles displaying all the information we get fed from
5//! various sources. It also contains supporting types for the console such
6//! as the throttler that ensures we don't eat up all your CPU time with
7//! printing to the console too often.
8//!
9//! Chances are if you're looking to want to do anything with the actual
10//! displaying of log lines. This is the place to be.
11//!
12//! ## Throttling
13//!
14//! When printing to standard-out, and standard-error we can cause severe
15//! CPU contention when we print _too often_ to these channels. As such we
16//! apply throttling (think like a buffered writer) to how often we update.
17//!
18//! There are a couple ways around this:
19//!
20//! 1. Manually create a super console with [`SuperConsole::new_with_outputs`],
21//!    and set the parameter `should_throttle` to false. You can still specify
22//!    standard-out/standard-error. ***this will fully disable throttling, and
23//!    as a result CPU can go out of control.***
24//! 2. Store a reference to [`SuperConsole`], and call [`SuperConsole::tick`]
25//!    once the throttling limit has surpassed. You can call tick as many times
26//!    as you want, and it will _always_ be safe.
27
28pub mod render_state;
29pub mod renderers;
30pub mod tracing;
31
32use crate::{
33	app_name_to_prefix,
34	display::{
35		render_state::UnlockedRendererState,
36		renderers::{
37			ColorConsoleRenderer, ConsoleOutputFeatures, ConsoleRenderer, JSONConsoleRenderer,
38			TextConsoleRenderer,
39		},
40		tracing::SuperConsoleLogMessage,
41	},
42	errors::LisaError,
43	input::InputProvider,
44	tasks::TaskEventLogProvider,
45};
46use parking_lot::{Mutex, RawMutex, lock_api::MutexGuard};
47use std::{
48	io::{Stderr, Stdout, Write as IoWrite, stderr as get_stderr, stdout as get_stdout},
49	sync::Arc,
50	time::Duration,
51};
52use tokio::{
53	signal::ctrl_c,
54	sync::{
55		Mutex as AsyncMutex,
56		mpsc::{Receiver as BoundedReceiver, Sender as BoundedSender, channel as bounded_channel},
57	},
58	task::Builder as TaskBuilder,
59	time::sleep,
60};
61
62/// How many logs can be queued (this gets flushed every ~15ms, so doesn't need to be too large).
63const LOG_CHANNEL_SIZE: usize = 64_usize;
64
65/// Effectively the console "renderer".
66///
67/// [`SuperConsole`] is responsible for keeping track of the 'rendering'
68/// state, and managing it effectively. This crucially does not handle any of
69/// the input tracking or any of the other console related bits.
70///
71/// It is called [`SuperConsole`] as it's output is heavily inspired by the
72/// Buck1 renderer also with the name [`SuperConsole`]. Buck2 also has a
73/// concept of a [`SuperConsole`], but I don't like it's output at all. So
74/// don't confuse it with the rust library 'superconsole', or it's buck2
75/// counterpart.
76pub struct SuperConsole<
77	StdoutTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
78	StderrTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
79> {
80	/// Queue a flush off the underlying messages.
81	flush_sender: BoundedSender<()>,
82	/// Used to signal that a flush _has_ occured.
83	did_do_flush: AsyncMutex<BoundedReceiver<()>>,
84	/// The messages to actually log to the screen.
85	log_messages: BoundedSender<SuperConsoleLogMessage>,
86	/// The underlying renderer that actually prints to stdout, and stderr.
87	state: Arc<Mutex<UnlockedRendererState<StdoutTy, StderrTy>>>,
88	/// A channel used to stop tick tasking.
89	stop_tick_task: BoundedSender<()>,
90}
91
92impl SuperConsole<Stdout, Stderr> {
93	/// Create a new super console that will print to the programs STDOUT/STDERR.
94	///
95	/// This will dynamically choose a renderer to use based off the default
96	/// renderers that are present in lisa (color, json, and text).
97	///
98	/// ## Errors
99	///
100	/// If we could not identify a renderer to use, If we cannot spawn a
101	/// background task to render inputs.
102	pub fn new(app_name: &'static str) -> Result<Self, LisaError> {
103		let stdout_sink = get_stdout();
104		let stderr_sink = get_stderr();
105
106		let environment_prefix = app_name_to_prefix(app_name);
107		let stdout_renderer = Self::choose_default_renderer(&stdout_sink, &environment_prefix)
108			.ok_or(LisaError::NoRendererFound)?;
109		let stderr_renderer = Self::choose_default_renderer(&stderr_sink, &environment_prefix)
110			.ok_or(LisaError::NoRendererFound)?;
111
112		let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
113		let (flush_sender, flush_receiver) = bounded_channel(1);
114		let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
115		let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
116
117		let state = Arc::new(Mutex::new(UnlockedRendererState::new(
118			app_name,
119			&environment_prefix,
120			(stdout_sink, stderr_sink),
121			(stdout_renderer, stderr_renderer),
122			log_receiver,
123		)));
124		Self::spawn_tick_task(
125			flush_completed_sender,
126			flush_receiver,
127			stop_tick_receiver,
128			state.clone(),
129		)?;
130
131		Ok(Self {
132			did_do_flush: AsyncMutex::new(flush_completed_receiver),
133			flush_sender,
134			log_messages,
135			state,
136			stop_tick_task: stop_tick_sender,
137		})
138	}
139
140	/// Create a new super console that will print to the programs STDOUT/STDERR.
141	///
142	/// In this case the renderers are explicitly passed in allowing for fully
143	/// custom rendering. It is the callers job to make sure that they have
144	/// respected all and any user preferences for _which_ renderer they want to
145	/// use.
146	///
147	/// ## Errors
148	///
149	/// If we cannot spawn a background task to render inputs.
150	pub fn new_preselected_renderers(
151		app_name: &'static str,
152		stdout_renderer: Box<dyn ConsoleRenderer>,
153		stderr_renderer: Box<dyn ConsoleRenderer>,
154	) -> Result<Self, LisaError> {
155		let stdout_sink = get_stdout();
156		let stderr_sink = get_stderr();
157
158		let environment_prefix = app_name_to_prefix(app_name);
159		let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
160		let (flush_sender, flush_receiver) = bounded_channel(1);
161		let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
162		let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
163
164		let state = Arc::new(Mutex::new(UnlockedRendererState::new(
165			app_name,
166			&environment_prefix,
167			(stdout_sink, stderr_sink),
168			(stdout_renderer, stderr_renderer),
169			log_receiver,
170		)));
171		Self::spawn_tick_task(
172			flush_completed_sender,
173			flush_receiver,
174			stop_tick_receiver,
175			state.clone(),
176		)?;
177
178		Ok(Self {
179			did_do_flush: AsyncMutex::new(flush_completed_receiver),
180			flush_sender,
181			log_messages,
182			state,
183			stop_tick_task: stop_tick_sender,
184		})
185	}
186}
187
188impl<
189	StdoutTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
190	StderrTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
191> SuperConsole<StdoutTy, StderrTy>
192{
193	/// Create a new super console where outputs go somewhere specific (may not be
194	/// stdout/stderr).
195	///
196	/// ## Errors
197	///
198	/// If we cannot find a renderer for your given stdout/stderr types, or cannot
199	/// spawn a background task to progress the input.
200	pub fn new_with_outputs(
201		app_name: &'static str,
202		stdout: StdoutTy,
203		stderr: StderrTy,
204	) -> Result<Self, LisaError> {
205		let environment_prefix = app_name_to_prefix(app_name);
206		let stdout_renderer = Self::choose_default_renderer(&stdout, &environment_prefix)
207			.ok_or(LisaError::NoRendererFound)?;
208		let stderr_renderer = Self::choose_default_renderer(&stderr, &environment_prefix)
209			.ok_or(LisaError::NoRendererFound)?;
210
211		let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
212		let (flush_sender, flush_receiver) = bounded_channel(1);
213		let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
214		let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
215
216		let state = Arc::new(Mutex::new(UnlockedRendererState::new(
217			app_name,
218			&environment_prefix,
219			(stdout, stderr),
220			(stdout_renderer, stderr_renderer),
221			log_receiver,
222		)));
223		Self::spawn_tick_task(
224			flush_completed_sender,
225			flush_receiver,
226			stop_tick_receiver,
227			state.clone(),
228		)?;
229
230		Ok(Self {
231			did_do_flush: AsyncMutex::new(flush_completed_receiver),
232			flush_sender,
233			log_messages,
234			state,
235			stop_tick_task: stop_tick_sender,
236		})
237	}
238
239	/// Create a new console outputting to specific places, with specific
240	/// renderers.
241	///
242	/// It is up to the caller to inherit all user preferences for the renderers.
243	///
244	/// ## Errors
245	///
246	/// If we cannot spawn a task to tick along the console.
247	pub fn new_with_outputs_and_preselected_renderers(
248		app_name: &'static str,
249		stdout: StdoutTy,
250		stderr: StderrTy,
251		stdout_renderer: Box<dyn ConsoleRenderer>,
252		stderr_renderer: Box<dyn ConsoleRenderer>,
253	) -> Result<Self, LisaError> {
254		let environment_prefix = app_name_to_prefix(app_name);
255		let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
256		let (flush_sender, flush_receiver) = bounded_channel(1);
257		let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
258		let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
259
260		let state = Arc::new(Mutex::new(UnlockedRendererState::new(
261			app_name,
262			&environment_prefix,
263			(stdout, stderr),
264			(stdout_renderer, stderr_renderer),
265			log_receiver,
266		)));
267		Self::spawn_tick_task(
268			flush_completed_sender,
269			flush_receiver,
270			stop_tick_receiver,
271			state.clone(),
272		)?;
273
274		Ok(Self {
275			did_do_flush: AsyncMutex::new(flush_completed_receiver),
276			flush_sender,
277			log_messages,
278			state,
279			stop_tick_task: stop_tick_sender,
280		})
281	}
282
283	/// Request this console immediately working on flushing it's contents.
284	///
285	/// THIS WILL NOT RETURN UNTIL THE FLUSH HAS BEEN COMPLETED.
286	pub async fn flush(&self) {
287		let mut did_do_flush_lock = self.did_do_flush.lock().await;
288		_ = self.flush_sender.send(()).await;
289		_ = did_do_flush_lock.recv().await;
290	}
291
292	/// Get the current render state.
293	///
294	/// *NOTE: WHILE HOLDING TO THIS YOU WILL BE LOCKING LOGS FROM RENDERING*.
295	pub fn get_render_state(
296		&self,
297	) -> MutexGuard<'_, RawMutex, UnlockedRendererState<StdoutTy, StderrTy>> {
298		self.state.lock()
299	}
300
301	/// Set the current input provider.
302	pub fn set_input_provider<Ty: InputProvider + Sized + 'static>(&self, provider: Ty) {
303		self.state.lock().set_input_provider(Box::new(provider));
304	}
305
306	/// Set the current event log task provider.
307	pub fn set_task_provider<Ty: TaskEventLogProvider + Sized + 'static>(&self, provider: Ty) {
308		self.state.lock().set_task_provider(Box::new(provider));
309	}
310
311	/// Set a channel to receive inputs on rather than needing to call
312	/// [`Self::get_unprocessed_inputs`].
313	pub fn set_input_channel(&self, channel: BoundedSender<String>) {
314		self.state.lock().set_completed_input_channel(channel);
315	}
316
317	/// Mark the current input as active.
318	///
319	/// ## Errors
320	///
321	/// If the input provider errors trying to be marked as active.
322	pub fn set_input_active(&self, active: bool) -> Result<(), LisaError> {
323		self.state.lock().set_input_active(active)
324	}
325
326	/// Get a series of unprocessed inputs from the input provider.
327	///
328	/// In general prefer using [`Self::set_input_channel`] which gives you a
329	/// channel you can poll for inputs with locking this like with poll for
330	/// inputs, and rendering.
331	pub fn get_unprocessed_inputs(&self) -> Vec<String> {
332		self.state.lock().get_unprocessed_inputs()
333	}
334
335	/// Look at all the default renderers, and return whichever one is
336	/// compatible if any are.
337	#[must_use]
338	pub fn choose_default_renderer(
339		features: &dyn ConsoleOutputFeatures,
340		environment_prefix: &str,
341	) -> Option<Box<dyn ConsoleRenderer>> {
342		let color = ColorConsoleRenderer::new();
343		if color.should_use_renderer(features, environment_prefix) {
344			return Some(Box::new(color));
345		}
346		std::mem::drop(color);
347
348		let json = JSONConsoleRenderer::new();
349		if json.should_use_renderer(features, environment_prefix) {
350			return Some(Box::new(json));
351		}
352		std::mem::drop(json);
353
354		let text = TextConsoleRenderer::new();
355		if text.should_use_renderer(features, environment_prefix) {
356			return Some(Box::new(text));
357		}
358		std::mem::drop(text);
359
360		None
361	}
362
363	/// Cross OS wrapper for getting the current terminal height & width.
364	///
365	/// This backs off of `termios` on many non-windows OS, and then
366	/// `GetConsoleScreenBufferInfo` on windows. This will look at standard
367	/// in file descriptor (as standard out/standard error can be individually
368	/// redirected).
369	#[allow(
370		// Dependent on OS...
371		unreachable_code,
372	)]
373	#[must_use]
374	pub fn terminal_height_and_width() -> Option<(u16, u16)> {
375		#[cfg(any(
376			target_os = "aix",
377			target_os = "linux",
378			target_os = "android",
379			target_os = "macos",
380			target_os = "ios",
381			target_os = "freebsd",
382			target_os = "openbsd",
383			target_os = "netbsd",
384			target_os = "dragonfly",
385			target_os = "solaris",
386			target_os = "illumos",
387			target_os = "haiku",
388		))]
389		{
390			use crate::termios::tcgetwinsize;
391			let winsize = tcgetwinsize(0).ok()?;
392			return Some((winsize.ws_row, winsize.ws_col));
393		}
394
395		#[cfg(target_os = "windows")]
396		{
397			use windows::Win32::System::Console::{
398				CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle,
399				STD_INPUT_HANDLE,
400			};
401
402			let mut buffer_info = CONSOLE_SCREEN_BUFFER_INFO::default();
403			let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) }.ok()?;
404			unsafe { GetConsoleScreenBufferInfo(handle, &raw mut buffer_info).ok()? };
405			return Some((
406				u16::try_from(buffer_info.dwSize.Y).unwrap_or(u16::MIN),
407				u16::try_from(buffer_info.dwSize.X).unwrap_or(u16::MIN),
408			));
409		}
410
411		None
412	}
413
414	/// Cross OS wrapper for getting the current terminal height.
415	///
416	/// This backs off of `termios` on many non-windows OS, and then
417	/// `GetConsoleScreenBufferInfo` on windows. This will look at standard
418	/// in file descriptor (as standard out/standard error can be individually
419	/// redirected).
420	#[must_use]
421	pub fn terminal_height() -> Option<u16> {
422		Self::terminal_height_and_width().map(|tuple| tuple.0)
423	}
424
425	/// Cross OS wrapper for getting the current terminal width.
426	///
427	/// This backs off of `termios` on many non-windows OS, and then
428	/// `GetConsoleScreenBufferInfo` on windows. This will look at standard
429	/// in file descriptor (as standard out/standard error can be individually
430	/// redirected).
431	#[must_use]
432	pub fn terminal_width() -> Option<u16> {
433		Self::terminal_height_and_width().map(|tuple| tuple.1)
434	}
435
436	/// The function that is used to actually queue a log message.
437	///
438	/// This is what gets called by tracing and queues a message to be
439	/// rendered. Called when a tokio runtime _does_ exist on the thread.
440	pub(crate) async fn log(&self, log_message: SuperConsoleLogMessage) {
441		_ = self.log_messages.send(log_message).await;
442	}
443
444	/// The function that is used to actually queue a log message.
445	///
446	/// This is what gets called by tracing and queues a message to be rendered,
447	/// when a tokio runtime is not present on the thread.
448	pub(crate) async fn log_sync(&self, log_message: SuperConsoleLogMessage) {
449		_ = self.log_messages.send(log_message).await;
450	}
451
452	fn spawn_tick_task(
453		flush_completed_sender: BoundedSender<()>,
454		mut flush_recveiver: BoundedReceiver<()>,
455		mut receiver: BoundedReceiver<()>,
456		state: Arc<Mutex<UnlockedRendererState<StdoutTy, StderrTy>>>,
457	) -> Result<(), LisaError> {
458		TaskBuilder::new()
459			.name("lisa::display::SuperConsole::tick")
460			.spawn(async move {
461				loop {
462					let mut flush_queued = false;
463
464					tokio::select! {
465						() = sleep(Duration::from_millis(12)) => {}
466						_ = flush_recveiver.recv() => {
467							flush_queued = true;
468						}
469						_ = receiver.recv() => {
470							break;
471						}
472						_ = ctrl_c() => {
473							break;
474						}
475					}
476
477					_ = state.lock().render_if_needed();
478					if flush_queued {
479						_ = flush_completed_sender.send(()).await;
480					}
481				}
482			})?;
483
484		Ok(())
485	}
486}
487
488impl<
489	StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
490	StderrTy: ConsoleOutputFeatures + IoWrite + Send,
491> Drop for SuperConsole<StdoutTy, StderrTy>
492{
493	fn drop(&mut self) {
494		let cloned_flush_sender = self.flush_sender.clone();
495		let cloned_stop_tick_task = self.stop_tick_task.clone();
496
497		_ = TaskBuilder::new()
498			.name("lisa::display::SuperConsole::flush_drop")
499			.spawn(async move {
500				_ = cloned_flush_sender.send(()).await;
501				_ = cloned_stop_tick_task.send(()).await;
502			});
503	}
504}
505
506#[cfg(test)]
507mod unit_tests {
508	use super::*;
509
510	#[test]
511	pub fn is_send_sync() {
512		fn accepts_send_sync<Ty: Send + Sync>() {}
513
514		accepts_send_sync::<SuperConsole<Stdout, Stderr>>();
515	}
516
517	#[test]
518	pub fn term_size_matches_terminal_size_crate() {
519		assert_eq!(
520			SuperConsole::<Stdout, Stderr>::terminal_width(),
521			terminal_size::terminal_size().map(|item| item.0.0),
522			"{:?} != {:?}, (us == term_size) [WIDTH]",
523			SuperConsole::<Stdout, Stderr>::terminal_width()
524				.expect("Failed to query terminal width"),
525			terminal_size::terminal_size().map(|item| item.0.0)
526		);
527
528		assert_eq!(
529			SuperConsole::<Stdout, Stderr>::terminal_height(),
530			terminal_size::terminal_size().map(|item| item.1.0),
531			"{:?} != {:?}, (us == term_size) [HEIGHT]",
532			SuperConsole::<Stdout, Stderr>::terminal_width()
533				.expect("Failed to query terminal height"),
534			terminal_size::terminal_size().map(|item| item.1.0)
535		);
536	}
537}