1use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
13use crate::error::Result;
14use std::path::PathBuf;
15
16const ELECTRON_APPS: &[(&str, &str, &str)] = &[
18 ("Slack", "Slack", "💬"),
20 ("Discord", "discord", "🎮"),
21 ("Spotify", "Spotify", "🎵"),
22 ("Microsoft Teams", "Microsoft Teams", "👥"),
23 ("Microsoft Teams (Classic)", "Microsoft/Teams", "👥"),
24 ("Notion", "Notion", "📝"),
25 ("Figma", "Figma", "🎨"),
26 ("Obsidian", "obsidian", "💎"),
27 ("Postman", "Postman", "📮"),
28 ("Insomnia", "Insomnia", "🌙"),
29 ("Hyper", "Hyper", "⚡"),
30 ("GitKraken", "GitKraken", "🐙"),
31 ("Atom", "Atom", "⚛️"),
32 ("Signal", "Signal", "🔒"),
33 ("WhatsApp", "WhatsApp", "📱"),
34 ("Telegram Desktop", "Telegram Desktop", "✈️"),
35 ("Linear", "Linear", "📊"),
36 ("Loom", "Loom", "🎥"),
37 ("Cron", "Cron", "📅"),
38 ("Raycast", "com.raycast.macos", "🔍"),
39 ("1Password", "1Password", "🔐"),
40 ("Bitwarden", "Bitwarden", "🔐"),
41 ("Franz", "Franz", "📬"),
42 ("Station", "Station", "🚉"),
43 ("Skype", "Skype", "📞"),
44 ("Zoom", "zoom.us", "📹"),
45 ("Webex", "Cisco Webex Meetings", "🌐"),
46 ("Miro", "Miro", "🖼️"),
47 ("ClickUp", "ClickUp", "✅"),
48 ("Todoist", "Todoist", "☑️"),
49 ("Trello", "Trello", "📋"),
50];
51
52pub struct ElectronCleaner {
54 home: PathBuf,
55}
56
57impl ElectronCleaner {
58 pub fn new() -> Option<Self> {
60 let home = dirs::home_dir()?;
61 Some(Self { home })
62 }
63
64 pub fn detect(&self) -> Result<Vec<CleanableItem>> {
66 let mut items = Vec::new();
67
68 #[cfg(target_os = "macos")]
69 {
70 items.extend(self.detect_macos_apps()?);
71 }
72
73 #[cfg(target_os = "linux")]
74 {
75 items.extend(self.detect_linux_apps()?);
76 }
77
78 #[cfg(target_os = "windows")]
79 {
80 items.extend(self.detect_windows_apps()?);
81 }
82
83 Ok(items)
84 }
85
86 #[cfg(target_os = "macos")]
87 fn detect_macos_apps(&self) -> Result<Vec<CleanableItem>> {
88 let mut items = Vec::new();
89
90 let app_support = self.home.join("Library/Application Support");
91 let caches = self.home.join("Library/Caches");
92
93 for (app_name, folder_name, icon) in ELECTRON_APPS {
94 let mut app_items = Vec::new();
95
96 let support_path = app_support.join(folder_name);
98 if support_path.exists() {
99 let cache_subdirs = ["Cache", "CachedData", "GPUCache", "Code Cache", "Service Worker", "blob_storage"];
101
102 for subdir in cache_subdirs {
103 let cache_path = support_path.join(subdir);
104 if cache_path.exists() {
105 let (size, file_count) = calculate_dir_size(&cache_path)?;
106 if size > 20_000_000 {
107 app_items.push((cache_path, size, file_count, subdir));
108 }
109 }
110 }
111
112 if let Ok(entries) = std::fs::read_dir(&support_path) {
114 for entry in entries.filter_map(|e| e.ok()) {
115 let name = entry.file_name().to_string_lossy().to_string();
116 if name.starts_with("app-") || name.contains(".old") {
118 let (size, file_count) = calculate_dir_size(&entry.path())?;
119 if size > 50_000_000 {
120 app_items.push((entry.path(), size, file_count, "Old Version"));
121 }
122 }
123 }
124 }
125 }
126
127 let cache_variants = [
129 folder_name.to_string(),
130 format!("com.{}.desktop", folder_name.to_lowercase()),
131 format!("com.{}", folder_name.to_lowercase()),
132 ];
133
134 for variant in cache_variants {
135 let cache_path = caches.join(&variant);
136 if cache_path.exists() {
137 let (size, file_count) = calculate_dir_size(&cache_path)?;
138 if size > 30_000_000 {
139 app_items.push((cache_path, size, file_count, "System Cache"));
140 }
141 }
142 }
143
144 let total_size: u64 = app_items.iter().map(|(_, s, _, _)| *s).sum();
146
147 if total_size > 50_000_000 {
148 if app_items.len() == 1 {
149 let (path, size, file_count, cache_type) = app_items.remove(0);
150 items.push(CleanableItem {
151 name: format!("{} {}", app_name, cache_type),
152 category: "Electron Apps".to_string(),
153 subcategory: app_name.to_string(),
154 icon,
155 path,
156 size,
157 file_count: Some(file_count),
158 last_modified: None,
159 description: "Electron app cache. Will be rebuilt on next launch.",
160 safe_to_delete: SafetyLevel::SafeWithCost,
161 clean_command: None,
162 });
163 } else {
164 let support_path = app_support.join(folder_name);
166 if support_path.exists() {
167 items.push(CleanableItem {
168 name: format!("{} Caches", app_name),
169 category: "Electron Apps".to_string(),
170 subcategory: app_name.to_string(),
171 icon,
172 path: support_path,
173 size: total_size,
174 file_count: Some(app_items.iter().map(|(_, _, c, _)| *c).sum()),
175 last_modified: None,
176 description: "Electron app cache and data. Consider cleaning individual subdirectories.",
177 safe_to_delete: SafetyLevel::Caution,
178 clean_command: None,
179 });
180 }
181 }
182 }
183 }
184
185 items.extend(self.detect_chromium_caches()?);
187
188 Ok(items)
189 }
190
191 #[cfg(target_os = "macos")]
192 fn detect_chromium_caches(&self) -> Result<Vec<CleanableItem>> {
193 let mut items = Vec::new();
194
195 let caches = self.home.join("Library/Caches");
196 if !caches.exists() {
197 return Ok(items);
198 }
199
200 if let Ok(entries) = std::fs::read_dir(&caches) {
202 for entry in entries.filter_map(|e| e.ok()) {
203 let path = entry.path();
204 let name = path.file_name()
205 .map(|n| n.to_string_lossy().to_string())
206 .unwrap_or_default();
207
208 let already_handled = ELECTRON_APPS.iter().any(|(_, folder, _)| {
210 name.to_lowercase().contains(&folder.to_lowercase())
211 });
212
213 if already_handled {
214 continue;
215 }
216
217 let gpu_cache = path.join("GPUCache");
219 let code_cache = path.join("Code Cache");
220
221 if gpu_cache.exists() || code_cache.exists() {
222 let (size, file_count) = calculate_dir_size(&path)?;
223 if size > 100_000_000 {
224 items.push(CleanableItem {
225 name: format!("App Cache: {}", name),
226 category: "Electron Apps".to_string(),
227 subcategory: "Other".to_string(),
228 icon: "📦",
229 path,
230 size,
231 file_count: Some(file_count),
232 last_modified: get_mtime(&entry.path()),
233 description: "Electron/Chromium-based app cache.",
234 safe_to_delete: SafetyLevel::SafeWithCost,
235 clean_command: None,
236 });
237 }
238 }
239 }
240 }
241
242 Ok(items)
243 }
244
245 #[cfg(target_os = "linux")]
246 fn detect_linux_apps(&self) -> Result<Vec<CleanableItem>> {
247 let mut items = Vec::new();
248
249 let config_dir = self.home.join(".config");
250
251 for (app_name, folder_name, icon) in ELECTRON_APPS {
252 let app_path = config_dir.join(folder_name);
253 if !app_path.exists() {
254 continue;
255 }
256
257 let cache_path = app_path.join("Cache");
259 if cache_path.exists() {
260 let (size, file_count) = calculate_dir_size(&cache_path)?;
261 if size > 50_000_000 {
262 items.push(CleanableItem {
263 name: format!("{} Cache", app_name),
264 category: "Electron Apps".to_string(),
265 subcategory: app_name.to_string(),
266 icon,
267 path: cache_path,
268 size,
269 file_count: Some(file_count),
270 last_modified: None,
271 description: "Electron app cache.",
272 safe_to_delete: SafetyLevel::SafeWithCost,
273 clean_command: None,
274 });
275 }
276 }
277 }
278
279 Ok(items)
280 }
281
282 #[cfg(target_os = "windows")]
283 fn detect_windows_apps(&self) -> Result<Vec<CleanableItem>> {
284 let mut items = Vec::new();
285
286 let appdata = self.home.join("AppData/Roaming");
287
288 for (app_name, folder_name, icon) in ELECTRON_APPS {
289 let app_path = appdata.join(folder_name);
290 if !app_path.exists() {
291 continue;
292 }
293
294 let cache_path = app_path.join("Cache");
295 if cache_path.exists() {
296 let (size, file_count) = calculate_dir_size(&cache_path)?;
297 if size > 50_000_000 {
298 items.push(CleanableItem {
299 name: format!("{} Cache", app_name),
300 category: "Electron Apps".to_string(),
301 subcategory: app_name.to_string(),
302 icon,
303 path: cache_path,
304 size,
305 file_count: Some(file_count),
306 last_modified: None,
307 description: "Electron app cache.",
308 safe_to_delete: SafetyLevel::SafeWithCost,
309 clean_command: None,
310 });
311 }
312 }
313 }
314
315 Ok(items)
316 }
317}
318
319impl Default for ElectronCleaner {
320 fn default() -> Self {
321 Self::new().expect("ElectronCleaner requires home directory")
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_electron_cleaner_creation() {
331 let cleaner = ElectronCleaner::new();
332 assert!(cleaner.is_some());
333 }
334
335 #[test]
336 fn test_electron_detection() {
337 if let Some(cleaner) = ElectronCleaner::new() {
338 let items = cleaner.detect().unwrap();
339 println!("Found {} Electron app items", items.len());
340 for item in &items {
341 println!(" {} {} ({} bytes)", item.icon, item.name, item.size);
342 }
343 }
344 }
345}