1use std::fmt;
2use std::sync::atomic::{AtomicUsize, Ordering};
3
4use git_cliff_core::error::{Error, Result};
5use indicatif::{ProgressState, ProgressStyle};
6use owo_colors::{OwoColorize, Style, Styled};
7use tracing::{Event, Level, Span, Subscriber};
8use tracing_indicatif::IndicatifLayer;
9use tracing_indicatif::span_ext::IndicatifSpanExt;
10use tracing_subscriber::fmt::FmtContext;
11use tracing_subscriber::fmt::format::{self, FormatEvent, FormatFields};
12use tracing_subscriber::layer::{Context, Layer, SubscriberExt};
13use tracing_subscriber::registry::LookupSpan;
14use tracing_subscriber::util::SubscriberInitExt;
15use tracing_subscriber::{EnvFilter, Registry};
16
17static MAX_MODULE_WIDTH: AtomicUsize = AtomicUsize::new(0);
19
20const ROOT_SPINNER_TICKS: &[&str] = &["◐", "◓", "◑", "◒"];
22const CHILD_SPINNER_TICKS: &[&str] = &["◴", "◷", "◶", "◵"];
24
25struct Padded<T> {
27 value: T,
28 width: usize,
29}
30
31impl<T: fmt::Display> fmt::Display for Padded<T> {
32 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33 write!(f, "{: <width$}", self.value, width = self.width)
34 }
35}
36
37fn max_target_width(target: &str) -> usize {
39 let max_width = MAX_MODULE_WIDTH.load(Ordering::Relaxed);
40 if max_width < target.len() {
41 MAX_MODULE_WIDTH.store(target.len(), Ordering::Relaxed);
42 target.len()
43 } else {
44 max_width
45 }
46}
47
48fn style_level(level: Level) -> Styled<&'static str> {
50 match level {
51 Level::ERROR => Style::new().red().bold().style("ERROR"),
52 Level::WARN => Style::new().yellow().bold().style("WARN"),
53 Level::INFO => Style::new().green().bold().style("INFO"),
54 Level::DEBUG => Style::new().blue().bold().style("DEBUG"),
55 Level::TRACE => Style::new().magenta().bold().style("TRACE"),
56 }
57}
58
59fn progress_color(state: &ProgressState) -> (u8, u8, u8) {
65 let elapsed = state.elapsed().as_secs_f32();
66 let t = (elapsed / 32.0).min(1.0);
67 if t < 0.5 {
68 let nt = t * 2.0;
69 (lerp(140, 230, nt), lerp(200, 210, nt), lerp(160, 150, nt))
70 } else {
71 let nt = (t - 0.5) * 2.0;
72 (lerp(230, 230, nt), lerp(210, 140, nt), lerp(150, 140, nt))
73 }
74}
75
76#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
78fn lerp(a: u8, b: u8, t: f32) -> u8 {
79 ((f32::from(a) + (f32::from(b) - f32::from(a)) * t).clamp(0.0, 255.0)) as u8
80}
81
82fn elapsed_subsec_key(state: &ProgressState, writer: &mut dyn fmt::Write) {
84 let seconds = state.elapsed().as_secs();
85 let sub_seconds = (state.elapsed().as_millis() % 1000) / 100;
86 let (r, g, b) = progress_color(state);
87 let _ = write!(
88 writer,
89 "{}",
90 Style::new()
91 .truecolor(r, g, b)
92 .style(format!("{seconds}.{sub_seconds}s"))
93 );
94}
95
96fn spinner_key(state: &ProgressState, writer: &mut dyn fmt::Write, ticks: &'static [&'static str]) {
98 let index = ((state.elapsed().as_millis() / 100) as usize) % ticks.len();
99 let (r, g, b) = progress_color(state);
100 let _ = write!(
101 writer,
102 "{}",
103 Style::new().truecolor(r, g, b).style(ticks[index])
104 );
105}
106
107fn root_spinner_key(state: &ProgressState, writer: &mut dyn fmt::Write) {
108 spinner_key(state, writer, ROOT_SPINNER_TICKS);
109}
110
111fn child_spinner_key(state: &ProgressState, writer: &mut dyn fmt::Write) {
112 spinner_key(state, writer, CHILD_SPINNER_TICKS);
113}
114
115fn indicatif_progress_style(spinner_key: fn(&ProgressState, &mut dyn fmt::Write)) -> ProgressStyle {
123 ProgressStyle::with_template(
124 "{span_child_prefix}{spinner} {wide_msg} {span_name} {span_fields} [{elapsed_subsec}]",
125 )
126 .unwrap()
127 .with_key("elapsed_subsec", elapsed_subsec_key)
128 .with_key("spinner", spinner_key)
129}
130
131struct SpinnerStyleLayer;
133
134impl<S> Layer<S> for SpinnerStyleLayer
135where
136 S: Subscriber + for<'a> LookupSpan<'a>,
137{
138 fn on_enter(&self, id: &tracing::span::Id, ctx: Context<'_, S>) {
139 let ticks = if ctx.span(id).is_some_and(|span| span.parent().is_some()) {
140 child_spinner_key
141 } else {
142 root_spinner_key
143 };
144 Span::current().pb_set_style(&indicatif_progress_style(ticks));
145 }
146}
147
148struct GitCliffFormatter;
150
151impl<S, N> FormatEvent<S, N> for GitCliffFormatter
152where
153 S: Subscriber + for<'a> LookupSpan<'a>,
154 N: for<'a> FormatFields<'a> + 'static,
155{
156 fn format_event(
157 &self,
158 ctx: &FmtContext<'_, S, N>,
159 mut writer: format::Writer<'_>,
160 event: &Event<'_>,
161 ) -> fmt::Result {
162 let metadata = event.metadata();
163 let level = style_level(*metadata.level());
164 let target = metadata.target();
165 let max_width = max_target_width(target);
166 write!(
167 &mut writer,
168 "{} {} > ",
169 Padded {
170 value: level,
171 width: 5,
172 },
173 Padded {
174 value: target.bright_black().bold(),
175 width: max_width,
176 },
177 )?;
178 if let Some(scope) = ctx.event_scope() {
179 for span in scope.from_root() {
180 write!(writer, "{}", span.name().bright_black().bold())?;
181 write!(writer, "{}", ": ".bright_black().bold())?;
182 }
183 }
184 ctx.field_format().format_fields(writer.by_ref(), event)?;
185 writeln!(writer)
186 }
187}
188
189pub fn init() -> Result<()> {
191 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
193 let indicatif_layer = IndicatifLayer::new()
194 .with_progress_style(indicatif_progress_style(root_spinner_key))
195 .with_span_child_prefix_symbol("↳ ")
196 .with_span_child_prefix_indent(" ");
197 let fmt_layer = tracing_subscriber::fmt::layer()
198 .with_writer(indicatif_layer.get_stderr_writer())
199 .with_ansi(true)
200 .event_format(GitCliffFormatter);
201 let subscriber = Registry::default()
202 .with(env_filter)
203 .with(indicatif_layer)
204 .with(SpinnerStyleLayer)
205 .with(fmt_layer);
206 subscriber
207 .try_init()
208 .map_err(|e| Error::LoggerError(e.to_string()))
209}