Skip to main content

null_e/tui/
app.rs

1//! TUI Application state and logic
2
3use 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
12/// Message sent from scan thread
13pub enum ScanMessage {
14    /// Progress update
15    Progress { dirs_scanned: usize, message: String },
16    /// Scan completed for projects
17    CompleteProjects(ScanResult),
18    /// Scan completed for caches
19    CompleteCaches(Vec<CacheEntry>),
20    /// Scan completed for cleaners
21    CompleteCleaners(Vec<CleanerEntry>),
22    /// Scan error
23    Error(String),
24}
25
26/// Scan mode - what type of scan to perform
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ScanMode {
29    /// Scan everything at once
30    All,
31    /// Scan for development projects (node_modules, target, venv, etc.)
32    Projects,
33    /// Scan global caches (npm, pip, cargo, brew, etc.)
34    Caches,
35    /// Scan Xcode artifacts
36    Xcode,
37    /// Scan Docker resources
38    Docker,
39    /// Scan IDE caches
40    IDECaches,
41    /// Scan ML/AI caches
42    MLCaches,
43    /// Scan Android Studio
44    Android,
45    /// Scan Electron apps
46    Electron,
47    /// Scan Cloud CLI (AWS, GCP, Azure, kubectl, Terraform)
48    Cloud,
49    /// Scan Homebrew/Package Managers
50    PackageManagers,
51    /// Scan game dev (Unity, Unreal, Godot)
52    GameDev,
53    /// Scan misc tools (Vagrant, Go, Ruby, Git LFS)
54    MiscTools,
55    /// Scan test browsers (Playwright, Cypress, Puppeteer)
56    TestBrowsers,
57    /// Scan system (Trash, Downloads, Temp)
58    System,
59    /// Scan logs
60    Logs,
61    /// Scan language runtimes (nvm, pyenv, rbenv, rustup, sdkman, gvm)
62    Runtimes,
63    /// Analyze system binaries for duplicates and conflicts
64    BinaryAnalysis,
65}
66
67impl ScanMode {
68    /// Get all scan modes
69    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    /// Get display name
93    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    /// Get description
117    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    /// Get icon
141    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/// Cache entry for display
166#[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/// Cleaner entry for display
178#[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    /// Optional command to run instead of deleting the path (for Docker, etc.)
188    pub clean_command: Option<String>,
189}
190
191/// Main TUI application state
192pub struct App {
193    /// Current screen/state
194    pub state: AppState,
195    /// Selected scan mode
196    pub scan_mode: ScanMode,
197    /// Menu selection index (for ready screen)
198    pub menu_index: usize,
199    /// Projects to display
200    pub projects: Vec<ProjectEntry>,
201    /// Caches to display
202    pub caches: Vec<CacheEntry>,
203    /// Cleaner items to display
204    pub cleaners: Vec<CleanerEntry>,
205    /// Currently selected index
206    pub selected: usize,
207    /// Scroll offset for the list
208    pub scroll_offset: usize,
209    /// Expanded project indices
210    pub expanded: HashSet<usize>,
211    /// Status message
212    pub status_message: Option<String>,
213    /// Should quit
214    pub should_quit: bool,
215    /// Show help popup
216    pub show_help: bool,
217    /// Current tab/category
218    pub current_tab: usize,
219    /// Available tabs
220    pub tabs: Vec<String>,
221    /// Search query
222    pub search_query: String,
223    /// Is searching
224    pub is_searching: bool,
225    /// Scan paths
226    pub scan_paths: Vec<PathBuf>,
227    /// Scan progress (0.0 - 1.0)
228    pub scan_progress: f64,
229    /// Scan message
230    pub scan_message: String,
231    /// Directories scanned
232    pub dirs_scanned: usize,
233    /// Total size found
234    pub total_size: u64,
235    /// Scan result receiver (for async scanning)
236    scan_receiver: Option<Receiver<ScanMessage>>,
237    /// Animation frame counter
238    pub anim_frame: usize,
239    /// Use permanent delete (rm -rf) instead of trash
240    pub permanent_delete: bool,
241    /// Items pending deletion (path, optional clean_command)
242    pub pending_delete_items: Vec<(PathBuf, Option<String>)>,
243}
244
245/// Application state/screen
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum AppState {
248    /// Initial state, showing menu
249    Ready,
250    /// Currently scanning
251    Scanning,
252    /// Showing project results
253    Results,
254    /// Showing cache results
255    CacheResults,
256    /// Showing cleaner results
257    CleanerResults,
258    /// Confirming deletion
259    Confirming,
260    /// Cleaning in progress
261    Cleaning,
262    /// Error state
263    Error(String),
264}
265
266/// A project entry in the list
267#[derive(Debug, Clone)]
268pub struct ProjectEntry {
269    /// The project data
270    pub project: Project,
271    /// Is this entry selected for deletion
272    pub selected: bool,
273    /// Is this entry visible (after filtering)
274    pub visible: bool,
275}
276
277impl App {
278    /// Create a new app with scan paths
279    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    /// Toggle permanent delete option
317    pub fn toggle_permanent_delete(&mut self) {
318        self.permanent_delete = !self.permanent_delete;
319    }
320
321    /// Tick the animation frame (call on every tick for smooth animations)
322    pub fn tick_animation(&mut self) {
323        self.anim_frame = self.anim_frame.wrapping_add(1);
324    }
325
326    /// Move menu selection up
327    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    /// Move menu selection down
338    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    /// Start scanning based on current mode
349    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        // Create channel for communication
362        let (tx, rx): (Sender<ScanMessage>, Receiver<ScanMessage>) = mpsc::channel();
363        self.scan_receiver = Some(rx);
364
365        // Clone paths for the thread
366        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        // Spawn scanning thread
375        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    /// Scan everything at once
400    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        // Collect all cleaners
407        let mut all_cleaners: Vec<CleanerEntry> = Vec::new();
408
409        // 1. Scan caches
410        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        // 2. Scan Xcode
431        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        // 3. Scan Docker
453        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        // 4. Scan IDE caches
476        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        // 5. Scan ML caches
498        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        // 6. Scan Android
520        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        // 7. Scan Electron
542        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        // 8. Scan Cloud CLI
564        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        // 9. Scan Package Managers
586        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        // 10. Scan Game Dev
608        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        // 11. Scan Misc Tools
630        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        // 12. Scan Test Browsers
652        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        // 13. Scan System
674        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        // 14. Scan Logs
696        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        // 15. Scan Language Runtimes
718        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        // 16. Binary Analysis (duplicates, unused managers)
740        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        // 17. Scan projects
763        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        // Use home directory if paths is just current dir (for better project discovery)
772        let project_paths = if paths.len() == 1 && paths[0] == std::env::current_dir().unwrap_or_default() {
773            // Default to home directory for project scanning
774            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    /// Scan for projects
803    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        // Use home directory if paths is just current dir
813        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    /// Scan for global caches
835    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    /// Scan Xcode artifacts
868    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    /// Scan Docker resources
897    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    /// Scan IDE caches
927    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    /// Scan ML caches
956    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    /// Scan Android
985    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    /// Scan Electron apps
1014    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    /// Scan Cloud CLI
1043    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    /// Scan Package Managers
1072    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    /// Scan Game Dev
1101    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    /// Scan Misc Tools
1130    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    /// Scan Test Browsers
1159    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    /// Scan System
1188    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    /// Scan Logs
1217    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    /// Scan Language Runtimes (nvm, pyenv, rbenv, rustup, sdkman, gvm)
1246    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    /// Scan system binaries for duplicates and conflicts
1275    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    /// Check for scan updates (call this on tick)
1319    pub fn check_scan_progress(&mut self) {
1320        // Increment animation frame
1321        self.anim_frame = self.anim_frame.wrapping_add(1);
1322
1323        if let Some(ref rx) = self.scan_receiver {
1324            // Try to receive without blocking
1325            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    /// Handle project scan completion
1358    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        // Sort by size (largest first)
1370        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    /// Handle cache scan completion
1386    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    /// Handle cleaner scan completion
1402    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    /// Move selection up
1418    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    /// Move selection down
1433    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    /// Get total item count based on current state
1448    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    /// Page up
1458    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    /// Page down
1468    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    /// Go to top
1483    pub fn go_top(&mut self) {
1484        self.selected = 0;
1485        self.scroll_offset = 0;
1486    }
1487
1488    /// Go to bottom
1489    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    /// Ensure selected item is visible - use default viewport of 15
1498    fn ensure_visible(&mut self) {
1499        self.ensure_visible_with_height(15);
1500    }
1501
1502    /// Ensure selected item is visible with viewport height
1503    pub fn ensure_visible_with_height(&mut self, viewport_height: usize) {
1504        if viewport_height == 0 {
1505            return;
1506        }
1507
1508        // Always keep selection visible with some padding
1509        let padding = 2;
1510
1511        if self.selected < self.scroll_offset + padding {
1512            // Selection is too close to top - scroll up
1513            self.scroll_offset = self.selected.saturating_sub(padding);
1514        } else if self.selected >= self.scroll_offset + viewport_height - padding {
1515            // Selection is too close to bottom - scroll down
1516            self.scroll_offset = self.selected.saturating_sub(viewport_height - padding - 1);
1517        }
1518    }
1519
1520    /// Toggle selection of current item
1521    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    /// Toggle expanded state of current item
1561    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    /// Expand current item (right arrow)
1580    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    /// Collapse current item (left arrow)
1595    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    /// Scroll up by one item
1610    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    /// Scroll down by one item
1620    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    /// Select all visible items
1631    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    /// Deselect all items
1660    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    /// Get visible projects
1674    pub fn visible_projects(&self) -> Vec<&ProjectEntry> {
1675        self.projects.iter().filter(|p| p.visible).collect()
1676    }
1677
1678    /// Count visible projects
1679    pub fn visible_count(&self) -> usize {
1680        self.projects.iter().filter(|p| p.visible).count()
1681    }
1682
1683    /// Get selected projects
1684    pub fn selected_projects(&self) -> Vec<&ProjectEntry> {
1685        self.projects.iter().filter(|p| p.selected).collect()
1686    }
1687
1688    /// Get total selected size
1689    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    /// Get number of selected items
1709    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    /// Update status message
1717    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    /// Filter projects by current tab
1737    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            // Also apply search filter
1757            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        // Reset selection if current selection is not visible
1770        if self.selected >= self.visible_count() {
1771            self.selected = 0;
1772        }
1773        self.scroll_offset = 0;
1774    }
1775
1776    /// Switch to next tab
1777    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    /// Switch to previous tab
1783    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    /// Toggle help
1793    pub fn toggle_help(&mut self) {
1794        self.show_help = !self.show_help;
1795    }
1796
1797    /// Enter search mode
1798    pub fn start_search(&mut self) {
1799        self.is_searching = true;
1800        self.search_query.clear();
1801    }
1802
1803    /// Exit search mode
1804    pub fn end_search(&mut self) {
1805        self.is_searching = false;
1806    }
1807
1808    /// Add character to search query
1809    pub fn search_push(&mut self, c: char) {
1810        self.search_query.push(c);
1811        self.filter_by_tab();
1812    }
1813
1814    /// Remove last character from search query
1815    pub fn search_pop(&mut self) {
1816        self.search_query.pop();
1817        self.filter_by_tab();
1818    }
1819
1820    /// Get current project (selected)
1821    pub fn current_project(&self) -> Option<&ProjectEntry> {
1822        self.visible_projects().get(self.selected).copied()
1823    }
1824
1825    /// Is project expanded
1826    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    /// Request deletion confirmation
1842    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    /// Cancel deletion
1851    pub fn cancel_delete(&mut self) {
1852        // Return to appropriate results state
1853        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    /// Start deletion - collects paths and sets Cleaning state
1866    /// Actual deletion happens on next tick to allow UI to render
1867    pub fn start_delete(&mut self) {
1868        self.state = AppState::Cleaning;
1869
1870        // Collect items to delete (path, optional clean_command)
1871        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        // Add cache items (no clean_command)
1879        for cache in &self.caches {
1880            if cache.selected {
1881                items.push((cache.path.clone(), None));
1882            }
1883        }
1884
1885        // Add cleaner items (may have clean_command for Docker, etc.)
1886        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    /// Check if we have pending deletions to process
1896    pub fn has_pending_delete(&self) -> bool {
1897        self.state == AppState::Cleaning && !self.pending_delete_items.is_empty()
1898    }
1899
1900    /// Take pending delete items (clears them)
1901    pub fn take_pending_delete_items(&mut self) -> Vec<(PathBuf, Option<String>)> {
1902        std::mem::take(&mut self.pending_delete_items)
1903    }
1904
1905    /// Mark deletion complete
1906    pub fn deletion_complete(&mut self, success_count: usize, fail_count: usize, freed: u64) {
1907        // Remove deleted items
1908        self.projects.retain(|p| !p.selected);
1909        self.caches.retain(|c| !c.selected);
1910        self.cleaners.retain(|c| !c.selected);
1911
1912        // Return to appropriate state
1913        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        // Recalculate total
1931        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        // Reset selection
1936        let count = self.item_count();
1937        if self.selected >= count && count > 0 {
1938            self.selected = count - 1;
1939        }
1940    }
1941
1942    /// Go back to menu
1943    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
1961/// Format size in human-readable form
1962pub 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}