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 begin_execution(&mut self, selected_cleaners: Vec<PendingOperation>) -> Result<()> {
443 let has_root_operations = selected_cleaners.iter().any(|(_, _, _, _, root)| *root);
444
445 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 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(); self.current_cleaner_index = 0;
469
470 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 for (cat_idx, item_idx, _, _, _) in &selected_cleaners {
480 self.categories[*cat_idx].items[*item_idx].status = Some(Status::Pending);
481 }
482
483 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 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 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 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 self.categories[*cat_idx].items[*item_idx].status = Some(Status::Running);
536 self.demo_operations_completed += 1;
537 }
538 }
539
540 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 for (cat_idx, item_idx, name, function, requires_root) in running_operations {
558 self.operation_logs.push(format!("Starting: {}", name));
559
560 let result: anyhow::Result<cleansys_core::CleaningResult> =
562 if requires_root && !self.is_root && !self.password_prompt.is_authenticated() {
563 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 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 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 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 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 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; }
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 self.password_prompt.is_visible() {
687 match key.code {
688 KeyCode::Enter => {
689 match self.password_prompt.submit() {
691 Ok(true) => {
692 self.needs_sudo = false;
694 self.password_prompt.hide();
695
696 let selected_cleaners = self.pending_operations.clone();
698 self.pending_operations.clear();
699
700 if !selected_cleaners.is_empty() {
701 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 for category in &mut self.categories {
716 for item in &mut category.items {
717 item.bytes_cleaned = 0;
718 }
719 }
720
721 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 }
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 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 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 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 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 (KeyCode::Char('q'), _) => {
799 if self.show_help {
800 self.show_help = false;
801 } else if self.is_running {
802 self.is_running = false;
804 self.cancel_sudo_operations();
805 } else {
806 return Ok(true);
807 }
808 }
809
810 (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 (KeyCode::Char(' '), KeyModifiers::NONE) => {
841 if !self.show_help {
842 self.toggle_selected();
843 }
844 }
845 (KeyCode::Enter, _) => {
848 if !self.show_help {
849 self.request_run()?;
850 }
851 }
852 (KeyCode::Char('d'), _) => {
855 if !self.show_help && !self.is_running {
856 self.run_preview();
857 }
858 }
859 (KeyCode::Char('?' | 'h'), _) => {
861 self.toggle_help();
862 }
863
864 (KeyCode::Char('/'), _) => {
866 if !self.show_help {
867 self.toggle_search();
868 }
869 }
870 (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 self.show_progress_screen = false;
880 }
881 }
882 (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 (KeyCode::Char('a'), _) => {
895 if !self.show_help {
896 self.select_all();
897 }
898 }
899 (KeyCode::Char('n'), _) => {
901 if !self.show_help {
902 self.deselect_all();
903 }
904 }
905 (KeyCode::Char('A'), _) => {
907 if !self.show_help {
908 self.select_all_everywhere();
909 }
910 }
911 (KeyCode::Char('N'), _) => {
913 if !self.show_help {
914 self.deselect_all_everywhere();
915 }
916 }
917
918 (KeyCode::Char('m'), _) => {
920 if !self.show_help {
921 self.toggle_compact_mode();
922 }
923 }
924 (KeyCode::Char('s'), _) => {
926 if !self.show_help && self.is_running {
927 self.toggle_auto_scroll();
928 }
929 }
930 (KeyCode::Char('p'), _) => {
932 if !self.show_help {
933 self.toggle_performance_stats();
934 }
935 }
936 (KeyCode::Char('v'), _) => {
938 if !self.show_help {
939 self.cycle_view_mode();
940 }
941 }
942 (KeyCode::Char('o'), _) => {
944 if !self.show_help {
945 self.cycle_sort_mode();
946 }
947 }
948 (KeyCode::Char('f'), _) => {
950 if !self.show_help {
951 self.cycle_filter_mode();
952 }
953 }
954 (KeyCode::Char(' '), KeyModifiers::CONTROL) => {
956 if self.is_running {
957 self.toggle_pause();
958 }
959 }
960 (KeyCode::Char('y'), _) => {
962 if !self.show_help {
963 self.toggle_confirmation_mode();
964 }
965 }
966 (KeyCode::Char('c'), _) => {
968 if !self.show_help {
969 self.toggle_chart_type();
970 }
971 }
972 (KeyCode::Char('x'), _) => {
974 if !self.show_help {
975 self.clear_errors();
976 }
977 }
978 (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 (KeyCode::Backspace, _) => {
988 if self.search_active {
989 self.remove_search_char();
990 }
991 }
992 (KeyCode::PageUp, _) => {
994 if self.is_running || self.show_progress_screen {
995 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 for _ in 0..10 {
1005 self.scroll_detailed_list_down();
1006 }
1007 }
1008 }
1009 (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 }
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 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 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 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 }
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 end_time.duration_since(start_time)
1183 } else {
1184 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 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 let total_items = if !self.detailed_cleaned_items.is_empty() {
1230 self.detailed_cleaned_items.len() * 3 } else {
1232 45 };
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 } else {
1245 45 };
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 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 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 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)), 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)), }
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}