use crate::{
display::{
renderers::{
ConsoleOutputFeatures, ConsoleRenderer,
color::{
fields::{create_combined_message, create_field_tailers},
helpers::{
EMPTY_HEADER, calculate_message_width, calculate_tailer_width,
chunk_string_into_width, create_header, erase_line, move_cursor, pad_to_width,
},
terminal::TerminalState,
},
},
tracing::{FlattenedTracingField, SuperConsoleLogMessage},
},
errors::LisaError,
input::{InputProvider, TerminalInputEvent},
tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use owo_colors::OwoColorize;
use parking_lot::Mutex;
use std::{
env::var as env_var,
fmt::Write,
hash::BuildHasherDefault,
sync::atomic::{AtomicBool, Ordering},
};
use tracing::Level;
mod fields;
mod helpers;
mod terminal;
#[derive(Debug)]
pub struct ColorConsoleRenderer {
force_pause: AtomicBool,
new_ps1: Mutex<Option<String>>,
state: Mutex<TerminalState>,
task_lines_rendered: Mutex<u16>,
}
impl ColorConsoleRenderer {
#[must_use]
pub fn new() -> Self {
Self {
force_pause: AtomicBool::new(false),
new_ps1: Mutex::new(None),
state: Mutex::new(TerminalState::new(Self::default_ps1_impl())),
task_lines_rendered: Mutex::new(0),
}
}
#[must_use]
fn default_ps1_impl() -> String {
"$ ".to_owned()
}
}
impl Default for ColorConsoleRenderer {
fn default() -> Self {
Self::new()
}
}
impl ConsoleRenderer for ColorConsoleRenderer {
fn should_use_renderer(
&self,
stream_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("color");
}
for no_color_var in ["NO_COLOR", "NOCOLOR"] {
if env_var(no_color_var).as_deref() == Ok("1") {
return false;
}
}
for color_var in ["CLICOLOR", "CLI_COLOR", "CLICOLOR_FORCE"] {
let env = env_var(color_var);
if env.as_deref() == Ok("0") {
return false;
}
if env.as_deref() == Ok("1") {
return true;
}
}
if !stream_features.is_atty() {
return false;
}
if !stream_features.enable_ansi() {
return false;
}
true
}
fn render_message(
&self,
app_name: &'static str,
log: SuperConsoleLogMessage,
term_width: u16,
) -> Result<String, LisaError> {
let mut data = String::new();
let header = create_header(app_name, &log)?;
let tailer_width = calculate_tailer_width(term_width);
let msg_width = calculate_message_width(term_width);
let (real_msg, skip_cause) =
if *log.level() == Level::ERROR && log.metadata().contains_key("cause") {
let mut new_message = log
.message()
.map_or("<no message>".to_owned(), ToOwned::to_owned);
new_message.push('\n');
write!(
&mut new_message,
"{}",
log.metadata()
.get("cause")
.unwrap_or_else(|| unreachable!())
)?;
(Some(new_message), true)
} else {
(log.message().map(ToOwned::to_owned).clone(), false)
};
let excessive_field_count = if skip_cause { 4 } else { 3 };
let actual_field_count: usize = log
.metadata()
.values()
.map(FlattenedTracingField::field_count)
.sum();
if actual_field_count > excessive_field_count || log.force_combine() {
let messages = create_combined_message(
msg_width + tailer_width,
log.metadata(),
real_msg.unwrap_or("<no message>".to_owned()),
log.should_hide_fields_for_humans(),
skip_cause,
);
for (idx, msg) in messages.into_iter().enumerate() {
write!(
&mut data,
"{}{}",
if idx == 0 {
header.as_str()
} else {
EMPTY_HEADER
},
msg,
)?;
writeln!(&mut data)?;
}
} else {
let empty_map = FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default());
let mut fields = create_field_tailers(
tailer_width,
if log.should_hide_fields_for_humans() {
&empty_map
} else {
log.metadata()
},
skip_cause,
false,
);
let mut messages =
chunk_string_into_width(msg_width, &real_msg.unwrap_or("<no message>".to_owned()));
while fields.len() < messages.len() {
fields.push(pad_to_width("|".to_owned(), tailer_width));
}
while messages.len() < fields.len() {
messages.push(pad_to_width(String::new(), msg_width));
}
for idx in 0..fields.len() {
write!(
&mut data,
"{}{}{}",
if idx == 0 {
header.as_str()
} else {
EMPTY_HEADER
},
messages[idx],
fields[idx],
)?;
writeln!(&mut data)?;
}
}
Ok(data)
}
fn default_ps1(&self) -> String {
Self::default_ps1_impl()
}
fn supports_ansi(&self) -> bool {
true
}
fn should_pause_log_events(&self, _provider: &dyn InputProvider) -> bool {
self.force_pause.load(Ordering::Acquire)
}
fn render_input(
&self,
_app_name: &'static str,
provider: &dyn InputProvider,
term_width: u16,
) -> Result<String, LisaError> {
let msg_width = calculate_message_width(term_width);
let tailer_width = calculate_tailer_width(term_width);
let mut state_lock = self.state.lock();
let new_ps1_lock = self.new_ps1.lock();
Ok(state_lock.render_current_standalone(
new_ps1_lock.as_deref(),
msg_width,
tailer_width,
&provider.current_input(),
))
}
fn clear_input(&self, _term_width: u16) -> String {
let state_lock = self.state.lock();
state_lock.clear_current_render()
}
fn clear_task_list(&self, _task_list_size: usize) -> String {
let mut data = String::new();
let task_lines_lock = self.task_lines_rendered.lock();
for _ in 0..*task_lines_lock {
if data.is_empty() {
data = move_cursor(helpers::CursorDirection::Left, 9999);
} else {
data.push_str(&move_cursor(helpers::CursorDirection::Up, 1));
}
data.push_str(&erase_line(helpers::ClearLine::EntireLine));
}
std::mem::drop(task_lines_lock);
data
}
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> {
let Some(running_since) = tasks_running_since else {
return Ok(String::with_capacity(0));
};
if current_task_states.is_empty() {
return Ok(String::with_capacity(0));
}
let mut task_lines_lock = self.task_lines_rendered.lock();
let max_lines_rendered = term_height / 10;
*task_lines_lock = 2;
let my_time = Utc::now();
let mut data = String::new();
data.push('[');
write!(&mut data, "{}", '+'.bright_green())?;
data.push_str("] Running...");
let duration_since_start_time_delta = my_time.signed_duration_since(running_since);
writeln!(
&mut data,
"{}.{}s",
std::cmp::min(0, duration_since_start_time_delta.num_seconds()),
std::cmp::min(0, duration_since_start_time_delta.subsec_micros()),
)?;
let mut keys_to_sort = current_task_states.keys().collect::<Vec<_>>();
keys_to_sort.sort_by(|one, two| {
let first_comp = one.0.cmp(&two.0);
let second_comp = one.1.cmp(&two.1);
if second_comp == std::cmp::Ordering::Equal {
first_comp
} else {
second_comp
}
});
let mut did_break_early = false;
for (tasks_rendered, key) in keys_to_sort.into_iter().enumerate() {
if u16::try_from(tasks_rendered).unwrap_or(u16::MAX) > max_lines_rendered {
did_break_early = true;
break;
}
*task_lines_lock += 1;
let (task_start_time, task_name, status) =
current_task_states.get(key).expect("Guaranteed to exist!");
let time_delta_since_task_start = my_time.signed_duration_since(task_start_time);
match status {
LisaTaskStatus::Inactive => {
writeln!(
&mut data,
"{}",
format!(
" | => {}/{}|{task_name}: {status} [{}.{:04}s...]",
key.0,
key.1,
std::cmp::min(0, time_delta_since_task_start.num_seconds()),
std::cmp::min(0, time_delta_since_task_start.subsec_micros()),
)
.white()
.bold()
)?;
}
LisaTaskStatus::Running(_) => {
writeln!(
&mut data,
"{}",
format!(
" | => {}/{}|{task_name}: {status} [{}.{:04}s...]",
key.0,
key.1,
std::cmp::min(0, time_delta_since_task_start.num_seconds()),
std::cmp::min(0, time_delta_since_task_start.subsec_micros()),
)
.cyan()
.italic()
)?;
}
LisaTaskStatus::Waiting(_) => {
writeln!(
&mut data,
"{}",
format!(
" | => {}/{}|{task_name}: {status} [{}.{:04}s...]",
key.0,
key.1,
std::cmp::min(0, duration_since_start_time_delta.num_seconds()),
std::cmp::min(0, duration_since_start_time_delta.subsec_micros()),
)
.yellow()
.italic()
)?;
}
}
}
if did_break_early {
writeln!(
&mut data,
" | => {} tasks also running...",
current_task_states.len() - usize::from(*task_lines_lock),
)?;
*task_lines_lock += 1;
}
std::mem::drop(task_lines_lock);
Ok(data)
}
fn on_input(
&self,
event: TerminalInputEvent,
provider: &dyn InputProvider,
) -> Result<String, LisaError> {
let mut state_lock = self.state.lock();
Ok(state_lock.on_input_event(provider, event, &self.force_pause))
}
fn update_ps1(&self, new_ps1: String) {
let mut new_ps1_lock = self.new_ps1.lock();
_ = new_ps1_lock.insert(new_ps1);
}
}