rm_lisa/display/renderers/
mod.rs1mod color;
23mod json;
24mod text;
25
26pub use crate::display::renderers::{
27 color::ColorConsoleRenderer, json::JSONConsoleRenderer, text::TextConsoleRenderer,
28};
29
30use crate::{
31 display::tracing::SuperConsoleLogMessage,
32 errors::LisaError,
33 input::{InputProvider, TerminalInputEvent},
34 tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
35};
36use chrono::{DateTime, Utc};
37use fnv::FnvHashMap;
38use regex::Regex;
39use std::{
40 collections::VecDeque,
41 fs::File,
42 io::{
43 BufWriter, Cursor, Empty, IsTerminal, LineWriter, PipeWriter, Sink, Stderr, StderrLock,
44 Stdout, StdoutLock, Write,
45 },
46 net::TcpStream,
47 sync::{Arc, OnceLock},
48};
49
50#[cfg(unix)]
51use std::os::unix::net::UnixStream;
52#[cfg(target_os = "windows")]
53use std::{
54 env::var as env_var,
55 sync::{
56 Once,
57 atomic::{AtomicBool, Ordering as AtomicOrdering},
58 },
59};
60#[cfg(target_os = "windows")]
61use windows::Win32::System::Console::{
62 CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetStdHandle,
63 STD_ERROR_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode,
64};
65
66#[cfg(target_os = "windows")]
67static STDOUT_ANSI_INITIALIZER: Once = Once::new();
69#[cfg(target_os = "windows")]
70static STDOUT_SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false);
72
73#[cfg(target_os = "windows")]
74static STDERR_ANSI_INITIALIZER: Once = Once::new();
76#[cfg(target_os = "windows")]
77static STDERR_SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false);
79static ANSI_ESCAPE_CODE_REGEX: OnceLock<Regex> = OnceLock::new();
82
83pub fn get_ansi_escape_code_regex() -> Regex {
85 ANSI_ESCAPE_CODE_REGEX
86 .get_or_init(|| {
87 let Ok(regex) = Regex::new(
88 r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])",
89 ) else {
90 unimplemented!("Regex is validated pre-compile time...");
91 };
92
93 regex
94 })
95 .clone()
96}
97
98pub trait ConsoleRenderer: Send + Sync {
100 #[must_use]
114 fn should_use_renderer(
115 &self,
116 stream_features: &dyn ConsoleOutputFeatures,
117 environment_prefix: &str,
118 ) -> bool;
119
120 #[must_use]
126 fn supports_ansi(&self) -> bool;
127
128 #[must_use]
130 fn default_ps1(&self) -> String;
131
132 fn update_ps1(&self, new_ps1: String);
135
136 #[must_use]
138 fn clear_input(&self, term_width: u16) -> String;
139 #[must_use]
141 fn clear_task_list(&self, task_list_size: usize) -> String;
142 #[must_use]
149 fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool;
150
151 fn render_message(
161 &self,
162 app_name: &'static str,
163 log: SuperConsoleLogMessage,
164 term_width: u16,
165 ) -> Result<String, LisaError>;
166
167 fn render_input(
177 &self,
178 app_name: &'static str,
179 provider: &dyn InputProvider,
180 term_width: u16,
181 ) -> Result<String, LisaError>;
182
183 fn rerender_tasks(
194 &self,
195 new_task_events: &[TaskEvent],
196 current_task_states: &FnvHashMap<
197 GloballyUniqueTaskId,
198 (DateTime<Utc>, String, LisaTaskStatus),
199 >,
200 tasks_running_since: Option<DateTime<Utc>>,
201 term_height: u16,
202 ) -> Result<String, LisaError>;
203
204 fn on_input(
214 &self,
215 event: TerminalInputEvent,
216 provider: &dyn InputProvider,
217 ) -> Result<String, LisaError>;
218}
219
220pub trait ConsoleOutputFeatures {
225 #[must_use]
227 fn is_atty(&self) -> bool;
228
229 #[must_use]
232 fn enable_ansi(&self) -> bool;
233}
234
235impl ConsoleOutputFeatures for Stdout {
236 fn is_atty(&self) -> bool {
237 self.is_terminal()
238 }
239
240 #[cfg(not(target_os = "windows"))]
242 fn enable_ansi(&self) -> bool {
243 true
244 }
245
246 #[cfg(target_os = "windows")]
247 fn enable_ansi(&self) -> bool {
248 enable_ansi_stdout()
249 }
250}
251impl ConsoleOutputFeatures for StdoutLock<'_> {
252 fn is_atty(&self) -> bool {
253 self.is_terminal()
254 }
255
256 #[cfg(not(target_os = "windows"))]
258 fn enable_ansi(&self) -> bool {
259 true
260 }
261
262 #[cfg(target_os = "windows")]
263 fn enable_ansi(&self) -> bool {
264 enable_ansi_stdout()
265 }
266}
267
268impl ConsoleOutputFeatures for Stderr {
269 fn is_atty(&self) -> bool {
270 self.is_terminal()
271 }
272
273 #[cfg(not(target_os = "windows"))]
275 fn enable_ansi(&self) -> bool {
276 true
277 }
278
279 #[cfg(target_os = "windows")]
280 fn enable_ansi(&self) -> bool {
281 enable_ansi_stderr()
282 }
283}
284impl ConsoleOutputFeatures for StderrLock<'_> {
285 fn is_atty(&self) -> bool {
286 self.is_terminal()
287 }
288
289 #[cfg(not(target_os = "windows"))]
291 fn enable_ansi(&self) -> bool {
292 true
293 }
294
295 #[cfg(target_os = "windows")]
296 fn enable_ansi(&self) -> bool {
297 enable_ansi_stderr()
298 }
299}
300
301macro_rules! impl_default_output_features {
303 ($type:ty) => {
304 impl ConsoleOutputFeatures for $type {
305 fn is_atty(&self) -> bool {
306 false
307 }
308
309 fn enable_ansi(&self) -> bool {
310 true
311 }
312 }
313 };
314}
315impl_default_output_features!(File);
316impl_default_output_features!(TcpStream);
317impl_default_output_features!(&mut [u8]);
318#[cfg(unix)]
319impl_default_output_features!(UnixStream);
320impl_default_output_features!(Arc<File>);
321impl_default_output_features!(Cursor<&mut [u8]>);
322impl_default_output_features!(Empty);
323impl_default_output_features!(PipeWriter);
324impl_default_output_features!(Sink);
325#[cfg(unix)]
326impl_default_output_features!(&'_ UnixStream);
327impl_default_output_features!(Cursor<&mut Vec<u8>>);
328impl_default_output_features!(Cursor<Box<[u8]>>);
329impl_default_output_features!(Cursor<Vec<u8>>);
330impl_default_output_features!(VecDeque<u8>);
331impl_default_output_features!(Vec<u8>);
332
333impl<Inner: ConsoleOutputFeatures> ConsoleOutputFeatures for Box<Inner> {
334 fn is_atty(&self) -> bool {
335 (*(*self)).is_atty()
336 }
337
338 fn enable_ansi(&self) -> bool {
339 (*(*self)).enable_ansi()
340 }
341}
342
343impl<Inner: ConsoleOutputFeatures + Write> ConsoleOutputFeatures for BufWriter<Inner> {
344 fn is_atty(&self) -> bool {
345 self.get_ref().is_atty()
346 }
347
348 fn enable_ansi(&self) -> bool {
349 self.get_ref().enable_ansi()
350 }
351}
352
353impl<Inner: ConsoleOutputFeatures + Write> ConsoleOutputFeatures for LineWriter<Inner> {
354 fn is_atty(&self) -> bool {
355 self.get_ref().is_atty()
356 }
357
358 fn enable_ansi(&self) -> bool {
359 self.get_ref().enable_ansi()
360 }
361}
362
363impl<const N: usize> ConsoleOutputFeatures for Cursor<[u8; N]> {
364 fn is_atty(&self) -> bool {
365 false
366 }
367
368 fn enable_ansi(&self) -> bool {
369 true
370 }
371}
372
373#[cfg(target_os = "windows")]
374#[must_use]
375fn enable_ansi_stdout() -> bool {
376 STDOUT_ANSI_INITIALIZER.call_once(|| {
377 let vt_processing_result = {
382 if let Ok(handle) = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) } {
383 let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
384 let mut current_console_mode = CONSOLE_MODE(0);
385 let was_success_get =
386 unsafe { GetConsoleMode(handle, &raw mut current_console_mode).is_ok() };
387
388 let mut was_success_set = false;
389 if was_success_get {
390 if current_console_mode.0 & mask.0 == 0 {
391 current_console_mode.0 |= mask.0;
392 was_success_set =
393 unsafe { SetConsoleMode(handle, current_console_mode).is_ok() };
394 } else {
395 was_success_set = true;
396 }
397 }
398
399 was_success_get && was_success_set
400 } else {
401 false
402 }
403 };
404
405 STDOUT_SUPPORTS_ANSI_ESCAPE_CODES.store(
406 vt_processing_result || env_var("TERM").is_ok_and(|term| term != "dumb"),
407 AtomicOrdering::SeqCst,
408 );
409 });
410
411 STDOUT_SUPPORTS_ANSI_ESCAPE_CODES.load(AtomicOrdering::SeqCst)
412}
413
414#[cfg(target_os = "windows")]
415#[must_use]
416fn enable_ansi_stderr() -> bool {
417 STDERR_ANSI_INITIALIZER.call_once(|| {
418 let vt_processing_result = {
423 if let Ok(handle) = unsafe { GetStdHandle(STD_ERROR_HANDLE) } {
424 let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
425 let mut current_console_mode = CONSOLE_MODE(0);
426 let was_success_get =
427 unsafe { GetConsoleMode(handle, &raw mut current_console_mode).is_ok() };
428
429 let mut was_success_set = false;
430 if was_success_get {
431 if current_console_mode.0 & mask.0 == 0 {
432 current_console_mode.0 |= mask.0;
433 was_success_set =
434 unsafe { SetConsoleMode(handle, current_console_mode).is_ok() };
435 } else {
436 was_success_set = true;
437 }
438 }
439
440 was_success_get && was_success_set
441 } else {
442 false
443 }
444 };
445
446 STDERR_SUPPORTS_ANSI_ESCAPE_CODES.store(
447 vt_processing_result || env_var("TERM").is_ok_and(|term| term != "dumb"),
448 AtomicOrdering::SeqCst,
449 );
450 });
451
452 STDERR_SUPPORTS_ANSI_ESCAPE_CODES.load(AtomicOrdering::SeqCst)
453}
454
455#[cfg(test)]
456mod unit_tests {
457 use super::*;
458
459 #[test]
461 pub fn can_get_regex() {
462 _ = get_ansi_escape_code_regex();
463 }
464
465 #[test]
466 pub fn stdout_output_features() {
467 let stdout = std::io::stdout();
468
469 assert_eq!(
470 (&stdout as &dyn ConsoleOutputFeatures).is_atty(),
471 stdout.is_terminal(),
472 "Failed to check if stdout is a tty!",
473 );
474 _ = stdout.enable_ansi();
476
477 let locked = stdout.lock();
478 assert_eq!(
479 (&locked as &dyn ConsoleOutputFeatures).is_atty(),
480 locked.is_terminal(),
481 "Failed to check if stdout lock is a tty!",
482 );
483 _ = locked.enable_ansi();
485 }
486
487 #[test]
488 pub fn stderr_output_features() {
489 let stderr = std::io::stderr();
490
491 assert_eq!(
492 (&stderr as &dyn ConsoleOutputFeatures).is_atty(),
493 stderr.is_terminal(),
494 "Failed to check if stderr is a tty!",
495 );
496 _ = stderr.enable_ansi();
498
499 let locked = stderr.lock();
500 assert_eq!(
501 (&locked as &dyn ConsoleOutputFeatures).is_atty(),
502 locked.is_terminal(),
503 "Failed to check if stderr lock is a tty!",
504 );
505 _ = locked.enable_ansi();
507 }
508
509 #[test]
510 pub fn default_output_features() {
511 {
513 let temporary_file = tempfile::tempfile().expect("Failed to create temporary file!");
514 assert!(!temporary_file.is_atty());
515 assert!(temporary_file.enable_ansi());
516
517 let arc = Arc::new(temporary_file);
518 assert!(!arc.is_atty());
519 assert!(arc.enable_ansi());
520 }
521
522 {
524 let mut data: Vec<u8> = Vec::new();
525
526 assert!(!data.is_atty());
527 assert!(data.enable_ansi());
528
529 assert!(!(&mut data as &mut [u8]).is_atty());
530 assert!((&mut data as &mut [u8]).enable_ansi());
531
532 let cursor = Cursor::new(data);
533 assert!(!cursor.is_atty());
534 assert!(cursor.enable_ansi());
535
536 let dequeue: VecDeque<u8> = VecDeque::new();
537 assert!(!dequeue.is_atty());
538 assert!(dequeue.enable_ansi());
539 }
540
541 {
543 let mut data: Vec<u8> = Vec::new();
544
545 {
546 let to_cursor: &mut [u8] = &mut data;
547 let cursor = Cursor::new(to_cursor);
548
549 assert!(!cursor.is_atty());
550 assert!(cursor.enable_ansi());
551 }
552
553 {
554 let cursor: Cursor<&mut Vec<u8>> = Cursor::new(&mut data);
555 assert!(!cursor.is_atty());
556 assert!(cursor.enable_ansi());
557 }
558
559 {
560 let cursor: Cursor<Box<[u8]>> = Cursor::new(data.into_boxed_slice());
561 assert!(!cursor.is_atty());
562 assert!(cursor.enable_ansi());
563 }
564 }
565
566 {
567 let empty = std::io::empty();
568 assert!(!empty.is_atty());
569 assert!(empty.enable_ansi());
570 }
571 }
572}