use crate::{
display::{renderers::get_ansi_escape_code_regex, tracing::SuperConsoleLogMessage},
errors::LisaError,
};
use fnv::FnvHasher;
use owo_colors::{AnsiColors, DynColors, OwoColorize};
use std::{
fmt::Write,
hash::{Hash, Hasher},
str::FromStr,
};
use tracing::Level;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
const ESC: char = '\x1B';
pub const EMPTY_HEADER: &str = " |";
static COLOR_CHOICES: &[AnsiColors] = &[
AnsiColors::BrightGreen,
AnsiColors::BrightRed,
AnsiColors::BrightCyan,
AnsiColors::BrightYellow,
AnsiColors::BrightMagenta,
AnsiColors::BrightBlue,
AnsiColors::Green,
AnsiColors::Red,
AnsiColors::Cyan,
AnsiColors::Yellow,
AnsiColors::Magenta,
AnsiColors::Blue,
];
#[allow(clippy::inline_always)]
#[inline(always)]
#[must_use]
pub const fn header_width() -> usize {
EMPTY_HEADER.len()
}
pub fn create_header(
app_name: &'static str,
log: &SuperConsoleLogMessage,
) -> Result<String, LisaError> {
let mut header = String::with_capacity(header_width());
let subsystem = log.subsytem().unwrap_or(app_name);
let subsystem_width = subsystem.width();
let mut padding_needing = 6_usize.saturating_sub(subsystem_width);
while padding_needing > 0 {
header.push(' ');
padding_needing -= 1;
}
if subsystem_width < 6 {
write!(
&mut header,
"{}/",
subsystem.color(color_for_subsystem(subsystem, log.color())),
)?;
} else {
let mut short_subsystem = String::with_capacity(6);
let mut current_width = 0_usize;
for char in subsystem.chars() {
let char_width = char.width().unwrap_or_default();
if char_width + current_width > 3 {
break;
}
short_subsystem.push(char);
current_width += char_width;
}
while current_width < 6 {
short_subsystem.push('.');
current_width += 1;
}
write!(
&mut header,
"{}/",
subsystem.color(color_for_subsystem(subsystem, log.color())),
)?;
}
write!(
&mut header,
"{}",
match *log.level() {
Level::ERROR => format!("{}", "ERROR".red().bold()),
Level::WARN => format!("{}", "WARN ".bright_red().bold()),
Level::INFO => format!("{}", "INFO ".white().bold()),
Level::DEBUG => format!("{}", "DEBUG".cyan().bold()),
Level::TRACE => format!("{}", "TRACE".magenta().bold()),
},
)?;
header.push('|');
Ok(header)
}
#[must_use]
pub const fn calculate_message_width(terminal_width: u16) -> usize {
debug_assert!(
terminal_width >= 40,
"terminal width below 40? this should never happen!"
);
(terminal_width as usize) - 13_usize - calculate_tailer_width(terminal_width)
}
#[must_use]
pub const fn calculate_tailer_width(terminal_width: u16) -> usize {
debug_assert!(
terminal_width >= 40,
"terminal width below 40? this should never happen!"
);
(1 + (10 * (terminal_width / 40))) as usize
}
#[must_use]
pub fn move_cursor(direction: CursorDirection, width: usize) -> String {
if width == 0 {
return String::with_capacity(0);
}
let formatted = format!("{width}");
let mut result = String::with_capacity(3 + formatted.len());
result.push(ESC);
result.push('[');
result.push_str(&formatted);
result.push(match direction {
CursorDirection::Up => 'A',
CursorDirection::Down => 'B',
CursorDirection::Left => 'D',
CursorDirection::Right => 'C',
});
result
}
#[must_use]
pub fn erase_line(config: ClearLine) -> String {
let mut result = String::with_capacity(4);
result.push(ESC);
result.push('[');
result.push(match config {
ClearLine::CursorToBeginning => '1',
ClearLine::CursorToEnd => '0',
ClearLine::EntireLine => '2',
});
result.push('K');
result
}
#[must_use]
pub fn pad_to_width(mut to_pad: String, ensure: usize) -> String {
let mut owned = None;
if to_pad.contains('\x1B') {
owned = Some(get_ansi_escape_code_regex().replace_all(&to_pad, ""));
}
let mut current_character_width = owned
.as_deref()
.unwrap_or(&to_pad)
.chars()
.map(|character| character.width().unwrap_or_default())
.sum::<usize>();
while current_character_width < ensure {
to_pad.push(' ');
current_character_width += 1;
}
to_pad
}
pub fn chunk_string_into_width(target_message_width: usize, message: &str) -> Vec<String> {
let mut messages = Vec::new();
let mut current_buff = String::with_capacity(target_message_width);
let mut current_width = 0_usize;
let mut skip_amount = 0_usize;
let mut iterator = message.chars().peekable();
while let Some(character) = iterator.next() {
let character_width = character.width().unwrap_or_default();
if character == ESC {
if iterator.peek() == Some(&'[') {
let mut csi_iterator = iterator.clone();
_ = csi_iterator.next();
skip_amount += 2;
let mut parameter_bytes_valid = true;
for character in csi_iterator {
let raw = character as u32;
if (0x30_u32..=0x3F_u32).contains(&raw) && parameter_bytes_valid {
skip_amount += 1;
} else if (0x20_u32..=0x2F_u32).contains(&raw) {
parameter_bytes_valid = false;
skip_amount += 1;
} else if (0x40_u32..=0x7E_u32).contains(&raw) {
skip_amount += 1;
break;
} else {
skip_amount = 0;
break;
}
}
}
}
if skip_amount > 0 {
current_buff.push(character);
skip_amount -= 1;
continue;
}
if character == '\n' {
messages.push(pad_to_width(current_buff, target_message_width));
current_buff = String::with_capacity(target_message_width);
current_width = 0;
} else if character == '\r' || character == '�' {
} else {
if current_width + character_width >= target_message_width {
messages.push(pad_to_width(current_buff, target_message_width));
current_buff = String::with_capacity(target_message_width);
current_width = 0;
}
current_buff.push(character);
current_width += character_width;
}
}
if !current_buff.is_empty() {
messages.push(pad_to_width(current_buff, target_message_width));
}
messages
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CursorDirection {
Up,
Down,
Left,
Right,
}
#[allow(unused)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ClearLine {
CursorToBeginning,
CursorToEnd,
EntireLine,
}
#[must_use]
fn color_for_subsystem(subsystem: &str, color: Option<&str>) -> DynColors {
if let Some(explicit_color) = color
&& let Ok(explicit) = DynColors::from_str(explicit_color)
{
return explicit;
}
let mut hasher = FnvHasher::with_key(69420);
subsystem.hash(&mut hasher);
let idx =
usize::try_from(hasher.finish() % u64::try_from(COLOR_CHOICES.len()).unwrap_or(u64::MIN))
.unwrap_or(usize::MIN);
DynColors::Ansi(COLOR_CHOICES[idx])
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
pub fn calculate_tailer_sizes_at_widths() {
assert_eq!(calculate_tailer_width(40), 11);
assert_eq!(calculate_tailer_width(80), 21);
assert_eq!(calculate_tailer_width(100), 21);
assert_eq!(calculate_tailer_width(120), 31);
}
#[test]
pub fn calculate_message_sizes_at_widths() {
assert_eq!(calculate_message_width(40), 16);
assert_eq!(calculate_message_width(80), 46);
assert_eq!(calculate_message_width(100), 66);
assert_eq!(calculate_message_width(120), 76);
}
}