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
38pub 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
74pub 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 pub awaiting_run_confirmation: bool,
120 pub preview_open: bool,
122 pub preview_results: Vec<(String, cleansys_core::CleaningResult)>,
124 pub needs_admin_notice: bool,
127 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 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 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)); 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 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 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 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 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 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 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 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 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 pub fn cancel_run_confirmation(&mut self) {
395 self.awaiting_run_confirmation = false;
396 self.pending_run_selection.clear();
397 }
398
399 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 pub fn close_preview(&mut self) {
435 self.preview_open = false;
436 self.preview_results.clear();
437 }
438
439 fn start_operations(&mut self, selected_cleaners: &[PendingOperation]) {
449 self.is_running = true;
450 self.show_progress_screen = true;
451 self.operation_start_time = Some(Instant::now());
452 self.operation_end_time = None;
453 self.total_bytes_cleaned = 0;
454 self.demo_operation_timer = Some(Instant::now());
455 self.demo_operations_completed = 0;
456 self.result_messages.clear();
457 self.operation_logs.clear();
458 self.detailed_cleaned_items.clear(); self.current_cleaner_index = 0;
460
461 for category in &mut self.categories {
463 for item in &mut category.items {
464 item.bytes_cleaned = 0;
465 item.status = None;
466 }
467 }
468
469 for (cat_idx, item_idx, _, _, _) in selected_cleaners {
471 self.categories[*cat_idx].items[*item_idx].status = Some(Status::Pending);
472 }
473
474 self.update_counters();
475
476 }
480
481 #[cfg(debug_assertions)]
486 pub fn start_operations_for_tests(&mut self, selected_cleaners: &[PendingOperation]) {
487 self.start_operations(selected_cleaners);
488 }
489
490 fn begin_execution(&mut self, selected_cleaners: Vec<PendingOperation>) -> Result<()> {
494 let has_root_operations = selected_cleaners.iter().any(|(_, _, _, _, root)| *root);
495
496 if has_root_operations && !self.is_root && !self.password_prompt.is_authenticated() {
504 self.pending_operations.clone_from(&selected_cleaners);
505 if cleansys_core::utils::supports_sudo_prompt() {
506 self.needs_sudo = true;
507 self.password_prompt.show();
508 } else {
509 self.needs_admin_notice = true;
510 }
511 return Ok(());
512 }
513
514 self.start_operations(&selected_cleaners);
515
516 Ok(())
517 }
518
519 pub fn update_animation(&mut self) {
520 let now = Instant::now();
521 if now.duration_since(self.last_frame_time).as_millis() > 100 {
522 self.animation_frame = (self.animation_frame + 1) % 10;
523 self.last_frame_time = now;
524 }
525
526 if self.is_running {
528 self.update_demo_operations();
529 }
530 }
531
532 pub fn update_demo_operations(&mut self) {
533 if let Some(start_time) = self.demo_operation_timer {
534 let elapsed = start_time.elapsed().as_millis();
535
536 type Operation = (usize, usize, String, CleanerFn, bool);
538 let mut pending_operations: Vec<Operation> = Vec::new();
539 for (cat_idx, category) in self.categories.iter().enumerate() {
540 for (item_idx, item) in category.items.iter().enumerate() {
541 if matches!(item.status, Some(Status::Pending)) {
542 pending_operations.push((
543 cat_idx,
544 item_idx,
545 item.name.to_string(),
546 item.function,
547 item.requires_root,
548 ));
549 }
550 }
551 }
552
553 let operations_to_start = (elapsed / 1500) as usize;
557 if operations_to_start > self.demo_operations_completed
558 && !pending_operations.is_empty()
559 {
560 if let Some((cat_idx, item_idx, _name, _function, _requires_root)) =
561 pending_operations.first()
562 {
563 self.categories[*cat_idx].items[*item_idx].status = Some(Status::Running);
565 self.demo_operations_completed += 1;
566 }
567 }
568
569 let mut running_operations: Vec<Operation> = Vec::new();
571 for (cat_idx, category) in self.categories.iter().enumerate() {
572 for (item_idx, item) in category.items.iter().enumerate() {
573 if matches!(item.status, Some(Status::Running)) {
574 running_operations.push((
575 cat_idx,
576 item_idx,
577 item.name.to_string(),
578 item.function,
579 item.requires_root,
580 ));
581 }
582 }
583 }
584
585 for (cat_idx, item_idx, name, function, requires_root) in running_operations {
587 self.operation_logs.push(format!("Starting: {}", name));
588
589 let result: anyhow::Result<cleansys_core::CleaningResult> =
591 if requires_root && !self.is_root && !self.password_prompt.is_authenticated() {
592 self.needs_sudo = true;
594 self.password_prompt.show();
595 self.is_running = false;
596 self.operation_logs
597 .push(format!("🔒 {}: Waiting for sudo authentication...", name));
598 Err(anyhow::anyhow!("Waiting for sudo authentication"))
600 } else {
601 self.operation_logs.push(format!("🔄 Executing: {}", name));
602 function(cleansys_core::RunOptions::execute())
603 };
604
605 match result {
607 Ok(cleaning_result) => {
608 let bytes = cleaning_result.total_bytes;
609 let msg = if requires_root {
610 format!(
611 "Cleaned {} (root) ({}, {} item(s))",
612 name,
613 format_size(bytes),
614 cleaning_result.item_count()
615 )
616 } else {
617 format!(
618 "Cleaned {} ({}, {} item(s))",
619 name,
620 format_size(bytes),
621 cleaning_result.item_count()
622 )
623 };
624 self.categories[cat_idx].items[item_idx].status =
625 Some(Status::Success(msg));
626 self.categories[cat_idx].items[item_idx].bytes_cleaned = bytes;
627 self.total_bytes_cleaned += bytes;
628 self.operation_logs.push(format!(
629 "✅ Completed {}: {} freed across {} item(s)",
630 name,
631 format_size(bytes),
632 cleaning_result.item_count()
633 ));
634
635 let category_name = self.categories[cat_idx].name.clone();
637 for item in &cleaning_result.items {
638 self.operation_logs.push(format!(
639 " → {} ({})",
640 item.path_str(),
641 format_size(item.size)
642 ));
643 self.add_detailed_cleaned_item(
644 item.path_str(),
645 item.size,
646 category_name.clone(),
647 name.clone(),
648 item.item_type.clone().into(),
649 );
650 }
651 self.categories[cat_idx].items[item_idx].last_result =
652 Some(cleaning_result);
653
654 if bytes == 0 {
655 self.operation_logs.push(format!(
656 "ℹ️ {}: nothing to clean (already empty on {})",
657 name,
658 cleansys_core::cleaners::platform::platform_name()
659 ));
660 }
661 }
662 Err(e) => {
663 let error_msg = if requires_root && !self.is_root {
664 "Requires sudo - restart with 'sudo cleansys'".to_string()
665 } else {
666 format!(
667 "Failed: {}",
668 e.to_string()
669 .split(':')
670 .next_back()
671 .unwrap_or("Unknown error")
672 .trim()
673 )
674 };
675 self.categories[cat_idx].items[item_idx].status =
676 Some(Status::Error(error_msg.clone()));
677 self.operation_logs
678 .push(format!("❌ Failed {}: {}", name, error_msg));
679
680 if requires_root
682 && !self.is_root
683 && !self
684 .result_messages
685 .iter()
686 .any(|msg| msg.contains("sudo cleansys"))
687 {
688 self.result_messages.push(
689 "💡 System cleaners require root privileges. Run 'sudo cleansys' to clean system files.".to_string()
690 );
691 }
692 }
693 }
694 }
695 }
696 }
697
698 pub fn cancel_sudo_operations(&mut self) {
699 for category in &mut self.categories {
701 for item in &mut category.items {
702 if item.selected && matches!(item.status, Some(Status::Running | Status::Pending)) {
703 item.status = Some(Status::Error("Operation cancelled by user".to_string()));
704 item.selected = false; }
706 }
707 }
708
709 self.result_messages
710 .push("Cleaning operations cancelled by user.".to_string());
711 }
712
713 pub fn handle_key(&mut self, key: KeyEvent) -> Result<bool> {
714 if self.password_prompt.is_visible() {
716 match key.code {
717 KeyCode::Enter => {
718 match self.password_prompt.submit() {
720 Ok(true) => {
721 self.needs_sudo = false;
723 self.password_prompt.hide();
724
725 let selected_cleaners = self.pending_operations.clone();
727 self.pending_operations.clear();
728
729 if !selected_cleaners.is_empty() {
730 self.start_operations(&selected_cleaners);
731 }
732 }
733 Ok(false) => {
734 }
736 Err(e) => {
737 self.operation_logs
738 .push(format!("❌ Authentication error: {}", e));
739 self.password_prompt.hide();
740 self.needs_sudo = false;
741 self.pending_operations.clear();
742 }
743 }
744 }
745 KeyCode::Esc => {
746 self.password_prompt.cancel();
748 self.needs_sudo = false;
749 self.pending_operations.clear();
750 }
751 KeyCode::Char(c) => {
752 self.password_prompt.add_char(c);
753 }
754 KeyCode::Backspace => {
755 self.password_prompt.remove_char();
756 }
757 _ => {}
758 }
759 return Ok(false);
760 }
761
762 if self.needs_admin_notice {
764 match key.code {
765 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
766 self.needs_admin_notice = false;
767 self.pending_operations.clear();
768 }
769 _ => {}
770 }
771 return Ok(false);
772 }
773
774 if self.awaiting_run_confirmation {
776 match key.code {
777 KeyCode::Enter | KeyCode::Char('y') => {
778 self.confirm_pending_run()?;
779 }
780 KeyCode::Esc | KeyCode::Char('n') => {
781 self.cancel_run_confirmation();
782 }
783 _ => {}
784 }
785 return Ok(false);
786 }
787
788 if self.preview_open {
790 match key.code {
791 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
792 self.close_preview();
793 }
794 _ => {}
795 }
796 return Ok(false);
797 }
798
799 match (key.code, key.modifiers) {
800 (KeyCode::Char('q'), _) => {
802 if self.show_help {
803 self.show_help = false;
804 } else if self.is_running {
805 self.is_running = false;
807 self.cancel_sudo_operations();
808 } else {
809 return Ok(true);
810 }
811 }
812
813 (KeyCode::Down, _) => {
815 if !self.show_help {
816 if self.is_running || self.show_progress_screen {
817 self.scroll_detailed_list_down();
818 } else {
819 self.next_item();
820 }
821 }
822 }
823 (KeyCode::Up, _) => {
824 if !self.show_help {
825 if self.is_running || self.show_progress_screen {
826 self.scroll_detailed_list_up();
827 } else {
828 self.previous_item();
829 }
830 }
831 }
832 (KeyCode::Tab, _) => {
833 if !self.show_help {
834 self.next_category();
835 }
836 }
837 (KeyCode::BackTab, _) => {
838 if !self.show_help {
839 self.previous_category();
840 }
841 }
842 (KeyCode::Char(' '), KeyModifiers::NONE) => {
844 if !self.show_help {
845 self.toggle_selected();
846 }
847 }
848 (KeyCode::Enter, _) => {
851 if !self.show_help {
852 self.request_run()?;
853 }
854 }
855 (KeyCode::Char('d'), _) => {
858 if !self.show_help && !self.is_running {
859 self.run_preview();
860 }
861 }
862 (KeyCode::Char('?' | 'h'), _) => {
864 self.toggle_help();
865 }
866
867 (KeyCode::Char('/'), _) => {
869 if !self.show_help {
870 self.toggle_search();
871 }
872 }
873 (KeyCode::Esc, _) => {
875 if self.search_active {
876 self.clear_search();
877 } else if self.is_running {
878 self.is_running = false;
879 self.cancel_sudo_operations();
880 } else if self.show_progress_screen {
881 self.show_progress_screen = false;
883 }
884 }
885 (KeyCode::Char('j'), _) => {
887 if !self.show_help {
888 self.scroll_detailed_list_down();
889 }
890 }
891 (KeyCode::Char('k'), _) => {
892 if !self.show_help {
893 self.scroll_detailed_list_up();
894 }
895 }
896 (KeyCode::Char('a'), _) => {
898 if !self.show_help {
899 self.select_all();
900 }
901 }
902 (KeyCode::Char('n'), _) => {
904 if !self.show_help {
905 self.deselect_all();
906 }
907 }
908 (KeyCode::Char('A'), _) => {
910 if !self.show_help {
911 self.select_all_everywhere();
912 }
913 }
914 (KeyCode::Char('N'), _) => {
916 if !self.show_help {
917 self.deselect_all_everywhere();
918 }
919 }
920
921 (KeyCode::Char('m'), _) => {
923 if !self.show_help {
924 self.toggle_compact_mode();
925 }
926 }
927 (KeyCode::Char('s'), _) => {
929 if !self.show_help && self.is_running {
930 self.toggle_auto_scroll();
931 }
932 }
933 (KeyCode::Char('p'), _) => {
935 if !self.show_help {
936 self.toggle_performance_stats();
937 }
938 }
939 (KeyCode::Char('v'), _) => {
941 if !self.show_help {
942 self.cycle_view_mode();
943 }
944 }
945 (KeyCode::Char('o'), _) => {
947 if !self.show_help {
948 self.cycle_sort_mode();
949 }
950 }
951 (KeyCode::Char('f'), _) => {
953 if !self.show_help {
954 self.cycle_filter_mode();
955 }
956 }
957 (KeyCode::Char(' '), KeyModifiers::CONTROL) => {
959 if self.is_running {
960 self.toggle_pause();
961 }
962 }
963 (KeyCode::Char('y'), _) => {
965 if !self.show_help {
966 self.toggle_confirmation_mode();
967 }
968 }
969 (KeyCode::Char('c'), _) => {
971 if !self.show_help {
972 self.toggle_chart_type();
973 }
974 }
975 (KeyCode::Char('x'), _) => {
977 if !self.show_help {
978 self.clear_errors();
979 }
980 }
981 (KeyCode::Char(c), _) => {
983 if self.search_active {
984 self.add_search_char(c);
985 } else if !self.show_help {
986 self.toggle_selected();
987 }
988 }
989 (KeyCode::Backspace, _) => {
991 if self.search_active {
992 self.remove_search_char();
993 }
994 }
995 (KeyCode::PageUp, _) => {
997 if self.is_running || self.show_progress_screen {
998 for _ in 0..10 {
1000 self.scroll_detailed_list_up();
1001 }
1002 }
1003 }
1004 (KeyCode::PageDown, _) => {
1005 if self.is_running || self.show_progress_screen {
1006 for _ in 0..10 {
1008 self.scroll_detailed_list_down();
1009 }
1010 }
1011 }
1012 (KeyCode::Home, _) => {
1014 if !self.show_help {
1015 if self.is_running || self.show_progress_screen {
1016 self.detailed_list_scroll_state.select(Some(0));
1017 } else {
1018 self.item_list_state.select(Some(0));
1019 }
1020 }
1021 }
1022 (KeyCode::End, _) if !self.show_help => {
1023 if self.is_running || self.show_progress_screen {
1024 if !self.detailed_cleaned_items.is_empty() {
1025 let last_index = (self.detailed_cleaned_items.len() * 3).saturating_sub(1);
1026 self.detailed_list_scroll_state.select(Some(last_index));
1027 }
1028 } else {
1029 let len = self.categories[self.category_index].items.len();
1030 if len > 0 {
1031 self.item_list_state.select(Some(len - 1));
1032 }
1033 }
1034 }
1035 _ => {}
1036 }
1037
1038 Ok(false)
1039 }
1040
1041 pub fn handle_resize(&mut self, width: u16, height: u16) {
1042 self.terminal_width = width;
1043 self.terminal_height = height;
1044 }
1045
1046 pub fn toggle_compact_mode(&mut self) {
1047 self.compact_mode = !self.compact_mode;
1048 self.view_mode = if self.compact_mode {
1049 ViewMode::Compact
1050 } else {
1051 ViewMode::Standard
1052 };
1053 }
1054
1055 pub fn toggle_auto_scroll(&mut self) {
1056 }
1058
1059 pub fn toggle_performance_stats(&mut self) {
1060 self.show_performance_stats = !self.show_performance_stats;
1061 }
1062
1063 pub fn cycle_view_mode(&mut self) {
1064 self.view_mode = match self.view_mode {
1065 ViewMode::Standard => ViewMode::Compact,
1066 ViewMode::Compact => ViewMode::Detailed,
1067 ViewMode::Detailed => ViewMode::Performance,
1068 ViewMode::Performance => ViewMode::Standard,
1069 };
1070 }
1071
1072 pub fn cycle_sort_mode(&mut self) {
1073 self.sort_mode = match self.sort_mode {
1074 SortMode::Name => SortMode::Size,
1075 SortMode::Size => SortMode::Status,
1076 SortMode::Status => SortMode::Category,
1077 SortMode::Category => SortMode::Name,
1078 };
1079 }
1080
1081 pub fn cycle_filter_mode(&mut self) {
1082 self.filter_mode = match self.filter_mode {
1083 FilterMode::All => FilterMode::Selected,
1084 FilterMode::Selected => FilterMode::Completed,
1085 FilterMode::Completed => FilterMode::Errors,
1086 FilterMode::Errors => FilterMode::UserOnly,
1087 FilterMode::UserOnly => FilterMode::SystemOnly,
1088 FilterMode::SystemOnly => FilterMode::All,
1089 };
1090 }
1091
1092 pub fn toggle_pause(&mut self) {
1093 self.paused = !self.paused;
1094 }
1095
1096 pub fn toggle_confirmation_mode(&mut self) {
1097 self.confirmation_mode = !self.confirmation_mode;
1098 }
1099
1100 pub fn update_counters(&mut self) {
1101 self.selected_cleaners_count = self
1102 .categories
1103 .iter()
1104 .flat_map(|cat| &cat.items)
1105 .filter(|item| item.selected)
1106 .count();
1107
1108 self.errors_count = self
1109 .categories
1110 .iter()
1111 .flat_map(|cat| &cat.items)
1112 .filter(|item| matches!(item.status, Some(Status::Error(_))))
1113 .count();
1114
1115 self.operation_count = self
1116 .categories
1117 .iter()
1118 .flat_map(|cat| &cat.items)
1119 .filter(|item| item.status.is_some())
1120 .count();
1121
1122 if self.is_running && self.operation_count > 0 {
1124 let running_count = self
1125 .categories
1126 .iter()
1127 .flat_map(|cat| &cat.items)
1128 .filter(|item| matches!(item.status, Some(Status::Running)))
1129 .count();
1130
1131 let pending_count = self
1132 .categories
1133 .iter()
1134 .flat_map(|cat| &cat.items)
1135 .filter(|item| matches!(item.status, Some(Status::Pending)))
1136 .count();
1137
1138 let selected_count = self
1139 .categories
1140 .iter()
1141 .flat_map(|cat| &cat.items)
1142 .filter(|item| item.selected)
1143 .count();
1144
1145 if running_count == 0 && pending_count == 0 && selected_count > 0 {
1147 self.is_running = false;
1148 self.demo_operation_timer = None;
1149 self.operation_end_time = Some(Instant::now());
1150
1151 if !self
1153 .result_messages
1154 .iter()
1155 .any(|msg| msg.contains("Completed"))
1156 {
1157 let summary = format!(
1158 "Cleaning completed! Total space freed: {}",
1159 format_size(self.total_bytes_cleaned)
1160 );
1161 self.result_messages
1162 .push(format!("✅ {summary} (Press ESC to return to main menu)"));
1163 crate::notifications::notify_completion(&summary);
1164 }
1165 }
1167 }
1168 }
1169
1170 pub fn clear_errors(&mut self) {
1171 for category in &mut self.categories {
1172 for item in &mut category.items {
1173 if matches!(item.status, Some(Status::Error(_))) {
1174 item.status = None;
1175 }
1176 }
1177 }
1178 self.errors_count = 0;
1179 }
1180
1181 pub fn get_elapsed_time(&self) -> String {
1182 if let Some(start_time) = self.operation_start_time {
1183 let elapsed = if let Some(end_time) = self.operation_end_time {
1184 end_time.duration_since(start_time)
1186 } else {
1187 start_time.elapsed()
1189 };
1190
1191 if elapsed.as_secs() < 60 {
1192 format!("{}s", elapsed.as_secs())
1193 } else {
1194 format!("{}m {}s", elapsed.as_secs() / 60, elapsed.as_secs() % 60)
1195 }
1196 } else {
1197 "0s".to_string()
1198 }
1199 }
1200
1201 pub fn add_detailed_cleaned_item(
1202 &mut self,
1203 path: String,
1204 size: u64,
1205 category: String,
1206 cleaner_name: String,
1207 item_type: CleanedItemType,
1208 ) {
1209 let item = DetailedCleanedItem {
1210 path,
1211 size,
1212 category,
1213 cleaner_name,
1214 timestamp: SystemTime::now(),
1215 item_type,
1216 };
1217 self.detailed_cleaned_items.push(item);
1218
1219 if self.detailed_cleaned_items.len() > 1000 {
1221 self.detailed_cleaned_items.remove(0);
1222 }
1223 }
1224
1225 pub fn scroll_detailed_list_up(&mut self) {
1226 if let Some(selected) = self.detailed_list_scroll_state.selected() {
1227 if selected > 0 {
1228 self.detailed_list_scroll_state.select(Some(selected - 1));
1229 }
1230 } else {
1231 let total_items = if !self.detailed_cleaned_items.is_empty() {
1233 self.detailed_cleaned_items.len() * 3 } else {
1235 45 };
1237 if total_items > 0 {
1238 self.detailed_list_scroll_state
1239 .select(Some(total_items - 1));
1240 }
1241 }
1242 }
1243
1244 pub fn scroll_detailed_list_down(&mut self) {
1245 let total_items = if !self.detailed_cleaned_items.is_empty() {
1246 self.detailed_cleaned_items.len() * 3 } else {
1248 45 };
1250
1251 if let Some(selected) = self.detailed_list_scroll_state.selected() {
1252 if selected < total_items.saturating_sub(1) {
1253 self.detailed_list_scroll_state.select(Some(selected + 1));
1254 }
1255 } else if total_items > 0 {
1256 self.detailed_list_scroll_state.select(Some(0));
1257 }
1258 }
1259
1260 pub fn get_filtered_detailed_items(&self) -> Vec<&DetailedCleanedItem> {
1261 let mut items: Vec<&DetailedCleanedItem> = self
1262 .detailed_cleaned_items
1263 .iter()
1264 .filter(|item| {
1265 if !self.search_query.is_empty() {
1267 let query_lower = self.search_query.to_lowercase();
1268 return item.path.to_lowercase().contains(&query_lower)
1269 || item.category.to_lowercase().contains(&query_lower)
1270 || item.cleaner_name.to_lowercase().contains(&query_lower);
1271 }
1272
1273 if !self.detailed_view_filter.is_empty() {
1275 return item
1276 .category
1277 .to_lowercase()
1278 .contains(&self.detailed_view_filter.to_lowercase());
1279 }
1280
1281 true
1282 })
1283 .collect();
1284
1285 match self.sort_mode {
1287 SortMode::Name => items.sort_by(|a, b| a.path.cmp(&b.path)),
1288 SortMode::Size => items.sort_by_key(|b| std::cmp::Reverse(b.size)), SortMode::Category => items.sort_by(|a, b| a.category.cmp(&b.category)),
1290 SortMode::Status => items.sort_by_key(|b| std::cmp::Reverse(b.timestamp)), }
1292
1293 items
1294 }
1295
1296 pub fn toggle_chart_type(&mut self) {
1297 self.chart_type = match self.chart_type {
1298 ChartType::Bar => ChartType::PieCount,
1299 ChartType::PieCount => ChartType::PieSize,
1300 ChartType::PieSize => ChartType::Bar,
1301 };
1302 }
1303}