1use crate::core::{Project, ScanConfig, ScanResult, Scanner};
4use crate::plugins::PluginRegistry;
5use crate::scanner::ParallelScanner;
6use std::collections::HashSet;
7use std::path::PathBuf;
8use std::sync::mpsc::{self, Receiver, Sender};
9use std::sync::Arc;
10use std::thread;
11
12pub enum ScanMessage {
14 Progress { dirs_scanned: usize, message: String },
16 CompleteProjects(ScanResult),
18 CompleteCaches(Vec<CacheEntry>),
20 CompleteCleaners(Vec<CleanerEntry>),
22 Error(String),
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ScanMode {
29 All,
31 Projects,
33 Caches,
35 Xcode,
37 Docker,
39 IDECaches,
41 MLCaches,
43 Android,
45 Electron,
47 Cloud,
49 PackageManagers,
51 GameDev,
53 MiscTools,
55 TestBrowsers,
57 System,
59 Logs,
61 Runtimes,
63 BinaryAnalysis,
65}
66
67impl ScanMode {
68 pub fn all_modes() -> Vec<ScanMode> {
70 vec![
71 ScanMode::All,
72 ScanMode::Projects,
73 ScanMode::Caches,
74 ScanMode::System,
75 ScanMode::Docker,
76 ScanMode::Xcode,
77 ScanMode::Android,
78 ScanMode::IDECaches,
79 ScanMode::MLCaches,
80 ScanMode::Electron,
81 ScanMode::Cloud,
82 ScanMode::PackageManagers,
83 ScanMode::GameDev,
84 ScanMode::MiscTools,
85 ScanMode::TestBrowsers,
86 ScanMode::Logs,
87 ScanMode::Runtimes,
88 ScanMode::BinaryAnalysis,
89 ]
90 }
91
92 pub fn name(&self) -> &'static str {
94 match self {
95 ScanMode::All => "๐ฅ SCAN ALL",
96 ScanMode::Projects => "Dev Projects",
97 ScanMode::Caches => "Global Caches",
98 ScanMode::Xcode => "Xcode",
99 ScanMode::Docker => "Docker",
100 ScanMode::IDECaches => "IDE Caches",
101 ScanMode::MLCaches => "ML/AI Models",
102 ScanMode::Android => "Android",
103 ScanMode::Electron => "Electron Apps",
104 ScanMode::Cloud => "Cloud CLI",
105 ScanMode::PackageManagers => "Package Managers",
106 ScanMode::GameDev => "Game Dev",
107 ScanMode::MiscTools => "Misc Tools",
108 ScanMode::TestBrowsers => "Test Browsers",
109 ScanMode::System => "System",
110 ScanMode::Logs => "Logs",
111 ScanMode::Runtimes => "Language Runtimes",
112 ScanMode::BinaryAnalysis => "Binary Analysis",
113 }
114 }
115
116 pub fn description(&self) -> &'static str {
118 match self {
119 ScanMode::All => "Everything! Maximum cleanup",
120 ScanMode::Projects => "node_modules, target, venv, .gradle, vendor",
121 ScanMode::Caches => "npm, pip, cargo, brew, CocoaPods",
122 ScanMode::Xcode => "DerivedData, Archives, Simulators",
123 ScanMode::Docker => "Images, Containers, Volumes",
124 ScanMode::IDECaches => "JetBrains, VS Code, Cursor",
125 ScanMode::MLCaches => "Huggingface, Ollama, PyTorch",
126 ScanMode::Android => "AVD, SDK, Gradle caches",
127 ScanMode::Electron => "Slack, Discord, Teams, etc.",
128 ScanMode::Cloud => "AWS, GCP, Azure, kubectl, Terraform",
129 ScanMode::PackageManagers => "Homebrew, apt, chocolatey",
130 ScanMode::GameDev => "Unity, Unreal, Godot",
131 ScanMode::MiscTools => "Vagrant, Go, Ruby, Git LFS, Maven",
132 ScanMode::TestBrowsers => "Playwright, Cypress, Puppeteer",
133 ScanMode::System => "Trash, Downloads, Temp files",
134 ScanMode::Logs => "System logs, crash reports",
135 ScanMode::Runtimes => "nvm, pyenv, rbenv, rustup, sdkman, gvm",
136 ScanMode::BinaryAnalysis => "Duplicates, conflicts, unused managers",
137 }
138 }
139
140 pub fn icon(&self) -> &'static str {
142 match self {
143 ScanMode::All => "๐ฅ",
144 ScanMode::Projects => "๐ฆ",
145 ScanMode::Caches => "๐๏ธ",
146 ScanMode::Xcode => "๐",
147 ScanMode::Docker => "๐ณ",
148 ScanMode::IDECaches => "๐ป",
149 ScanMode::MLCaches => "๐ค",
150 ScanMode::Android => "๐ค",
151 ScanMode::Electron => "โก",
152 ScanMode::Cloud => "โ๏ธ",
153 ScanMode::PackageManagers => "๐ฆ",
154 ScanMode::GameDev => "๐ฎ",
155 ScanMode::MiscTools => "๐ง",
156 ScanMode::TestBrowsers => "๐งช",
157 ScanMode::System => "๐๏ธ",
158 ScanMode::Logs => "๐",
159 ScanMode::Runtimes => "๐ง",
160 ScanMode::BinaryAnalysis => "๐",
161 }
162 }
163}
164
165#[derive(Debug, Clone)]
167pub struct CacheEntry {
168 pub name: String,
169 pub path: PathBuf,
170 pub size: u64,
171 pub icon: String,
172 pub description: String,
173 pub selected: bool,
174 pub visible: bool,
175}
176
177#[derive(Debug, Clone)]
179pub struct CleanerEntry {
180 pub name: String,
181 pub path: PathBuf,
182 pub size: u64,
183 pub icon: String,
184 pub category: String,
185 pub selected: bool,
186 pub visible: bool,
187 pub clean_command: Option<String>,
189}
190
191pub struct App {
193 pub state: AppState,
195 pub scan_mode: ScanMode,
197 pub menu_index: usize,
199 pub projects: Vec<ProjectEntry>,
201 pub caches: Vec<CacheEntry>,
203 pub cleaners: Vec<CleanerEntry>,
205 pub selected: usize,
207 pub scroll_offset: usize,
209 pub expanded: HashSet<usize>,
211 pub status_message: Option<String>,
213 pub should_quit: bool,
215 pub show_help: bool,
217 pub current_tab: usize,
219 pub tabs: Vec<String>,
221 pub search_query: String,
223 pub is_searching: bool,
225 pub scan_paths: Vec<PathBuf>,
227 pub scan_progress: f64,
229 pub scan_message: String,
231 pub dirs_scanned: usize,
233 pub total_size: u64,
235 scan_receiver: Option<Receiver<ScanMessage>>,
237 pub anim_frame: usize,
239 pub permanent_delete: bool,
241 pub pending_delete_items: Vec<(PathBuf, Option<String>)>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum AppState {
248 Ready,
250 Scanning,
252 Results,
254 CacheResults,
256 CleanerResults,
258 Confirming,
260 Cleaning,
262 Error(String),
264}
265
266#[derive(Debug, Clone)]
268pub struct ProjectEntry {
269 pub project: Project,
271 pub selected: bool,
273 pub visible: bool,
275}
276
277impl App {
278 pub fn new(paths: Vec<PathBuf>) -> Self {
280 Self {
281 state: AppState::Ready,
282 scan_mode: ScanMode::All,
283 menu_index: 0,
284 projects: Vec::new(),
285 caches: Vec::new(),
286 cleaners: Vec::new(),
287 selected: 0,
288 scroll_offset: 0,
289 expanded: HashSet::new(),
290 status_message: Some("Select a scan mode and press Enter".to_string()),
291 should_quit: false,
292 show_help: false,
293 current_tab: 0,
294 tabs: vec![
295 "All".to_string(),
296 "Node".to_string(),
297 "Rust".to_string(),
298 "Python".to_string(),
299 "Java".to_string(),
300 "Other".to_string(),
301 ],
302 search_query: String::new(),
303 is_searching: false,
304 scan_paths: paths,
305 scan_progress: 0.0,
306 scan_message: String::new(),
307 dirs_scanned: 0,
308 total_size: 0,
309 scan_receiver: None,
310 anim_frame: 0,
311 permanent_delete: false,
312 pending_delete_items: Vec::new(),
313 }
314 }
315
316 pub fn toggle_permanent_delete(&mut self) {
318 self.permanent_delete = !self.permanent_delete;
319 }
320
321 pub fn tick_animation(&mut self) {
323 self.anim_frame = self.anim_frame.wrapping_add(1);
324 }
325
326 pub fn menu_up(&mut self) {
328 let modes = ScanMode::all_modes();
329 if self.menu_index > 0 {
330 self.menu_index -= 1;
331 } else {
332 self.menu_index = modes.len() - 1;
333 }
334 self.scan_mode = modes[self.menu_index];
335 }
336
337 pub fn menu_down(&mut self) {
339 let modes = ScanMode::all_modes();
340 if self.menu_index < modes.len() - 1 {
341 self.menu_index += 1;
342 } else {
343 self.menu_index = 0;
344 }
345 self.scan_mode = modes[self.menu_index];
346 }
347
348 pub fn start_scan(&mut self) {
350 self.state = AppState::Scanning;
351 self.projects.clear();
352 self.caches.clear();
353 self.cleaners.clear();
354 self.scan_progress = 0.0;
355 self.scan_message = format!("Initializing {} scan...", self.scan_mode.name());
356 self.dirs_scanned = 0;
357 self.anim_frame = 0;
358 self.selected = 0;
359 self.scroll_offset = 0;
360
361 let (tx, rx): (Sender<ScanMessage>, Receiver<ScanMessage>) = mpsc::channel();
363 self.scan_receiver = Some(rx);
364
365 let paths = if self.scan_paths.is_empty() {
367 vec![std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))]
368 } else {
369 self.scan_paths.clone()
370 };
371
372 let mode = self.scan_mode;
373
374 thread::spawn(move || {
376 match mode {
377 ScanMode::All => Self::scan_all(tx, paths),
378 ScanMode::Projects => Self::scan_projects(tx, paths),
379 ScanMode::Caches => Self::scan_caches(tx),
380 ScanMode::Xcode => Self::scan_xcode(tx),
381 ScanMode::Docker => Self::scan_docker(tx),
382 ScanMode::IDECaches => Self::scan_ide_caches(tx),
383 ScanMode::MLCaches => Self::scan_ml_caches(tx),
384 ScanMode::Android => Self::scan_android(tx),
385 ScanMode::Electron => Self::scan_electron(tx),
386 ScanMode::Cloud => Self::scan_cloud(tx),
387 ScanMode::PackageManagers => Self::scan_package_managers(tx),
388 ScanMode::GameDev => Self::scan_gamedev(tx),
389 ScanMode::MiscTools => Self::scan_misc_tools(tx),
390 ScanMode::TestBrowsers => Self::scan_test_browsers(tx),
391 ScanMode::System => Self::scan_system(tx),
392 ScanMode::Logs => Self::scan_logs(tx),
393 ScanMode::Runtimes => Self::scan_runtimes(tx),
394 ScanMode::BinaryAnalysis => Self::scan_binaries(tx),
395 }
396 });
397 }
398
399 fn scan_all(tx: Sender<ScanMessage>, paths: Vec<PathBuf>) {
401 let _ = tx.send(ScanMessage::Progress {
402 dirs_scanned: 0,
403 message: "Scanning everything...".to_string(),
404 });
405
406 let mut all_cleaners: Vec<CleanerEntry> = Vec::new();
408
409 let _ = tx.send(ScanMessage::Progress {
411 dirs_scanned: 0,
412 message: "Scanning global caches...".to_string(),
413 });
414 if let Ok(mut caches) = crate::caches::detect_caches() {
415 let _ = crate::caches::calculate_all_sizes(&mut caches);
416 for c in caches.into_iter().filter(|c| c.size > 0) {
417 all_cleaners.push(CleanerEntry {
418 name: c.name.clone(),
419 path: c.path.clone(),
420 size: c.size,
421 icon: c.icon.to_string(),
422 category: "Cache".to_string(),
423 selected: false,
424 visible: true,
425 clean_command: None,
426 });
427 }
428 }
429
430 let _ = tx.send(ScanMessage::Progress {
432 dirs_scanned: 0,
433 message: "Scanning Xcode...".to_string(),
434 });
435 if let Some(cleaner) = crate::cleaners::xcode::XcodeCleaner::new() {
436 if let Ok(items) = cleaner.detect() {
437 for item in items {
438 all_cleaners.push(CleanerEntry {
439 name: item.name,
440 path: item.path,
441 size: item.size,
442 icon: item.icon.to_string(),
443 category: item.category,
444 selected: false,
445 visible: true,
446 clean_command: item.clean_command.clone(),
447 });
448 }
449 }
450 }
451
452 let _ = tx.send(ScanMessage::Progress {
454 dirs_scanned: 0,
455 message: "Scanning Docker...".to_string(),
456 });
457 let docker = crate::cleaners::docker::DockerCleaner::new();
458 if docker.is_available() {
459 if let Ok(items) = docker.detect() {
460 for item in items {
461 all_cleaners.push(CleanerEntry {
462 name: item.name,
463 path: item.path,
464 size: item.size,
465 icon: item.icon.to_string(),
466 category: item.category,
467 selected: false,
468 visible: true,
469 clean_command: item.clean_command.clone(),
470 });
471 }
472 }
473 }
474
475 let _ = tx.send(ScanMessage::Progress {
477 dirs_scanned: 0,
478 message: "Scanning IDE caches...".to_string(),
479 });
480 if let Some(cleaner) = crate::cleaners::ide::IdeCleaner::new() {
481 if let Ok(items) = cleaner.detect() {
482 for item in items {
483 all_cleaners.push(CleanerEntry {
484 name: item.name,
485 path: item.path,
486 size: item.size,
487 icon: item.icon.to_string(),
488 category: item.category,
489 selected: false,
490 visible: true,
491 clean_command: item.clean_command.clone(),
492 });
493 }
494 }
495 }
496
497 let _ = tx.send(ScanMessage::Progress {
499 dirs_scanned: 0,
500 message: "Scanning ML/AI models...".to_string(),
501 });
502 if let Some(cleaner) = crate::cleaners::ml::MlCleaner::new() {
503 if let Ok(items) = cleaner.detect() {
504 for item in items {
505 all_cleaners.push(CleanerEntry {
506 name: item.name,
507 path: item.path,
508 size: item.size,
509 icon: item.icon.to_string(),
510 category: item.category,
511 selected: false,
512 visible: true,
513 clean_command: item.clean_command.clone(),
514 });
515 }
516 }
517 }
518
519 let _ = tx.send(ScanMessage::Progress {
521 dirs_scanned: 0,
522 message: "Scanning Android...".to_string(),
523 });
524 if let Some(cleaner) = crate::cleaners::android::AndroidCleaner::new() {
525 if let Ok(items) = cleaner.detect() {
526 for item in items {
527 all_cleaners.push(CleanerEntry {
528 name: item.name,
529 path: item.path,
530 size: item.size,
531 icon: item.icon.to_string(),
532 category: item.category,
533 selected: false,
534 visible: true,
535 clean_command: item.clean_command.clone(),
536 });
537 }
538 }
539 }
540
541 let _ = tx.send(ScanMessage::Progress {
543 dirs_scanned: 0,
544 message: "Scanning Electron apps...".to_string(),
545 });
546 if let Some(cleaner) = crate::cleaners::electron::ElectronCleaner::new() {
547 if let Ok(items) = cleaner.detect() {
548 for item in items {
549 all_cleaners.push(CleanerEntry {
550 name: item.name,
551 path: item.path,
552 size: item.size,
553 icon: item.icon.to_string(),
554 category: item.category,
555 selected: false,
556 visible: true,
557 clean_command: item.clean_command.clone(),
558 });
559 }
560 }
561 }
562
563 let _ = tx.send(ScanMessage::Progress {
565 dirs_scanned: 0,
566 message: "Scanning Cloud CLI...".to_string(),
567 });
568 if let Some(cleaner) = crate::cleaners::cloud::CloudCliCleaner::new() {
569 if let Ok(items) = cleaner.detect() {
570 for item in items {
571 all_cleaners.push(CleanerEntry {
572 name: item.name,
573 path: item.path,
574 size: item.size,
575 icon: item.icon.to_string(),
576 category: item.category,
577 selected: false,
578 visible: true,
579 clean_command: item.clean_command.clone(),
580 });
581 }
582 }
583 }
584
585 let _ = tx.send(ScanMessage::Progress {
587 dirs_scanned: 0,
588 message: "Scanning package managers...".to_string(),
589 });
590 if let Some(cleaner) = crate::cleaners::homebrew::HomebrewCleaner::new() {
591 if let Ok(items) = cleaner.detect() {
592 for item in items {
593 all_cleaners.push(CleanerEntry {
594 name: item.name,
595 path: item.path,
596 size: item.size,
597 icon: item.icon.to_string(),
598 category: item.category,
599 selected: false,
600 visible: true,
601 clean_command: item.clean_command.clone(),
602 });
603 }
604 }
605 }
606
607 let _ = tx.send(ScanMessage::Progress {
609 dirs_scanned: 0,
610 message: "Scanning game dev tools...".to_string(),
611 });
612 if let Some(cleaner) = crate::cleaners::gamedev::GameDevCleaner::new() {
613 if let Ok(items) = cleaner.detect() {
614 for item in items {
615 all_cleaners.push(CleanerEntry {
616 name: item.name,
617 path: item.path,
618 size: item.size,
619 icon: item.icon.to_string(),
620 category: item.category,
621 selected: false,
622 visible: true,
623 clean_command: item.clean_command.clone(),
624 });
625 }
626 }
627 }
628
629 let _ = tx.send(ScanMessage::Progress {
631 dirs_scanned: 0,
632 message: "Scanning misc tools...".to_string(),
633 });
634 if let Some(cleaner) = crate::cleaners::misc::MiscCleaner::new() {
635 if let Ok(items) = cleaner.detect() {
636 for item in items {
637 all_cleaners.push(CleanerEntry {
638 name: item.name,
639 path: item.path,
640 size: item.size,
641 icon: item.icon.to_string(),
642 category: item.category,
643 selected: false,
644 visible: true,
645 clean_command: item.clean_command.clone(),
646 });
647 }
648 }
649 }
650
651 let _ = tx.send(ScanMessage::Progress {
653 dirs_scanned: 0,
654 message: "Scanning test browsers...".to_string(),
655 });
656 if let Some(cleaner) = crate::cleaners::browsers_test::TestBrowsersCleaner::new() {
657 if let Ok(items) = cleaner.detect() {
658 for item in items {
659 all_cleaners.push(CleanerEntry {
660 name: item.name,
661 path: item.path,
662 size: item.size,
663 icon: item.icon.to_string(),
664 category: item.category,
665 selected: false,
666 visible: true,
667 clean_command: item.clean_command.clone(),
668 });
669 }
670 }
671 }
672
673 let _ = tx.send(ScanMessage::Progress {
675 dirs_scanned: 0,
676 message: "Scanning system files...".to_string(),
677 });
678 if let Some(cleaner) = crate::cleaners::system::SystemCleaner::new() {
679 if let Ok(items) = cleaner.detect() {
680 for item in items {
681 all_cleaners.push(CleanerEntry {
682 name: item.name,
683 path: item.path,
684 size: item.size,
685 icon: item.icon.to_string(),
686 category: item.category,
687 selected: false,
688 visible: true,
689 clean_command: item.clean_command.clone(),
690 });
691 }
692 }
693 }
694
695 let _ = tx.send(ScanMessage::Progress {
697 dirs_scanned: 0,
698 message: "Scanning logs...".to_string(),
699 });
700 if let Some(cleaner) = crate::cleaners::logs::LogsCleaner::new() {
701 if let Ok(items) = cleaner.detect() {
702 for item in items {
703 all_cleaners.push(CleanerEntry {
704 name: item.name,
705 path: item.path,
706 size: item.size,
707 icon: item.icon.to_string(),
708 category: item.category,
709 selected: false,
710 visible: true,
711 clean_command: item.clean_command.clone(),
712 });
713 }
714 }
715 }
716
717 let _ = tx.send(ScanMessage::Progress {
719 dirs_scanned: 0,
720 message: "Scanning language runtimes...".to_string(),
721 });
722 if let Some(cleaner) = crate::cleaners::runtimes::RuntimesCleaner::new() {
723 if let Ok(items) = cleaner.detect() {
724 for item in items {
725 all_cleaners.push(CleanerEntry {
726 name: item.name,
727 path: item.path,
728 size: item.size,
729 icon: item.icon.to_string(),
730 category: item.category,
731 selected: false,
732 visible: true,
733 clean_command: item.clean_command.clone(),
734 });
735 }
736 }
737 }
738
739 let _ = tx.send(ScanMessage::Progress {
741 dirs_scanned: 0,
742 message: "Analyzing system binaries...".to_string(),
743 });
744 if let Some(analyzer) = crate::cleaners::binaries::BinaryAnalyzer::new() {
745 if let Ok(result) = analyzer.analyze() {
746 let items = analyzer.to_cleanable_items(&result);
747 for item in items {
748 all_cleaners.push(CleanerEntry {
749 name: item.name,
750 path: item.path,
751 size: item.size,
752 icon: item.icon.to_string(),
753 category: item.category,
754 selected: false,
755 visible: true,
756 clean_command: item.clean_command.clone(),
757 });
758 }
759 }
760 }
761
762 let _ = tx.send(ScanMessage::Progress {
764 dirs_scanned: 0,
765 message: "Scanning development projects...".to_string(),
766 });
767 let registry = Arc::new(PluginRegistry::with_builtins());
768 let scanner = ParallelScanner::new(registry);
769 let mut config = ScanConfig::default();
770
771 let project_paths = if paths.len() == 1 && paths[0] == std::env::current_dir().unwrap_or_default() {
773 dirs::home_dir()
775 .map(|h| vec![h])
776 .unwrap_or(paths.clone())
777 } else {
778 paths
779 };
780 config.roots = project_paths;
781
782 if let Ok(result) = scanner.scan(&config) {
783 for p in result.projects {
784 for artifact in &p.artifacts {
785 all_cleaners.push(CleanerEntry {
786 name: format!("{} ({})", p.name, artifact.name()),
787 path: artifact.path.clone(),
788 size: artifact.size,
789 icon: p.kind.icon().to_string(),
790 category: format!("{:?}", p.kind),
791 selected: false,
792 visible: true,
793 clean_command: None,
794 });
795 }
796 }
797 }
798
799 let _ = tx.send(ScanMessage::CompleteCleaners(all_cleaners));
800 }
801
802 fn scan_projects(tx: Sender<ScanMessage>, paths: Vec<PathBuf>) {
804 let _ = tx.send(ScanMessage::Progress {
805 dirs_scanned: 0,
806 message: "Scanning for development projects...".to_string(),
807 });
808
809 let registry = Arc::new(PluginRegistry::with_builtins());
810 let scanner = ParallelScanner::new(registry);
811
812 let project_paths = if paths.len() == 1 && paths[0] == std::env::current_dir().unwrap_or_default() {
814 dirs::home_dir()
815 .map(|h| vec![h])
816 .unwrap_or(paths.clone())
817 } else {
818 paths
819 };
820
821 let mut config = ScanConfig::default();
822 config.roots = project_paths;
823
824 match scanner.scan(&config) {
825 Ok(result) => {
826 let _ = tx.send(ScanMessage::CompleteProjects(result));
827 }
828 Err(e) => {
829 let _ = tx.send(ScanMessage::Error(e.to_string()));
830 }
831 }
832 }
833
834 fn scan_caches(tx: Sender<ScanMessage>) {
836 let _ = tx.send(ScanMessage::Progress {
837 dirs_scanned: 0,
838 message: "Detecting global caches...".to_string(),
839 });
840
841 match crate::caches::detect_caches() {
842 Ok(mut caches) => {
843 let _ = crate::caches::calculate_all_sizes(&mut caches);
844
845 let entries: Vec<CacheEntry> = caches
846 .into_iter()
847 .filter(|c| c.size > 0)
848 .map(|c| CacheEntry {
849 name: c.name.clone(),
850 path: c.path.clone(),
851 size: c.size,
852 icon: c.icon.to_string(),
853 description: c.description.to_string(),
854 selected: false,
855 visible: true,
856 })
857 .collect();
858
859 let _ = tx.send(ScanMessage::CompleteCaches(entries));
860 }
861 Err(e) => {
862 let _ = tx.send(ScanMessage::Error(e.to_string()));
863 }
864 }
865 }
866
867 fn scan_xcode(tx: Sender<ScanMessage>) {
869 let _ = tx.send(ScanMessage::Progress {
870 dirs_scanned: 0,
871 message: "Scanning Xcode artifacts...".to_string(),
872 });
873
874 if let Some(cleaner) = crate::cleaners::xcode::XcodeCleaner::new() {
875 if let Ok(items) = cleaner.detect() {
876 let entries: Vec<CleanerEntry> = items
877 .into_iter()
878 .map(|item| CleanerEntry {
879 name: item.name,
880 path: item.path,
881 size: item.size,
882 icon: item.icon.to_string(),
883 category: item.category,
884 selected: false,
885 visible: true,
886 clean_command: item.clean_command,
887 })
888 .collect();
889 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
890 return;
891 }
892 }
893 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
894 }
895
896 fn scan_docker(tx: Sender<ScanMessage>) {
898 let _ = tx.send(ScanMessage::Progress {
899 dirs_scanned: 0,
900 message: "Scanning Docker resources...".to_string(),
901 });
902
903 let cleaner = crate::cleaners::docker::DockerCleaner::new();
904 if cleaner.is_available() {
905 if let Ok(items) = cleaner.detect() {
906 let entries: Vec<CleanerEntry> = items
907 .into_iter()
908 .map(|item| CleanerEntry {
909 name: item.name,
910 path: item.path,
911 size: item.size,
912 icon: item.icon.to_string(),
913 category: item.category,
914 selected: false,
915 visible: true,
916 clean_command: item.clean_command,
917 })
918 .collect();
919 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
920 return;
921 }
922 }
923 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
924 }
925
926 fn scan_ide_caches(tx: Sender<ScanMessage>) {
928 let _ = tx.send(ScanMessage::Progress {
929 dirs_scanned: 0,
930 message: "Scanning IDE caches...".to_string(),
931 });
932
933 if let Some(cleaner) = crate::cleaners::ide::IdeCleaner::new() {
934 if let Ok(items) = cleaner.detect() {
935 let entries: Vec<CleanerEntry> = items
936 .into_iter()
937 .map(|item| CleanerEntry {
938 name: item.name,
939 path: item.path,
940 size: item.size,
941 icon: item.icon.to_string(),
942 category: item.category,
943 selected: false,
944 visible: true,
945 clean_command: item.clean_command,
946 })
947 .collect();
948 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
949 return;
950 }
951 }
952 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
953 }
954
955 fn scan_ml_caches(tx: Sender<ScanMessage>) {
957 let _ = tx.send(ScanMessage::Progress {
958 dirs_scanned: 0,
959 message: "Scanning ML/AI caches...".to_string(),
960 });
961
962 if let Some(cleaner) = crate::cleaners::ml::MlCleaner::new() {
963 if let Ok(items) = cleaner.detect() {
964 let entries: Vec<CleanerEntry> = items
965 .into_iter()
966 .map(|item| CleanerEntry {
967 name: item.name,
968 path: item.path,
969 size: item.size,
970 icon: item.icon.to_string(),
971 category: item.category,
972 selected: false,
973 visible: true,
974 clean_command: item.clean_command,
975 })
976 .collect();
977 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
978 return;
979 }
980 }
981 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
982 }
983
984 fn scan_android(tx: Sender<ScanMessage>) {
986 let _ = tx.send(ScanMessage::Progress {
987 dirs_scanned: 0,
988 message: "Scanning Android Studio...".to_string(),
989 });
990
991 if let Some(cleaner) = crate::cleaners::android::AndroidCleaner::new() {
992 if let Ok(items) = cleaner.detect() {
993 let entries: Vec<CleanerEntry> = items
994 .into_iter()
995 .map(|item| CleanerEntry {
996 name: item.name,
997 path: item.path,
998 size: item.size,
999 icon: item.icon.to_string(),
1000 category: item.category,
1001 selected: false,
1002 visible: true,
1003 clean_command: item.clean_command,
1004 })
1005 .collect();
1006 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1007 return;
1008 }
1009 }
1010 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1011 }
1012
1013 fn scan_electron(tx: Sender<ScanMessage>) {
1015 let _ = tx.send(ScanMessage::Progress {
1016 dirs_scanned: 0,
1017 message: "Scanning Electron app caches...".to_string(),
1018 });
1019
1020 if let Some(cleaner) = crate::cleaners::electron::ElectronCleaner::new() {
1021 if let Ok(items) = cleaner.detect() {
1022 let entries: Vec<CleanerEntry> = items
1023 .into_iter()
1024 .map(|item| CleanerEntry {
1025 name: item.name,
1026 path: item.path,
1027 size: item.size,
1028 icon: item.icon.to_string(),
1029 category: item.category,
1030 selected: false,
1031 visible: true,
1032 clean_command: item.clean_command,
1033 })
1034 .collect();
1035 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1036 return;
1037 }
1038 }
1039 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1040 }
1041
1042 fn scan_cloud(tx: Sender<ScanMessage>) {
1044 let _ = tx.send(ScanMessage::Progress {
1045 dirs_scanned: 0,
1046 message: "Scanning Cloud CLI caches...".to_string(),
1047 });
1048
1049 if let Some(cleaner) = crate::cleaners::cloud::CloudCliCleaner::new() {
1050 if let Ok(items) = cleaner.detect() {
1051 let entries: Vec<CleanerEntry> = items
1052 .into_iter()
1053 .map(|item| CleanerEntry {
1054 name: item.name,
1055 path: item.path,
1056 size: item.size,
1057 icon: item.icon.to_string(),
1058 category: item.category,
1059 selected: false,
1060 visible: true,
1061 clean_command: item.clean_command,
1062 })
1063 .collect();
1064 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1065 return;
1066 }
1067 }
1068 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1069 }
1070
1071 fn scan_package_managers(tx: Sender<ScanMessage>) {
1073 let _ = tx.send(ScanMessage::Progress {
1074 dirs_scanned: 0,
1075 message: "Scanning package managers...".to_string(),
1076 });
1077
1078 if let Some(cleaner) = crate::cleaners::homebrew::HomebrewCleaner::new() {
1079 if let Ok(items) = cleaner.detect() {
1080 let entries: Vec<CleanerEntry> = items
1081 .into_iter()
1082 .map(|item| CleanerEntry {
1083 name: item.name,
1084 path: item.path,
1085 size: item.size,
1086 icon: item.icon.to_string(),
1087 category: item.category,
1088 selected: false,
1089 visible: true,
1090 clean_command: item.clean_command,
1091 })
1092 .collect();
1093 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1094 return;
1095 }
1096 }
1097 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1098 }
1099
1100 fn scan_gamedev(tx: Sender<ScanMessage>) {
1102 let _ = tx.send(ScanMessage::Progress {
1103 dirs_scanned: 0,
1104 message: "Scanning game development tools...".to_string(),
1105 });
1106
1107 if let Some(cleaner) = crate::cleaners::gamedev::GameDevCleaner::new() {
1108 if let Ok(items) = cleaner.detect() {
1109 let entries: Vec<CleanerEntry> = items
1110 .into_iter()
1111 .map(|item| CleanerEntry {
1112 name: item.name,
1113 path: item.path,
1114 size: item.size,
1115 icon: item.icon.to_string(),
1116 category: item.category,
1117 selected: false,
1118 visible: true,
1119 clean_command: item.clean_command,
1120 })
1121 .collect();
1122 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1123 return;
1124 }
1125 }
1126 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1127 }
1128
1129 fn scan_misc_tools(tx: Sender<ScanMessage>) {
1131 let _ = tx.send(ScanMessage::Progress {
1132 dirs_scanned: 0,
1133 message: "Scanning misc development tools...".to_string(),
1134 });
1135
1136 if let Some(cleaner) = crate::cleaners::misc::MiscCleaner::new() {
1137 if let Ok(items) = cleaner.detect() {
1138 let entries: Vec<CleanerEntry> = items
1139 .into_iter()
1140 .map(|item| CleanerEntry {
1141 name: item.name,
1142 path: item.path,
1143 size: item.size,
1144 icon: item.icon.to_string(),
1145 category: item.category,
1146 selected: false,
1147 visible: true,
1148 clean_command: item.clean_command,
1149 })
1150 .collect();
1151 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1152 return;
1153 }
1154 }
1155 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1156 }
1157
1158 fn scan_test_browsers(tx: Sender<ScanMessage>) {
1160 let _ = tx.send(ScanMessage::Progress {
1161 dirs_scanned: 0,
1162 message: "Scanning test browser binaries...".to_string(),
1163 });
1164
1165 if let Some(cleaner) = crate::cleaners::browsers_test::TestBrowsersCleaner::new() {
1166 if let Ok(items) = cleaner.detect() {
1167 let entries: Vec<CleanerEntry> = items
1168 .into_iter()
1169 .map(|item| CleanerEntry {
1170 name: item.name,
1171 path: item.path,
1172 size: item.size,
1173 icon: item.icon.to_string(),
1174 category: item.category,
1175 selected: false,
1176 visible: true,
1177 clean_command: item.clean_command,
1178 })
1179 .collect();
1180 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1181 return;
1182 }
1183 }
1184 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1185 }
1186
1187 fn scan_system(tx: Sender<ScanMessage>) {
1189 let _ = tx.send(ScanMessage::Progress {
1190 dirs_scanned: 0,
1191 message: "Scanning system files...".to_string(),
1192 });
1193
1194 if let Some(cleaner) = crate::cleaners::system::SystemCleaner::new() {
1195 if let Ok(items) = cleaner.detect() {
1196 let entries: Vec<CleanerEntry> = items
1197 .into_iter()
1198 .map(|item| CleanerEntry {
1199 name: item.name,
1200 path: item.path,
1201 size: item.size,
1202 icon: item.icon.to_string(),
1203 category: item.category,
1204 selected: false,
1205 visible: true,
1206 clean_command: item.clean_command,
1207 })
1208 .collect();
1209 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1210 return;
1211 }
1212 }
1213 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1214 }
1215
1216 fn scan_logs(tx: Sender<ScanMessage>) {
1218 let _ = tx.send(ScanMessage::Progress {
1219 dirs_scanned: 0,
1220 message: "Scanning log files...".to_string(),
1221 });
1222
1223 if let Some(cleaner) = crate::cleaners::logs::LogsCleaner::new() {
1224 if let Ok(items) = cleaner.detect() {
1225 let entries: Vec<CleanerEntry> = items
1226 .into_iter()
1227 .map(|item| CleanerEntry {
1228 name: item.name,
1229 path: item.path,
1230 size: item.size,
1231 icon: item.icon.to_string(),
1232 category: item.category,
1233 selected: false,
1234 visible: true,
1235 clean_command: item.clean_command,
1236 })
1237 .collect();
1238 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1239 return;
1240 }
1241 }
1242 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1243 }
1244
1245 fn scan_runtimes(tx: Sender<ScanMessage>) {
1247 let _ = tx.send(ScanMessage::Progress {
1248 dirs_scanned: 0,
1249 message: "Scanning language runtimes...".to_string(),
1250 });
1251
1252 if let Some(cleaner) = crate::cleaners::runtimes::RuntimesCleaner::new() {
1253 if let Ok(items) = cleaner.detect() {
1254 let entries: Vec<CleanerEntry> = items
1255 .into_iter()
1256 .map(|item| CleanerEntry {
1257 name: item.name,
1258 path: item.path,
1259 size: item.size,
1260 icon: item.icon.to_string(),
1261 category: item.category,
1262 selected: false,
1263 visible: true,
1264 clean_command: item.clean_command,
1265 })
1266 .collect();
1267 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1268 return;
1269 }
1270 }
1271 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1272 }
1273
1274 fn scan_binaries(tx: Sender<ScanMessage>) {
1276 let _ = tx.send(ScanMessage::Progress {
1277 dirs_scanned: 0,
1278 message: "Analyzing system binaries...".to_string(),
1279 });
1280
1281 if let Some(analyzer) = crate::cleaners::binaries::BinaryAnalyzer::new() {
1282 let _ = tx.send(ScanMessage::Progress {
1283 dirs_scanned: 0,
1284 message: "Discovering binaries via which -a...".to_string(),
1285 });
1286
1287 if let Ok(result) = analyzer.analyze() {
1288 let _ = tx.send(ScanMessage::Progress {
1289 dirs_scanned: result.binaries.len(),
1290 message: format!(
1291 "Found {} binaries, {} duplicates...",
1292 result.binaries.len(),
1293 result.duplicates.len()
1294 ),
1295 });
1296
1297 let items = analyzer.to_cleanable_items(&result);
1298 let entries: Vec<CleanerEntry> = items
1299 .into_iter()
1300 .map(|item| CleanerEntry {
1301 name: item.name,
1302 path: item.path,
1303 size: item.size,
1304 icon: item.icon.to_string(),
1305 category: item.category,
1306 selected: false,
1307 visible: true,
1308 clean_command: item.clean_command,
1309 })
1310 .collect();
1311 let _ = tx.send(ScanMessage::CompleteCleaners(entries));
1312 return;
1313 }
1314 }
1315 let _ = tx.send(ScanMessage::CompleteCleaners(vec![]));
1316 }
1317
1318 pub fn check_scan_progress(&mut self) {
1320 self.anim_frame = self.anim_frame.wrapping_add(1);
1322
1323 if let Some(ref rx) = self.scan_receiver {
1324 while let Ok(msg) = rx.try_recv() {
1326 match msg {
1327 ScanMessage::Progress { dirs_scanned, message } => {
1328 self.dirs_scanned = dirs_scanned;
1329 self.scan_message = message;
1330 }
1331 ScanMessage::CompleteProjects(result) => {
1332 self.handle_project_scan_complete(result);
1333 self.scan_receiver = None;
1334 return;
1335 }
1336 ScanMessage::CompleteCaches(caches) => {
1337 self.handle_cache_scan_complete(caches);
1338 self.scan_receiver = None;
1339 return;
1340 }
1341 ScanMessage::CompleteCleaners(cleaners) => {
1342 self.handle_cleaner_scan_complete(cleaners);
1343 self.scan_receiver = None;
1344 return;
1345 }
1346 ScanMessage::Error(err) => {
1347 self.state = AppState::Error(err.clone());
1348 self.status_message = Some(format!("Error: {}", err));
1349 self.scan_receiver = None;
1350 return;
1351 }
1352 }
1353 }
1354 }
1355 }
1356
1357 fn handle_project_scan_complete(&mut self, scan_result: ScanResult) {
1359 self.projects = scan_result
1360 .projects
1361 .iter()
1362 .map(|p: &Project| ProjectEntry {
1363 project: p.clone(),
1364 selected: false,
1365 visible: true,
1366 })
1367 .collect();
1368
1369 self.projects
1371 .sort_by(|a, b| b.project.cleanable_size.cmp(&a.project.cleanable_size));
1372
1373 self.total_size = scan_result.total_cleanable;
1374 self.dirs_scanned = scan_result.directories_scanned;
1375 self.state = AppState::Results;
1376 self.status_message = Some(format!(
1377 "Found {} projects ({}) - Use j/k to navigate, Space to select",
1378 self.projects.len(),
1379 format_size(self.total_size)
1380 ));
1381 self.selected = 0;
1382 self.scroll_offset = 0;
1383 }
1384
1385 fn handle_cache_scan_complete(&mut self, caches: Vec<CacheEntry>) {
1387 self.caches = caches;
1388 self.caches.sort_by(|a, b| b.size.cmp(&a.size));
1389
1390 self.total_size = self.caches.iter().map(|c| c.size).sum();
1391 self.state = AppState::CacheResults;
1392 self.status_message = Some(format!(
1393 "Found {} caches ({}) - Use j/k to navigate, Space to select",
1394 self.caches.len(),
1395 format_size(self.total_size)
1396 ));
1397 self.selected = 0;
1398 self.scroll_offset = 0;
1399 }
1400
1401 fn handle_cleaner_scan_complete(&mut self, cleaners: Vec<CleanerEntry>) {
1403 self.cleaners = cleaners;
1404 self.cleaners.sort_by(|a, b| b.size.cmp(&a.size));
1405
1406 self.total_size = self.cleaners.iter().map(|c| c.size).sum();
1407 self.state = AppState::CleanerResults;
1408 self.status_message = Some(format!(
1409 "Found {} items ({}) - Use j/k to navigate, Space to select",
1410 self.cleaners.len(),
1411 format_size(self.total_size)
1412 ));
1413 self.selected = 0;
1414 self.scroll_offset = 0;
1415 }
1416
1417 pub fn select_up(&mut self) {
1419 let count = self.item_count();
1420 if count == 0 {
1421 return;
1422 }
1423
1424 if self.selected > 0 {
1425 self.selected -= 1;
1426 } else {
1427 self.selected = count - 1;
1428 }
1429 self.ensure_visible();
1430 }
1431
1432 pub fn select_down(&mut self) {
1434 let count = self.item_count();
1435 if count == 0 {
1436 return;
1437 }
1438
1439 if self.selected < count - 1 {
1440 self.selected += 1;
1441 } else {
1442 self.selected = 0;
1443 }
1444 self.ensure_visible();
1445 }
1446
1447 fn item_count(&self) -> usize {
1449 match self.state {
1450 AppState::Results => self.visible_count(),
1451 AppState::CacheResults => self.caches.iter().filter(|c| c.visible).count(),
1452 AppState::CleanerResults => self.cleaners.iter().filter(|c| c.visible).count(),
1453 _ => 0,
1454 }
1455 }
1456
1457 pub fn page_up(&mut self, page_size: usize) {
1459 if self.selected >= page_size {
1460 self.selected -= page_size;
1461 } else {
1462 self.selected = 0;
1463 }
1464 self.ensure_visible();
1465 }
1466
1467 pub fn page_down(&mut self, page_size: usize) {
1469 let count = self.item_count();
1470 if count == 0 {
1471 return;
1472 }
1473
1474 if self.selected + page_size < count {
1475 self.selected += page_size;
1476 } else {
1477 self.selected = count - 1;
1478 }
1479 self.ensure_visible();
1480 }
1481
1482 pub fn go_top(&mut self) {
1484 self.selected = 0;
1485 self.scroll_offset = 0;
1486 }
1487
1488 pub fn go_bottom(&mut self) {
1490 let count = self.item_count();
1491 if count > 0 {
1492 self.selected = count - 1;
1493 }
1494 self.ensure_visible();
1495 }
1496
1497 fn ensure_visible(&mut self) {
1499 self.ensure_visible_with_height(15);
1500 }
1501
1502 pub fn ensure_visible_with_height(&mut self, viewport_height: usize) {
1504 if viewport_height == 0 {
1505 return;
1506 }
1507
1508 let padding = 2;
1510
1511 if self.selected < self.scroll_offset + padding {
1512 self.scroll_offset = self.selected.saturating_sub(padding);
1514 } else if self.selected >= self.scroll_offset + viewport_height - padding {
1515 self.scroll_offset = self.selected.saturating_sub(viewport_height - padding - 1);
1517 }
1518 }
1519
1520 pub fn toggle_select(&mut self) {
1522 match self.state {
1523 AppState::Results => self.toggle_select_project(),
1524 AppState::CacheResults => self.toggle_select_cache(),
1525 AppState::CleanerResults => self.toggle_select_cleaner(),
1526 _ => {}
1527 }
1528 self.update_status();
1529 }
1530
1531 fn toggle_select_project(&mut self) {
1532 let selected_idx = self.selected;
1533 let visible_indices: Vec<usize> = self
1534 .projects
1535 .iter()
1536 .enumerate()
1537 .filter(|(_, p)| p.visible)
1538 .map(|(i, _)| i)
1539 .collect();
1540
1541 if let Some(&idx) = visible_indices.get(selected_idx) {
1542 if let Some(entry) = self.projects.get_mut(idx) {
1543 entry.selected = !entry.selected;
1544 }
1545 }
1546 }
1547
1548 fn toggle_select_cache(&mut self) {
1549 if let Some(cache) = self.caches.get_mut(self.selected) {
1550 cache.selected = !cache.selected;
1551 }
1552 }
1553
1554 fn toggle_select_cleaner(&mut self) {
1555 if let Some(cleaner) = self.cleaners.get_mut(self.selected) {
1556 cleaner.selected = !cleaner.selected;
1557 }
1558 }
1559
1560 pub fn toggle_expand(&mut self) {
1562 let visible_indices: Vec<usize> = self
1563 .projects
1564 .iter()
1565 .enumerate()
1566 .filter(|(_, p)| p.visible)
1567 .map(|(i, _)| i)
1568 .collect();
1569
1570 if let Some(&idx) = visible_indices.get(self.selected) {
1571 if self.expanded.contains(&idx) {
1572 self.expanded.remove(&idx);
1573 } else {
1574 self.expanded.insert(idx);
1575 }
1576 }
1577 }
1578
1579 pub fn expand(&mut self) {
1581 let visible_indices: Vec<usize> = self
1582 .projects
1583 .iter()
1584 .enumerate()
1585 .filter(|(_, p)| p.visible)
1586 .map(|(i, _)| i)
1587 .collect();
1588
1589 if let Some(&idx) = visible_indices.get(self.selected) {
1590 self.expanded.insert(idx);
1591 }
1592 }
1593
1594 pub fn collapse(&mut self) {
1596 let visible_indices: Vec<usize> = self
1597 .projects
1598 .iter()
1599 .enumerate()
1600 .filter(|(_, p)| p.visible)
1601 .map(|(i, _)| i)
1602 .collect();
1603
1604 if let Some(&idx) = visible_indices.get(self.selected) {
1605 self.expanded.remove(&idx);
1606 }
1607 }
1608
1609 pub fn scroll_up(&mut self) {
1611 if self.scroll_offset > 0 {
1612 self.scroll_offset -= 1;
1613 if self.selected > self.scroll_offset + 20 {
1614 self.selected = self.scroll_offset + 20;
1615 }
1616 }
1617 }
1618
1619 pub fn scroll_down(&mut self) {
1621 let count = self.item_count();
1622 if count > 0 && self.scroll_offset < count.saturating_sub(1) {
1623 self.scroll_offset += 1;
1624 if self.selected < self.scroll_offset {
1625 self.selected = self.scroll_offset;
1626 }
1627 }
1628 }
1629
1630 pub fn select_all(&mut self) {
1632 match self.state {
1633 AppState::Results => {
1634 for entry in &mut self.projects {
1635 if entry.visible {
1636 entry.selected = true;
1637 }
1638 }
1639 }
1640 AppState::CacheResults => {
1641 for cache in &mut self.caches {
1642 if cache.visible {
1643 cache.selected = true;
1644 }
1645 }
1646 }
1647 AppState::CleanerResults => {
1648 for cleaner in &mut self.cleaners {
1649 if cleaner.visible {
1650 cleaner.selected = true;
1651 }
1652 }
1653 }
1654 _ => {}
1655 }
1656 self.update_status();
1657 }
1658
1659 pub fn deselect_all(&mut self) {
1661 for entry in &mut self.projects {
1662 entry.selected = false;
1663 }
1664 for cache in &mut self.caches {
1665 cache.selected = false;
1666 }
1667 for cleaner in &mut self.cleaners {
1668 cleaner.selected = false;
1669 }
1670 self.update_status();
1671 }
1672
1673 pub fn visible_projects(&self) -> Vec<&ProjectEntry> {
1675 self.projects.iter().filter(|p| p.visible).collect()
1676 }
1677
1678 pub fn visible_count(&self) -> usize {
1680 self.projects.iter().filter(|p| p.visible).count()
1681 }
1682
1683 pub fn selected_projects(&self) -> Vec<&ProjectEntry> {
1685 self.projects.iter().filter(|p| p.selected).collect()
1686 }
1687
1688 pub fn selected_size(&self) -> u64 {
1690 let project_size: u64 = self.projects
1691 .iter()
1692 .filter(|p| p.selected)
1693 .map(|p| p.project.cleanable_size)
1694 .sum();
1695 let cache_size: u64 = self.caches
1696 .iter()
1697 .filter(|c| c.selected)
1698 .map(|c| c.size)
1699 .sum();
1700 let cleaner_size: u64 = self.cleaners
1701 .iter()
1702 .filter(|c| c.selected)
1703 .map(|c| c.size)
1704 .sum();
1705 project_size + cache_size + cleaner_size
1706 }
1707
1708 pub fn selected_count(&self) -> usize {
1710 let projects = self.projects.iter().filter(|p| p.selected).count();
1711 let caches = self.caches.iter().filter(|c| c.selected).count();
1712 let cleaners = self.cleaners.iter().filter(|c| c.selected).count();
1713 projects + caches + cleaners
1714 }
1715
1716 fn update_status(&mut self) {
1718 let selected = self.selected_count();
1719 let size = self.selected_size();
1720 if selected > 0 {
1721 self.status_message = Some(format!(
1722 "Selected: {} items ({}) - Press 'd' to delete",
1723 selected,
1724 format_size(size)
1725 ));
1726 } else {
1727 let count = self.item_count();
1728 self.status_message = Some(format!(
1729 "Found {} items ({}) - Use j/k to navigate, Space to select",
1730 count,
1731 format_size(self.total_size)
1732 ));
1733 }
1734 }
1735
1736 pub fn filter_by_tab(&mut self) {
1738 let tab = &self.tabs[self.current_tab];
1739
1740 for entry in &mut self.projects {
1741 entry.visible = match tab.as_str() {
1742 "All" => true,
1743 "Node" => entry.project.kind.is_node(),
1744 "Rust" => entry.project.kind.is_rust(),
1745 "Python" => entry.project.kind.is_python(),
1746 "Java" => entry.project.kind.is_java(),
1747 "Other" => {
1748 !entry.project.kind.is_node()
1749 && !entry.project.kind.is_rust()
1750 && !entry.project.kind.is_python()
1751 && !entry.project.kind.is_java()
1752 }
1753 _ => true,
1754 };
1755
1756 if entry.visible && !self.search_query.is_empty() {
1758 let query = self.search_query.to_lowercase();
1759 entry.visible = entry.project.name.to_lowercase().contains(&query)
1760 || entry
1761 .project
1762 .root
1763 .to_string_lossy()
1764 .to_lowercase()
1765 .contains(&query);
1766 }
1767 }
1768
1769 if self.selected >= self.visible_count() {
1771 self.selected = 0;
1772 }
1773 self.scroll_offset = 0;
1774 }
1775
1776 pub fn next_tab(&mut self) {
1778 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1779 self.filter_by_tab();
1780 }
1781
1782 pub fn prev_tab(&mut self) {
1784 if self.current_tab > 0 {
1785 self.current_tab -= 1;
1786 } else {
1787 self.current_tab = self.tabs.len() - 1;
1788 }
1789 self.filter_by_tab();
1790 }
1791
1792 pub fn toggle_help(&mut self) {
1794 self.show_help = !self.show_help;
1795 }
1796
1797 pub fn start_search(&mut self) {
1799 self.is_searching = true;
1800 self.search_query.clear();
1801 }
1802
1803 pub fn end_search(&mut self) {
1805 self.is_searching = false;
1806 }
1807
1808 pub fn search_push(&mut self, c: char) {
1810 self.search_query.push(c);
1811 self.filter_by_tab();
1812 }
1813
1814 pub fn search_pop(&mut self) {
1816 self.search_query.pop();
1817 self.filter_by_tab();
1818 }
1819
1820 pub fn current_project(&self) -> Option<&ProjectEntry> {
1822 self.visible_projects().get(self.selected).copied()
1823 }
1824
1825 pub fn is_expanded(&self, visible_index: usize) -> bool {
1827 let visible_indices: Vec<usize> = self
1828 .projects
1829 .iter()
1830 .enumerate()
1831 .filter(|(_, p)| p.visible)
1832 .map(|(i, _)| i)
1833 .collect();
1834
1835 visible_indices
1836 .get(visible_index)
1837 .map(|&idx| self.expanded.contains(&idx))
1838 .unwrap_or(false)
1839 }
1840
1841 pub fn request_delete(&mut self) {
1843 if self.selected_count() > 0 {
1844 self.state = AppState::Confirming;
1845 } else {
1846 self.status_message = Some("No items selected. Use Space to select items.".to_string());
1847 }
1848 }
1849
1850 pub fn cancel_delete(&mut self) {
1852 if !self.projects.is_empty() {
1854 self.state = AppState::Results;
1855 } else if !self.caches.is_empty() {
1856 self.state = AppState::CacheResults;
1857 } else if !self.cleaners.is_empty() {
1858 self.state = AppState::CleanerResults;
1859 } else {
1860 self.state = AppState::Ready;
1861 }
1862 self.update_status();
1863 }
1864
1865 pub fn start_delete(&mut self) {
1868 self.state = AppState::Cleaning;
1869
1870 let mut items: Vec<(PathBuf, Option<String>)> = self
1872 .projects
1873 .iter()
1874 .filter(|p| p.selected)
1875 .flat_map(|p| p.project.artifacts.iter().map(|a| (a.path.clone(), None)))
1876 .collect();
1877
1878 for cache in &self.caches {
1880 if cache.selected {
1881 items.push((cache.path.clone(), None));
1882 }
1883 }
1884
1885 for cleaner in &self.cleaners {
1887 if cleaner.selected {
1888 items.push((cleaner.path.clone(), cleaner.clean_command.clone()));
1889 }
1890 }
1891
1892 self.pending_delete_items = items;
1893 }
1894
1895 pub fn has_pending_delete(&self) -> bool {
1897 self.state == AppState::Cleaning && !self.pending_delete_items.is_empty()
1898 }
1899
1900 pub fn take_pending_delete_items(&mut self) -> Vec<(PathBuf, Option<String>)> {
1902 std::mem::take(&mut self.pending_delete_items)
1903 }
1904
1905 pub fn deletion_complete(&mut self, success_count: usize, fail_count: usize, freed: u64) {
1907 self.projects.retain(|p| !p.selected);
1909 self.caches.retain(|c| !c.selected);
1910 self.cleaners.retain(|c| !c.selected);
1911
1912 if !self.projects.is_empty() {
1914 self.state = AppState::Results;
1915 } else if !self.caches.is_empty() {
1916 self.state = AppState::CacheResults;
1917 } else if !self.cleaners.is_empty() {
1918 self.state = AppState::CleanerResults;
1919 } else {
1920 self.state = AppState::Ready;
1921 }
1922
1923 self.status_message = Some(format!(
1924 "Deleted {} items, freed {} ({} failed)",
1925 success_count,
1926 format_size(freed),
1927 fail_count
1928 ));
1929
1930 self.total_size = self.projects.iter().map(|p| p.project.cleanable_size).sum::<u64>()
1932 + self.caches.iter().map(|c| c.size).sum::<u64>()
1933 + self.cleaners.iter().map(|c| c.size).sum::<u64>();
1934
1935 let count = self.item_count();
1937 if self.selected >= count && count > 0 {
1938 self.selected = count - 1;
1939 }
1940 }
1941
1942 pub fn go_back(&mut self) {
1944 self.state = AppState::Ready;
1945 self.projects.clear();
1946 self.caches.clear();
1947 self.cleaners.clear();
1948 self.selected = 0;
1949 self.scroll_offset = 0;
1950 self.total_size = 0;
1951 self.status_message = Some("Select a scan mode and press Enter".to_string());
1952 }
1953}
1954
1955impl Default for App {
1956 fn default() -> Self {
1957 Self::new(vec![])
1958 }
1959}
1960
1961pub fn format_size(bytes: u64) -> String {
1963 const KB: u64 = 1024;
1964 const MB: u64 = KB * 1024;
1965 const GB: u64 = MB * 1024;
1966
1967 if bytes >= GB {
1968 format!("{:.2} GB", bytes as f64 / GB as f64)
1969 } else if bytes >= MB {
1970 format!("{:.2} MB", bytes as f64 / MB as f64)
1971 } else if bytes >= KB {
1972 format!("{:.2} KB", bytes as f64 / KB as f64)
1973 } else {
1974 format!("{} B", bytes)
1975 }
1976}