mod color;
mod json;
mod text;
pub use crate::display::renderers::{
color::ColorConsoleRenderer, json::JSONConsoleRenderer, text::TextConsoleRenderer,
};
use crate::{
display::tracing::SuperConsoleLogMessage,
errors::LisaError,
input::{InputProvider, TerminalInputEvent},
tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use regex::Regex;
use std::{
collections::VecDeque,
fs::File,
io::{
BufWriter, Cursor, Empty, IsTerminal, LineWriter, PipeWriter, Sink, Stderr, StderrLock,
Stdout, StdoutLock, Write,
},
net::TcpStream,
sync::{Arc, OnceLock},
};
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[cfg(target_os = "windows")]
use std::{
env::var as env_var,
sync::{
Once,
atomic::{AtomicBool, Ordering as AtomicOrdering},
},
};
#[cfg(target_os = "windows")]
use windows::Win32::System::Console::{
CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetStdHandle,
STD_ERROR_HANDLE, STD_OUTPUT_HANDLE, SetConsoleMode,
};
#[cfg(target_os = "windows")]
static STDOUT_ANSI_INITIALIZER: Once = Once::new();
#[cfg(target_os = "windows")]
static STDOUT_SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "windows")]
static STDERR_ANSI_INITIALIZER: Once = Once::new();
#[cfg(target_os = "windows")]
static STDERR_SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false);
static ANSI_ESCAPE_CODE_REGEX: OnceLock<Regex> = OnceLock::new();
pub fn get_ansi_escape_code_regex() -> Regex {
ANSI_ESCAPE_CODE_REGEX
.get_or_init(|| {
let Ok(regex) = Regex::new(
r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])",
) else {
unimplemented!("Regex is validated pre-compile time...");
};
regex
})
.clone()
}
pub trait ConsoleRenderer: Send + Sync {
#[must_use]
fn should_use_renderer(
&self,
stream_features: &dyn ConsoleOutputFeatures,
environment_prefix: &str,
) -> bool;
#[must_use]
fn supports_ansi(&self) -> bool;
#[must_use]
fn default_ps1(&self) -> String;
fn update_ps1(&self, new_ps1: String);
#[must_use]
fn clear_input(&self, term_width: u16) -> String;
#[must_use]
fn clear_task_list(&self, task_list_size: usize) -> String;
#[must_use]
fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool;
fn render_message(
&self,
app_name: &'static str,
log: SuperConsoleLogMessage,
term_width: u16,
) -> Result<String, LisaError>;
fn render_input(
&self,
app_name: &'static str,
provider: &dyn InputProvider,
term_width: u16,
) -> Result<String, LisaError>;
fn rerender_tasks(
&self,
new_task_events: &[TaskEvent],
current_task_states: &FnvHashMap<
GloballyUniqueTaskId,
(DateTime<Utc>, String, LisaTaskStatus),
>,
tasks_running_since: Option<DateTime<Utc>>,
term_height: u16,
) -> Result<String, LisaError>;
fn on_input(
&self,
event: TerminalInputEvent,
provider: &dyn InputProvider,
) -> Result<String, LisaError>;
}
pub trait ConsoleOutputFeatures {
#[must_use]
fn is_atty(&self) -> bool;
#[must_use]
fn enable_ansi(&self) -> bool;
}
impl ConsoleOutputFeatures for Stdout {
fn is_atty(&self) -> bool {
self.is_terminal()
}
#[cfg(not(target_os = "windows"))]
fn enable_ansi(&self) -> bool {
true
}
#[cfg(target_os = "windows")]
fn enable_ansi(&self) -> bool {
enable_ansi_stdout()
}
}
impl ConsoleOutputFeatures for StdoutLock<'_> {
fn is_atty(&self) -> bool {
self.is_terminal()
}
#[cfg(not(target_os = "windows"))]
fn enable_ansi(&self) -> bool {
true
}
#[cfg(target_os = "windows")]
fn enable_ansi(&self) -> bool {
enable_ansi_stdout()
}
}
impl ConsoleOutputFeatures for Stderr {
fn is_atty(&self) -> bool {
self.is_terminal()
}
#[cfg(not(target_os = "windows"))]
fn enable_ansi(&self) -> bool {
true
}
#[cfg(target_os = "windows")]
fn enable_ansi(&self) -> bool {
enable_ansi_stderr()
}
}
impl ConsoleOutputFeatures for StderrLock<'_> {
fn is_atty(&self) -> bool {
self.is_terminal()
}
#[cfg(not(target_os = "windows"))]
fn enable_ansi(&self) -> bool {
true
}
#[cfg(target_os = "windows")]
fn enable_ansi(&self) -> bool {
enable_ansi_stderr()
}
}
macro_rules! impl_default_output_features {
($type:ty) => {
impl ConsoleOutputFeatures for $type {
fn is_atty(&self) -> bool {
false
}
fn enable_ansi(&self) -> bool {
true
}
}
};
}
impl_default_output_features!(File);
impl_default_output_features!(TcpStream);
impl_default_output_features!(&mut [u8]);
#[cfg(unix)]
impl_default_output_features!(UnixStream);
impl_default_output_features!(Arc<File>);
impl_default_output_features!(Cursor<&mut [u8]>);
impl_default_output_features!(Empty);
impl_default_output_features!(PipeWriter);
impl_default_output_features!(Sink);
#[cfg(unix)]
impl_default_output_features!(&'_ UnixStream);
impl_default_output_features!(Cursor<&mut Vec<u8>>);
impl_default_output_features!(Cursor<Box<[u8]>>);
impl_default_output_features!(Cursor<Vec<u8>>);
impl_default_output_features!(VecDeque<u8>);
impl_default_output_features!(Vec<u8>);
impl<Inner: ConsoleOutputFeatures> ConsoleOutputFeatures for Box<Inner> {
fn is_atty(&self) -> bool {
(*(*self)).is_atty()
}
fn enable_ansi(&self) -> bool {
(*(*self)).enable_ansi()
}
}
impl<Inner: ConsoleOutputFeatures + Write> ConsoleOutputFeatures for BufWriter<Inner> {
fn is_atty(&self) -> bool {
self.get_ref().is_atty()
}
fn enable_ansi(&self) -> bool {
self.get_ref().enable_ansi()
}
}
impl<Inner: ConsoleOutputFeatures + Write> ConsoleOutputFeatures for LineWriter<Inner> {
fn is_atty(&self) -> bool {
self.get_ref().is_atty()
}
fn enable_ansi(&self) -> bool {
self.get_ref().enable_ansi()
}
}
impl<const N: usize> ConsoleOutputFeatures for Cursor<[u8; N]> {
fn is_atty(&self) -> bool {
false
}
fn enable_ansi(&self) -> bool {
true
}
}
#[cfg(target_os = "windows")]
#[must_use]
fn enable_ansi_stdout() -> bool {
STDOUT_ANSI_INITIALIZER.call_once(|| {
let vt_processing_result = {
if let Ok(handle) = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) } {
let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
let mut current_console_mode = CONSOLE_MODE(0);
let was_success_get =
unsafe { GetConsoleMode(handle, &raw mut current_console_mode).is_ok() };
let mut was_success_set = false;
if was_success_get {
if current_console_mode.0 & mask.0 == 0 {
current_console_mode.0 |= mask.0;
was_success_set =
unsafe { SetConsoleMode(handle, current_console_mode).is_ok() };
} else {
was_success_set = true;
}
}
was_success_get && was_success_set
} else {
false
}
};
STDOUT_SUPPORTS_ANSI_ESCAPE_CODES.store(
vt_processing_result || env_var("TERM").is_ok_and(|term| term != "dumb"),
AtomicOrdering::SeqCst,
);
});
STDOUT_SUPPORTS_ANSI_ESCAPE_CODES.load(AtomicOrdering::SeqCst)
}
#[cfg(target_os = "windows")]
#[must_use]
fn enable_ansi_stderr() -> bool {
STDERR_ANSI_INITIALIZER.call_once(|| {
let vt_processing_result = {
if let Ok(handle) = unsafe { GetStdHandle(STD_ERROR_HANDLE) } {
let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
let mut current_console_mode = CONSOLE_MODE(0);
let was_success_get =
unsafe { GetConsoleMode(handle, &raw mut current_console_mode).is_ok() };
let mut was_success_set = false;
if was_success_get {
if current_console_mode.0 & mask.0 == 0 {
current_console_mode.0 |= mask.0;
was_success_set =
unsafe { SetConsoleMode(handle, current_console_mode).is_ok() };
} else {
was_success_set = true;
}
}
was_success_get && was_success_set
} else {
false
}
};
STDERR_SUPPORTS_ANSI_ESCAPE_CODES.store(
vt_processing_result || env_var("TERM").is_ok_and(|term| term != "dumb"),
AtomicOrdering::SeqCst,
);
});
STDERR_SUPPORTS_ANSI_ESCAPE_CODES.load(AtomicOrdering::SeqCst)
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
pub fn can_get_regex() {
_ = get_ansi_escape_code_regex();
}
#[test]
pub fn stdout_output_features() {
let stdout = std::io::stdout();
assert_eq!(
(&stdout as &dyn ConsoleOutputFeatures).is_atty(),
stdout.is_terminal(),
"Failed to check if stdout is a tty!",
);
_ = stdout.enable_ansi();
let locked = stdout.lock();
assert_eq!(
(&locked as &dyn ConsoleOutputFeatures).is_atty(),
locked.is_terminal(),
"Failed to check if stdout lock is a tty!",
);
_ = locked.enable_ansi();
}
#[test]
pub fn stderr_output_features() {
let stderr = std::io::stderr();
assert_eq!(
(&stderr as &dyn ConsoleOutputFeatures).is_atty(),
stderr.is_terminal(),
"Failed to check if stderr is a tty!",
);
_ = stderr.enable_ansi();
let locked = stderr.lock();
assert_eq!(
(&locked as &dyn ConsoleOutputFeatures).is_atty(),
locked.is_terminal(),
"Failed to check if stderr lock is a tty!",
);
_ = locked.enable_ansi();
}
#[test]
pub fn default_output_features() {
{
let temporary_file = tempfile::tempfile().expect("Failed to create temporary file!");
assert!(!temporary_file.is_atty());
assert!(temporary_file.enable_ansi());
let arc = Arc::new(temporary_file);
assert!(!arc.is_atty());
assert!(arc.enable_ansi());
}
{
let mut data: Vec<u8> = Vec::new();
assert!(!data.is_atty());
assert!(data.enable_ansi());
assert!(!(&mut data as &mut [u8]).is_atty());
assert!((&mut data as &mut [u8]).enable_ansi());
let cursor = Cursor::new(data);
assert!(!cursor.is_atty());
assert!(cursor.enable_ansi());
let dequeue: VecDeque<u8> = VecDeque::new();
assert!(!dequeue.is_atty());
assert!(dequeue.enable_ansi());
}
{
let mut data: Vec<u8> = Vec::new();
{
let to_cursor: &mut [u8] = &mut data;
let cursor = Cursor::new(to_cursor);
assert!(!cursor.is_atty());
assert!(cursor.enable_ansi());
}
{
let cursor: Cursor<&mut Vec<u8>> = Cursor::new(&mut data);
assert!(!cursor.is_atty());
assert!(cursor.enable_ansi());
}
{
let cursor: Cursor<Box<[u8]>> = Cursor::new(data.into_boxed_slice());
assert!(!cursor.is_atty());
assert!(cursor.enable_ansi());
}
}
{
let empty = std::io::empty();
assert!(!empty.is_atty());
assert!(empty.enable_ansi());
}
}
}