1use owo_colors::OwoColorize;
6use std::io::{self, IsTerminal};
7use std::sync::OnceLock;
8use tracing::{Event, Level, Subscriber};
9use tracing_subscriber::Layer;
10use tracing_subscriber::layer::Context;
11use tracing_subscriber::registry::LookupSpan;
12
13static VERBOSE: OnceLock<bool> = OnceLock::new();
16
17pub fn is_verbose() -> bool {
19 VERBOSE.get().copied().unwrap_or(false)
20}
21
22fn use_colors() -> bool {
24 io::stderr().is_terminal() && std::env::var("NO_COLOR").is_err()
25}
26
27mod icons {
30 pub const DEBUG: &str = "[debug]";
31 pub const INFO: &str = "ℹ";
32 pub const WARN: &str = "⚠";
33 pub const ERROR: &str = "✗";
34 pub const TRACE: &str = "[trace]";
35}
36
37pub struct CliLayer {
46 verbose: bool,
47}
48
49impl CliLayer {
50 pub fn new(verbose: bool) -> Self {
54 Self { verbose }
55 }
56}
57
58impl<S> Layer<S> for CliLayer
59where
60 S: Subscriber + for<'a> LookupSpan<'a>,
61{
62 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
63 let meta = event.metadata();
64 let level = meta.level();
65
66 if !self.verbose && (*level == Level::DEBUG || *level == Level::TRACE) {
68 return;
69 }
70
71 let mut visitor = MessageVisitor::default();
73 event.record(&mut visitor);
74 let message = visitor.message.unwrap_or_default();
75
76 let (icon, colored_icon, colored_message) = match *level {
78 Level::ERROR => (
79 icons::ERROR,
80 format_colored(icons::ERROR, "red"),
81 format_colored(&message, "red"),
82 ),
83 Level::WARN => (
84 icons::WARN,
85 format_colored(icons::WARN, "yellow"),
86 format_colored(&message, "yellow"),
87 ),
88 Level::INFO => (
89 icons::INFO,
90 format_colored(icons::INFO, "blue"),
91 message.clone(),
92 ),
93 Level::DEBUG => (
94 icons::DEBUG,
95 format_colored(icons::DEBUG, "bright_black"),
96 message.clone(),
97 ),
98 Level::TRACE => (
99 icons::TRACE,
100 format_colored(icons::TRACE, "bright_black"),
101 format_colored(&message, "bright_black"),
102 ),
103 };
104
105 if use_colors() {
107 eprintln!("{} {}", colored_icon, colored_message);
108 } else {
109 eprintln!("{} {}", icon, message);
110 }
111 }
112}
113
114fn format_colored(text: &str, color: &str) -> String {
116 if !use_colors() {
117 return text.to_string();
118 }
119
120 match color {
121 "red" => text.red().to_string(),
122 "yellow" => text.yellow().to_string(),
123 "blue" => text.blue().to_string(),
124 "green" => text.green().to_string(),
125 "cyan" => text.cyan().to_string(),
126 "bright_black" => text.bright_black().to_string(),
127 _ => text.to_string(),
128 }
129}
130
131#[derive(Default)]
135struct MessageVisitor {
136 message: Option<String>,
137}
138
139impl tracing::field::Visit for MessageVisitor {
140 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
141 if field.name() == "message" {
142 self.message = Some(format!("{:?}", value));
143 }
144 }
145
146 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
147 if field.name() == "message" {
148 self.message = Some(value.to_string());
149 }
150 }
151}
152
153pub fn init(verbose: bool) {
172 use tracing_subscriber::EnvFilter;
173 use tracing_subscriber::layer::SubscriberExt;
174 use tracing_subscriber::util::SubscriberInitExt;
175
176 let _ = VERBOSE.set(verbose);
178
179 let default_level = if verbose { "debug" } else { "info" };
181 let filter =
182 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
183
184 tracing_subscriber::registry()
186 .with(filter)
187 .with(CliLayer::new(verbose))
188 .init();
189}
190
191#[cfg(test)]
192mod tests {
193
194 #[test]
195 fn test_is_verbose_default_false() {
196 }
200
201 #[test]
202 fn test_format_colored_no_color() {
203 }
206}