1pub 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
62const LOG_CHANNEL_SIZE: usize = 64_usize;
64
65pub struct SuperConsole<
77 StdoutTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
78 StderrTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
79> {
80 flush_sender: BoundedSender<()>,
82 did_do_flush: AsyncMutex<BoundedReceiver<()>>,
84 log_messages: BoundedSender<SuperConsoleLogMessage>,
86 state: Arc<Mutex<UnlockedRendererState<StdoutTy, StderrTy>>>,
88 stop_tick_task: BoundedSender<()>,
90}
91
92impl SuperConsole<Stdout, Stderr> {
93 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 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 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 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 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 pub fn get_render_state(
296 &self,
297 ) -> MutexGuard<'_, RawMutex, UnlockedRendererState<StdoutTy, StderrTy>> {
298 self.state.lock()
299 }
300
301 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 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 pub fn set_input_channel(&self, channel: BoundedSender<String>) {
314 self.state.lock().set_completed_input_channel(channel);
315 }
316
317 pub fn set_input_active(&self, active: bool) -> Result<(), LisaError> {
323 self.state.lock().set_input_active(active)
324 }
325
326 pub fn get_unprocessed_inputs(&self) -> Vec<String> {
332 self.state.lock().get_unprocessed_inputs()
333 }
334
335 #[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 #[allow(
370 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 #[must_use]
421 pub fn terminal_height() -> Option<u16> {
422 Self::terminal_height_and_width().map(|tuple| tuple.0)
423 }
424
425 #[must_use]
432 pub fn terminal_width() -> Option<u16> {
433 Self::terminal_height_and_width().map(|tuple| tuple.1)
434 }
435
436 pub(crate) async fn log(&self, log_message: SuperConsoleLogMessage) {
441 _ = self.log_messages.send(log_message).await;
442 }
443
444 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}