cli_tutor/ui/
progress_overlay.rs1use crate::app::App;
3use ratatui::{
4 layout::{Constraint, Direction, Layout, Rect},
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7 widgets::{Block, Borders, Clear, Paragraph},
8 Frame,
9};
10
11pub fn render(app: &App, frame: &mut Frame, area: Rect) {
12 let overlay = centered_rect(70, 80, area);
13 frame.render_widget(Clear, overlay);
14
15 let nc = app.config.no_color;
16
17 let mut lines: Vec<Line<'static>> = vec![
19 Line::from(Span::styled(
20 "Progress Summary",
21 crate::ui::s(
22 Style::default()
23 .fg(Color::Cyan)
24 .add_modifier(Modifier::BOLD),
25 nc,
26 ),
27 )),
28 Line::from(""),
29 ];
30
31 let show_times = app.config.timed_challenge;
32 let header = if show_times {
33 " Module Completed Best Time"
34 } else {
35 " Module Completed"
36 };
37 lines.push(Line::from(Span::styled(
38 header,
39 crate::ui::s(
40 Style::default().add_modifier(Modifier::UNDERLINED),
41 nc,
42 ),
43 )));
44 lines.push(Line::from(""));
45
46 let mut total_completed = 0usize;
47 let mut total_exercises = 0usize;
48
49 for m in &app.modules {
50 let ex_count = m.exercises.len();
51 let completed = app
52 .progress
53 .modules
54 .get(&m.module.name)
55 .map(|p| p.completed.len())
56 .unwrap_or(0);
57 total_completed += completed;
58 total_exercises += ex_count;
59
60 let pct = if ex_count > 0 {
61 completed * 100 / ex_count
62 } else {
63 0
64 };
65 let bar = progress_bar(pct);
66
67 let time_str = if show_times {
69 let best = m
70 .exercises
71 .iter()
72 .filter_map(|ex| app.progress.best_time(&m.module.name, &ex.id))
73 .min();
74 match best {
75 Some(ms) => format!(" {:.1}s", ms as f64 / 1000.0),
76 None => " —".to_string(),
77 }
78 } else {
79 String::new()
80 };
81
82 let done_mark = if completed == ex_count && ex_count > 0 {
83 " ✓"
84 } else {
85 " "
86 };
87 let color = if completed == ex_count && ex_count > 0 {
88 Color::Green
89 } else if completed > 0 {
90 Color::Yellow
91 } else {
92 Color::DarkGray
93 };
94
95 let row = format!(
96 " {:<16}{}{}/{} {}{}{}",
97 m.module.name,
98 done_mark,
99 completed,
100 ex_count,
101 bar,
102 if pct < 100 {
103 format!(" {pct}%")
104 } else {
105 " ".to_string()
106 },
107 time_str,
108 );
109 lines.push(Line::from(Span::styled(
110 row,
111 crate::ui::s(Style::default().fg(color), nc),
112 )));
113 }
114
115 lines.push(Line::from(""));
117 let total_pct = if total_exercises > 0 {
118 total_completed * 100 / total_exercises
119 } else {
120 0
121 };
122 lines.push(Line::from(Span::styled(
123 format!(
124 " Total {}/{} ({total_pct}%)",
125 total_completed, total_exercises
126 ),
127 crate::ui::s(Style::default().add_modifier(Modifier::BOLD), nc),
128 )));
129 lines.push(Line::from(""));
130 lines.push(Line::from(Span::styled(
131 "Press P to close",
132 crate::ui::s(Style::default().fg(Color::DarkGray), nc),
133 )));
134
135 let para = Paragraph::new(lines).block(
136 Block::default()
137 .borders(Borders::ALL)
138 .title(" Progress ")
139 .style(crate::ui::s(Style::default().bg(Color::Black), nc)),
140 );
141 frame.render_widget(para, overlay);
142}
143
144fn progress_bar(pct: usize) -> String {
145 let filled = pct / 10; let empty = 10 - filled;
147 format!("[{}{}]", "█".repeat(filled), "░".repeat(empty))
148}
149
150fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
151 let vert = Layout::default()
152 .direction(Direction::Vertical)
153 .constraints([
154 Constraint::Percentage((100 - percent_y) / 2),
155 Constraint::Percentage(percent_y),
156 Constraint::Percentage((100 - percent_y) / 2),
157 ])
158 .split(r);
159 Layout::default()
160 .direction(Direction::Horizontal)
161 .constraints([
162 Constraint::Percentage((100 - percent_x) / 2),
163 Constraint::Percentage(percent_x),
164 Constraint::Percentage((100 - percent_x) / 2),
165 ])
166 .split(vert[1])[1]
167}