use anyhow::Result;
use chrono::Utc;
use colored::Colorize;
use crate::storage::Storage;
use crate::utils::confirm;
struct Tombstone {
index: usize,
label: String,
}
macro_rules! collect_tombstones {
($items:expr, $cutoff:expr, $label_fn:expr) => {
$items
.iter()
.enumerate()
.filter_map(|(i, item)| {
item.deleted_at.and_then(|deleted_at| {
if deleted_at <= $cutoff {
Some(Tombstone {
index: i,
label: $label_fn(item),
})
} else {
None
}
})
})
.collect::<Vec<_>>()
};
}
fn purge_indices<T>(items: &mut Vec<T>, tombstones: &[Tombstone]) {
let mut indices: Vec<usize> = tombstones.iter().map(|t| t.index).collect();
indices.sort_unstable_by(|a, b| b.cmp(a));
for i in indices {
items.remove(i);
}
}
pub fn execute(storage: &impl Storage, days: u32, dry_run: bool, yes: bool) -> Result<()> {
let (mut tasks, mut projects, mut notes, mut resources) = storage.load_all_with_resources()?;
let cutoff = Utc::now() - chrono::Duration::days(days as i64);
let task_tombs = collect_tombstones!(&tasks, cutoff, |t: &crate::models::Task| t.text.clone());
let project_tombs = collect_tombstones!(&projects, cutoff, |p: &crate::models::Project| p
.name
.clone());
let note_tombs = collect_tombstones!(¬es, cutoff, |n: &crate::models::Note| n
.title
.clone()
.unwrap_or_else(|| {
n.body
.lines()
.find(|l| !l.trim().is_empty())
.map(|l| l.trim_start_matches('#').trim().to_string())
.unwrap_or_default()
}));
let resource_tombs = collect_tombstones!(&resources, cutoff, |r: &crate::models::Resource| r
.title
.clone());
let total = task_tombs.len() + project_tombs.len() + note_tombs.len() + resource_tombs.len();
if total == 0 {
println!(
"{}",
format!(
"\nNo tombstones older than {} day{} found.\n",
days,
if days == 1 { "" } else { "s" }
)
.dimmed()
);
return Ok(());
}
println!(
"\n{} tombstone{} older than {} day{} would be permanently removed:\n",
total.to_string().yellow(),
if total == 1 { "" } else { "s" },
days,
if days == 1 { "" } else { "s" },
);
let print_section = |label: &str, tombs: &[Tombstone]| {
if !tombs.is_empty() {
println!(" {}:", label.dimmed());
for t in tombs {
println!(" {} {}", "✗".dimmed(), t.label.dimmed());
}
}
};
print_section("tasks", &task_tombs);
print_section("projects", &project_tombs);
print_section("notes", ¬e_tombs);
print_section("resources", &resource_tombs);
println!();
if dry_run {
println!("{}", "Dry run — nothing was removed.".dimmed());
return Ok(());
}
if !yes && !confirm("Permanently delete these tombstones? [y/N]:")? {
println!("{}", "Purge cancelled.".dimmed());
return Ok(());
}
purge_indices(&mut tasks, &task_tombs);
purge_indices(&mut projects, &project_tombs);
purge_indices(&mut notes, ¬e_tombs);
purge_indices(&mut resources, &resource_tombs);
storage.save_all(&tasks, &projects, ¬es)?;
storage.save_resources(&resources)?;
println!(
"{} Permanently removed {} tombstone{}.",
"✓".green(),
total.to_string().green(),
if total == 1 { "" } else { "s" },
);
Ok(())
}