1use ratatui::{
2 layout::{Constraint, Direction, Layout, Rect},
3 style::{Color, Modifier, Style},
4 symbols,
5 text::{Line, Span},
6 widgets::{Axis, Block, Borders, Chart, Clear, Dataset, List, ListItem, Paragraph, Wrap},
7 Frame,
8};
9use tui_checkbox::{symbols as checkbox_symbols, Checkbox};
11use tui_spinner::{FluxFrames, FluxSpinner};
12
13use crate::app::{App, ChartType, CleanedItemType};
14use crate::pie_chart::create_pie_chart_from_distribution;
15use cleansys_core::{format_size, Status};
16
17pub fn ui(f: &mut Frame, app: &mut App) {
18 app.update_animation();
20
21 let (title_height, footer_height, min_content_height) = if app.terminal_height < 20 {
23 (2, 2, 6)
25 } else if app.terminal_height < 30 {
26 (2, 2, 8)
28 } else if app.terminal_height < 40 {
29 (3, 3, 10)
31 } else {
32 (3, 3, 12)
34 };
35
36 let chunks = Layout::default()
37 .direction(Direction::Vertical)
38 .constraints([
39 Constraint::Length(title_height), Constraint::Min(min_content_height), Constraint::Length(footer_height), ])
43 .split(f.area());
44
45 render_title(f, app, chunks[0]);
46
47 if app.show_help {
48 render_help(f, chunks[1]);
49 } else if app.is_running || app.show_progress_screen {
50 render_progress_screen(f, app, chunks[1]);
51 } else {
52 render_main_content(f, app, chunks[1]);
53 }
54
55 render_footer(f, app, chunks[2]);
56
57 if app.password_prompt.is_visible() {
59 app.password_prompt.render(f, f.area());
60 }
61
62 if app.needs_admin_notice {
63 render_admin_notice(f, f.area());
64 } else if app.awaiting_run_confirmation {
65 render_confirm_run(f, app, f.area());
66 } else if app.preview_open {
67 render_preview(f, app, f.area());
68 }
69}
70
71fn render_title(f: &mut Frame, app: &App, area: Rect) {
72 let title_lines = if app.terminal_width < 80 {
74 let mut lines = vec![Line::from(vec![
76 Span::styled(
77 "Cleansys",
78 Style::default()
79 .fg(Color::Cyan)
80 .add_modifier(Modifier::BOLD),
81 ),
82 Span::raw(" - System Cleaner"),
83 if app.terminal_width < 60 || app.terminal_height < 20 {
84 Span::styled(
85 format!(" [{}x{}]", app.terminal_width, app.terminal_height),
86 Style::default().fg(Color::DarkGray),
87 )
88 } else {
89 Span::raw("")
90 },
91 ])];
92
93 lines.push(Line::from(vec![
95 Span::styled("?", Style::default().add_modifier(Modifier::BOLD)),
96 Span::raw(" help | "),
97 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
98 Span::raw(" quit"),
99 ]));
100
101 lines
102 } else {
103 vec![
105 Line::from(vec![
106 Span::styled(
107 "Cleansys",
108 Style::default()
109 .fg(Color::Cyan)
110 .add_modifier(Modifier::BOLD),
111 ),
112 Span::raw(" - Modern System Cleaner for Linux"),
113 ]),
114 Line::from(vec![
115 Span::raw("Press "),
116 Span::styled("?", Style::default().add_modifier(Modifier::BOLD)),
117 Span::raw(" for help, "),
118 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
119 Span::raw(" to quit"),
120 ]),
121 ]
122 };
123
124 let title = Paragraph::new(title_lines).block(Block::default().borders(Borders::BOTTOM));
125
126 f.render_widget(title, area);
127
128 if app.is_running && area.width > 14 {
131 let spinner_width = 12u16;
132 let spinner_area = Rect {
133 x: area.x + area.width.saturating_sub(spinner_width + 1),
134 y: area.y,
135 width: spinner_width,
136 height: 1,
137 };
138
139 let label = Line::from(vec![
140 Span::raw(" "),
141 Span::styled(
142 "RUNNING",
143 Style::default()
144 .fg(Color::Yellow)
145 .add_modifier(Modifier::BOLD),
146 ),
147 Span::raw(" "),
148 ]);
149 let label_width = label.width() as u16;
150
151 let (label_area, glyph_area) = (
152 Rect {
153 width: label_width.min(spinner_width),
154 ..spinner_area
155 },
156 Rect {
157 x: spinner_area.x + label_width.min(spinner_width),
158 width: spinner_area.width.saturating_sub(label_width),
159 ..spinner_area
160 },
161 );
162
163 f.render_widget(Paragraph::new(label), label_area);
164 f.render_widget(
165 FluxSpinner::new(app.animation_frame as u64)
166 .frames(FluxFrames::CLASSIC)
167 .color(Color::Cyan),
168 glyph_area,
169 );
170 }
171}
172
173fn render_main_content(f: &mut Frame, app: &mut App, area: Rect) {
174 let (categories_percent, content_percent) = if app.terminal_width < 80 {
176 (25, 75)
178 } else if app.terminal_width < 120 {
179 (30, 70)
181 } else {
182 (35, 65)
184 };
185
186 let horizontal_chunks = Layout::default()
187 .direction(Direction::Horizontal)
188 .constraints([
189 Constraint::Percentage(categories_percent), Constraint::Percentage(content_percent), ])
192 .split(area);
193
194 render_categories(f, app, horizontal_chunks[0]);
195
196 if app.detailed_view {
197 render_details(f, app, horizontal_chunks[1]);
198 } else {
199 render_cleaners(f, app, horizontal_chunks[1]);
200 }
201}
202
203fn render_progress_screen(f: &mut Frame, app: &mut App, area: Rect) {
204 render_unified_progress_view(f, app, area);
206}
207
208fn render_unified_progress_view(f: &mut Frame, app: &mut App, area: Rect) {
209 app.update_counters();
211
212 if area.width < 50 || area.height < 15 {
214 render_ultra_compact_view(f, app, area);
215 return;
216 }
217
218 let main_chunks = Layout::default()
220 .direction(Direction::Vertical)
221 .constraints([
222 Constraint::Percentage(if app.terminal_height >= 35 {
223 55
224 } else if app.terminal_height >= 25 {
225 50
226 } else {
227 45
228 }), Constraint::Percentage(if app.terminal_height >= 35 {
230 45
231 } else if app.terminal_height >= 25 {
232 50
233 } else {
234 55
235 }), ])
237 .margin(1)
238 .split(area);
239
240 render_combined_progress_overview(f, app, main_chunks[0]);
242
243 render_removed_items_window(f, app, main_chunks[1]);
245}
246
247fn render_combined_progress_overview(f: &mut Frame, app: &App, area: Rect) {
248 let block = Block::default()
249 .title("📊 Progress Overview & Operations")
250 .title_style(
251 Style::default()
252 .fg(Color::Cyan)
253 .add_modifier(Modifier::BOLD),
254 )
255 .borders(Borders::ALL)
256 .border_style(Style::default().fg(Color::Cyan));
257
258 let inner_area = block.inner(area);
259
260 let stats_height = if area.height < 15 {
262 5 } else if area.height < 20 {
264 7 } else if area.height < 25 {
266 9 } else {
268 12 };
270
271 let main_sections = Layout::default()
273 .direction(Direction::Vertical)
274 .constraints([
275 Constraint::Length(stats_height), Constraint::Min(6), ])
278 .split(inner_area);
279
280 render_progress_stats_and_chart(f, app, main_sections[0]);
282
283 render_operations_summary(f, app, main_sections[1]);
285
286 f.render_widget(block, area);
287}
288
289fn render_progress_stats_and_chart(f: &mut Frame, app: &App, area: Rect) {
290 let elapsed_time = app.get_elapsed_time();
291 let total_ops = app.operation_count;
292 let completed_ops = total_ops.saturating_sub(app.errors_count);
293 let progress_percent = completed_ops
294 .checked_mul(100)
295 .and_then(|v| v.checked_div(total_ops))
296 .unwrap_or(0);
297
298 let show_chart = area.width >= 80; let horizontal_chunks = if show_chart {
302 let stats_percent = if area.width < 100 {
303 45 } else if area.width < 130 {
305 40 } else {
307 35 };
309
310 Layout::default()
311 .direction(Direction::Horizontal)
312 .constraints([
313 Constraint::Percentage(stats_percent),
314 Constraint::Percentage(100 - stats_percent),
315 ])
316 .split(area)
317 } else {
318 Layout::default()
320 .direction(Direction::Horizontal)
321 .constraints([Constraint::Percentage(100)])
322 .split(area)
323 };
324
325 let stats_lines = vec![
327 Line::from(vec![
328 Span::styled(
329 "Progress: ",
330 Style::default()
331 .fg(Color::White)
332 .add_modifier(Modifier::BOLD),
333 ),
334 Span::styled(
335 format!("{}%", progress_percent),
336 Style::default()
337 .fg(Color::Green)
338 .add_modifier(Modifier::BOLD),
339 ),
340 Span::raw(format!(" ({}/{})", completed_ops, total_ops)),
341 Span::raw(" ⏱️ "),
342 Span::styled(
343 elapsed_time,
344 Style::default()
345 .fg(Color::Cyan)
346 .add_modifier(Modifier::BOLD),
347 ),
348 ]),
349 Line::from(vec![
350 Span::raw("█".repeat((progress_percent * 35) / 100)),
351 Span::styled(
352 "░".repeat(35 - (progress_percent * 35) / 100),
353 Style::default().fg(Color::DarkGray),
354 ),
355 ]),
356 Line::from(vec![
357 Span::styled("✅ ", Style::default().fg(Color::Green)),
358 Span::styled(
359 format!("{} OK", completed_ops),
360 Style::default().fg(Color::Green),
361 ),
362 Span::raw(" "),
363 Span::styled("⚡ ", Style::default().fg(Color::Yellow)),
364 Span::styled(
365 format!(
366 "{} Active",
367 if app.is_running {
368 total_ops.saturating_sub(completed_ops)
369 } else {
370 0
371 }
372 ),
373 Style::default().fg(Color::Yellow),
374 ),
375 Span::raw(" "),
376 Span::styled("❌ ", Style::default().fg(Color::Red)),
377 Span::styled(
378 format!("{} Errors", app.errors_count),
379 Style::default().fg(Color::Red),
380 ),
381 ]),
382 Line::from(vec![
383 Span::styled(
384 "💾 Total freed: ",
385 Style::default()
386 .fg(Color::White)
387 .add_modifier(Modifier::BOLD),
388 ),
389 Span::styled(
390 format_size(app.total_bytes_cleaned),
391 Style::default()
392 .fg(Color::Green)
393 .add_modifier(Modifier::BOLD),
394 ),
395 ]),
396 ];
397
398 let stats_para = Paragraph::new(stats_lines);
399 f.render_widget(stats_para, horizontal_chunks[0]);
400
401 if show_chart && horizontal_chunks.len() > 1 {
403 match app.chart_type {
404 ChartType::Bar => {
405 render_vertical_bar_chart(f, app, horizontal_chunks[1]);
406 }
407 ChartType::PieCount => {
408 render_pie_chart_distribution(f, app, horizontal_chunks[1]);
409 }
410 ChartType::PieSize => {
411 render_pie_chart_size_distribution(f, app, horizontal_chunks[1]);
412 }
413 }
414 }
415}
416
417fn render_ultra_compact_view(f: &mut Frame, app: &App, area: Rect) {
418 let elapsed_time = app.get_elapsed_time();
419 let total_ops = app.operation_count;
420 let completed_ops = total_ops.saturating_sub(app.errors_count);
421 let progress_percent = completed_ops
422 .checked_mul(100)
423 .and_then(|v| v.checked_div(total_ops))
424 .unwrap_or(0);
425
426 let compact_lines = vec![
428 Line::from(vec![Span::styled(
429 format!("Cleansys [{}x{}]", area.width, area.height),
430 Style::default()
431 .fg(Color::Cyan)
432 .add_modifier(Modifier::BOLD),
433 )]),
434 Line::from(vec![
435 Span::styled(
436 format!("{}% ", progress_percent),
437 Style::default()
438 .fg(Color::Green)
439 .add_modifier(Modifier::BOLD),
440 ),
441 Span::raw("█".repeat(
442 ((progress_percent * (area.width.saturating_sub(10) as usize)) / 100).min(30),
443 )),
444 ]),
445 Line::from(vec![
446 Span::styled(
447 format!("✅{} ❌{} ", completed_ops, app.errors_count),
448 Style::default().fg(Color::White),
449 ),
450 Span::styled(
451 format_size(app.total_bytes_cleaned),
452 Style::default()
453 .fg(Color::Green)
454 .add_modifier(Modifier::BOLD),
455 ),
456 ]),
457 Line::from(vec![
458 Span::styled(
459 format!("⏱️{} ", elapsed_time),
460 Style::default().fg(Color::Cyan),
461 ),
462 Span::styled(
463 if app.is_running { "RUNNING" } else { "DONE" },
464 Style::default().fg(if app.is_running {
465 Color::Yellow
466 } else {
467 Color::Green
468 }),
469 ),
470 ]),
471 ];
472
473 let block = Block::default()
474 .borders(Borders::ALL)
475 .border_style(Style::default().fg(Color::DarkGray));
476
477 let para = Paragraph::new(compact_lines)
478 .block(block)
479 .wrap(Wrap { trim: true });
480
481 f.render_widget(para, area);
482}
483
484fn render_vertical_bar_chart(f: &mut Frame, app: &App, area: Rect) {
485 let category_distribution = app.get_category_distribution();
487
488 if !category_distribution.is_empty() {
490 let limited_data: Vec<_> = category_distribution.iter().take(6).collect();
492 let max_count = limited_data
493 .iter()
494 .map(|(_, count, _)| *count)
495 .max()
496 .unwrap_or(1) as f64;
497
498 let chart_data: Vec<(f64, f64)> = limited_data
499 .iter()
500 .enumerate()
501 .map(|(i, (_, count, _))| (i as f64, *count as f64))
502 .collect();
503
504 let category_names: Vec<&str> = limited_data
505 .iter()
506 .map(|(name, _, _)| {
507 if area.width < 80 {
509 if name.len() > 6 {
510 &name[..6]
511 } else {
512 name
513 }
514 } else if area.width < 100 {
515 if name.len() > 8 {
516 &name[..8]
517 } else {
518 name
519 }
520 } else if name.len() > 12 {
521 &name[..12]
522 } else {
523 name
524 }
525 })
526 .collect();
527
528 let dataset = Dataset::default()
530 .name("Cleaned Items")
531 .marker(symbols::Marker::Block)
532 .style(
533 Style::default()
534 .fg(Color::Cyan)
535 .add_modifier(Modifier::BOLD),
536 )
537 .data(&chart_data);
538
539 let x_labels = if category_names.len() <= 3 {
541 vec![
542 Span::raw(category_names.first().unwrap_or(&"").to_string()),
543 Span::raw(category_names.get(1).unwrap_or(&"").to_string()),
544 Span::raw(category_names.get(2).unwrap_or(&"").to_string()),
545 ]
546 } else {
547 vec![
548 Span::raw(category_names.first().unwrap_or(&"").to_string()),
549 Span::raw(
550 category_names
551 .get(category_names.len() / 2)
552 .unwrap_or(&"")
553 .to_string(),
554 ),
555 Span::raw(category_names.last().unwrap_or(&"").to_string()),
556 ]
557 };
558
559 let y_max = (max_count * 1.1).max(1.0); let y_labels = vec![
562 Span::raw("0"),
563 Span::raw(format!("{}", (y_max / 2.0) as u64)),
564 Span::raw(format!("{}", y_max as u64)),
565 ];
566
567 let chart = Chart::new(vec![dataset])
568 .block(
569 Block::default()
570 .title(if area.width < 50 {
571 "Items (Bar)"
572 } else {
573 "Items Distribution (Bar Chart)"
574 })
575 .title_style(
576 Style::default()
577 .fg(Color::Cyan)
578 .add_modifier(Modifier::BOLD),
579 )
580 .borders(Borders::ALL)
581 .border_style(Style::default().fg(Color::Cyan)),
582 )
583 .x_axis(
584 Axis::default()
585 .title(if area.width >= 80 { "Categories" } else { "" })
586 .style(Style::default().fg(Color::White))
587 .bounds([0.0, (category_names.len().max(3) - 1) as f64])
588 .labels(x_labels),
589 )
590 .y_axis(
591 Axis::default()
592 .title(if area.width >= 80 { "Count" } else { "" })
593 .style(Style::default().fg(Color::White))
594 .bounds([0.0, y_max])
595 .labels(y_labels),
596 );
597
598 f.render_widget(chart, area);
599 }
600}
601
602fn render_operations_summary(f: &mut Frame, app: &App, area: Rect) {
603 let columns = Layout::default()
605 .direction(Direction::Horizontal)
606 .constraints([
607 Constraint::Percentage(48), Constraint::Percentage(4), Constraint::Percentage(48), ])
611 .split(area);
612
613 let user_operations = vec![
615 ListItem::new(Line::from(vec![Span::styled(
616 "👤 USER OPERATIONS",
617 Style::default()
618 .fg(Color::Green)
619 .add_modifier(Modifier::BOLD),
620 )])),
621 ListItem::new(Line::from(vec![])),
622 ListItem::new(Line::from(vec![
623 Span::styled("📦 ", Style::default().fg(Color::Green)),
624 Span::styled("Package Caches", Style::default().fg(Color::White)),
625 ])),
626 ListItem::new(Line::from(vec![
627 Span::styled("🗑️ ", Style::default().fg(Color::Green)),
628 Span::styled("Trash & Temp Files", Style::default().fg(Color::White)),
629 ])),
630 ListItem::new(Line::from(vec![
631 Span::styled("🌐 ", Style::default().fg(Color::Green)),
632 Span::styled("Browser Caches", Style::default().fg(Color::White)),
633 ])),
634 ];
635
636 let system_operations = vec![
638 ListItem::new(Line::from(vec![Span::styled(
639 "🔒 SYSTEM OPERATIONS",
640 Style::default()
641 .fg(Color::Yellow)
642 .add_modifier(Modifier::BOLD),
643 )])),
644 ListItem::new(Line::from(vec![])),
645 ListItem::new(Line::from(vec![
646 Span::styled(
647 "📦 ",
648 if app.is_root {
649 Style::default().fg(Color::Green)
650 } else {
651 Style::default().fg(Color::Yellow)
652 },
653 ),
654 Span::styled("Package Caches", Style::default().fg(Color::White)),
655 if !app.is_root {
656 Span::styled(" (sudo)", Style::default().fg(Color::Yellow))
657 } else {
658 Span::raw("")
659 },
660 ])),
661 ListItem::new(Line::from(vec![
662 Span::styled(
663 "📝 ",
664 if app.is_root {
665 Style::default().fg(Color::Green)
666 } else {
667 Style::default().fg(Color::Yellow)
668 },
669 ),
670 Span::styled("System Logs", Style::default().fg(Color::White)),
671 if !app.is_root {
672 Span::styled(" (sudo)", Style::default().fg(Color::Yellow))
673 } else {
674 Span::raw("")
675 },
676 ])),
677 ListItem::new(Line::from(vec![
678 Span::styled(
679 "🗄️ ",
680 if app.is_root {
681 Style::default().fg(Color::Green)
682 } else {
683 Style::default().fg(Color::Yellow)
684 },
685 ),
686 Span::styled("System Temp Files", Style::default().fg(Color::White)),
687 if !app.is_root {
688 Span::styled(" (sudo)", Style::default().fg(Color::Yellow))
689 } else {
690 Span::raw("")
691 },
692 ])),
693 ];
694
695 let user_list = List::new(user_operations);
696 let system_list = List::new(system_operations);
697
698 f.render_widget(user_list, columns[0]);
699 f.render_widget(system_list, columns[2]);
700}
701
702fn render_pie_chart_distribution(f: &mut Frame, app: &App, area: Rect) {
703 let category_distribution = app.get_category_distribution();
704
705 if !category_distribution.is_empty() && area.width >= 20 && area.height >= 8 {
708 let chart = create_pie_chart_from_distribution(
709 &category_distribution,
710 "Items Distribution (Count)",
711 false, )
713 .show_percentages(area.width >= 40)
714 .show_legend(area.width >= 50 || area.height >= 16);
715
716 f.render_widget(chart, area);
717 }
718}
719
720fn render_pie_chart_size_distribution(f: &mut Frame, app: &App, area: Rect) {
721 let category_distribution = app.get_category_distribution();
722
723 if !category_distribution.is_empty() && area.width >= 20 && area.height >= 8 {
726 let chart = create_pie_chart_from_distribution(
727 &category_distribution,
728 "Items Distribution (Size)",
729 true, )
731 .show_percentages(area.width >= 40)
732 .show_legend(area.width >= 50 || area.height >= 16);
733
734 f.render_widget(chart, area);
735 }
736}
737
738fn render_removed_items_window(f: &mut Frame, app: &mut App, area: Rect) {
739 let title = if app.is_running {
740 "📋 Operation Progress"
741 } else if app.show_progress_screen {
742 "📋 Cleaning Results - Removed Items"
743 } else {
744 "📋 Removed Items Details"
745 };
746
747 let block = Block::default()
748 .title(title)
749 .title_style(
750 Style::default()
751 .fg(Color::Yellow)
752 .add_modifier(Modifier::BOLD),
753 )
754 .borders(Borders::ALL)
755 .border_style(Style::default().fg(Color::Yellow));
756
757 let inner_area = block.inner(area);
758
759 let mut display_items = Vec::new();
760
761 if app.is_running && !app.operation_logs.is_empty() {
763 for log_entry in app.operation_logs.iter().rev().take(15) {
764 let (icon, color) = if log_entry.contains("✅") {
765 ("✅", Color::Green)
766 } else if log_entry.contains("❌") {
767 ("❌", Color::Red)
768 } else if log_entry.contains("🔄") {
769 ("🔄", Color::Yellow)
770 } else if log_entry.contains("📊") {
771 ("📊", Color::Cyan)
772 } else {
773 ("ℹ️", Color::White)
774 };
775
776 display_items.push(ListItem::new(Line::from(vec![
777 Span::styled(format!("{} ", icon), Style::default().fg(color)),
778 Span::styled(log_entry.clone(), Style::default().fg(Color::White)),
779 ])));
780 }
781 } else {
782 let filtered_items = app.get_filtered_detailed_items();
784
785 if !filtered_items.is_empty() {
786 for (index, item) in filtered_items.iter().enumerate() {
787 let icon = match item.item_type {
788 CleanedItemType::File => "📄",
789 CleanedItemType::Directory => "📁",
790 CleanedItemType::Log => "📝",
791 };
792
793 display_items.push(ListItem::new(Line::from(vec![
795 Span::styled(format!("{} ", icon), Style::default().fg(Color::Yellow)),
796 Span::styled(item.path.clone(), Style::default().fg(Color::White)),
797 Span::raw(" "),
798 Span::styled(
799 format!("({})", format_size(item.size)),
800 Style::default()
801 .fg(Color::Green)
802 .add_modifier(Modifier::BOLD),
803 ),
804 ])));
805
806 display_items.push(ListItem::new(Line::from(vec![
808 Span::raw(" "),
809 Span::styled("📂 ", Style::default().fg(Color::Blue)),
810 Span::styled(item.category.clone(), Style::default().fg(Color::Blue)),
811 Span::raw(" • "),
812 Span::styled("🔧 ", Style::default().fg(Color::Cyan)),
813 Span::styled(item.cleaner_name.clone(), Style::default().fg(Color::Cyan)),
814 ])));
815
816 if index < filtered_items.len() - 1 {
818 display_items.push(ListItem::new(Line::from(vec![])));
819 }
820 }
821 } else if !app.is_running && app.show_progress_screen && app.total_bytes_cleaned > 0 {
822 display_items.push(ListItem::new(Line::from(vec![
824 Span::styled("✅ ", Style::default().fg(Color::Green)),
825 Span::styled(
826 "Cleaning completed successfully",
827 Style::default()
828 .fg(Color::Green)
829 .add_modifier(Modifier::BOLD),
830 ),
831 ])));
832 display_items.push(ListItem::new(Line::from(vec![])));
833
834 display_items.push(ListItem::new(Line::from(vec![
835 Span::styled("📊 ", Style::default().fg(Color::Cyan)),
836 Span::styled("Total space freed: ", Style::default().fg(Color::White)),
837 Span::styled(
838 format_size(app.total_bytes_cleaned),
839 Style::default()
840 .fg(Color::Green)
841 .add_modifier(Modifier::BOLD),
842 ),
843 ])));
844 display_items.push(ListItem::new(Line::from(vec![])));
845
846 for category in &app.categories {
848 for item in &category.items {
849 if item.bytes_cleaned > 0 {
850 display_items.push(ListItem::new(Line::from(vec![
851 Span::styled("🔧 ", Style::default().fg(Color::Yellow)),
852 Span::styled(item.name.clone(), Style::default().fg(Color::White)),
853 Span::raw(": "),
854 Span::styled(
855 format_size(item.bytes_cleaned),
856 Style::default().fg(Color::Green),
857 ),
858 ])));
859 }
860 }
861 }
862
863 if display_items.len() == 3 {
864 display_items.push(ListItem::new(Line::from(vec![])));
866 display_items.push(ListItem::new(Line::from(vec![
867 Span::styled("ℹ️ ", Style::default().fg(Color::Blue)),
868 Span::styled(
869 "Detailed file list not available in TUI mode",
870 Style::default().fg(Color::DarkGray),
871 ),
872 ])));
873 }
874 }
875 }
876
877 let items_list = List::new(display_items)
878 .block(Block::default())
879 .highlight_style(
880 Style::default()
881 .bg(Color::DarkGray)
882 .add_modifier(Modifier::BOLD),
883 )
884 .highlight_symbol("► ");
885
886 f.render_stateful_widget(items_list, inner_area, &mut app.detailed_list_scroll_state);
887 f.render_widget(block, area);
888}
889
890fn render_categories(f: &mut Frame, app: &App, area: Rect) {
891 let categories: Vec<ListItem> = app
893 .categories
894 .iter()
895 .enumerate()
896 .map(|(i, category)| {
897 let content = Line::from(format!("{} ({})", category.name, category.description));
898 let style = if i == app.category_index {
899 Style::default()
900 .fg(Color::Yellow)
901 .add_modifier(Modifier::BOLD)
902 } else {
903 Style::default()
904 };
905 ListItem::new(content).style(style)
906 })
907 .collect();
908
909 let categories_list = List::new(categories)
910 .block(
911 Block::default()
912 .title("📂 Categories")
913 .borders(Borders::ALL),
914 )
915 .highlight_style(
916 Style::default()
917 .add_modifier(Modifier::BOLD)
918 .fg(Color::Yellow),
919 );
920
921 f.render_widget(categories_list, area);
922}
923
924fn render_cleaners(f: &mut Frame, app: &mut App, area: Rect) {
925 let current_category = &app.categories[app.category_index];
926
927 let items: Vec<ListItem> = current_category
928 .items
929 .iter()
930 .map(|item| {
931 let mut parts = vec![];
932
933 let checkbox_style = if item.selected {
936 Style::default()
937 .fg(Color::Green)
938 .add_modifier(Modifier::BOLD)
939 } else {
940 Style::default().fg(Color::White)
941 };
942
943 let _checkbox = Checkbox::new("", item.selected)
945 .checked_symbol(checkbox_symbols::CHECKED_X)
946 .unchecked_symbol(checkbox_symbols::UNCHECKED_SPACE);
947
948 let checkbox_symbol = if item.selected {
950 checkbox_symbols::CHECKED_X
951 } else {
952 checkbox_symbols::UNCHECKED_SPACE
953 };
954
955 parts.push(Span::styled(checkbox_symbol, checkbox_style));
956 parts.push(Span::raw(" "));
957
958 let name_style = if item.requires_root && !app.is_root {
960 Style::default().fg(Color::DarkGray)
961 } else {
962 Style::default().fg(Color::White)
963 };
964 parts.push(Span::styled(&item.name, name_style));
965
966 if item.requires_root {
968 parts.push(Span::styled(" (root)", Style::default().fg(Color::Red)));
969 }
970
971 if let Some(status) = &item.status {
973 match status {
974 Status::Running => {
975 parts.push(Span::styled(
976 " [Running]",
977 Style::default().fg(Color::Yellow),
978 ));
979 }
980 Status::Success(msg) => {
981 parts.push(Span::styled(
982 format!(" [{}]", msg),
983 Style::default().fg(Color::Green),
984 ));
985 }
986 Status::Error(msg) => {
987 parts.push(Span::styled(
988 format!(" [Error: {}]", msg),
989 Style::default().fg(Color::Red),
990 ));
991 }
992 Status::Pending => {
993 parts.push(Span::styled(
994 " [Pending]",
995 Style::default().fg(Color::DarkGray),
996 ));
997 }
998 }
999 }
1000
1001 if item.bytes_cleaned > 0 {
1003 parts.push(Span::styled(
1004 format!(" (Freed: {})", format_size(item.bytes_cleaned)),
1005 Style::default().fg(Color::Green),
1006 ));
1007 }
1008
1009 ListItem::new(Line::from(parts))
1010 })
1011 .collect();
1012
1013 let items_list = List::new(items)
1014 .block(
1015 Block::default()
1016 .title(format!("{} Items", current_category.name))
1017 .borders(Borders::ALL),
1018 )
1019 .highlight_style(
1020 Style::default()
1021 .add_modifier(Modifier::BOLD)
1022 .bg(Color::DarkGray),
1023 )
1024 .highlight_symbol("> ");
1025
1026 f.render_stateful_widget(items_list, area, &mut app.item_list_state);
1027}
1028
1029fn render_details(f: &mut Frame, app: &App, area: Rect) {
1030 let current_category = &app.categories[app.category_index];
1031
1032 if let Some(selected) = app.item_list_state.selected() {
1033 if selected < current_category.items.len() {
1034 let item = ¤t_category.items[selected];
1035
1036 let mut text = vec![
1037 Line::from(vec![Span::styled(
1038 format!("{} Keyboard Controls", item.name),
1039 Style::default()
1040 .fg(Color::Cyan)
1041 .add_modifier(Modifier::BOLD),
1042 )]),
1043 Line::from(vec![Span::raw("")]),
1044 Line::from(vec![
1045 Span::raw("Description: "),
1046 Span::styled(&item.description, Style::default().fg(Color::White)),
1047 ]),
1048 Line::from(vec![Span::raw("")]),
1049 Line::from(vec![
1050 Span::raw("Requires root: "),
1051 if item.requires_root {
1052 Span::styled("Yes", Style::default().fg(Color::Red))
1053 } else {
1054 Span::styled("No", Style::default().fg(Color::Green))
1055 },
1056 ]),
1057 Line::from(vec![
1058 Span::raw("Status: "),
1059 match &item.status {
1060 Some(Status::Running) => {
1061 let spinner = Status::Running.get_animation_frame(app.animation_frame);
1062 Span::styled(
1063 format!("{} Running...", spinner),
1064 Style::default().fg(Color::Yellow),
1065 )
1066 }
1067 Some(Status::Success(msg)) => {
1068 Span::styled(format!("✓ {}", msg), Style::default().fg(Color::Green))
1069 }
1070 Some(Status::Error(msg)) => Span::styled(
1071 format!("✗ Error: {}", msg),
1072 Style::default().fg(Color::Red),
1073 ),
1074 Some(Status::Pending) => {
1075 Span::styled("• Waiting to start", Style::default().fg(Color::DarkGray))
1076 }
1077 None => Span::raw("Not run"),
1078 },
1079 ]),
1080 ];
1081
1082 if item.bytes_cleaned > 0 {
1083 text.push(Line::from(vec![
1084 Span::raw("Space freed: "),
1085 Span::styled(
1086 format!("{:.2} GB", item.bytes_cleaned as f64 / 1_073_741_824.0),
1087 Style::default().fg(Color::Green),
1088 ),
1089 ]));
1090 }
1091
1092 let details = Paragraph::new(text)
1093 .block(Block::default().title("Details").borders(Borders::ALL))
1094 .wrap(Wrap { trim: true });
1095
1096 f.render_widget(details, area);
1097 }
1098 }
1099}
1100
1101fn render_footer(f: &mut Frame, app: &App, area: Rect) {
1102 let block = Block::default()
1103 .borders(Borders::TOP)
1104 .border_style(Style::default().fg(Color::DarkGray));
1105
1106 let inner_area = block.inner(area);
1107
1108 if app.is_running || app.show_progress_screen {
1109 let footer_chunks = Layout::default()
1111 .direction(Direction::Horizontal)
1112 .constraints([
1113 Constraint::Percentage(60), Constraint::Percentage(40), ])
1116 .split(inner_area);
1117
1118 let status_text = vec![Line::from(vec![
1120 Span::styled(
1121 "Status: ",
1122 Style::default()
1123 .fg(Color::White)
1124 .add_modifier(Modifier::BOLD),
1125 ),
1126 if app.paused {
1127 Span::styled(
1128 "PAUSED",
1129 Style::default()
1130 .fg(Color::Yellow)
1131 .add_modifier(Modifier::BOLD),
1132 )
1133 } else if app.is_running {
1134 Span::styled(
1135 "CLEANING",
1136 Style::default()
1137 .fg(Color::Green)
1138 .add_modifier(Modifier::BOLD),
1139 )
1140 } else if app.operation_end_time.is_some() {
1141 Span::styled(
1142 "FINISHED",
1143 Style::default()
1144 .fg(Color::Cyan)
1145 .add_modifier(Modifier::BOLD),
1146 )
1147 } else {
1148 Span::styled(
1149 "READY",
1150 Style::default()
1151 .fg(Color::White)
1152 .add_modifier(Modifier::BOLD),
1153 )
1154 },
1155 Span::raw(" • "),
1156 Span::styled("Total Freed: ", Style::default().fg(Color::White)),
1157 Span::styled(
1158 format_size(app.total_bytes_cleaned),
1159 Style::default()
1160 .fg(Color::Green)
1161 .add_modifier(Modifier::BOLD),
1162 ),
1163 ])];
1164
1165 let controls_text = if app.is_running {
1167 vec![Line::from(vec![
1168 Span::styled(
1169 "ESC",
1170 Style::default()
1171 .fg(Color::Yellow)
1172 .add_modifier(Modifier::BOLD),
1173 ),
1174 Span::raw(": Cancel "),
1175 Span::styled(
1176 "↑/↓",
1177 Style::default()
1178 .fg(Color::Cyan)
1179 .add_modifier(Modifier::BOLD),
1180 ),
1181 Span::raw(": Scroll Items "),
1182 Span::styled(
1183 "q",
1184 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
1185 ),
1186 Span::raw(": Quit"),
1187 ])]
1188 } else {
1189 vec![Line::from(vec![
1191 Span::styled(
1192 "ESC",
1193 Style::default()
1194 .fg(Color::Yellow)
1195 .add_modifier(Modifier::BOLD),
1196 ),
1197 Span::raw(": Return to Menu "),
1198 Span::styled(
1199 "↑/↓",
1200 Style::default()
1201 .fg(Color::Cyan)
1202 .add_modifier(Modifier::BOLD),
1203 ),
1204 Span::raw(": Scroll Items "),
1205 Span::styled(
1206 "q",
1207 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
1208 ),
1209 Span::raw(": Quit"),
1210 ])]
1211 };
1212
1213 let status_para = Paragraph::new(status_text);
1214 let controls_para =
1215 Paragraph::new(controls_text).alignment(ratatui::layout::Alignment::Right);
1216
1217 f.render_widget(status_para, footer_chunks[0]);
1218 f.render_widget(controls_para, footer_chunks[1]);
1219 } else {
1220 let footer_chunks = Layout::default()
1222 .direction(Direction::Horizontal)
1223 .constraints([
1224 Constraint::Percentage(40), Constraint::Percentage(60), ])
1227 .split(inner_area);
1228
1229 let status_text = vec![Line::from(vec![
1231 Span::styled(
1232 "User: ",
1233 Style::default()
1234 .fg(Color::White)
1235 .add_modifier(Modifier::BOLD),
1236 ),
1237 if app.is_root {
1238 Span::styled(
1239 "root",
1240 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
1241 )
1242 } else {
1243 Span::styled(
1244 "standard",
1245 Style::default()
1246 .fg(Color::Green)
1247 .add_modifier(Modifier::BOLD),
1248 )
1249 },
1250 Span::raw(" • "),
1251 Span::styled("Selected: ", Style::default().fg(Color::White)),
1252 Span::styled(
1253 format!("{}", app.selected_cleaners_count),
1254 Style::default()
1255 .fg(Color::Blue)
1256 .add_modifier(Modifier::BOLD),
1257 ),
1258 ])];
1259
1260 let controls_text = vec![Line::from(vec![
1262 Span::styled(
1263 "Space",
1264 Style::default()
1265 .fg(Color::Yellow)
1266 .add_modifier(Modifier::BOLD),
1267 ),
1268 Span::raw(": Select "),
1269 Span::styled(
1270 "Enter",
1271 Style::default()
1272 .fg(Color::Green)
1273 .add_modifier(Modifier::BOLD),
1274 ),
1275 Span::raw(": Run "),
1276 Span::styled(
1277 "Tab",
1278 Style::default()
1279 .fg(Color::Blue)
1280 .add_modifier(Modifier::BOLD),
1281 ),
1282 Span::raw(": Category "),
1283 Span::styled(
1284 "?",
1285 Style::default()
1286 .fg(Color::Magenta)
1287 .add_modifier(Modifier::BOLD),
1288 ),
1289 Span::raw(": Help "),
1290 Span::styled(
1291 "q",
1292 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
1293 ),
1294 Span::raw(": Quit"),
1295 ])];
1296
1297 let status_para = Paragraph::new(status_text);
1298 let controls_para =
1299 Paragraph::new(controls_text).alignment(ratatui::layout::Alignment::Right);
1300
1301 f.render_widget(status_para, footer_chunks[0]);
1302 f.render_widget(controls_para, footer_chunks[1]);
1303 }
1304
1305 f.render_widget(block, area);
1306}
1307
1308fn render_help(f: &mut Frame, area: Rect) {
1309 let help_text = vec![
1310 Line::from(vec![Span::styled(
1311 "🔍 Cleansys Help",
1312 Style::default()
1313 .fg(Color::Cyan)
1314 .add_modifier(Modifier::BOLD),
1315 )]),
1316 Line::from(vec![Span::raw("")]),
1317 Line::from(vec![Span::styled(
1318 "📍 Navigation:",
1319 Style::default().add_modifier(Modifier::BOLD),
1320 )]),
1321 Line::from(vec![Span::raw(" ↑/↓: Navigate items")]),
1322 Line::from(vec![Span::raw(" Tab/Shift+Tab: Switch categories")]),
1323 Line::from(vec![Span::raw("")]),
1324 Line::from(vec![Span::styled(
1325 "🔧 Actions:",
1326 Style::default().add_modifier(Modifier::BOLD),
1327 )]),
1328 Line::from(vec![Span::raw(" Space: Toggle selection")]),
1329 Line::from(vec![Span::raw(
1330 " Enter: Run selected cleaners (asks for confirmation)",
1331 )]),
1332 Line::from(vec![Span::raw(
1333 " d: Preview selected cleaners (dry-run, deletes nothing)",
1334 )]),
1335 Line::from(vec![Span::raw(" a: Select all in current category")]),
1336 Line::from(vec![Span::raw(" n: Deselect all in current category")]),
1337 Line::from(vec![Span::raw(" A: Select all across every category")]),
1338 Line::from(vec![Span::raw(" N: Deselect all across every category")]),
1339 Line::from(vec![Span::raw(
1340 " c: Cycle chart type (Bar → Count Pie → Size Pie → Bar)",
1341 )]),
1342 Line::from(vec![Span::raw(" /: Search in detailed view")]),
1343 Line::from(vec![Span::raw("")]),
1344 Line::from(vec![Span::styled(
1345 "🎛️ Advanced Controls:",
1346 Style::default().add_modifier(Modifier::BOLD),
1347 )]),
1348 Line::from(vec![Span::raw(" m: Toggle compact mode")]),
1349 Line::from(vec![Span::raw(
1350 " v: Cycle view mode (Standard/Compact/Detailed/Performance)",
1351 )]),
1352 Line::from(vec![Span::raw(" p: Toggle performance statistics")]),
1353 Line::from(vec![Span::raw(
1354 " s: Toggle auto-scroll log (during operations)",
1355 )]),
1356 Line::from(vec![Span::raw(" o: Cycle sort mode")]),
1357 Line::from(vec![Span::raw(" f: Cycle filter mode")]),
1358 Line::from(vec![Span::raw(" y: Toggle confirmation prompts")]),
1359 Line::from(vec![Span::raw(" x: Clear all errors")]),
1360 Line::from(vec![Span::raw(
1361 " j/k: Scroll detailed items list (vi-style)",
1362 )]),
1363 Line::from(vec![Span::raw(" /: Search files/paths in detailed view")]),
1364 Line::from(vec![Span::raw(
1365 " ESC: Clear search / Cancel operation / Return to menu",
1366 )]),
1367 Line::from(vec![Span::raw(" Backspace: Remove search character")]),
1368 Line::from(vec![Span::raw(" PgUp/PgDn: Scroll operation log")]),
1369 Line::from(vec![Span::raw(" Home/End: Jump to first/last item")]),
1370 Line::from(vec![Span::raw(" Ctrl+Space: Pause/Resume operations")]),
1371 Line::from(vec![Span::raw("")]),
1372 Line::from(vec![Span::styled(
1373 "🔍 Search Features:",
1374 Style::default().add_modifier(Modifier::BOLD),
1375 )]),
1376 Line::from(vec![Span::raw(
1377 " Search matches file paths, categories, and cleaner names",
1378 )]),
1379 Line::from(vec![Span::raw(
1380 " Real-time filtering with highlighted results",
1381 )]),
1382 Line::from(vec![Span::raw(" Category distribution shown at bottom")]),
1383 Line::from(vec![Span::raw("")]),
1384 Line::from(vec![Span::styled(
1385 "📊 Chart Types (press 'c' to cycle):",
1386 Style::default().add_modifier(Modifier::BOLD),
1387 )]),
1388 Line::from(vec![Span::raw(
1389 " Bar Chart: Traditional vertical bars for comparison",
1390 )]),
1391 Line::from(vec![Span::raw(
1392 " Pie Count: Circular chart showing item distribution by count",
1393 )]),
1394 Line::from(vec![Span::raw(
1395 " Pie Size: Circular chart showing space usage by category",
1396 )]),
1397 Line::from(vec![Span::raw("")]),
1398 Line::from(vec![Span::styled(
1399 "🔒 System Operations:",
1400 Style::default().add_modifier(Modifier::BOLD),
1401 )]),
1402 Line::from(vec![Span::raw(
1403 " System cleaners require sudo/root privileges",
1404 )]),
1405 Line::from(vec![Span::raw(
1406 " Run 'sudo cleansys' or provide password when prompted",
1407 )]),
1408 Line::from(vec![Span::raw(
1409 " Items marked (sudo) will request elevated privileges",
1410 )]),
1411 Line::from(vec![Span::raw("")]),
1412 Line::from(vec![Span::styled(
1413 "🔄 Other:",
1414 Style::default().add_modifier(Modifier::BOLD),
1415 )]),
1416 Line::from(vec![Span::raw(" ?: Show/hide help")]),
1417 Line::from(vec![Span::raw(" q: Exit application")]),
1418 ];
1419
1420 let help = Paragraph::new(help_text)
1421 .block(Block::default().title("📚 Help").borders(Borders::ALL))
1422 .wrap(Wrap { trim: true });
1423
1424 f.render_widget(help, area);
1425}
1426
1427fn centered_popup(area: Rect, width_pct: u16, height_pct: u16) -> Rect {
1430 let width = (area.width * width_pct / 100).clamp(30, area.width.saturating_sub(2).max(30));
1431 let height = (area.height * height_pct / 100).clamp(10, area.height.saturating_sub(2).max(10));
1432 let x = area.x + (area.width.saturating_sub(width)) / 2;
1433 let y = area.y + (area.height.saturating_sub(height)) / 2;
1434 Rect {
1435 x,
1436 y,
1437 width,
1438 height,
1439 }
1440}
1441
1442fn render_confirm_run(f: &mut Frame, app: &App, area: Rect) {
1445 let popup = centered_popup(area, 70, 60);
1446
1447 let selected_count = app.pending_run_selection.len();
1448 let mut lines = vec![
1449 Line::from(vec![Span::styled(
1450 "⚠️ Confirm Cleaning",
1451 Style::default()
1452 .fg(Color::Yellow)
1453 .add_modifier(Modifier::BOLD),
1454 )]),
1455 Line::from(vec![Span::raw("")]),
1456 Line::from(vec![Span::raw(format!(
1457 "This will permanently delete files for {selected_count} selected cleaner(s):"
1458 ))]),
1459 Line::from(vec![Span::raw("")]),
1460 ];
1461
1462 for (_, _, name, _, requires_root) in app.pending_run_selection.iter().take(15) {
1463 let suffix = if *requires_root { " (root)" } else { "" };
1464 lines.push(Line::from(vec![Span::raw(format!(" • {name}{suffix}"))]));
1465 }
1466 if app.pending_run_selection.len() > 15 {
1467 lines.push(Line::from(vec![Span::styled(
1468 format!(" … and {} more", app.pending_run_selection.len() - 15),
1469 Style::default().fg(Color::DarkGray),
1470 )]));
1471 }
1472
1473 lines.push(Line::from(vec![Span::raw("")]));
1474 lines.push(Line::from(vec![Span::styled(
1475 "Enter/y: Run now Esc/n: Cancel",
1476 Style::default()
1477 .fg(Color::Green)
1478 .add_modifier(Modifier::BOLD),
1479 )]));
1480
1481 let popup_widget = Paragraph::new(lines)
1482 .block(
1483 Block::default()
1484 .title("Confirm")
1485 .borders(Borders::ALL)
1486 .border_style(Style::default().fg(Color::Yellow)),
1487 )
1488 .wrap(Wrap { trim: true });
1489
1490 f.render_widget(Clear, popup);
1491 f.render_widget(popup_widget, popup);
1492}
1493
1494fn render_preview(f: &mut Frame, app: &App, area: Rect) {
1497 let popup = centered_popup(area, 80, 75);
1498
1499 let total_bytes: u64 = app.preview_results.iter().map(|(_, r)| r.total_bytes).sum();
1500 let total_items: usize = app
1501 .preview_results
1502 .iter()
1503 .map(|(_, r)| r.item_count())
1504 .sum();
1505
1506 let mut lines = vec![
1507 Line::from(vec![Span::styled(
1508 "🔍 Preview (dry-run)",
1509 Style::default()
1510 .fg(Color::Cyan)
1511 .add_modifier(Modifier::BOLD),
1512 )]),
1513 Line::from(vec![Span::raw("")]),
1514 Line::from(vec![Span::raw(format!(
1515 "Would free {} across {total_items} item(s). Nothing has been deleted.",
1516 format_size(total_bytes)
1517 ))]),
1518 Line::from(vec![Span::raw("")]),
1519 ];
1520
1521 if app.preview_results.is_empty() {
1522 lines.push(Line::from(vec![Span::styled(
1523 "Nothing to clean — all selected cleaners are already empty.",
1524 Style::default().fg(Color::DarkGray),
1525 )]));
1526 }
1527
1528 for (name, result) in &app.preview_results {
1529 lines.push(Line::from(vec![Span::styled(
1530 format!(
1531 "{name} — {} across {} item(s)",
1532 format_size(result.total_bytes),
1533 result.item_count()
1534 ),
1535 Style::default().add_modifier(Modifier::BOLD),
1536 )]));
1537 for item in result.items.iter().take(3) {
1538 lines.push(Line::from(vec![Span::raw(format!(
1539 " • {} ({})",
1540 item.path_str(),
1541 format_size(item.size)
1542 ))]));
1543 }
1544 if result.items.len() > 3 {
1545 lines.push(Line::from(vec![Span::styled(
1546 format!(" … and {} more", result.items.len() - 3),
1547 Style::default().fg(Color::DarkGray),
1548 )]));
1549 }
1550 lines.push(Line::from(vec![Span::raw("")]));
1551 }
1552
1553 lines.push(Line::from(vec![Span::styled(
1554 "Enter/Esc/q: Close",
1555 Style::default()
1556 .fg(Color::Green)
1557 .add_modifier(Modifier::BOLD),
1558 )]));
1559
1560 let popup_widget = Paragraph::new(lines)
1561 .block(
1562 Block::default()
1563 .title("Preview")
1564 .borders(Borders::ALL)
1565 .border_style(Style::default().fg(Color::Cyan)),
1566 )
1567 .wrap(Wrap { trim: true });
1568
1569 f.render_widget(Clear, popup);
1570 f.render_widget(popup_widget, popup);
1571}
1572
1573fn render_admin_notice(f: &mut Frame, area: Rect) {
1577 let popup = centered_popup(area, 60, 30);
1578
1579 let lines = vec![
1580 Line::from(vec![Span::styled(
1581 "⚠️ Administrator privileges required",
1582 Style::default()
1583 .fg(Color::Yellow)
1584 .add_modifier(Modifier::BOLD),
1585 )]),
1586 Line::from(vec![Span::raw("")]),
1587 Line::from(vec![Span::raw(
1588 "One or more selected cleaners need Administrator privileges.",
1589 )]),
1590 Line::from(vec![Span::raw(
1591 "Restart CleanSys as Administrator to use them.",
1592 )]),
1593 Line::from(vec![Span::raw("")]),
1594 Line::from(vec![Span::styled(
1595 "Enter/Esc/q: Close",
1596 Style::default()
1597 .fg(Color::Green)
1598 .add_modifier(Modifier::BOLD),
1599 )]),
1600 ];
1601
1602 let popup_widget = Paragraph::new(lines)
1603 .block(
1604 Block::default()
1605 .title("Administrator required")
1606 .borders(Borders::ALL)
1607 .border_style(Style::default().fg(Color::Yellow)),
1608 )
1609 .wrap(Wrap { trim: true });
1610
1611 f.render_widget(Clear, popup);
1612 f.render_widget(popup_widget, popup);
1613}