use crate::{
display::{
renderers::{ConsoleOutputFeatures, ConsoleRenderer, get_ansi_escape_code_regex},
tracing::SuperConsoleLogMessage,
},
errors::LisaError,
input::{InputProvider, TerminalInputEvent},
tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
};
use chrono::prelude::*;
use fnv::FnvHashMap;
use parking_lot::RwLock;
use regex::Regex;
use std::{
borrow::Cow,
env::var as env_var,
fmt::Write,
sync::atomic::{AtomicBool, Ordering},
};
#[derive(Debug)]
pub struct TextConsoleRenderer {
ansi_escapes: Regex,
force_pause: AtomicBool,
ps1: RwLock<String>,
}
impl TextConsoleRenderer {
#[must_use]
pub fn new() -> Self {
Self {
ansi_escapes: get_ansi_escape_code_regex(),
force_pause: AtomicBool::new(false),
ps1: RwLock::new("> ".to_owned()),
}
}
}
impl Default for TextConsoleRenderer {
fn default() -> Self {
Self::new()
}
}
impl ConsoleRenderer for TextConsoleRenderer {
fn should_use_renderer(
&self,
_features: &dyn ConsoleOutputFeatures,
environment_prefix: &str,
) -> bool {
if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
return explicit_renderer.trim().eq_ignore_ascii_case("text");
}
for no_color_var in ["NO_COLOR", "NOCOLOR"] {
if env_var(no_color_var).as_deref() == Ok("1") {
return true;
}
}
for color_var in ["CLICOLOR", "CLI_COLOR", "CLICOLOR_FORCE"] {
let env = env_var(color_var);
if env.as_deref() == Ok("0") {
return true;
}
if env.as_deref() == Ok("1") {
return false;
}
}
true
}
fn render_message(
&self,
app_name: &'static str,
log: SuperConsoleLogMessage,
_term_width: u16,
) -> Result<String, LisaError> {
let mut line = String::new();
if log.should_decorate() {
write!(
&mut line,
"{}/{}|",
log.subsytem().unwrap_or(app_name),
log.level(),
)?;
}
if let Some(msg) = log.message() {
line += &msg.replace('\n', " ").replace('\r', "");
} else {
line += "<no message>";
}
if log.should_decorate() {
write!(&mut line, "|")?;
}
if !log.metadata().is_empty() && !log.should_hide_fields_for_humans() {
let mut has_written = false;
for (key, value) in log.metadata() {
if has_written {
line.push(',');
}
write!(&mut line, "{key}={value}")?;
has_written = true;
}
}
write!(
&mut line,
"|{:04}/{:02}/{:02} {:02}:{:02}:{:02}.{:04}",
log.at().year(),
log.at().month0(),
log.at().day0(),
log.at().hour(),
log.at().minute(),
log.at().second(),
log.at().timestamp_subsec_millis(),
)?;
writeln!(&mut line)?;
Ok(match self.ansi_escapes.replace_all(&line, "") {
Cow::Borrowed(_) => line,
Cow::Owned(owned) => owned,
})
}
fn default_ps1(&self) -> String {
"> ".to_owned()
}
fn update_ps1(&self, new_ps1: String) {
let mut guarded = self.ps1.write();
*guarded = new_ps1;
}
fn supports_ansi(&self) -> bool {
false
}
fn clear_input(&self, _term_width: u16) -> String {
String::with_capacity(0)
}
fn render_input(
&self,
_app_name: &'static str,
_provider: &dyn InputProvider,
_term_width: u16,
) -> Result<String, LisaError> {
Ok(String::with_capacity(0))
}
fn clear_task_list(&self, _task_list_size: usize) -> String {
String::with_capacity(0)
}
fn rerender_tasks(
&self,
new_task_events: &[TaskEvent],
_current_task_states: &FnvHashMap<
GloballyUniqueTaskId,
(DateTime<Utc>, String, LisaTaskStatus),
>,
_running_since: Option<DateTime<Utc>>,
_term_height: u16,
) -> Result<String, LisaError> {
let mut result = String::new();
for event in new_task_events {
match event {
TaskEvent::TaskStart(thread, task, name, status) => {
write!(
&mut result,
"{thread}/{task}|task started with name: [{name}]|status={status}",
)?;
}
TaskEvent::TaskStatusUpdate(thread, task, new_status) => {
write!(
&mut result,
"{thread}/{task}|task has a new status|status={new_status}",
)?;
}
TaskEvent::TaskEnd(thread, task) => {
write!(&mut result, "{thread}/{task}|task ended")?;
}
}
result.push('\n');
}
Ok(result)
}
fn on_input(
&self,
event: TerminalInputEvent,
provider: &dyn InputProvider,
) -> Result<String, LisaError> {
match event {
TerminalInputEvent::InputStarted => {
let ps1_read = self.ps1.read();
Ok(ps1_read.clone())
}
TerminalInputEvent::InputFinished => Ok("\n".to_owned()),
TerminalInputEvent::InputAppend(character) => {
let mut new = String::with_capacity(1);
new.push(character);
Ok(new)
}
TerminalInputEvent::InputMassAppend(data) => Ok(data),
TerminalInputEvent::InputChanged(_) => {
let ps1_read = self.ps1.read();
let mut data = String::with_capacity(1 + ps1_read.len());
data.push('\n');
data.push_str(ps1_read.as_str());
data.push_str(&provider.current_input());
Ok(data)
}
TerminalInputEvent::InputCancelled => Ok("<CANCELLED>\n".to_owned()),
TerminalInputEvent::ClearScreen => Ok(String::with_capacity(0)),
TerminalInputEvent::CursorMoveLeft(_) | TerminalInputEvent::CursorMoveRight(_) => {
Ok(String::with_capacity(0))
}
TerminalInputEvent::ToggleOutputPause => {
self.force_pause.fetch_not(Ordering::Release);
Ok(String::with_capacity(0))
}
}
}
fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool {
provider.input_in_progress() || self.force_pause.load(Ordering::Acquire)
}
}