use crate::info::utils::info_field::InfoField;
use owo_colors::OwoColorize;
use serde::Serialize;
use tokei;
include!(concat!(env!("OUT_DIR"), "/language.rs"));
const LANGUAGES_BAR_LENGTH: usize = 26;
#[derive(Serialize)]
pub struct LanguageWithPercentage {
pub language: Language,
pub percentage: f64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LanguagesInfo {
pub languages_with_percentage: Vec<LanguageWithPercentage>,
#[serde(skip_serializing)]
true_color: bool,
#[serde(skip_serializing)]
number_of_languages_to_display: usize,
#[serde(skip_serializing)]
info_color: DynColors,
#[serde(skip_serializing)]
nerd_fonts: bool,
}
impl LanguagesInfo {
pub fn new(
loc_by_language: &[(Language, usize)],
true_color: bool,
number_of_languages_to_display: usize,
info_color: DynColors,
nerd_fonts: bool,
) -> Self {
let total: usize = loc_by_language.iter().map(|(_, v)| v).sum();
let weight_by_language: Vec<(Language, f64)> = loc_by_language
.iter()
.map(|(k, v)| {
let mut val = *v as f64;
val /= total as f64;
val *= 100_f64;
(*k, val)
})
.collect();
let languages_with_percentage = weight_by_language
.into_iter()
.map(|(language, percentage)| LanguageWithPercentage {
language,
percentage,
})
.collect();
Self {
languages_with_percentage,
true_color,
number_of_languages_to_display,
info_color,
nerd_fonts,
}
}
}
impl std::fmt::Display for LanguagesInfo {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let color_palette = [
DynColors::Ansi(AnsiColors::Red),
DynColors::Ansi(AnsiColors::Green),
DynColors::Ansi(AnsiColors::Yellow),
DynColors::Ansi(AnsiColors::Blue),
DynColors::Ansi(AnsiColors::Magenta),
DynColors::Ansi(AnsiColors::Cyan),
];
let languages: Vec<LanguageDisplayData> = prepare_languages(self, &color_palette);
let mut languages_info = build_language_bar(&languages);
for (i, language_display_data) in languages.iter().enumerate() {
let formatted_number = format!("{:.*}", 1, language_display_data.percentage);
let chip = language_display_data
.chip_icon
.color(language_display_data.chip_color);
let language_str = format!(
"{} {} ",
chip,
format!("{0} ({formatted_number} %)", language_display_data.language)
.color(self.info_color)
);
if i % 2 == 0 {
write!(
languages_info,
"\n{:<width$}{}",
"",
language_str,
width = self.title().len() + 2
)
.unwrap();
} else {
languages_info.push_str(language_str.trim_end());
}
}
write!(f, "{languages_info}")
}
}
#[derive(Debug, PartialEq)]
struct LanguageDisplayData {
language: String,
percentage: f64,
chip_color: DynColors,
chip_icon: char,
}
fn prepare_languages(
languages_info: &LanguagesInfo,
color_palette: &[DynColors],
) -> Vec<LanguageDisplayData> {
let mut iter = languages_info
.languages_with_percentage
.iter()
.enumerate()
.map(
|(
i,
&LanguageWithPercentage {
language,
percentage,
},
)| {
let chip_color = if languages_info.true_color {
language.get_chip_color()
} else {
color_palette[i % color_palette.len()]
};
let chip_icon = language.get_chip_icon(languages_info.nerd_fonts);
LanguageDisplayData {
language: language.to_string(),
percentage,
chip_color,
chip_icon,
}
},
);
if languages_info.languages_with_percentage.len()
> languages_info.number_of_languages_to_display
{
let mut languages = iter
.by_ref()
.take(languages_info.number_of_languages_to_display)
.collect::<Vec<_>>();
let other_perc = iter.fold(0.0, |acc, x| acc + x.percentage);
languages.push(LanguageDisplayData {
language: "Other".to_string(),
percentage: other_perc,
chip_color: DynColors::Ansi(AnsiColors::White),
chip_icon: DEFAULT_CHIP_ICON,
});
languages
} else {
iter.collect()
}
}
fn build_language_bar(languages: &[LanguageDisplayData]) -> String {
languages
.iter()
.fold(String::new(), |mut output, language_display_data| {
let bar_width = std::cmp::max(
(language_display_data.percentage / 100. * LANGUAGES_BAR_LENGTH as f64).round()
as usize,
1,
);
let _ = write!(
output,
"{:<width$}",
"".on_color(language_display_data.chip_color),
width = bar_width
);
output
})
}
#[typetag::serialize]
impl InfoField for LanguagesInfo {
fn value(&self) -> String {
self.to_string()
}
fn title(&self) -> String {
let mut title: String = "Language".into();
if self.languages_with_percentage.len() > 1 {
title.push('s');
}
title
}
}
pub fn loc(language_type: &tokei::LanguageType, language: &tokei::Language) -> usize {
__loc(language_type, language.code, language.comments)
+ language
.children
.iter()
.fold(0, |sum, (lang_type, reports)| {
sum + reports.iter().fold(0, |sum, report| {
let stats = report.stats.summarise();
sum + __loc(lang_type, stats.code, stats.comments)
})
})
}
fn __loc(language_type: &tokei::LanguageType, code: usize, comments: usize) -> usize {
match language_type {
tokei::LanguageType::Markdown => code + comments,
_ => code,
}
}
#[cfg(test)]
mod test {
use super::*;
use rstest::rstest;
#[test]
fn test_display_languages_info() {
let languages_info = LanguagesInfo {
languages_with_percentage: vec![LanguageWithPercentage {
language: Language::Go,
percentage: 100_f64,
}],
true_color: false,
number_of_languages_to_display: 6,
info_color: DynColors::Ansi(AnsiColors::White),
nerd_fonts: false,
};
let expected_languages_info = format!(
"{:<width$}\n{:<pad$}{} {} ",
"".on_color(DynColors::Ansi(AnsiColors::Red)),
"",
DEFAULT_CHIP_ICON.color(DynColors::Ansi(AnsiColors::Red)),
"Go (100.0 %)".color(DynColors::Ansi(AnsiColors::White)),
width = LANGUAGES_BAR_LENGTH,
pad = "Language".len() + 2
);
assert_eq!(languages_info.value(), expected_languages_info);
}
#[test]
fn should_display_correct_number_of_languages() {
let languages_info = LanguagesInfo {
languages_with_percentage: vec![
LanguageWithPercentage {
language: Language::Go,
percentage: 30_f64,
},
LanguageWithPercentage {
language: Language::Erlang,
percentage: 40_f64,
},
LanguageWithPercentage {
language: Language::Java,
percentage: 20_f64,
},
LanguageWithPercentage {
language: Language::Rust,
percentage: 10_f64,
},
],
true_color: false,
number_of_languages_to_display: 2,
info_color: DynColors::Ansi(AnsiColors::White),
nerd_fonts: false,
};
assert!(
languages_info.value().contains(
&"Go (30.0 %)"
.color(DynColors::Ansi(AnsiColors::White))
.to_string()
)
);
assert!(
languages_info.value().contains(
&"Erlang (40.0 %)"
.color(DynColors::Ansi(AnsiColors::White))
.to_string()
)
);
assert!(
languages_info.value().contains(
&"Other (30.0 %)"
.color(DynColors::Ansi(AnsiColors::White))
.to_string()
)
);
}
#[test]
fn test_build_language_bar_multiple_languages() {
let languages: Vec<LanguageDisplayData> = vec![
LanguageDisplayData {
language: "Rust".to_string(),
percentage: 60.0,
chip_color: DynColors::Ansi(AnsiColors::Red),
chip_icon: DEFAULT_CHIP_ICON,
},
LanguageDisplayData {
language: "Python".to_string(),
percentage: 40.0,
chip_color: DynColors::Ansi(AnsiColors::Yellow),
chip_icon: DEFAULT_CHIP_ICON,
},
];
let result = build_language_bar(&languages);
let rust_bar_width = (0.6 * LANGUAGES_BAR_LENGTH as f64).round() as usize;
let python_bar_width = (0.4 * LANGUAGES_BAR_LENGTH as f64).round() as usize;
let rust_bar = " ".repeat(rust_bar_width);
let python_bar = " ".repeat(python_bar_width);
let expected_result = format!(
"{}{}",
rust_bar.on_color(DynColors::Ansi(AnsiColors::Red)),
python_bar.on_color(DynColors::Ansi(AnsiColors::Yellow))
);
assert_eq!(result, expected_result);
}
#[test]
fn test_prepare_languages() {
let languages_info = LanguagesInfo {
languages_with_percentage: vec![
LanguageWithPercentage {
language: Language::Go,
percentage: 40_f64,
},
LanguageWithPercentage {
language: Language::Erlang,
percentage: 30_f64,
},
LanguageWithPercentage {
language: Language::Java,
percentage: 20_f64,
},
LanguageWithPercentage {
language: Language::Rust,
percentage: 10_f64,
},
],
true_color: false,
number_of_languages_to_display: 2,
info_color: DynColors::Ansi(AnsiColors::White),
nerd_fonts: false,
};
let color_palette = [
DynColors::Ansi(AnsiColors::Red),
DynColors::Ansi(AnsiColors::Green),
];
let result = prepare_languages(&languages_info, &color_palette);
let expected_result = vec![
LanguageDisplayData {
language: Language::Go.to_string(),
percentage: 40_f64,
chip_color: DynColors::Ansi(AnsiColors::Red),
chip_icon: DEFAULT_CHIP_ICON,
},
LanguageDisplayData {
language: Language::Erlang.to_string(),
percentage: 30_f64,
chip_color: DynColors::Ansi(AnsiColors::Green),
chip_icon: DEFAULT_CHIP_ICON,
},
LanguageDisplayData {
language: "Other".to_string(),
percentage: 30_f64,
chip_color: DynColors::Ansi(AnsiColors::White),
chip_icon: DEFAULT_CHIP_ICON,
},
];
assert_eq!(result, expected_result);
}
#[rstest]
#[case(Language::Go, true, '\u{e627}')]
#[case(Language::Abap, true, DEFAULT_CHIP_ICON)] #[case(Language::Rust, false, DEFAULT_CHIP_ICON)]
fn test_language_get_chip_icon(
#[case] language: Language,
#[case] use_nerd_fonts: bool,
#[case] expected_chip_icon: char,
) {
let result = language.get_chip_icon(use_nerd_fonts);
assert_eq!(result, expected_chip_icon);
}
}