Skip to main content

cli_tutor/ui/
command_list.rs

1use crate::app::App;
2use ratatui::{
3    layout::{Constraint, Direction, Layout, Rect},
4    style::{Color, Modifier, Style},
5    text::{Line, Span},
6    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
7    Frame,
8};
9
10pub fn render(app: &App, frame: &mut Frame, area: Rect) {
11    let nc = app.config.no_color;
12
13    // module_search.SEARCH.2 — split area to show search bar when active
14    let (search_area, list_area) = if app.search_active {
15        let chunks = Layout::default()
16            .direction(Direction::Vertical)
17            .constraints([Constraint::Length(1), Constraint::Min(0)])
18            .split(area);
19        (Some(chunks[0]), chunks[1])
20    } else {
21        (None, area)
22    };
23
24    // module_search.SEARCH.2 — render search bar
25    if let Some(sa) = search_area {
26        let search_line = Line::from(vec![
27            Span::styled(
28                "/ ",
29                crate::ui::s(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), nc),
30            ),
31            Span::raw(app.search_query.clone()),
32            Span::styled(
33                " ",
34                crate::ui::s(Style::default().bg(Color::White).fg(Color::Black), nc),
35            ),
36        ]);
37        frame.render_widget(Paragraph::new(search_line), sa);
38    }
39
40    let visible = app.visible_module_indices();
41
42    let items: Vec<ListItem> = visible
43        .iter()
44        .map(|&i| {
45            let m = &app.modules[i];
46            let ex_count = m.exercises.len();
47            let completed = app
48                .progress
49                .modules
50                .get(&m.module.name)
51                .map(|p| p.completed.len())
52                .unwrap_or(0);
53
54            let badge = if completed == ex_count && ex_count > 0 {
55                " ✓"
56            } else {
57                ""
58            };
59
60            let is_selected = i == app.selected_module;
61
62            let line = Line::from(vec![
63                Span::styled(
64                    format!("{:<10}", m.module.name),
65                    if is_selected {
66                        crate::ui::s(Style::default().add_modifier(Modifier::BOLD), nc)
67                    } else {
68                        Style::default()
69                    },
70                ),
71                Span::styled(
72                    format!("[{:>2}]", ex_count),
73                    crate::ui::s(Style::default().fg(Color::DarkGray), nc),
74                ),
75                Span::styled(
76                    badge,
77                    crate::ui::s(Style::default().fg(Color::Green), nc),
78                ),
79            ]);
80            ListItem::new(line)
81        })
82        .collect();
83
84    // Find position of selected_module within the visible list for ListState
85    let selected_pos = visible.iter().position(|&i| i == app.selected_module);
86    let mut state = ListState::default();
87    state.select(selected_pos);
88
89    let list = List::new(items)
90        .block(Block::default().borders(Borders::RIGHT).title("Modules"))
91        .highlight_style(crate::ui::s(
92            Style::default().bg(Color::White).fg(Color::Black),
93            nc,
94        ));
95
96    frame.render_stateful_widget(list, list_area, &mut state);
97}