use std::io::{self, Write};
use std::path::PathBuf;
use nu_ansi_term::Style;
use crate::loc::LangStat;
use crate::options::parser::CodeContent;
use crate::output::icons::{icon_for_name_ext, iconify_style};
use crate::theme::Theme;
const BAR_WIDTH: usize = 16;
const EIGHTHS: [char; 8] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct Options {
pub content: CodeContent,
}
pub struct Render<'a> {
pub theme: &'a Theme,
pub opts: &'a Options,
pub roots: Vec<PathBuf>,
pub show_icons: bool,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum Align {
Left,
Right,
}
struct Cell {
text: String,
style: Style,
align: Align,
}
impl Cell {
fn new(text: String, style: Style, align: Align) -> Self {
Self { text, style, align }
}
fn width(&self) -> usize {
self.text.chars().count()
}
}
impl Render<'_> {
pub fn render<W: Write>(self, w: &mut W) -> io::Result<()> {
let report = crate::loc::count_roots(&self.roots);
if report.is_empty() {
let style = self.theme.ui.punctuation.unwrap_or_default();
return writeln!(w, "{}", style.paint("No recognised source code found."));
}
let numerics =
locale::Numeric::load_user_locale().unwrap_or_else(|_| locale::Numeric::english());
let header = self.theme.ui.header.unwrap_or_default();
let lang_style = self.theme.ui.date.unwrap_or_default();
let count_style = self
.theme
.ui
.size
.unwrap_or_default()
.number_byte
.unwrap_or_default();
let dim = self.theme.ui.punctuation.unwrap_or_default();
let bar_style = self
.theme
.ui
.filekinds
.unwrap_or_default()
.directory
.unwrap_or_default();
let total_style = if self.theme.ui.colourful.unwrap_or_default() {
Style::default().bold()
} else {
Style::default()
};
let with_lines = matches!(self.opts.content, CodeContent::Lines | CodeContent::Both);
let with_percent = matches!(self.opts.content, CodeContent::Percent | CodeContent::Both);
let mut langs: Vec<&LangStat> = report.languages().collect();
langs.sort_by(|a, b| {
b.counts
.code
.cmp(&a.counts.code)
.then_with(|| a.language.name.cmp(b.language.name))
});
let total = report.total();
let max_code = langs.first().map_or(0, |s| s.counts.code);
let lang_cell = |stat: &LangStat| {
let name = stat.language.name;
if self.show_icons {
let (rep_name, rep_ext) = &stat.rep_file;
let icon = icon_for_name_ext(rep_name, rep_ext.as_deref());
format!("{icon} {name}")
} else {
name.to_string()
}
};
let plain_lang = |name: &str| {
if self.show_icons {
format!(" {name}")
} else {
name.to_string()
}
};
let num = |n: usize, style: Style| Cell::new(numerics.format_int(n), style, Align::Right);
let pct = |part: usize, style: Style| {
let text = if total.code == 0 {
"-".to_string()
} else {
format!("{:.1}%", (part as f64) * 100.0 / (total.code as f64))
};
Cell::new(text, style, Align::Right)
};
let mut header_row = vec![Cell::new(plain_lang("Language"), header, Align::Left)];
let mut body: Vec<Vec<Cell>> = langs
.iter()
.map(|s| vec![Cell::new(lang_cell(s), lang_style, Align::Left)])
.collect();
let mut total_row = vec![Cell::new(plain_lang("Total"), total_style, Align::Left)];
header_row.push(Cell::new("Files".into(), header, Align::Right));
for (row, s) in body.iter_mut().zip(&langs) {
row.push(num(s.files, count_style));
}
total_row.push(num(report.total_files(), total_style));
if with_lines {
type Get = fn(&LangStat) -> usize;
let metrics: [(&str, Get); 4] = [
("Lines", |s| s.counts.lines),
("Code", |s| s.counts.code),
("Comments", |s| s.counts.comments),
("Blanks", |s| s.counts.blanks),
];
for (title, get) in metrics {
header_row.push(Cell::new(title.into(), header, Align::Right));
let style = if title == "Code" { count_style } else { dim };
for (row, s) in body.iter_mut().zip(&langs) {
row.push(num(get(s), style));
}
}
total_row.push(num(total.lines, total_style));
total_row.push(num(total.code, total_style));
total_row.push(num(total.comments, total_style));
total_row.push(num(total.blanks, total_style));
}
if with_percent {
header_row.push(Cell::new("Code %".into(), header, Align::Right));
for (row, s) in body.iter_mut().zip(&langs) {
row.push(pct(s.counts.code, count_style));
}
total_row.push(pct(total.code, total_style));
header_row.push(Cell::new(String::new(), header, Align::Left));
for (row, s) in body.iter_mut().zip(&langs) {
row.push(Cell::new(
bar(s.counts.code, max_code),
bar_style,
Align::Left,
));
}
total_row.push(Cell::new(String::new(), total_style, Align::Left));
}
let columns = header_row.len();
let mut widths = vec![0; columns];
for row in std::iter::once(&header_row)
.chain(body.iter())
.chain(std::iter::once(&total_row))
{
for (width, cell) in widths.iter_mut().zip(row.iter()) {
*width = (*width).max(cell.width());
}
}
writeln!(w, "{}", paint_row(&header_row, &widths, self.show_icons))?;
for row in &body {
writeln!(w, "{}", paint_row(row, &widths, self.show_icons))?;
}
let rule_width = 1 + widths.iter().sum::<usize>() + 2 * (columns - 1);
writeln!(w, "{}", dim.paint("─".repeat(rule_width)))?;
writeln!(w, "{}", paint_row(&total_row, &widths, self.show_icons))?;
Ok(())
}
}
fn bar(value: usize, max: usize) -> String {
if max == 0 || value == 0 {
return String::new();
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
let units = (((value as f64) / (max as f64)) * ((BAR_WIDTH * 8) as f64)).round() as usize;
let units = units.max(1);
let mut bar = "█".repeat(units / 8);
if !units.is_multiple_of(8) {
bar.push(EIGHTHS[units % 8 - 1]);
}
bar
}
fn paint_row(cells: &[Cell], widths: &[usize], iconify_first: bool) -> String {
let mut out = String::from(" ");
for (i, cell) in cells.iter().enumerate() {
if i > 0 {
out.push_str(" ");
}
let padding = " ".repeat(widths[i].saturating_sub(cell.width()));
if cell.text.is_empty() {
out.push_str(&padding);
continue;
}
let painted = if i == 0 && iconify_first && cell.width() > 2 {
let split = cell
.text
.char_indices()
.nth(2)
.map_or(cell.text.len(), |(pos, _)| pos);
let (prefix, name) = cell.text.split_at(split);
format!(
"{}{}",
iconify_style(cell.style).paint(prefix),
cell.style.paint(name)
)
} else {
cell.style.paint(cell.text.as_str()).to_string()
};
match cell.align {
Align::Left => {
out.push_str(&painted);
out.push_str(&padding);
}
Align::Right => {
out.push_str(&padding);
out.push_str(&painted);
}
}
}
out.truncate(out.trim_end().len());
out
}