use crate::json::{JsonValue, stringify_pretty_multi_line};
use colored::{ColoredString, Colorize};
use std::fmt::{Debug, Display};
use std::io::Write;
use std::sync::Arc;
use tokio::sync::Mutex;
struct PrettyPrinter {
indention: String,
#[cfg(not(any(test, feature = "test")))]
output: Arc<Mutex<Box<dyn Write + Send>>>,
#[cfg(any(test, feature = "test"))]
output: Arc<Mutex<Vec<u8>>>,
}
impl PrettyPrinter {
pub fn new() -> Self {
#[cfg(not(any(test, feature = "test")))]
use std::io::stderr;
Self {
indention: String::from(" "),
#[cfg(not(any(test, feature = "test")))]
output: Arc::new(Mutex::new(Box::new(stderr()))),
#[cfg(any(test, feature = "test"))]
output: Arc::new(Mutex::new(Vec::new())),
}
}
async fn write(&self, text: String) {
self
.output
.lock()
.await
.write_all(text.as_bytes())
.expect("write to stderr");
}
#[cfg(any(test, feature = "test"))]
async fn stringify(&self) -> String {
use regex::{Regex, RegexBuilder};
use std::sync::LazyLock;
static RE_COLORS: LazyLock<Regex> = LazyLock::new(|| {
RegexBuilder::new("\u{001b}\\[[0-9;]*m")
.build()
.expect("valid regex literal")
});
let text: String = String::from_utf8(self.output.lock().await.to_vec()).expect("output is valid UTF-8");
RE_COLORS.replace_all(&text, "").to_string()
}
}
pub struct PrettyPrint {
prefix: String,
suffix: String,
printer: Arc<PrettyPrinter>,
}
impl PrettyPrint {
#[must_use]
pub fn new() -> Self {
Self {
prefix: String::new(),
suffix: String::from("\n"),
printer: Arc::new(PrettyPrinter::new()),
}
}
fn new_indented(&mut self) -> Self {
Self {
prefix: format!("{}{}", self.prefix, self.printer.indention),
suffix: self.suffix.clone(),
printer: self.printer.clone(),
}
}
pub async fn category(&mut self, text: &str) -> PrettyPrint {
self.write_line(text.white().bold().to_string() + ":").await;
self.new_indented()
}
pub async fn get_list(&mut self, text: &str) -> PrettyPrint {
self.write_line(text.white().to_string() + ":").await;
self.new_indented()
}
pub async fn add_warning(&self, text: &str) {
self.write_line(text.yellow().bold()).await;
}
pub async fn add_key_value<K: Display + ?Sized, V: Debug + ?Sized>(&self, key: &K, value: &V) {
self.write_line(format!("{key}: {}", get_formatted_value(value))).await;
}
pub async fn add_key_json<K: Display + ?Sized>(&self, key: &K, value: &JsonValue) {
let key_string = format!("{key}: ");
self
.write_line(format!(
"{key_string}{}",
stringify_pretty_multi_line(value, 80, 1, key_string.len()).bright_green()
))
.await;
}
pub async fn add_value<V: Debug>(&self, value: &V) {
self.write_line(get_formatted_value(value)).await;
}
pub async fn add_table(&self, name: &str, headers: &[&str], rows: &[Vec<String>]) {
let col_count = headers.len();
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate().take(col_count) {
widths[i] = widths[i].max(cell.len());
}
}
self.write_line(name.white().to_string() + ":").await;
let header_cells: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, h)| format!("{:>width$}", h, width = widths[i]).white().to_string())
.collect();
self.write_line(format!(" {}", header_cells.join(" │ "))).await;
let sep_cells: Vec<String> = widths.iter().map(|w| "─".repeat(*w)).collect();
self.write_line(format!(" {}", sep_cells.join("─┼─"))).await;
for row in rows {
let cells: Vec<String> = row
.iter()
.enumerate()
.take(col_count)
.map(|(i, cell)| format!("{:>width$}", cell, width = widths[i]).bright_cyan().to_string())
.collect();
self.write_line(format!(" {}", cells.join(" │ "))).await;
}
}
async fn write_line<T: Display>(&self, line: T) {
self
.printer
.write(format!("{}{}{}", self.prefix, line, self.suffix))
.await;
}
#[cfg(any(test, feature = "test"))]
pub async fn stringify(&self) -> String {
self.printer.stringify().await
}
}
impl Default for PrettyPrint {
fn default() -> Self {
Self::new()
}
}
fn get_formatted_value<V: Debug + ?Sized>(value: &V) -> ColoredString {
let type_name = std::any::type_name::<V>();
if type_name.starts_with("versatiles_lib::shared::") {
return format!("{value:?}").bright_blue();
}
match type_name {
"f32" | "f64" => format!("{value:?}").bright_cyan(),
"i128" | "i16" | "i32" | "i64" | "i8" | "isize" | "u128" | "u16" | "u32" | "u64" | "u8" | "usize" => {
format_integer(value).bright_cyan()
}
"alloc::string::String" | "str" | "&str" => format!("{value:?}").bright_magenta(),
_ => format!("{value:?}").bright_green(),
}
}
fn format_integer<V: Debug + ?Sized>(value: &V) -> String {
let mut text = format!("{value:?}");
let mut formatted = String::new();
while (text.len() > 3)
&& text
.chars()
.nth_back(3)
.expect("len > 3 guarantees nth_back(3)")
.is_numeric()
{
let i = text.len() - 3;
formatted = String::from("_") + &text[i..] + &formatted;
text = String::from(&text[..i]);
}
if formatted.is_empty() { text } else { text + &formatted }
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_new_pretty_print() {
let _printer = PrettyPrint::new();
}
#[tokio::test]
async fn test_writers() {
let mut printer = PrettyPrint::new();
printer.add_warning("test_warning_1").await;
let mut cat = printer.category("test_category_1").await;
cat.get_list("test_list_1").await.add_key_value("string_1", &4).await;
cat.add_warning("test_warning_2").await;
printer.add_warning("test_warning_3").await;
let result = printer.stringify().await;
assert_eq!(
&result,
"test_warning_1\ntest_category_1:\n test_list_1:\n string_1: 4\n test_warning_2\ntest_warning_3\n"
);
}
#[tokio::test]
async fn test_add_table() {
let printer = PrettyPrint::new();
printer
.add_table(
"data",
&["name", "count"],
&[
vec!["a".to_string(), "1".to_string()],
vec!["bb".to_string(), "200".to_string()],
],
)
.await;
let result = printer.stringify().await;
assert_eq!(
result.split('\n').collect::<Vec<_>>(),
[
"data:",
" name │ count",
" ─────┼──────",
" a │ 1",
" bb │ 200",
""
]
);
}
#[test]
#[should_panic(expected = "assertion")]
fn x() {
assert_eq!("Elon Musk", "Genius");
}
}