use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use serde::Serialize;
use crate::humantime;
use crate::size::DirStat;
pub type ProjectId = usize;
#[derive(Debug, Clone, Serialize)]
pub struct Project {
pub id: ProjectId,
pub root: PathBuf,
pub scan_root: PathBuf,
pub types: Vec<String>,
pub artifacts: Vec<Artifact>,
#[serde(serialize_with = "ser_time")]
pub last_activity: Option<SystemTime>,
pub source_files: u64,
pub git: GitStatus,
pub resolved: bool,
pub state: CleanState,
}
#[derive(Debug, Clone, Serialize)]
pub struct Artifact {
pub path: PathBuf,
pub pattern: String,
pub stat: Option<DirStat>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GitStatus {
Ignored,
NoRepo,
Disabled,
Unavailable,
Pending,
}
impl GitStatus {
pub fn label(self) -> &'static str {
match self {
Self::Ignored => "git-ignored",
Self::NoRepo => "no-git",
Self::Disabled => "unchecked",
Self::Unavailable => "git-error",
Self::Pending => "…",
}
}
pub fn is_unverified(self) -> bool {
!matches!(self, Self::Ignored)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case", tag = "state", content = "detail")]
pub enum CleanState {
Idle,
Running,
Done { freed: u64 },
Failed { errors: Vec<String> },
}
impl Project {
pub fn new(id: ProjectId, root: PathBuf, scan_root: PathBuf, types: Vec<String>) -> Self {
Self {
id,
root,
scan_root,
types,
artifacts: Vec::new(),
last_activity: None,
source_files: 0,
git: GitStatus::Pending,
resolved: false,
state: CleanState::Idle,
}
}
pub fn type_label(&self) -> String {
match self.types.len() {
0 => "unknown".to_string(),
1 => self.types[0].clone(),
_ => self.types.join("+"),
}
}
pub fn display_path(&self) -> String {
self.root
.strip_prefix(&self.scan_root)
.unwrap_or(&self.root)
.to_string_lossy()
.into_owned()
}
pub fn age(&self) -> Option<Duration> {
self.last_activity.map(humantime::age_of)
}
pub fn age_label(&self) -> String {
match self.age() {
Some(age) => humantime::format_age(age),
None => "unknown".to_string(),
}
}
pub fn size(&self) -> u64 {
self.artifacts
.iter()
.filter_map(|a| a.stat.as_ref())
.map(|s| s.bytes)
.sum()
}
pub fn file_count(&self) -> u64 {
self.artifacts
.iter()
.filter_map(|a| a.stat.as_ref())
.map(|s| s.files)
.sum()
}
pub fn is_measured(&self) -> bool {
self.resolved && self.artifacts.iter().all(|a| a.stat.is_some())
}
pub fn last_build(&self) -> Option<SystemTime> {
self.artifacts
.iter()
.filter_map(|a| a.stat.as_ref())
.filter_map(|s| s.newest)
.max()
}
pub fn artifact_paths(&self) -> Vec<PathBuf> {
self.artifacts.iter().map(|a| a.path.clone()).collect()
}
pub fn is_cleanable(&self) -> bool {
!self.artifacts.is_empty() && self.state == CleanState::Idle
}
}
#[derive(Debug, Clone)]
pub struct Filter {
pub older_than: Duration,
pub min_size: u64,
pub query: String,
pub with_artifacts_only: bool,
}
impl Default for Filter {
fn default() -> Self {
Self {
older_than: Duration::ZERO,
min_size: 0,
query: String::new(),
with_artifacts_only: true,
}
}
}
impl Filter {
pub fn accepts_age(&self, project: &Project) -> bool {
if self.older_than.is_zero() {
return true;
}
match project.age() {
Some(age) => age >= self.older_than,
None => false,
}
}
pub fn accepts(&self, project: &Project) -> bool {
if !self.accepts_age(project) {
return false;
}
if self.with_artifacts_only && project.resolved && project.artifacts.is_empty() {
return false;
}
if self.min_size > 0 && project.is_measured() && project.size() < self.min_size {
return false;
}
if !self.query.is_empty() {
let needle = self.query.to_lowercase();
let haystack = project.root.to_string_lossy().to_lowercase();
if !haystack.contains(&needle) && !project.type_label().contains(&needle) {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sort {
Size,
Age,
Path,
Type,
}
impl Sort {
pub const ALL: [Sort; 4] = [Sort::Size, Sort::Age, Sort::Path, Sort::Type];
pub fn label(self) -> &'static str {
match self {
Self::Size => "size",
Self::Age => "age",
Self::Path => "path",
Self::Type => "type",
}
}
pub fn next(self) -> Self {
let index = Self::ALL.iter().position(|s| *s == self).unwrap_or(0);
Self::ALL[(index + 1) % Self::ALL.len()]
}
pub fn apply(self, projects: &mut [&Project]) {
match self {
Self::Size => {
projects.sort_by(|a, b| b.size().cmp(&a.size()).then_with(|| a.root.cmp(&b.root)))
}
Self::Age => projects.sort_by(|a, b| {
let key = |p: &Project| p.last_activity.unwrap_or(SystemTime::UNIX_EPOCH);
key(a).cmp(&key(b)).then_with(|| a.root.cmp(&b.root))
}),
Self::Path => projects.sort_by(|a, b| a.root.cmp(&b.root)),
Self::Type => projects.sort_by(|a, b| {
a.type_label()
.cmp(&b.type_label())
.then_with(|| b.size().cmp(&a.size()))
}),
}
}
}
fn ser_time<S: serde::Serializer>(
value: &Option<SystemTime>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let secs = value.and_then(|t| {
t.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.map(|d| d.as_secs())
});
match secs {
Some(s) => serializer.serialize_some(&s),
None => serializer.serialize_none(),
}
}
pub fn tilde(path: &Path) -> String {
if let Some(home) = dirs::home_dir()
&& let Ok(rest) = path.strip_prefix(&home)
{
if rest.as_os_str().is_empty() {
return "~".to_string();
}
return format!("~/{}", rest.display());
}
path.display().to_string()
}