1pub mod xcode;
18pub mod android;
19pub mod docker;
20pub mod ml;
21pub mod ide;
22pub mod logs;
23pub mod homebrew;
24pub mod ios_deps;
25pub mod electron;
26pub mod gamedev;
27pub mod cloud;
28pub mod macos;
29
30use crate::error::Result;
31use serde::{Deserialize, Serialize};
32use std::path::PathBuf;
33use std::time::SystemTime;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CleanableItem {
38 pub name: String,
40 pub category: String,
42 pub subcategory: String,
44 pub icon: &'static str,
46 pub path: PathBuf,
48 pub size: u64,
50 pub file_count: Option<u64>,
52 pub last_modified: Option<SystemTime>,
54 pub description: &'static str,
56 pub safe_to_delete: SafetyLevel,
58 pub clean_command: Option<String>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64pub enum SafetyLevel {
65 Safe,
67 SafeWithCost,
69 Caution,
71 Dangerous,
73}
74
75impl SafetyLevel {
76 pub fn color_hint(&self) -> &'static str {
78 match self {
79 Self::Safe => "green",
80 Self::SafeWithCost => "yellow",
81 Self::Caution => "red",
82 Self::Dangerous => "magenta",
83 }
84 }
85
86 pub fn symbol(&self) -> &'static str {
88 match self {
89 Self::Safe => "✓",
90 Self::SafeWithCost => "~",
91 Self::Caution => "!",
92 Self::Dangerous => "âš ",
93 }
94 }
95}
96
97impl CleanableItem {
98 pub fn exists(&self) -> bool {
100 self.path.exists()
101 }
102
103 pub fn age_days(&self) -> Option<u64> {
105 self.last_modified
106 .and_then(|t| t.elapsed().ok())
107 .map(|d| d.as_secs() / 86400)
108 }
109
110 pub fn last_used_display(&self) -> String {
112 match self.age_days() {
113 Some(0) => "today".to_string(),
114 Some(1) => "yesterday".to_string(),
115 Some(d) if d < 7 => format!("{} days ago", d),
116 Some(d) if d < 30 => format!("{} weeks ago", d / 7),
117 Some(d) if d < 365 => format!("{} months ago", d / 30),
118 Some(d) => format!("{} years ago", d / 365),
119 None => "unknown".to_string(),
120 }
121 }
122}
123
124#[derive(Debug, Default)]
126pub struct CleanerSummary {
127 pub total_items: usize,
128 pub total_size: u64,
129 pub by_category: std::collections::HashMap<String, CategorySummary>,
130}
131
132#[derive(Debug, Default, Clone)]
134pub struct CategorySummary {
135 pub name: String,
136 pub icon: &'static str,
137 pub item_count: usize,
138 pub total_size: u64,
139}
140
141impl CleanerSummary {
142 pub fn from_items(items: &[CleanableItem]) -> Self {
143 let mut summary = Self::default();
144 summary.total_items = items.len();
145 summary.total_size = items.iter().map(|i| i.size).sum();
146
147 for item in items {
148 let entry = summary.by_category
149 .entry(item.category.clone())
150 .or_insert_with(|| CategorySummary {
151 name: item.category.clone(),
152 icon: item.icon,
153 ..Default::default()
154 });
155 entry.item_count += 1;
156 entry.total_size += item.size;
157 }
158
159 summary
160 }
161}
162
163pub fn calculate_dir_size(path: &std::path::Path) -> Result<(u64, u64)> {
165 use rayon::prelude::*;
166 use walkdir::WalkDir;
167
168 if !path.exists() {
169 return Ok((0, 0));
170 }
171
172 let entries: Vec<_> = WalkDir::new(path)
173 .into_iter()
174 .filter_map(|e| e.ok())
175 .collect();
176
177 let (size, count): (u64, u64) = entries
178 .par_iter()
179 .filter_map(|entry| entry.metadata().ok())
180 .filter(|m| m.is_file())
181 .fold(
182 || (0u64, 0u64),
183 |(size, count), m| (size + m.len(), count + 1),
184 )
185 .reduce(|| (0, 0), |(s1, c1), (s2, c2)| (s1 + s2, c1 + c2));
186
187 Ok((size, count))
188}
189
190pub fn get_mtime(path: &std::path::Path) -> Option<SystemTime> {
192 std::fs::metadata(path).ok()?.modified().ok()
193}