Skip to main content

cleansys_tui/
app.rs

1use anyhow::Result;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use crossterm::terminal;
4use ratatui::widgets::ListState;
5use std::time::Instant;
6
7use crate::components::password_prompt::PasswordPrompt;
8use cleansys_core::{check_root, format_size, CleanerCategory, CleanerFn, Status};
9use std::time::SystemTime;
10
11#[derive(Debug, Clone)]
12pub struct DetailedCleanedItem {
13    pub path: String,
14    pub size: u64,
15    pub category: String,
16    pub cleaner_name: String,
17    pub timestamp: SystemTime,
18    pub item_type: CleanedItemType,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub enum CleanedItemType {
23    File,
24    Directory,
25    Log,
26}
27
28impl From<cleansys_core::CleanedItemType> for CleanedItemType {
29    fn from(value: cleansys_core::CleanedItemType) -> Self {
30        match value {
31            cleansys_core::CleanedItemType::File => CleanedItemType::File,
32            cleansys_core::CleanedItemType::Directory => CleanedItemType::Directory,
33            cleansys_core::CleanedItemType::SymLink => CleanedItemType::File,
34        }
35    }
36}
37
38/// Type alias for pending operations: (category_index, item_index, name, function, requires_root)
39pub type PendingOperation = (usize, usize, String, CleanerFn, bool);
40
41#[derive(Debug, Clone, PartialEq)]
42pub enum ViewMode {
43    Standard,
44    Compact,
45    Detailed,
46    Performance,
47}
48
49#[derive(Debug, Clone, PartialEq)]
50pub enum SortMode {
51    Name,
52    Size,
53    Status,
54    Category,
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub enum FilterMode {
59    All,
60    Selected,
61    Completed,
62    Errors,
63    UserOnly,
64    SystemOnly,
65}
66
67#[derive(Debug, Clone, PartialEq)]
68pub enum ChartType {
69    Bar,
70    PieCount,
71    PieSize,
72}
73
74// `Status`, `CleanerItem`, and `CleanerCategory` now live in `cleansys-core`
75// so the TUI and GUI front-ends share the exact same domain model.
76
77pub struct App {
78    pub categories: Vec<CleanerCategory>,
79    pub category_index: usize,
80    pub item_list_state: ListState,
81    pub is_root: bool,
82    pub is_running: bool,
83    pub operation_start_time: Option<Instant>,
84    pub operation_end_time: Option<Instant>,
85    pub total_bytes_cleaned: u64,
86    pub show_help: bool,
87    pub result_messages: Vec<String>,
88    pub detailed_view: bool,
89    pub current_cleaner_index: usize,
90    pub animation_frame: usize,
91    pub last_frame_time: Instant,
92    pub terminal_width: u16,
93    pub terminal_height: u16,
94    pub compact_mode: bool,
95    pub show_performance_stats: bool,
96    pub operation_count: usize,
97    pub errors_count: usize,
98    pub paused: bool,
99    pub confirmation_mode: bool,
100    pub selected_cleaners_count: usize,
101    pub view_mode: ViewMode,
102    pub sort_mode: SortMode,
103    pub filter_mode: FilterMode,
104    pub detailed_cleaned_items: Vec<DetailedCleanedItem>,
105    pub detailed_list_scroll_state: ListState,
106    pub search_query: String,
107    pub search_active: bool,
108    pub detailed_view_filter: String,
109    pub demo_operation_timer: Option<Instant>,
110    pub demo_operations_completed: usize,
111    pub chart_type: ChartType,
112    pub operation_logs: Vec<String>,
113    pub show_progress_screen: bool,
114    pub password_prompt: PasswordPrompt,
115    pub needs_sudo: bool,
116    pub pending_operations: Vec<PendingOperation>,
117    /// Whether the "confirm this run" overlay is currently shown, awaiting
118    /// a yes/no answer before `pending_operations` actually executes.
119    pub awaiting_run_confirmation: bool,
120    /// Whether the preview (dry-run) results overlay is currently shown.
121    pub preview_open: bool,
122    /// Results of the most recent preview run: `(cleaner_name, result)`.
123    pub preview_results: Vec<(String, cleansys_core::CleaningResult)>,
124    /// Whether the "needs Administrator" notice is shown (Windows only;
125    /// there is no interactive sudo-password flow there).
126    pub needs_admin_notice: bool,
127    /// Selected cleaners staged while `awaiting_run_confirmation` is true.
128    pub pending_run_selection: Vec<PendingOperation>,
129}
130
131impl Default for App {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137impl App {
138    pub fn new() -> Self {
139        // Get initial terminal size
140        let (width, height) = terminal::size().unwrap_or((80, 24));
141
142        let mut app = App {
143            categories: Vec::new(),
144            category_index: 0,
145            item_list_state: ListState::default(),
146            is_root: check_root(),
147            is_running: false,
148            operation_start_time: None,
149            operation_end_time: None,
150            total_bytes_cleaned: 0,
151            show_help: false,
152            result_messages: Vec::new(),
153            detailed_view: false,
154            current_cleaner_index: 0,
155            animation_frame: 0,
156            last_frame_time: Instant::now(),
157            terminal_width: width,
158            terminal_height: height,
159            compact_mode: height < 25,
160            show_performance_stats: false,
161            operation_count: 0,
162            errors_count: 0,
163            paused: false,
164            confirmation_mode: true,
165            selected_cleaners_count: 0,
166            view_mode: if height < 25 {
167                ViewMode::Compact
168            } else {
169                ViewMode::Standard
170            },
171            sort_mode: SortMode::Category,
172            filter_mode: FilterMode::All,
173            detailed_cleaned_items: Vec::new(),
174            detailed_list_scroll_state: ListState::default(),
175            search_query: String::new(),
176            search_active: false,
177            detailed_view_filter: String::new(),
178            demo_operation_timer: None,
179            demo_operations_completed: 0,
180            chart_type: ChartType::PieCount,
181            operation_logs: Vec::new(),
182            show_progress_screen: false,
183            password_prompt: PasswordPrompt::new(),
184            needs_sudo: false,
185            pending_operations: Vec::new(),
186            awaiting_run_confirmation: false,
187            preview_open: false,
188            preview_results: Vec::new(),
189            needs_admin_notice: false,
190            pending_run_selection: Vec::new(),
191        };
192        app.item_list_state.select(Some(0));
193
194        app
195    }
196
197    pub fn toggle_search(&mut self) {
198        self.search_active = !self.search_active;
199        if !self.search_active {
200            self.search_query.clear();
201        }
202    }
203
204    pub fn clear_search(&mut self) {
205        self.search_active = false;
206        self.search_query.clear();
207        self.detailed_view_filter.clear();
208    }
209
210    pub fn add_search_char(&mut self, c: char) {
211        if self.search_active {
212            self.search_query.push(c);
213        }
214    }
215
216    pub fn remove_search_char(&mut self) {
217        if self.search_active {
218            self.search_query.pop();
219        }
220    }
221
222    pub fn get_category_distribution(&self) -> Vec<(String, usize, u64)> {
223        let mut category_map: std::collections::HashMap<String, (usize, u64)> =
224            std::collections::HashMap::new();
225
226        for item in &self.detailed_cleaned_items {
227            // Create a unique key that combines cleaner name with category type
228            // This differentiates between user and system cleaners with the same name
229            let display_name = if item.category.contains("System") {
230                format!("{} (System)", item.cleaner_name)
231            } else {
232                item.cleaner_name.clone()
233            };
234
235            let entry = category_map.entry(display_name).or_insert((0, 0));
236            entry.0 += 1;
237            entry.1 += item.size;
238        }
239
240        let mut categories: Vec<(String, usize, u64)> = category_map
241            .into_iter()
242            .map(|(name, (count, size))| (name, count, size))
243            .collect();
244
245        categories.sort_by_key(|b| std::cmp::Reverse(b.2)); // Sort by size descending
246        categories
247    }
248
249    pub fn next_item(&mut self) {
250        let items = &self.categories[self.category_index].items;
251        let i = match self.item_list_state.selected() {
252            Some(i) => {
253                if i >= items.len() - 1 {
254                    0
255                } else {
256                    i + 1
257                }
258            }
259            None => 0,
260        };
261        self.item_list_state.select(Some(i));
262    }
263
264    pub fn previous_item(&mut self) {
265        let items = &self.categories[self.category_index].items;
266        let i = match self.item_list_state.selected() {
267            Some(i) => {
268                if i == 0 {
269                    items.len() - 1
270                } else {
271                    i - 1
272                }
273            }
274            None => 0,
275        };
276        self.item_list_state.select(Some(i));
277    }
278
279    pub fn toggle_selected(&mut self) {
280        if let Some(i) = self.item_list_state.selected() {
281            let item = &mut self.categories[self.category_index].items[i];
282            // Allow selection even for root items, will prompt for password later
283            item.selected = !item.selected;
284        }
285    }
286
287    pub fn next_category(&mut self) {
288        if self.category_index < self.categories.len() - 1 {
289            self.category_index += 1;
290        } else {
291            self.category_index = 0;
292        }
293        // Reset selection in new category
294        self.item_list_state.select(Some(0));
295    }
296
297    pub fn previous_category(&mut self) {
298        if self.category_index > 0 {
299            self.category_index -= 1;
300        } else {
301            self.category_index = self.categories.len() - 1;
302        }
303        // Reset selection in new category
304        self.item_list_state.select(Some(0));
305    }
306
307    pub fn toggle_help(&mut self) {
308        self.show_help = !self.show_help;
309    }
310
311    pub fn select_all(&mut self) {
312        for item in &mut self.categories[self.category_index].items {
313            // Allow selection of all items, will handle root permissions later
314            item.selected = true;
315        }
316    }
317
318    pub fn deselect_all(&mut self) {
319        for item in &mut self.categories[self.category_index].items {
320            item.selected = false;
321        }
322    }
323
324    /// Select every item across every category (not just the active tab).
325    pub fn select_all_everywhere(&mut self) {
326        for category in &mut self.categories {
327            for item in &mut category.items {
328                item.selected = true;
329            }
330        }
331    }
332
333    /// Deselect every item across every category (not just the active tab).
334    pub fn deselect_all_everywhere(&mut self) {
335        for category in &mut self.categories {
336            for item in &mut category.items {
337                item.selected = false;
338            }
339        }
340    }
341
342    /// Gather selected cleaners and either show the confirmation overlay
343    /// (when `confirmation_mode` is on) or start execution immediately.
344    pub fn request_run(&mut self) -> Result<()> {
345        if self.is_running {
346            return Ok(());
347        }
348
349        let has_selected = self
350            .categories
351            .iter()
352            .any(|c| c.items.iter().any(|i| i.selected));
353
354        if !has_selected {
355            self.result_messages
356                .push("No items selected. Please select items to clean.".to_string());
357            return Ok(());
358        }
359
360        let mut selected_cleaners = Vec::new();
361        for (cat_idx, category) in self.categories.iter().enumerate() {
362            for (item_idx, item) in category.items.iter().enumerate() {
363                if item.selected {
364                    let name = item.name.clone();
365                    let function = item.function;
366                    selected_cleaners.push((cat_idx, item_idx, name, function, item.requires_root));
367                }
368            }
369        }
370
371        if selected_cleaners.is_empty() {
372            self.operation_logs
373                .push("No cleaners selected. Please select at least one cleaner.".to_string());
374            return Ok(());
375        }
376
377        if self.confirmation_mode {
378            self.pending_run_selection = selected_cleaners;
379            self.awaiting_run_confirmation = true;
380            Ok(())
381        } else {
382            self.begin_execution(selected_cleaners)
383        }
384    }
385
386    /// User confirmed the run in the confirmation overlay.
387    pub fn confirm_pending_run(&mut self) -> Result<()> {
388        self.awaiting_run_confirmation = false;
389        let selected_cleaners = std::mem::take(&mut self.pending_run_selection);
390        self.begin_execution(selected_cleaners)
391    }
392
393    /// User cancelled the confirmation overlay.
394    pub fn cancel_run_confirmation(&mut self) {
395        self.awaiting_run_confirmation = false;
396        self.pending_run_selection.clear();
397    }
398
399    /// Preview (dry-run) every selected cleaner synchronously: measures real
400    /// sizes/paths without deleting anything or invoking any mutating
401    /// external command, and shows the results in an overlay.
402    pub fn run_preview(&mut self) {
403        if self.is_running {
404            return;
405        }
406
407        let selected: Vec<(String, cleansys_core::CleanerFn)> = self
408            .categories
409            .iter()
410            .flat_map(|c| c.items.iter())
411            .filter(|i| i.selected)
412            .map(|i| (i.name.clone(), i.function))
413            .collect();
414
415        if selected.is_empty() {
416            self.result_messages
417                .push("No items selected. Please select items to preview.".to_string());
418            return;
419        }
420
421        self.preview_results.clear();
422        for (name, function) in selected {
423            match function(cleansys_core::RunOptions::preview()) {
424                Ok(result) => self.preview_results.push((name, result)),
425                Err(e) => self
426                    .operation_logs
427                    .push(format!("⚠️  Preview failed for {name}: {e}")),
428            }
429        }
430        self.preview_open = true;
431    }
432
433    /// Close the preview results overlay.
434    pub fn close_preview(&mut self) {
435        self.preview_open = false;
436        self.preview_results.clear();
437    }
438
439    /// Actually start execution of the given selected cleaners: prompts for
440    /// elevation if needed (sudo password on Unix, an "Administrator
441    /// required" notice on Windows), or starts the run directly.
442    fn begin_execution(&mut self, selected_cleaners: Vec<PendingOperation>) -> Result<()> {
443        let has_root_operations = selected_cleaners.iter().any(|(_, _, _, _, root)| *root);
444
445        // Check if we need elevation
446        if has_root_operations && !self.is_root {
447            self.pending_operations.clone_from(&selected_cleaners);
448            if cleansys_core::utils::supports_sudo_prompt() {
449                self.needs_sudo = true;
450                self.password_prompt.show();
451            } else {
452                self.needs_admin_notice = true;
453            }
454            return Ok(());
455        }
456
457        // Start processing
458        self.is_running = true;
459        self.show_progress_screen = true;
460        self.operation_start_time = Some(Instant::now());
461        self.operation_end_time = None;
462        self.total_bytes_cleaned = 0;
463        self.demo_operation_timer = Some(Instant::now());
464        self.demo_operations_completed = 0;
465        self.result_messages.clear();
466        self.operation_logs.clear();
467        self.detailed_cleaned_items.clear(); // Clear previous cleaning results
468        self.current_cleaner_index = 0;
469
470        // Reset bytes_cleaned for all items to start fresh
471        for category in &mut self.categories {
472            for item in &mut category.items {
473                item.bytes_cleaned = 0;
474                item.status = None;
475            }
476        }
477
478        // Set all selected cleaners to Pending
479        for (cat_idx, item_idx, _, _, _) in &selected_cleaners {
480            self.categories[*cat_idx].items[*item_idx].status = Some(Status::Pending);
481        }
482
483        // Operations will be processed by update_demo_operations over time.
484        // The is_running flag will be automatically turned off when all
485        // operations complete.
486
487        Ok(())
488    }
489
490    pub fn update_animation(&mut self) {
491        let now = Instant::now();
492        if now.duration_since(self.last_frame_time).as_millis() > 100 {
493            self.animation_frame = (self.animation_frame + 1) % 10;
494            self.last_frame_time = now;
495        }
496
497        // Update demo operations if running
498        if self.is_running {
499            self.update_demo_operations();
500        }
501    }
502
503    pub fn update_demo_operations(&mut self) {
504        if let Some(start_time) = self.demo_operation_timer {
505            let elapsed = start_time.elapsed().as_millis();
506
507            // Find next pending operation to start
508            type Operation = (usize, usize, String, CleanerFn, bool);
509            let mut pending_operations: Vec<Operation> = Vec::new();
510            for (cat_idx, category) in self.categories.iter().enumerate() {
511                for (item_idx, item) in category.items.iter().enumerate() {
512                    if matches!(item.status, Some(Status::Pending)) {
513                        pending_operations.push((
514                            cat_idx,
515                            item_idx,
516                            item.name.to_string(),
517                            item.function,
518                            item.requires_root,
519                        ));
520                    }
521                }
522            }
523
524            // Start next operation every 1.5 seconds (paced so the progress
525            // screen shows operations completing one at a time rather than
526            // all at once, even though each one runs synchronously).
527            let operations_to_start = (elapsed / 1500) as usize;
528            if operations_to_start > self.demo_operations_completed
529                && !pending_operations.is_empty()
530            {
531                if let Some((cat_idx, item_idx, _name, _function, _requires_root)) =
532                    pending_operations.first()
533                {
534                    // Set to running
535                    self.categories[*cat_idx].items[*item_idx].status = Some(Status::Running);
536                    self.demo_operations_completed += 1;
537                }
538            }
539
540            // Complete running operations after 2 seconds
541            let mut running_operations: Vec<Operation> = Vec::new();
542            for (cat_idx, category) in self.categories.iter().enumerate() {
543                for (item_idx, item) in category.items.iter().enumerate() {
544                    if matches!(item.status, Some(Status::Running)) {
545                        running_operations.push((
546                            cat_idx,
547                            item_idx,
548                            item.name.to_string(),
549                            item.function,
550                            item.requires_root,
551                        ));
552                    }
553                }
554            }
555
556            // Complete operations that have been running for at least 2 seconds
557            for (cat_idx, item_idx, name, function, requires_root) in running_operations {
558                self.operation_logs.push(format!("Starting: {}", name));
559
560                // Check if operation requires root and we don't have it
561                let result: anyhow::Result<cleansys_core::CleaningResult> =
562                    if requires_root && !self.is_root && !self.password_prompt.is_authenticated() {
563                        // Show password prompt and pause operations
564                        self.needs_sudo = true;
565                        self.password_prompt.show();
566                        self.is_running = false;
567                        self.operation_logs
568                            .push(format!("🔒 {}: Waiting for sudo authentication...", name));
569                        // Return error to mark this operation as pending
570                        Err(anyhow::anyhow!("Waiting for sudo authentication"))
571                    } else {
572                        self.operation_logs.push(format!("🔄 Executing: {}", name));
573                        function(cleansys_core::RunOptions::execute())
574                    };
575
576                // Process result
577                match result {
578                    Ok(cleaning_result) => {
579                        let bytes = cleaning_result.total_bytes;
580                        let msg = if requires_root {
581                            format!(
582                                "Cleaned {} (root) ({}, {} item(s))",
583                                name,
584                                format_size(bytes),
585                                cleaning_result.item_count()
586                            )
587                        } else {
588                            format!(
589                                "Cleaned {} ({}, {} item(s))",
590                                name,
591                                format_size(bytes),
592                                cleaning_result.item_count()
593                            )
594                        };
595                        self.categories[cat_idx].items[item_idx].status =
596                            Some(Status::Success(msg));
597                        self.categories[cat_idx].items[item_idx].bytes_cleaned = bytes;
598                        self.total_bytes_cleaned += bytes;
599                        self.operation_logs.push(format!(
600                            "✅ Completed {}: {} freed across {} item(s)",
601                            name,
602                            format_size(bytes),
603                            cleaning_result.item_count()
604                        ));
605
606                        // Record real per-item detail (path + size) for the detailed view.
607                        let category_name = self.categories[cat_idx].name.clone();
608                        for item in &cleaning_result.items {
609                            self.operation_logs.push(format!(
610                                "  → {} ({})",
611                                item.path_str(),
612                                format_size(item.size)
613                            ));
614                            self.add_detailed_cleaned_item(
615                                item.path_str(),
616                                item.size,
617                                category_name.clone(),
618                                name.clone(),
619                                item.item_type.clone().into(),
620                            );
621                        }
622                        self.categories[cat_idx].items[item_idx].last_result =
623                            Some(cleaning_result);
624
625                        if bytes == 0 {
626                            self.operation_logs.push(format!(
627                                "ℹ️  {}: nothing to clean (already empty on {})",
628                                name,
629                                cleansys_core::cleaners::platform::platform_name()
630                            ));
631                        }
632                    }
633                    Err(e) => {
634                        let error_msg = if requires_root && !self.is_root {
635                            "Requires sudo - restart with 'sudo cleansys'".to_string()
636                        } else {
637                            format!(
638                                "Failed: {}",
639                                e.to_string()
640                                    .split(':')
641                                    .next_back()
642                                    .unwrap_or("Unknown error")
643                                    .trim()
644                            )
645                        };
646                        self.categories[cat_idx].items[item_idx].status =
647                            Some(Status::Error(error_msg.clone()));
648                        self.operation_logs
649                            .push(format!("❌ Failed {}: {}", name, error_msg));
650
651                        // Add helpful message for sudo requirement
652                        if requires_root
653                            && !self.is_root
654                            && !self
655                                .result_messages
656                                .iter()
657                                .any(|msg| msg.contains("sudo cleansys"))
658                        {
659                            self.result_messages.push(
660                                "💡 System cleaners require root privileges. Run 'sudo cleansys' to clean system files.".to_string()
661                            );
662                        }
663                    }
664                }
665            }
666        }
667    }
668
669    pub fn cancel_sudo_operations(&mut self) {
670        // Mark all operations as cancelled
671        for category in &mut self.categories {
672            for item in &mut category.items {
673                if item.selected && matches!(item.status, Some(Status::Running | Status::Pending)) {
674                    item.status = Some(Status::Error("Operation cancelled by user".to_string()));
675                    item.selected = false; // Deselect the item
676                }
677            }
678        }
679
680        self.result_messages
681            .push("Cleaning operations cancelled by user.".to_string());
682    }
683
684    pub fn handle_key(&mut self, key: KeyEvent) -> Result<bool> {
685        // If password prompt is visible, handle password input first
686        if self.password_prompt.is_visible() {
687            match key.code {
688                KeyCode::Enter => {
689                    // Submit password and authenticate
690                    match self.password_prompt.submit() {
691                        Ok(true) => {
692                            // Authentication successful, proceed with operations
693                            self.needs_sudo = false;
694                            self.password_prompt.hide();
695
696                            // Now start the actual cleaning operations
697                            let selected_cleaners = self.pending_operations.clone();
698                            self.pending_operations.clear();
699
700                            if !selected_cleaners.is_empty() {
701                                // Start processing
702                                self.is_running = true;
703                                self.show_progress_screen = true;
704                                self.operation_start_time = Some(Instant::now());
705                                self.operation_end_time = None;
706                                self.total_bytes_cleaned = 0;
707                                self.demo_operation_timer = Some(Instant::now());
708                                self.demo_operations_completed = 0;
709                                self.result_messages.clear();
710                                self.operation_logs.clear();
711                                self.detailed_cleaned_items.clear();
712                                self.current_cleaner_index = 0;
713
714                                // Reset bytes_cleaned for all items to start fresh
715                                for category in &mut self.categories {
716                                    for item in &mut category.items {
717                                        item.bytes_cleaned = 0;
718                                    }
719                                }
720
721                                // Set all selected cleaners to Pending
722                                for (cat_idx, item_idx, _, _, _) in &selected_cleaners {
723                                    self.categories[*cat_idx].items[*item_idx].status =
724                                        Some(Status::Pending);
725                                }
726
727                                self.update_counters();
728                            }
729                        }
730                        Ok(false) => {
731                            // Authentication failed, stay on prompt
732                        }
733                        Err(e) => {
734                            self.operation_logs
735                                .push(format!("❌ Authentication error: {}", e));
736                            self.password_prompt.hide();
737                            self.needs_sudo = false;
738                            self.pending_operations.clear();
739                        }
740                    }
741                }
742                KeyCode::Esc => {
743                    // Cancel password prompt
744                    self.password_prompt.cancel();
745                    self.needs_sudo = false;
746                    self.pending_operations.clear();
747                }
748                KeyCode::Char(c) => {
749                    self.password_prompt.add_char(c);
750                }
751                KeyCode::Backspace => {
752                    self.password_prompt.remove_char();
753                }
754                _ => {}
755            }
756            return Ok(false);
757        }
758
759        // "Needs Administrator" notice (Windows only — no interactive sudo flow there)
760        if self.needs_admin_notice {
761            match key.code {
762                KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
763                    self.needs_admin_notice = false;
764                    self.pending_operations.clear();
765                }
766                _ => {}
767            }
768            return Ok(false);
769        }
770
771        // Run confirmation overlay
772        if self.awaiting_run_confirmation {
773            match key.code {
774                KeyCode::Enter | KeyCode::Char('y') => {
775                    self.confirm_pending_run()?;
776                }
777                KeyCode::Esc | KeyCode::Char('n') => {
778                    self.cancel_run_confirmation();
779                }
780                _ => {}
781            }
782            return Ok(false);
783        }
784
785        // Preview results overlay
786        if self.preview_open {
787            match key.code {
788                KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
789                    self.close_preview();
790                }
791                _ => {}
792            }
793            return Ok(false);
794        }
795
796        match (key.code, key.modifiers) {
797            // Quit
798            (KeyCode::Char('q'), _) => {
799                if self.show_help {
800                    self.show_help = false;
801                } else if self.is_running {
802                    // Cancel current cleaning operations
803                    self.is_running = false;
804                    self.cancel_sudo_operations();
805                } else {
806                    return Ok(true);
807                }
808            }
809
810            // Navigation
811            (KeyCode::Down, _) => {
812                if !self.show_help {
813                    if self.is_running || self.show_progress_screen {
814                        self.scroll_detailed_list_down();
815                    } else {
816                        self.next_item();
817                    }
818                }
819            }
820            (KeyCode::Up, _) => {
821                if !self.show_help {
822                    if self.is_running || self.show_progress_screen {
823                        self.scroll_detailed_list_up();
824                    } else {
825                        self.previous_item();
826                    }
827                }
828            }
829            (KeyCode::Tab, _) => {
830                if !self.show_help {
831                    self.next_category();
832                }
833            }
834            (KeyCode::BackTab, _) => {
835                if !self.show_help {
836                    self.previous_category();
837                }
838            }
839            // Selection
840            (KeyCode::Char(' '), KeyModifiers::NONE) => {
841                if !self.show_help {
842                    self.toggle_selected();
843                }
844            }
845            // Run cleaners (shows a confirmation overlay first, unless
846            // confirmation prompts are disabled via 'y').
847            (KeyCode::Enter, _) => {
848                if !self.show_help {
849                    self.request_run()?;
850                }
851            }
852            // Preview (dry-run) selected cleaners — measures real sizes/paths
853            // without deleting anything.
854            (KeyCode::Char('d'), _) => {
855                if !self.show_help && !self.is_running {
856                    self.run_preview();
857                }
858            }
859            // Help dialog
860            (KeyCode::Char('?' | 'h'), _) => {
861                self.toggle_help();
862            }
863
864            // Toggle search in removed items view
865            (KeyCode::Char('/'), _) => {
866                if !self.show_help {
867                    self.toggle_search();
868                }
869            }
870            // Clear search or cancel operations or return to main menu
871            (KeyCode::Esc, _) => {
872                if self.search_active {
873                    self.clear_search();
874                } else if self.is_running {
875                    self.is_running = false;
876                    self.cancel_sudo_operations();
877                } else if self.show_progress_screen {
878                    // Return to main menu from completed operations screen
879                    self.show_progress_screen = false;
880                }
881            }
882            // Scroll removed items list
883            (KeyCode::Char('j'), _) => {
884                if !self.show_help {
885                    self.scroll_detailed_list_down();
886                }
887            }
888            (KeyCode::Char('k'), _) => {
889                if !self.show_help {
890                    self.scroll_detailed_list_up();
891                }
892            }
893            // Select all in current category
894            (KeyCode::Char('a'), _) => {
895                if !self.show_help {
896                    self.select_all();
897                }
898            }
899            // Deselect all in current category
900            (KeyCode::Char('n'), _) => {
901                if !self.show_help {
902                    self.deselect_all();
903                }
904            }
905            // Select all across every category
906            (KeyCode::Char('A'), _) => {
907                if !self.show_help {
908                    self.select_all_everywhere();
909                }
910            }
911            // Deselect all across every category
912            (KeyCode::Char('N'), _) => {
913                if !self.show_help {
914                    self.deselect_all_everywhere();
915                }
916            }
917
918            // Toggle compact mode
919            (KeyCode::Char('m'), _) => {
920                if !self.show_help {
921                    self.toggle_compact_mode();
922                }
923            }
924            // Toggle auto scroll log
925            (KeyCode::Char('s'), _) => {
926                if !self.show_help && self.is_running {
927                    self.toggle_auto_scroll();
928                }
929            }
930            // Toggle performance stats
931            (KeyCode::Char('p'), _) => {
932                if !self.show_help {
933                    self.toggle_performance_stats();
934                }
935            }
936            // Cycle view mode
937            (KeyCode::Char('v'), _) => {
938                if !self.show_help {
939                    self.cycle_view_mode();
940                }
941            }
942            // Cycle sort mode
943            (KeyCode::Char('o'), _) => {
944                if !self.show_help {
945                    self.cycle_sort_mode();
946                }
947            }
948            // Cycle filter mode
949            (KeyCode::Char('f'), _) => {
950                if !self.show_help {
951                    self.cycle_filter_mode();
952                }
953            }
954            // Toggle pause/resume operations
955            (KeyCode::Char(' '), KeyModifiers::CONTROL) => {
956                if self.is_running {
957                    self.toggle_pause();
958                }
959            }
960            // Toggle confirmation mode
961            (KeyCode::Char('y'), _) => {
962                if !self.show_help {
963                    self.toggle_confirmation_mode();
964                }
965            }
966            // Toggle chart type
967            (KeyCode::Char('c'), _) => {
968                if !self.show_help {
969                    self.toggle_chart_type();
970                }
971            }
972            // Clear all errors
973            (KeyCode::Char('x'), _) => {
974                if !self.show_help {
975                    self.clear_errors();
976                }
977            }
978            // Handle search input (only when search is active)
979            (KeyCode::Char(c), _) => {
980                if self.search_active {
981                    self.add_search_char(c);
982                } else if !self.show_help {
983                    self.toggle_selected();
984                }
985            }
986            // Backspace in search
987            (KeyCode::Backspace, _) => {
988                if self.search_active {
989                    self.remove_search_char();
990                }
991            }
992            // Page scrolling for removed items (when in progress view)
993            (KeyCode::PageUp, _) => {
994                if self.is_running || self.show_progress_screen {
995                    // Scroll up by 10 items
996                    for _ in 0..10 {
997                        self.scroll_detailed_list_up();
998                    }
999                }
1000            }
1001            (KeyCode::PageDown, _) => {
1002                if self.is_running || self.show_progress_screen {
1003                    // Scroll down by 10 items
1004                    for _ in 0..10 {
1005                        self.scroll_detailed_list_down();
1006                    }
1007                }
1008            }
1009            // Enhanced navigation with Ctrl modifiers
1010            (KeyCode::Home, _) => {
1011                if !self.show_help {
1012                    if self.is_running || self.show_progress_screen {
1013                        self.detailed_list_scroll_state.select(Some(0));
1014                    } else {
1015                        self.item_list_state.select(Some(0));
1016                    }
1017                }
1018            }
1019            (KeyCode::End, _) if !self.show_help => {
1020                if self.is_running || self.show_progress_screen {
1021                    if !self.detailed_cleaned_items.is_empty() {
1022                        let last_index = (self.detailed_cleaned_items.len() * 3).saturating_sub(1);
1023                        self.detailed_list_scroll_state.select(Some(last_index));
1024                    }
1025                } else {
1026                    let len = self.categories[self.category_index].items.len();
1027                    if len > 0 {
1028                        self.item_list_state.select(Some(len - 1));
1029                    }
1030                }
1031            }
1032            _ => {}
1033        }
1034
1035        Ok(false)
1036    }
1037
1038    pub fn handle_resize(&mut self, width: u16, height: u16) {
1039        self.terminal_width = width;
1040        self.terminal_height = height;
1041    }
1042
1043    pub fn toggle_compact_mode(&mut self) {
1044        self.compact_mode = !self.compact_mode;
1045        self.view_mode = if self.compact_mode {
1046            ViewMode::Compact
1047        } else {
1048            ViewMode::Standard
1049        };
1050    }
1051
1052    pub fn toggle_auto_scroll(&mut self) {
1053        // Auto scroll functionality for operation logs
1054    }
1055
1056    pub fn toggle_performance_stats(&mut self) {
1057        self.show_performance_stats = !self.show_performance_stats;
1058    }
1059
1060    pub fn cycle_view_mode(&mut self) {
1061        self.view_mode = match self.view_mode {
1062            ViewMode::Standard => ViewMode::Compact,
1063            ViewMode::Compact => ViewMode::Detailed,
1064            ViewMode::Detailed => ViewMode::Performance,
1065            ViewMode::Performance => ViewMode::Standard,
1066        };
1067    }
1068
1069    pub fn cycle_sort_mode(&mut self) {
1070        self.sort_mode = match self.sort_mode {
1071            SortMode::Name => SortMode::Size,
1072            SortMode::Size => SortMode::Status,
1073            SortMode::Status => SortMode::Category,
1074            SortMode::Category => SortMode::Name,
1075        };
1076    }
1077
1078    pub fn cycle_filter_mode(&mut self) {
1079        self.filter_mode = match self.filter_mode {
1080            FilterMode::All => FilterMode::Selected,
1081            FilterMode::Selected => FilterMode::Completed,
1082            FilterMode::Completed => FilterMode::Errors,
1083            FilterMode::Errors => FilterMode::UserOnly,
1084            FilterMode::UserOnly => FilterMode::SystemOnly,
1085            FilterMode::SystemOnly => FilterMode::All,
1086        };
1087    }
1088
1089    pub fn toggle_pause(&mut self) {
1090        self.paused = !self.paused;
1091    }
1092
1093    pub fn toggle_confirmation_mode(&mut self) {
1094        self.confirmation_mode = !self.confirmation_mode;
1095    }
1096
1097    pub fn update_counters(&mut self) {
1098        self.selected_cleaners_count = self
1099            .categories
1100            .iter()
1101            .flat_map(|cat| &cat.items)
1102            .filter(|item| item.selected)
1103            .count();
1104
1105        self.errors_count = self
1106            .categories
1107            .iter()
1108            .flat_map(|cat| &cat.items)
1109            .filter(|item| matches!(item.status, Some(Status::Error(_))))
1110            .count();
1111
1112        self.operation_count = self
1113            .categories
1114            .iter()
1115            .flat_map(|cat| &cat.items)
1116            .filter(|item| item.status.is_some())
1117            .count();
1118
1119        // Auto-complete when all operations are finished
1120        if self.is_running && self.operation_count > 0 {
1121            let running_count = self
1122                .categories
1123                .iter()
1124                .flat_map(|cat| &cat.items)
1125                .filter(|item| matches!(item.status, Some(Status::Running)))
1126                .count();
1127
1128            let pending_count = self
1129                .categories
1130                .iter()
1131                .flat_map(|cat| &cat.items)
1132                .filter(|item| matches!(item.status, Some(Status::Pending)))
1133                .count();
1134
1135            let selected_count = self
1136                .categories
1137                .iter()
1138                .flat_map(|cat| &cat.items)
1139                .filter(|item| item.selected)
1140                .count();
1141
1142            // If no operations are running or pending, and we have selected items, mark as complete
1143            if running_count == 0 && pending_count == 0 && selected_count > 0 {
1144                self.is_running = false;
1145                self.demo_operation_timer = None;
1146                self.operation_end_time = Some(Instant::now());
1147
1148                // Add completion message
1149                if !self
1150                    .result_messages
1151                    .iter()
1152                    .any(|msg| msg.contains("Completed"))
1153                {
1154                    let summary = format!(
1155                        "Cleaning completed! Total space freed: {}",
1156                        format_size(self.total_bytes_cleaned)
1157                    );
1158                    self.result_messages
1159                        .push(format!("✅ {summary} (Press ESC to return to main menu)"));
1160                    crate::notifications::notify_completion(&summary);
1161                }
1162                // Keep show_progress_screen true so user stays on details screen
1163            }
1164        }
1165    }
1166
1167    pub fn clear_errors(&mut self) {
1168        for category in &mut self.categories {
1169            for item in &mut category.items {
1170                if matches!(item.status, Some(Status::Error(_))) {
1171                    item.status = None;
1172                }
1173            }
1174        }
1175        self.errors_count = 0;
1176    }
1177
1178    pub fn get_elapsed_time(&self) -> String {
1179        if let Some(start_time) = self.operation_start_time {
1180            let elapsed = if let Some(end_time) = self.operation_end_time {
1181                // Operation completed, show total time
1182                end_time.duration_since(start_time)
1183            } else {
1184                // Operation still running, show current elapsed time
1185                start_time.elapsed()
1186            };
1187
1188            if elapsed.as_secs() < 60 {
1189                format!("{}s", elapsed.as_secs())
1190            } else {
1191                format!("{}m {}s", elapsed.as_secs() / 60, elapsed.as_secs() % 60)
1192            }
1193        } else {
1194            "0s".to_string()
1195        }
1196    }
1197
1198    pub fn add_detailed_cleaned_item(
1199        &mut self,
1200        path: String,
1201        size: u64,
1202        category: String,
1203        cleaner_name: String,
1204        item_type: CleanedItemType,
1205    ) {
1206        let item = DetailedCleanedItem {
1207            path,
1208            size,
1209            category,
1210            cleaner_name,
1211            timestamp: SystemTime::now(),
1212            item_type,
1213        };
1214        self.detailed_cleaned_items.push(item);
1215
1216        // Keep only last 1000 items to prevent memory issues
1217        if self.detailed_cleaned_items.len() > 1000 {
1218            self.detailed_cleaned_items.remove(0);
1219        }
1220    }
1221
1222    pub fn scroll_detailed_list_up(&mut self) {
1223        if let Some(selected) = self.detailed_list_scroll_state.selected() {
1224            if selected > 0 {
1225                self.detailed_list_scroll_state.select(Some(selected - 1));
1226            }
1227        } else {
1228            // Start from the bottom when first navigating
1229            let total_items = if !self.detailed_cleaned_items.is_empty() {
1230                self.detailed_cleaned_items.len() * 3 // Account for spacing between items
1231            } else {
1232                45 // Sample items count for demo
1233            };
1234            if total_items > 0 {
1235                self.detailed_list_scroll_state
1236                    .select(Some(total_items - 1));
1237            }
1238        }
1239    }
1240
1241    pub fn scroll_detailed_list_down(&mut self) {
1242        let total_items = if !self.detailed_cleaned_items.is_empty() {
1243            self.detailed_cleaned_items.len() * 3 // Account for spacing between items
1244        } else {
1245            45 // Sample items count for demo
1246        };
1247
1248        if let Some(selected) = self.detailed_list_scroll_state.selected() {
1249            if selected < total_items.saturating_sub(1) {
1250                self.detailed_list_scroll_state.select(Some(selected + 1));
1251            }
1252        } else if total_items > 0 {
1253            self.detailed_list_scroll_state.select(Some(0));
1254        }
1255    }
1256
1257    pub fn get_filtered_detailed_items(&self) -> Vec<&DetailedCleanedItem> {
1258        let mut items: Vec<&DetailedCleanedItem> = self
1259            .detailed_cleaned_items
1260            .iter()
1261            .filter(|item| {
1262                // Apply search filter
1263                if !self.search_query.is_empty() {
1264                    let query_lower = self.search_query.to_lowercase();
1265                    return item.path.to_lowercase().contains(&query_lower)
1266                        || item.category.to_lowercase().contains(&query_lower)
1267                        || item.cleaner_name.to_lowercase().contains(&query_lower);
1268                }
1269
1270                // Apply category filter
1271                if !self.detailed_view_filter.is_empty() {
1272                    return item
1273                        .category
1274                        .to_lowercase()
1275                        .contains(&self.detailed_view_filter.to_lowercase());
1276                }
1277
1278                true
1279            })
1280            .collect();
1281
1282        // Sort based on current sort mode
1283        match self.sort_mode {
1284            SortMode::Name => items.sort_by(|a, b| a.path.cmp(&b.path)),
1285            SortMode::Size => items.sort_by_key(|b| std::cmp::Reverse(b.size)), // Largest first
1286            SortMode::Category => items.sort_by(|a, b| a.category.cmp(&b.category)),
1287            SortMode::Status => items.sort_by_key(|b| std::cmp::Reverse(b.timestamp)), // Most recent first
1288        }
1289
1290        items
1291    }
1292
1293    pub fn toggle_chart_type(&mut self) {
1294        self.chart_type = match self.chart_type {
1295            ChartType::Bar => ChartType::PieCount,
1296            ChartType::PieCount => ChartType::PieSize,
1297            ChartType::PieSize => ChartType::Bar,
1298        };
1299    }
1300}