null_e/cleaners/
mod.rs

1//! Specialized cleanup modules for different development environments
2//!
3//! This module contains cleanup handlers for:
4//! - Xcode (iOS/macOS development)
5//! - Android Studio
6//! - Docker
7//! - ML/AI tools (Huggingface, Ollama, PyTorch)
8//! - IDE caches (JetBrains, VS Code)
9//! - System logs
10//! - Homebrew
11//! - iOS Dependencies (CocoaPods, Carthage, SPM)
12//! - Electron apps
13//! - Game Development (Unity, Unreal, Godot)
14//! - Cloud CLI (AWS, GCP, Azure, kubectl)
15//! - macOS System (orphaned containers, caches)
16
17pub 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/// A cleanable item found by a cleaner module
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct CleanableItem {
38    /// Human-readable name
39    pub name: String,
40    /// Category (e.g., "Xcode", "Docker")
41    pub category: String,
42    /// Subcategory (e.g., "DerivedData", "Simulators")
43    pub subcategory: String,
44    /// Icon for display
45    pub icon: &'static str,
46    /// Full path
47    pub path: PathBuf,
48    /// Size in bytes
49    pub size: u64,
50    /// Number of files (if applicable)
51    pub file_count: Option<u64>,
52    /// Last modification time
53    pub last_modified: Option<SystemTime>,
54    /// Description of what this is
55    pub description: &'static str,
56    /// Is it safe to delete?
57    pub safe_to_delete: SafetyLevel,
58    /// Official clean command (if available)
59    pub clean_command: Option<String>,
60}
61
62/// Safety level for deletion
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64pub enum SafetyLevel {
65    /// Safe to delete, will be regenerated
66    Safe,
67    /// Safe but may slow down next build/operation
68    SafeWithCost,
69    /// Use caution - may lose some data
70    Caution,
71    /// Dangerous - may break things
72    Dangerous,
73}
74
75impl SafetyLevel {
76    /// Get a color hint for display
77    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    /// Get a symbol for display
87    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    /// Check if this item exists
99    pub fn exists(&self) -> bool {
100        self.path.exists()
101    }
102
103    /// Get age in days
104    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    /// Format the last used time
111    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/// Summary of cleanable items from all modules
125#[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/// Summary for a single category
133#[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
163/// Calculate directory size recursively
164pub 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
190/// Get last modification time of a path
191pub fn get_mtime(path: &std::path::Path) -> Option<SystemTime> {
192    std::fs::metadata(path).ok()?.modified().ok()
193}