use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering};
use git_cliff_core::error::{Error, Result};
use indicatif::{ProgressState, ProgressStyle};
use owo_colors::{OwoColorize, Style, Styled};
use tracing::{Event, Level, Span, Subscriber};
use tracing_indicatif::IndicatifLayer;
use tracing_indicatif::span_ext::IndicatifSpanExt;
use tracing_subscriber::fmt::FmtContext;
use tracing_subscriber::fmt::format::{self, FormatEvent, FormatFields};
use tracing_subscriber::layer::{Context, Layer, SubscriberExt};
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Registry};
static MAX_MODULE_WIDTH: AtomicUsize = AtomicUsize::new(0);
const ROOT_SPINNER_TICKS: &[&str] = &["◐", "◓", "◑", "◒"];
const CHILD_SPINNER_TICKS: &[&str] = &["◴", "◷", "◶", "◵"];
struct Padded<T> {
value: T,
width: usize,
}
impl<T: fmt::Display> fmt::Display for Padded<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{: <width$}", self.value, width = self.width)
}
}
fn max_target_width(target: &str) -> usize {
let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed);
if max_width < target.len() {
MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed);
target.len()
} else {
max_width
}
}
fn style_level(level: Level) -> Styled<&'static str> {
match level {
Level::ERROR => Style::new().red().bold().style("ERROR"),
Level::WARN => Style::new().yellow().bold().style("WARN"),
Level::INFO => Style::new().green().bold().style("INFO"),
Level::DEBUG => Style::new().blue().bold().style("DEBUG"),
Level::TRACE => Style::new().magenta().bold().style("TRACE"),
}
}
fn progress_color(state: &ProgressState) -> (u8, u8, u8) {
let elapsed = state.elapsed().as_secs_f32();
let t = (elapsed / 32.0).min(1.0);
if t < 0.5 {
let nt = t * 2.0;
(lerp(140, 230, nt), lerp(200, 210, nt), lerp(160, 150, nt))
} else {
let nt = (t - 0.5) * 2.0;
(lerp(230, 230, nt), lerp(210, 140, nt), lerp(150, 140, nt))
}
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn lerp(a: u8, b: u8, t: f32) -> u8 {
((f32::from(a) + (f32::from(b) - f32::from(a)) * t).clamp(0.0, 255.0)) as u8
}
fn elapsed_subsec_key(state: &ProgressState, writer: &mut dyn fmt::Write) {
let seconds = state.elapsed().as_secs();
let sub_seconds = (state.elapsed().as_millis() % 1000) / 100;
let (r, g, b) = progress_color(state);
let _ = write!(
writer,
"{}",
Style::new()
.truecolor(r, g, b)
.style(format!("{seconds}.{sub_seconds}s"))
);
}
fn spinner_key(state: &ProgressState, writer: &mut dyn fmt::Write, ticks: &'static [&'static str]) {
let index = ((state.elapsed().as_millis() / 100) as usize) % ticks.len();
let (r, g, b) = progress_color(state);
let _ = write!(
writer,
"{}",
Style::new().truecolor(r, g, b).style(ticks[index])
);
}
fn root_spinner_key(state: &ProgressState, writer: &mut dyn fmt::Write) {
spinner_key(state, writer, ROOT_SPINNER_TICKS);
}
fn child_spinner_key(state: &ProgressState, writer: &mut dyn fmt::Write) {
spinner_key(state, writer, CHILD_SPINNER_TICKS);
}
fn indicatif_progress_style(spinner_key: fn(&ProgressState, &mut dyn fmt::Write)) -> ProgressStyle {
ProgressStyle::with_template(
"{span_child_prefix}{spinner} {wide_msg} {span_name} {span_fields} [{elapsed_subsec}]",
)
.unwrap()
.with_key("elapsed_subsec", elapsed_subsec_key)
.with_key("spinner", spinner_key)
}
struct SpinnerStyleLayer;
impl<S> Layer<S> for SpinnerStyleLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_enter(&self, id: &tracing::span::Id, ctx: Context<'_, S>) {
let ticks = if ctx.span(id).is_some_and(|span| span.parent().is_some()) {
child_spinner_key
} else {
root_spinner_key
};
Span::current().pb_set_style(&indicatif_progress_style(ticks));
}
}
struct GitCliffFormatter;
impl<S, N> FormatEvent<S, N> for GitCliffFormatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &FmtContext<'_, S, N>,
mut writer: format::Writer<'_>,
event: &Event<'_>,
) -> fmt::Result {
let metadata = event.metadata();
let level = style_level(*metadata.level());
let target = metadata.target();
let max_width = max_target_width(target);
write!(
&mut writer,
"{} {} > ",
Padded {
value: level,
width: 5,
},
Padded {
value: target.bright_black().bold(),
width: max_width,
},
)?;
if let Some(scope) = ctx.event_scope() {
for span in scope.from_root() {
write!(writer, "{}", span.name().bright_black().bold())?;
write!(writer, "{}", ": ".bright_black().bold())?;
}
}
ctx.field_format().format_fields(writer.by_ref(), event)?;
writeln!(writer)
}
}
pub fn init() -> Result<()> {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
let indicatif_layer = IndicatifLayer::new()
.with_progress_style(indicatif_progress_style(root_spinner_key))
.with_span_child_prefix_symbol("↳ ")
.with_span_child_prefix_indent(" ");
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(indicatif_layer.get_stderr_writer())
.with_ansi(true)
.event_format(GitCliffFormatter);
let subscriber = Registry::default()
.with(env_filter)
.with(indicatif_layer)
.with(SpinnerStyleLayer)
.with(fmt_layer);
subscriber
.try_init()
.map_err(|e| Error::LoggerError(e.to_string()))
}