1use crate::app::{App, ContentView, DifficultyFilter};
2use crate::ui::{command_list, content_pane, help_overlay, progress_overlay};
3use ratatui::{
4 layout::{Constraint, Direction, Layout, Rect},
5 style::{Color, Modifier, Style},
6 text::{Line, Span},
7 widgets::{Block, Borders, Paragraph},
8 Frame,
9};
10
11pub fn render(app: &App, frame: &mut Frame) {
12 let area = frame.area();
13
14 if area.width < 80 || area.height < 24 {
15 render_resize_prompt(frame, area);
16 return;
17 }
18
19 let rows = Layout::default()
20 .direction(Direction::Vertical)
21 .constraints([
22 Constraint::Length(1),
23 Constraint::Min(0),
24 Constraint::Length(1),
25 ])
26 .split(area);
27
28 render_header(app, frame, rows[0]);
29 render_main(app, frame, rows[1]);
30 render_footer(app, frame, rows[2]);
31
32 if app.show_help {
33 help_overlay::render(app, frame, area);
34 }
35 if app.show_progress {
37 progress_overlay::render(app, frame, area);
38 }
39}
40
41fn render_header(app: &App, frame: &mut Frame, area: Rect) {
42 let nc = app.config.no_color;
43 let module = app.current_module();
44 let ex_count = app.exercise_count();
45 let progress_text = if ex_count > 0 {
46 let completed = app
47 .module_progress()
48 .map(|p| p.completed.len())
49 .unwrap_or(0);
50 format!(
51 " {} — {} ({}/{})",
52 module.module.name, module.module.description, completed, ex_count
53 )
54 } else {
55 format!(" {} — {}", module.module.name, module.module.description)
56 };
57
58 let view_label = match app.current_view {
59 ContentView::Intro => "[Intro]",
60 ContentView::Examples => "[Examples]",
61 ContentView::Exercise => "[Exercise]",
62 ContentView::FreePractice => "[Free Practice]",
63 };
64
65 let filter_label = match app.difficulty_filter {
67 DifficultyFilter::None => String::new(),
68 f => format!(" [{}]", f),
69 };
70
71 let title = Line::from(vec![
72 Span::styled(
73 " CLI Tutor ",
74 crate::ui::s(
75 Style::default()
76 .fg(Color::Cyan)
77 .add_modifier(Modifier::BOLD),
78 nc,
79 ),
80 ),
81 Span::styled(&progress_text, crate::ui::s(Style::default().fg(Color::White), nc)),
82 Span::styled(" ", Style::default()),
83 Span::styled(
84 view_label,
85 crate::ui::s(Style::default().fg(Color::Yellow), nc),
86 ),
87 Span::styled(
88 filter_label,
89 crate::ui::s(Style::default().fg(Color::Magenta), nc),
90 ),
91 Span::styled(
92 " [?] Help [P] Progress",
93 crate::ui::s(Style::default().fg(Color::DarkGray), nc),
94 ),
95 ]);
96
97 frame.render_widget(
98 Paragraph::new(title)
99 .style(crate::ui::s(Style::default().bg(Color::DarkGray), nc)),
100 area,
101 );
102}
103
104fn render_main(app: &App, frame: &mut Frame, area: Rect) {
105 let cols = Layout::default()
106 .direction(Direction::Horizontal)
107 .constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
108 .split(area);
109
110 command_list::render(app, frame, cols[0]);
111 content_pane::render(app, frame, cols[1]);
112}
113
114fn render_footer(app: &App, frame: &mut Frame, area: Rect) {
115 let nc = app.config.no_color;
116 let hints = match app.current_view {
117 ContentView::Intro | ContentView::Examples => {
118 " ↑↓: Module Tab: Next view PgUp/PgDn: Scroll /: Search d: Filter q: Quit"
119 }
120 ContentView::Exercise => {
121 " Enter: Submit ↑↓: History PgUp/PgDn: Scroll ^T: Hint ^S: Solution ^N/^P: Next/Prev ^R: Reset Esc: Back"
122 }
123 ContentView::FreePractice => {
124 " Enter: Run ↑↓: History PgUp/PgDn: Scroll ^L: Clear Tab/Esc: Back"
125 }
126 };
127
128 frame.render_widget(
129 Paragraph::new(hints)
130 .style(crate::ui::s(Style::default().bg(Color::DarkGray).fg(Color::White), nc)),
131 area,
132 );
133}
134
135fn render_resize_prompt(frame: &mut Frame, area: Rect) {
136 let msg = Paragraph::new("Please resize your terminal (min 80×24)")
137 .style(Style::default().fg(Color::Yellow))
138 .block(Block::default().borders(Borders::ALL));
139 let center = centered_rect(50, 20, area);
140 frame.render_widget(msg, center);
141}
142
143fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
144 let layout = Layout::default()
145 .direction(Direction::Vertical)
146 .constraints([
147 Constraint::Percentage((100 - percent_y) / 2),
148 Constraint::Percentage(percent_y),
149 Constraint::Percentage((100 - percent_y) / 2),
150 ])
151 .split(r);
152 Layout::default()
153 .direction(Direction::Horizontal)
154 .constraints([
155 Constraint::Percentage((100 - percent_x) / 2),
156 Constraint::Percentage(percent_x),
157 Constraint::Percentage((100 - percent_x) / 2),
158 ])
159 .split(layout[1])[1]
160}