Skip to content

Commit 08e4974

Browse files
art049claude
andcommitted
feat(cli): enhance local logger with richer visual hierarchy
- Group headers: replace ">>>" with a single bold orange play icon - Spinner: custom dot-animation with elapsed time counter - Completion: green checkmark with dimmed name and elapsed duration when groups end - Errors: red cross prefix; Warnings: yellow triangle prefix - Debug: dimmed dot prefix for quieter output - Info: consistent 2-space indent across all log levels - Main error display: show primary error once, causes in debug only Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a716ce7 commit 08e4974

2 files changed

Lines changed: 101 additions & 36 deletions

File tree

src/local_logger.rs

Lines changed: 93 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ use crate::logger::{GroupEvent, JsonEvent, get_group_event, get_json_event};
1616

1717
pub const CODSPEED_U8_COLOR_CODE: u8 = 208; // #FF8700
1818

19+
/// Spinner tick characters - smooth animation for a polished feel
20+
const SPINNER_TICKS: &[&str] = &[" ", ". ", "..", " ."];
21+
1922
lazy_static! {
2023
pub static ref SPINNER: Arc<Mutex<Option<ProgressBar>>> = Arc::new(Mutex::new(None));
2124
pub static ref IS_TTY: bool = std::io::IsTerminal::is_terminal(&std::io::stdout());
25+
static ref CURRENT_GROUP_NAME: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
2226
}
2327

