rm_lisa/display/
render_state.rs

1//! All the logic for the 'render state' of super console.
2//!
3//! This type is wrapped in a mutex, and is where most of the actual calls to print happen.
4
5use crate::{
6	display::{
7		SuperConsole,
8		renderers::{ConsoleOutputFeatures, ConsoleRenderer},
9		tracing::SuperConsoleLogMessage,
10	},
11	errors::LisaError,
12	input::{InputProvider, TerminalInputEvent},
13	tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent, TaskEventLogProvider},
14};
15use chrono::{DateTime, Utc};
16use fnv::FnvHashMap;
17use std::{env::var as env_var, hash::BuildHasherDefault, io::Write as IoWrite};
18use tokio::{
19	runtime::Handle,
20	sync::mpsc::{Receiver as BoundedReceiver, Sender as BoundedSender},
21};
22use tracing::warn;
23
24/// A task that is being tracked, is three components:
25///
26/// 1. The datetime the task started according to super console.
27/// 2. The name of the task.
28/// 3. The current status of the task.
29type TrackedTask = (DateTime<Utc>, String, LisaTaskStatus);
30
31/// The underlying renderer stat that is mutable, and is locked to get acted upon.
32pub struct UnlockedRendererState<
33	StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
34	StderrTy: ConsoleOutputFeatures + IoWrite + Send,
35> {
36	/// The underlying application name.
37	app_name: &'static str,
38	/// Any log events waiting to be rendered.
39	cached_log_events: Vec<SuperConsoleLogMessage>,
40	/// A channel to send all completed inputs on.
41	completed_input_senders: Option<BoundedSender<String>>,
42	/// If the app is configured to force a specific terminal height.
43	force_term_height: Option<u16>,
44	/// If the app is configured to force a specific terminal width.
45	force_term_width: Option<u16>,
46	/// If we've warned that our terminal size is a bit too small.
47	has_warned_about_terminal_size: bool,
48	/// The currently active input provider.
49	input_provider: Option<Box<dyn InputProvider>>,
50	/// Any log messages that are pending to be flushed.
51	log_messages: BoundedReceiver<SuperConsoleLogMessage>,
52	/// The STDERR output stream.
53	stderr: StderrTy,
54	/// The renderer capable of rendering messages on STDERR.
55	stderr_renderer: Box<dyn ConsoleRenderer>,
56	/// The STDOUT ouptut stream.
57	stdout: StdoutTy,
58	/// The renderer capable of rendering messages on STDOUT.
59	stdout_renderer: Box<dyn ConsoleRenderer>,
60	/// The task provider that will provide us updates.
61	task_provider: Option<Box<dyn TaskEventLogProvider>>,
62	/// The statuses of any tasks that are actively running.
63	task_statuses: FnvHashMap<GloballyUniqueTaskId, TrackedTask>,
64	/// When the tasks have started running since.
65	tasks_running_since: Option<DateTime<Utc>>,
66}
67
68impl<
69	StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
70	StderrTy: ConsoleOutputFeatures + IoWrite + Send,
71> UnlockedRendererState<StdoutTy, StderrTy>
72{
73	/// Create a new rendering state.
74	#[must_use]
75	pub fn new(
76		app_name: &'static str,
77		environment_prefix: &str,
78		(stdout, stderr): (StdoutTy, StderrTy),
79		(stdout_renderer, stderr_renderer): (Box<dyn ConsoleRenderer>, Box<dyn ConsoleRenderer>),
80		log_messages: BoundedReceiver<SuperConsoleLogMessage>,
81	) -> Self {
82		Self {
83			app_name,
84			cached_log_events: Vec::with_capacity(0),
85			completed_input_senders: None,
86			force_term_height: env_var(format!("{environment_prefix}_FORCE_TERM_HEIGHT"))
87				.ok()
88				.and_then(|item| item.parse::<u16>().ok()),
89			force_term_width: env_var(format!("{environment_prefix}_FORCE_TERM_WIDTH"))
90				.ok()
91				.and_then(|item| item.parse::<u16>().ok()),
92			has_warned_about_terminal_size: false,
93			input_provider: None,
94			log_messages,
95			stderr,
96			stderr_renderer,
97			stdout,
98			stdout_renderer,
99			task_provider: None,
100			task_statuses: FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default()),
101			tasks_running_since: None,
102		}
103	}
104
105	/// Get the current input provider.
106	#[must_use]
107	pub fn get_input_provider(&mut self) -> &mut Option<Box<dyn InputProvider>> {
108		&mut self.input_provider
109	}
110
111	pub fn set_input_provider(&mut self, iprovider: Box<dyn InputProvider>) {
112		self.input_provider = Some(iprovider);
113	}
114
115	pub fn set_task_provider(&mut self, tprovider: Box<dyn TaskEventLogProvider>) {
116		self.task_provider = Some(tprovider);
117	}
118
119	pub fn set_completed_input_channel(&mut self, channel: BoundedSender<String>) {
120		self.completed_input_senders = Some(channel);
121	}
122
123	pub fn get_unprocessed_inputs(&mut self) -> Vec<String> {
124		if let Some(iprovider) = self.input_provider.as_mut() {
125			iprovider.inputs()
126		} else {
127			Vec::with_capacity(0)
128		}
129	}
130
131	/// Try to mark our input as active.
132	///
133	/// ## Errors
134	///
135	/// If the input provider fails being marked as active.
136	pub fn set_input_active(&mut self, active: bool) -> Result<(), LisaError> {
137		if let Some(prov) = self.input_provider.as_mut() {
138			prov.set_active(active)?;
139		}
140
141		Ok(())
142	}
143
144	/// Render if any new events or log messages have occured.
145	///
146	/// ## Errors
147	///
148	/// If we cannot write to our stderr channel, or cannot render a message.
149	pub fn render_if_needed(&mut self) -> Result<(), LisaError> {
150		let mut pause_log_events = false;
151		if let Some(iprovider) = self.input_provider.as_ref()
152			&& iprovider.is_active()
153			&& iprovider.is_stdin()
154			&& self.stderr_renderer.should_pause_log_events(&**iprovider)
155		{
156			pause_log_events = true;
157		}
158
159		let mut new_logs = self.cached_log_events.drain(..).collect::<Vec<_>>();
160		while let Ok(value) = self.log_messages.try_recv() {
161			new_logs.push(value);
162		}
163		let mut input_events = Vec::with_capacity(0);
164		if let Some(iprovider) = self.input_provider.as_mut()
165			&& iprovider.is_active()
166			&& iprovider.is_stdin()
167		{
168			input_events = iprovider.poll_for_input(self.stderr_renderer.supports_ansi());
169		}
170		let mut task_events = Vec::with_capacity(0);
171		if let Some(tprovider) = self.task_provider.as_ref() {
172			task_events = tprovider.new_events();
173		}
174
175		if new_logs.is_empty() && input_events.is_empty() && task_events.is_empty() {
176			return Ok(());
177		}
178		if pause_log_events && input_events.is_empty() {
179			return Ok(());
180		}
181		self.render(pause_log_events, new_logs, input_events, &task_events)
182	}
183
184	/// Actually do a render.
185	fn render(
186		&mut self,
187		pause_logs: bool,
188		new_logs: Vec<SuperConsoleLogMessage>,
189		new_input_events: Vec<TerminalInputEvent>,
190		new_task_events: &[TaskEvent],
191	) -> Result<(), LisaError> {
192		let mut term_width = self
193			.force_term_width
194			.or_else(SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_width)
195			.unwrap_or(40_u16);
196		if term_width < 40 {
197			if !self.has_warned_about_terminal_size {
198				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!");
199				self.has_warned_about_terminal_size = true;
200			}
201			term_width = 40;
202		}
203
204		let had_input_events = !new_input_events.is_empty();
205		if let Some(iprovider) = self.input_provider.as_mut() {
206			for ievent in new_input_events {
207				let rendered = self.stderr_renderer.on_input(ievent, &**iprovider)?;
208				if !rendered.is_empty() {
209					self.stderr.write_all(rendered.as_bytes())?;
210				}
211			}
212
213			if let Some(channel) = self.completed_input_senders.as_ref() {
214				for input in iprovider.inputs() {
215					// Are we in a tokio runtime? if so we need to block in place.
216					if let Ok(h) = Handle::try_current() {
217						tokio::task::block_in_place(move || {
218							h.block_on(async {
219								_ = channel.send(input).await;
220							});
221						});
222					} else {
223						_ = channel.blocking_send(input);
224					}
225				}
226			}
227		}
228		// If this was just an input event we don't need to do a full clear...
229		if had_input_events && (new_logs.is_empty() && new_task_events.is_empty()) {
230			return Ok(());
231		}
232
233		self.clear_input(term_width)?;
234		self.clear_tasks()?;
235
236		if pause_logs {
237			self.cached_log_events = new_logs;
238		} else {
239			for log_line in new_logs {
240				if log_line.towards_stdout() {
241					let rendered =
242						self.stdout_renderer
243							.render_message(self.app_name, log_line, term_width)?;
244
245					self.stdout.write_all(rendered.as_bytes())?;
246				} else {
247					let rendered =
248						self.stderr_renderer
249							.render_message(self.app_name, log_line, term_width)?;
250
251					if !rendered.is_empty() {
252						self.stderr.write_all(rendered.as_bytes())?;
253					}
254				}
255			}
256		}
257
258		self.render_tasks(new_task_events)?;
259		self.render_input(term_width)?;
260
261		Ok(())
262	}
263
264	/// Clear the input that was previously rendered.
265	///
266	/// ## Errors
267	///
268	/// If we cannot write the clear input lines to STDERR.
269	fn clear_input(&mut self, term_width: u16) -> Result<(), LisaError> {
270		if let Some(iprovider) = self.input_provider.as_ref()
271			&& iprovider.is_stdin()
272			&& iprovider.is_active()
273		{
274			let clear_line = self.stderr_renderer.clear_input(term_width);
275			if !clear_line.is_empty() {
276				self.stderr.write_all(clear_line.as_bytes())?;
277			}
278		}
279
280		Ok(())
281	}
282
283	/// Clear all the tasks that were previously rendered.
284	///
285	/// ## Errors
286	///
287	/// If we cannot write the clear tasks lines to STDERR.
288	fn clear_tasks(&mut self) -> Result<(), LisaError> {
289		if self.task_provider.is_some() {
290			let clear_line = self
291				.stderr_renderer
292				.clear_task_list(self.task_statuses.len());
293			if !clear_line.is_empty() {
294				self.stderr.write_all(clear_line.as_bytes())?;
295			}
296		}
297
298		Ok(())
299	}
300
301	/// Render all the tasks and process any new task events.
302	fn render_tasks(&mut self, new_task_events: &[TaskEvent]) -> Result<(), LisaError> {
303		let my_date = Utc::now();
304		let mut was_empty = self.task_statuses.is_empty();
305
306		for task_event in new_task_events {
307			match task_event {
308				TaskEvent::TaskStart(thread_id, lisa_task_id, name, status) => {
309					if was_empty {
310						_ = self.tasks_running_since.insert(my_date);
311						was_empty = false;
312					}
313					self.task_statuses.insert(
314						(*thread_id, *lisa_task_id),
315						(my_date, name.clone(), status.clone()),
316					);
317				}
318				TaskEvent::TaskStatusUpdate(thread_id, lisa_task_id, new_status) => {
319					if let Some(mutable_data) =
320						self.task_statuses.get_mut(&(*thread_id, *lisa_task_id))
321					{
322						mutable_data.2 = new_status.clone();
323					}
324				}
325				TaskEvent::TaskEnd(thread_id, lisa_task_id) => {
326					self.task_statuses.remove(&(*thread_id, *lisa_task_id));
327				}
328			}
329		}
330
331		if !was_empty && self.task_statuses.is_empty() {
332			self.tasks_running_since = None;
333		}
334
335		let rendered = self.stderr_renderer.rerender_tasks(
336			new_task_events,
337			&self.task_statuses,
338			self.tasks_running_since,
339			self.force_term_height
340				.and_then(|_| SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_height())
341				.unwrap_or_default(),
342		)?;
343
344		if !rendered.is_empty() {
345			self.stderr.write_all(rendered.as_bytes())?;
346		}
347
348		Ok(())
349	}
350
351	fn render_input(&mut self, normalized_term_width: u16) -> Result<(), LisaError> {
352		if let Some(iprovider) = self.input_provider.as_ref()
353			&& iprovider.is_stdin()
354			&& iprovider.is_active()
355		{
356			let input_renderer = self.stderr_renderer.render_input(
357				self.app_name,
358				&**iprovider,
359				normalized_term_width,
360			)?;
361			if !input_renderer.is_empty() {
362				self.stderr.write_all(input_renderer.as_bytes())?;
363			}
364		}
365
366		Ok(())
367	}
368}