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