use terminal_size::{Width, terminal_size};
use crate::analyzer::ProjectStats;
use crate::lang::Languages;
use crate::logger::{Level, Log, Verbosity};
pub enum Mode {
Stats {
stats: ProjectStats,
},
Diff {
base: ProjectStats,
target: ProjectStats,
},
}
struct LangLines {
lang: Languages,
lines: u32,
}
struct Remainder {
frac: f64,
index: usize,
}
fn terminal_width() -> Option<u16> {
terminal_size().map(|(Width(w), _)| w)
}
fn format_thousands(n: u32) -> String {
if n == 0 {
return "0".to_string();
}
let mut n = n;
let mut out = String::new();
let mut count: u32 = 0;
while n > 0 {
if count > 0 && count % 3 == 0 {
out.push(',');
}
out.push(char::from_digit(n % 10, 10).unwrap());
n /= 10;
count += 1;
}
out.chars().rev().collect()
}
fn format_size(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1000.0 && unit < UNITS.len() - 1 {
value /= 1000.0;
unit += 1;
}
if unit == 0 {
format!("{} B", bytes) } else {
format!("{:.1} {}", value, UNITS[unit])
}
}
fn render_bar(langs: &[LangLines], half_width: usize, total_lines: u32) -> String {
let langs: Vec<&LangLines> = langs.iter().filter(|ll| ll.lines > 0).collect();
let n = langs.len();
if n == 0 || half_width == 0 || total_lines == 0 {
return String::new();
}
let total_units = half_width.max(n);
let mut units = Vec::with_capacity(n);
let mut required = 0usize;
let mut remainders: Vec<Remainder> = Vec::with_capacity(n);
for (i, ll) in langs.iter().enumerate() {
let quota = ll.lines as f64 / total_lines as f64 * total_units as f64;
let mut u = quota.floor() as usize;
if u == 0 {
u = 1;
}
units.push(u);
required += u;
remainders.push(Remainder {
frac: quota - quota.floor(),
index: i,
});
}
let total = total_units.max(required);
let mut remaining = total - required;
remainders.sort_by(|a, b| b.frac.total_cmp(&a.frac));
while remaining > 0 {
for r in &remainders {
if remaining == 0 {
break;
}
units[r.index] += 1;
remaining -= 1;
}
}
let mut bar = String::new();
for (i, ll) in langs.iter().enumerate() {
bar.push_str(&ll.lang.to_rgb().fg_str(&"━".repeat(units[i])));
}
bar
}
pub fn render(mode: Mode) -> Result<(), Log> {
let Some(terminal_w) = terminal_width() else {
return Err(Log::new(
Level::Error,
"Unable to get terminal size, please make sure to run this program in a terminal"
.to_string(),
Some(Verbosity::Normal),
));
};
let half_width = (terminal_w as usize) / 2;
match mode {
Mode::Stats { stats } => {
let mut langs: [LangLines; Languages::COUNT] = Languages::ALL.map(|lang| LangLines {
lang,
lines: stats.language_line_map[lang.idx()],
});
langs.sort_unstable_by_key(|ll| std::cmp::Reverse(ll.lines));
if stats.total_lines > 0 {
println!("Language Line Distribution:");
println!("{}", render_bar(&langs, half_width, stats.total_lines));
for ll in &langs {
if ll.lines == 0 {
continue;
}
let share = ll.lines as f64 / stats.total_lines as f64;
let pct = share * 100.0;
let pct_str = if pct < 0.1 {
"< 0.1%".to_string()
} else {
format!("{:.1}%", pct)
};
println!(
"{} {} {} ({} Lines)",
ll.lang.to_rgb().fg_str("●"),
ll.lang.to_str(),
pct_str,
ll.lines
);
}
println!();
}
println!(
"Total: {} lines | Files: {} | Size: {}",
format_thousands(stats.total_lines),
stats.total_files,
format_size(stats.total_bytes)
);
Ok(())
}
Mode::Diff { base, target } => {
let mut deltas: Vec<(Languages, i64)> = Vec::new();
for lang in Languages::ALL {
let delta = target.language_line_map[lang.idx()] as i64
- base.language_line_map[lang.idx()] as i64;
if delta != 0 {
deltas.push((lang, delta));
}
}
deltas.sort_unstable_by_key(|(_, d)| d.abs());
deltas.reverse();
println!("Line Diff:");
if !deltas.is_empty() {
let lang_lines: Vec<LangLines> = deltas
.iter()
.map(|(lang, d)| LangLines {
lang: *lang,
lines: d.unsigned_abs() as u32,
})
.collect();
let total_abs: u32 = lang_lines.iter().map(|ll| ll.lines).sum();
println!("{}", render_bar(&lang_lines, half_width, total_abs));
for (lang, d) in &deltas {
let sign = if *d >= 0 { "+" } else { "-" };
println!(
"{} {} {}{} Lines",
lang.to_rgb().fg_str("●"),
lang.to_str(),
sign,
format_thousands(d.unsigned_abs() as u32)
);
}
println!();
}
let line_delta = target.total_lines as i64 - base.total_lines as i64;
let file_delta = target.total_files as i64 - base.total_files as i64;
let byte_delta = target.total_bytes as i64 - base.total_bytes as i64;
println!(
"Total: {}{} Lines | Files: {}{} | Size: {}{}",
signed_prefix(line_delta),
format_thousands(line_delta.unsigned_abs() as u32),
signed_prefix(file_delta),
file_delta.unsigned_abs(),
signed_prefix(byte_delta),
format_size(byte_delta.unsigned_abs())
);
Ok(())
}
}
}
fn signed_prefix(n: i64) -> &'static str {
if n >= 0 { "+" } else { "-" }
}