use anyhow::Result;
use colored::Colorize;
use humansize::{DECIMAL, format_size};
use inquire::{MultiSelect, list_option::ListOption};
use rayon::prelude::*;
use crate::project::ProjectType;
use super::Project;
#[derive(Debug)]
pub struct Projects(Vec<Project>);
impl From<Vec<Project>> for Projects {
fn from(projects: Vec<Project>) -> Self {
Self(projects)
}
}
impl IntoParallelIterator for Projects {
type Iter = rayon::vec::IntoIter<Project>;
type Item = Project;
fn into_par_iter(self) -> Self::Iter {
self.0.into_par_iter()
}
}
impl<'a> IntoParallelIterator for &'a Projects {
type Iter = rayon::slice::Iter<'a, Project>;
type Item = &'a Project;
fn into_par_iter(self) -> Self::Iter {
self.0.par_iter()
}
}
impl Projects {
#[must_use]
pub fn get_total_size(&self) -> u64 {
self.0.iter().map(Project::total_size).sum()
}
pub fn interactive_selection(&self) -> Result<Vec<Project>> {
let items: Vec<String> = self
.0
.iter()
.map(|p| {
let icon = icon_for_project_type(&p.kind);
format!(
"{icon} {} ({})",
p.root_path.display(),
format_size(p.total_size(), DECIMAL)
)
})
.collect();
let defaults: Vec<usize> = (0..self.0.len()).collect();
let selections = MultiSelect::new("Select projects to clean:", items)
.with_default(&defaults)
.with_formatter(&|opts: &[ListOption<&String>]| {
opts.iter()
.map(|o| o.value.as_str())
.collect::<Vec<_>>()
.join("\n")
})
.prompt()?;
Ok(selections
.iter()
.filter_map(|selected_item| {
self.0
.iter()
.enumerate()
.find(|(_, p)| {
let icon = icon_for_project_type(&p.kind);
let expected = format!(
"{icon} {} ({})",
p.root_path.display(),
format_size(p.total_size(), DECIMAL)
);
&expected == selected_item
})
.map(|(i, _)| i)
})
.map(|i| self.0[i].clone())
.collect())
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn as_slice(&self) -> &[Project] {
&self.0
}
pub fn print_summary(&self, total_size: u64) {
let type_entries: &[(ProjectType, &str, &str)] = &[
(ProjectType::Rust, "[rs]", "Rust"),
(ProjectType::Node, "[js]", "Node.js"),
(ProjectType::Python, "[py]", "Python"),
(ProjectType::Go, "[go]", "Go"),
(ProjectType::Java, "[java]", "Java/Kotlin"),
(ProjectType::Cpp, "[cpp]", "C/C++"),
(ProjectType::Swift, "[swift]", "Swift"),
(ProjectType::DotNet, "[net]", ".NET/C#"),
];
for (kind, icon, label) in type_entries {
let (count, size) = self.0.iter().fold((0usize, 0u64), |(c, s), p| {
if &p.kind == kind {
(c + 1, s + p.total_size())
} else {
(c, s)
}
});
if count > 0 {
println!(
" {icon} {} {label} projects ({})",
count.to_string().bright_white(),
format_size(size, DECIMAL).bright_white()
);
}
}
println!(
" Total reclaimable space: {}",
format_size(total_size, DECIMAL).bright_green().bold()
);
}
}
const fn icon_for_project_type(kind: &ProjectType) -> &'static str {
match kind {
ProjectType::Rust => "[rs]",
ProjectType::Node => "[js]",
ProjectType::Python => "[py]",
ProjectType::Go => "[go]",
ProjectType::Java => "[java]",
ProjectType::Cpp => "[cpp]",
ProjectType::Swift => "[swift]",
ProjectType::DotNet => "[net]",
ProjectType::Ruby => "[rb]",
ProjectType::Elixir => "[ex]",
ProjectType::Deno => "[deno]",
ProjectType::Php => "[php]",
ProjectType::Haskell => "λ",
ProjectType::Dart => "[dart]",
ProjectType::Zig => "[zig]",
ProjectType::Scala => "[scala]",
}
}