use owo_colors::OwoColorize;
use std::io::{self, IsTerminal};
use std::sync::OnceLock;
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
static VERBOSE: OnceLock<bool> = OnceLock::new();
pub fn is_verbose() -> bool {
VERBOSE.get().copied().unwrap_or(false)
}
fn use_colors() -> bool {
io::stderr().is_terminal() && std::env::var("NO_COLOR").is_err()
}
mod icons {
pub const DEBUG: &str = "[debug]";
pub const INFO: &str = "ℹ";
pub const WARN: &str = "⚠";
pub const ERROR: &str = "✗";
pub const TRACE: &str = "[trace]";
}
pub struct CliLayer {
verbose: bool,
}
impl CliLayer {
pub fn new(verbose: bool) -> Self {
Self { verbose }
}
}
impl<S> Layer<S> for CliLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let meta = event.metadata();
let level = meta.level();
if !self.verbose && (*level == Level::DEBUG || *level == Level::TRACE) {
return;
}
let mut visitor = MessageVisitor::default();
event.record(&mut visitor);
let message = visitor.message.unwrap_or_default();
let (icon, colored_icon, colored_message) = match *level {
Level::ERROR => (
icons::ERROR,
format_colored(icons::ERROR, "red"),
format_colored(&message, "red"),
),
Level::WARN => (
icons::WARN,
format_colored(icons::WARN, "yellow"),
format_colored(&message, "yellow"),
),
Level::INFO => (
icons::INFO,
format_colored(icons::INFO, "blue"),
message.clone(),
),
Level::DEBUG => (
icons::DEBUG,
format_colored(icons::DEBUG, "bright_black"),
message.clone(),
),
Level::TRACE => (
icons::TRACE,
format_colored(icons::TRACE, "bright_black"),
format_colored(&message, "bright_black"),
),
};
if use_colors() {
eprintln!("{} {}", colored_icon, colored_message);
} else {
eprintln!("{} {}", icon, message);
}
}
}
fn format_colored(text: &str, color: &str) -> String {
if !use_colors() {
return text.to_string();
}
match color {
"red" => text.red().to_string(),
"yellow" => text.yellow().to_string(),
"blue" => text.blue().to_string(),
"green" => text.green().to_string(),
"cyan" => text.cyan().to_string(),
"bright_black" => text.bright_black().to_string(),
_ => text.to_string(),
}
}
#[derive(Default)]
struct MessageVisitor {
message: Option<String>,
}
impl tracing::field::Visit for MessageVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{:?}", value));
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.message = Some(value.to_string());
}
}
}
pub fn init(verbose: bool) {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let _ = VERBOSE.set(verbose);
let default_level = if verbose { "debug" } else { "info" };
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
tracing_subscriber::registry()
.with(filter)
.with(CliLayer::new(verbose))
.init();
}
#[cfg(test)]
mod tests {
#[test]
fn test_is_verbose_default_false() {
}
#[test]
fn test_format_colored_no_color() {
}
}