use std::fmt::Display;
use std::path::Path;
use indicatif::{ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
pub const CHECK: &str = "✓";
pub const BULLET: &str = "•";
pub const PROGRESS_MIN_FILES: usize = 20;
pub fn accent(value: impl Display) -> String {
value.cyan().to_string()
}
pub fn path(value: &Path) -> String {
value.display().to_string().cyan().to_string()
}
pub fn success(value: impl Display) -> String {
value.green().to_string()
}
pub fn warn(value: impl Display) -> String {
value.yellow().to_string()
}
pub fn danger(value: impl Display) -> String {
value.red().to_string()
}
pub fn dim(value: impl Display) -> String {
value.dimmed().to_string()
}
pub fn bold(value: impl Display) -> String {
value.bold().to_string()
}
const LINE_RANGE_CAP: usize = 12;
pub fn line_span(start_row: usize, end_row: usize) -> String {
if start_row == end_row {
format!("L{}", start_row + 1)
} else {
format!("L{}–{}", start_row + 1, end_row + 1)
}
}
pub fn format_line_ranges(rows: impl Iterator<Item = (usize, usize)>, verbose: bool) -> String {
let spans: Vec<String> = rows.map(|(start, end)| line_span(start, end)).collect();
if verbose || spans.len() <= LINE_RANGE_CAP {
return spans.join(", ");
}
let hidden = spans.len() - LINE_RANGE_CAP;
format!("{}, +{hidden} more", spans[..LINE_RANGE_CAP].join(", "))
}
pub fn clap_styles() -> clap::builder::Styles {
use clap::builder::styling::{AnsiColor, Style};
clap::builder::Styles::styled()
.header(Style::new().bold().fg_color(Some(AnsiColor::Cyan.into())))
.usage(Style::new().bold().fg_color(Some(AnsiColor::Cyan.into())))
.literal(Style::new().bold().fg_color(Some(AnsiColor::Green.into())))
.placeholder(Style::new().fg_color(Some(AnsiColor::BrightBlack.into())))
.valid(Style::new().fg_color(Some(AnsiColor::Green.into())))
.invalid(Style::new().bold().fg_color(Some(AnsiColor::Red.into())))
.error(Style::new().bold().fg_color(Some(AnsiColor::Red.into())))
}
pub fn progress_bar(len: u64) -> ProgressBar {
use std::io::IsTerminal;
if !std::io::stderr().is_terminal() {
return ProgressBar::hidden();
}
let bar = ProgressBar::new(len);
let style = ProgressStyle::with_template(" {spinner:.cyan} [{bar:28.cyan/dim}] {pos}/{len} files {msg}")
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("━━╌");
bar.set_style(style);
bar
}
pub fn print_summary(total_files: usize, modified_files: usize, comments_removed: usize, dry_run: bool) {
let prefix = if dry_run { "[DRY RUN] " } else { "" };
let modified_verb = if dry_run { "would be modified" } else { "modified" };
anstream::println!();
anstream::println!(
"{}{} {} files processed, {} {}, {} comments removed",
dim(prefix),
bold("Summary:"),
accent(total_files),
accent(modified_files),
modified_verb,
accent(comments_removed),
);
if total_files > 0 && modified_files == 0 {
anstream::println!(
"{}",
dim("All files were already comment-free or only contained preserved comments.")
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_span_renders_single_and_multi_line() {
assert_eq!(line_span(0, 0), "L1");
assert_eq!(line_span(6, 8), "L7–9");
}
#[test]
fn format_line_ranges_joins_spans() {
let rows = [(0, 0), (2, 2), (4, 5)];
assert_eq!(format_line_ranges(rows.into_iter(), false), "L1, L3, L5–6");
}
#[test]
fn format_line_ranges_caps_when_not_verbose() {
let rows: Vec<(usize, usize)> = (0..15).map(|row| (row, row)).collect();
let capped = format_line_ranges(rows.iter().copied(), false);
assert!(capped.ends_with("+3 more"), "expected cap suffix, got: {capped}");
assert_eq!(capped.matches('L').count(), LINE_RANGE_CAP);
}
#[test]
fn format_line_ranges_shows_all_when_verbose() {
let rows: Vec<(usize, usize)> = (0..15).map(|row| (row, row)).collect();
let full = format_line_ranges(rows.iter().copied(), true);
assert!(!full.contains("more"));
assert_eq!(full.matches('L').count(), 15);
}
}