2428
/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar.
@@ -67,26 +71,34 @@ impl Log for LocalLogger {
6771
if let Some(group_event) = get_group_event(record) {
6872
match group_event {
6973
GroupEvent::Start(name) | GroupEvent::StartOpened(name) => {
70-
eprintln!(
71-
"\n{}",
72-
style(format!("►►► {name} "))
73-
.bold()
74-
.color256(CODSPEED_U8_COLOR_CODE)
75-
);
74+
let header = format_group_header(&name);
75+
eprintln!("\n{header}");
76+
77+
// Store current group name for completion message
78+
if let Ok(mut current) = CURRENT_GROUP_NAME.lock() {
79+
*current = Some(name.clone());
80+
}
7681

7782
if *IS_TTY {
7883
let spinner = ProgressBar::new_spinner();
84+
let tick_strings: Vec<String> = SPINNER_TICKS
85+
.iter()
86+
.map(|s| format!("{}", style(s).color256(CODSPEED_U8_COLOR_CODE).dim()))
87+
.collect();
88+
let tick_strs: Vec<&str> =
89+
tick_strings.iter().map(|s| s.as_str()).collect();
90+
7991
spinner.set_style(
8092
ProgressStyle::with_template(
81-
format!(
82-
" {{spinner:>.{CODSPEED_U8_COLOR_CODE}}} {{wide_msg:.{CODSPEED_U8_COLOR_CODE}.bold}}"
83-
)
84-
.as_str(),
93+
&format!(
94+
" {{spinner}} {{wide_msg:.{CODSPEED_U8_COLOR_CODE}}} {{elapsed:.dim}}"
95+
),
8596
)
86-
.unwrap(),
97+
.unwrap()
98+
.tick_strings(&tick_strs),
8799
);
88100
spinner.set_message(format!("{name}..."));
89-
spinner.enable_steady_tick(Duration::from_millis(100));
101+
spinner.enable_steady_tick(Duration::from_millis(300));
90102
SPINNER.lock().unwrap().replace(spinner);
91103
} else {
92104
eprintln!("{name}...");
@@ -95,8 +107,22 @@ impl Log for LocalLogger {
95107
GroupEvent::End => {
96108
if *IS_TTY {
97109
let mut spinner = SPINNER.lock().unwrap();
98-
if let Some(spinner) = spinner.as_mut() {
99-
spinner.finish_and_clear();
110+
if let Some(pb) = spinner.as_mut() {
111+
let elapsed = pb.elapsed();
112+
pb.finish_and_clear();
113+
114+
// Show completion message with checkmark
115+
if let Ok(mut current) = CURRENT_GROUP_NAME.lock() {
116+
if let Some(name) = current.take() {
117+
let elapsed_str = format_elapsed(elapsed);
118+
eprintln!(
119+
" {} {} {}",
120+
style("\u{2714}").green().bold(),
121+
style(name).dim(),
122+
style(elapsed_str).dim(),
123+
);
124+
}
125+
}
100126
}
101127
}
102128
}
@@ -118,26 +144,62 @@ impl Log for LocalLogger {
118144
}
119145
}
120146

147+
/// Format a group header with styled prefix
148+
fn format_group_header(name: &str) -> String {
149+
let prefix = style("\u{25B6}").color256(CODSPEED_U8_COLOR_CODE).bold();
150+
let title = style(name).bold();
151+
format!("{prefix} {title}")
152+
}
153+
154+
/// Format elapsed duration in a compact human-readable way
155+
fn format_elapsed(duration: Duration) -> String {
156+
let secs = duration.as_secs();
157+
let millis = duration.as_millis();
158+
159+
if secs >= 60 {
160+
let mins = secs / 60;
161+
let remaining_secs = secs % 60;
162+
format!("{mins}m {remaining_secs}s")
163+
} else if secs > 0 {
164+
format!("{secs}.{:01}s", (millis % 1000) / 100)
165+
} else {
166+
format!("{millis}ms")
167+
}
168+
}
169+
121170
/// Print a log record to the console with the appropriate style
122171
fn print_record(record: &log::Record) {
123-
let error_style = Style::new().red();
124-
let info_style = Style::new().white();
125-
let warn_style = Style::new().yellow();
126-
let debug_style = Style::new().blue().dim();
127-
let trace_style = Style::new().black().dim();
128-
129172
match record.level() {
130-
log::Level::Error => eprintln!("{}", error_style.apply_to(record.args())),
131-
log::Level::Warn => eprintln!("{}", warn_style.apply_to(record.args())),
132-
log::Level::Info => eprintln!("{}", info_style.apply_to(record.args())),
133-
log::Level::Debug => eprintln!(
134-
"{}",
135-
debug_style.apply_to(format!("[DEBUG::{}] {}", record.target(), record.args())),
136-
),
137-
log::Level::Trace => eprintln!(
138-
"{}",
139-
trace_style.apply_to(format!("[TRACE::{}] {}", record.target(), record.args()))
140-
),
173+
log::Level::Error => {
174+
let prefix = style("\u{2717}").red().bold();
175+
let msg = Style::new().red().apply_to(record.args());
176+
eprintln!(" {prefix} {msg}");
177+
}
178+
log::Level::Warn => {
179+
let prefix = style("\u{25B2}").yellow();
180+
let msg = Style::new().yellow().apply_to(record.args());
181+
eprintln!(" {prefix} {msg}");
182+
}
183+
log::Level::Info => {
184+
let msg = Style::new().white().apply_to(record.args());
185+
eprintln!(" {msg}");
186+
}
187+
log::Level::Debug => {
188+
let prefix = style("\u{00B7}").dim();
189+
let msg = Style::new()
190+
.blue()
191+
.dim()
192+
.apply_to(format!("{}", record.args()));
193+
eprintln!(" {prefix} {msg}");
194+
}
195+
log::Level::Trace => {
196+
let msg = Style::new().black().dim().apply_to(format!(
197+
"[TRACE::{}] {}",
198+
record.target(),
199+
record.args()
200+
));
201+
eprintln!(" {msg}");
202+
}
141203
}
142204
}
143205

src/main.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ use log::log_enabled;
66
async fn main() {
77
let res = cli::run().await;
88
if let Err(err) = res {
9-
for cause in err.chain() {
9+
// Show the primary error
10+
let mut chain = err.chain();
11+
if let Some(primary) = chain.next() {
1012
if log_enabled!(log::Level::Error) {
11-
log::error!("{} {}", style("Error:").bold().red(), style(cause).red());
13+
log::error!("{}", style(primary).red());
1214
} else {
13-
eprintln!("Error: {cause}");
15+
eprintln!("{} {}", style("Error:").bold().red(), style(primary).red());
1416
}
1517
}
18+
// Show causes in debug mode
1619
if log_enabled!(log::Level::Debug) {
17-
for e in err.chain().skip(1) {
18-
log::debug!("Caused by: {e}");
20+
for cause in chain {
21+
log::debug!("Caused by: {cause}");
1922
}
2023
}
2124
clean_logger();

0 commit comments

Comments
 (0)