Skip to main content

cli_tutor/ui/
progress_overlay.rs

1// progress_summary.OVERLAY.1 — modal showing per-module completion and best times
2use 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    // progress_summary.OVERLAY.2 — per-module rows
18    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        // progress_summary.OVERLAY.4 — show best times when timed_challenge enabled
68        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    // progress_summary.OVERLAY.3 — totals row
116    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; // 0-10 blocks
146    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}