1use ratatui::{
4 Frame,
5 layout::{Constraint, Direction, Layout},
6 text::{Line, Span},
7 widgets::Paragraph,
8};
9
10use super::app::{App, StageStatus, SubTask};
11use super::layout::calculate_layout;
12use super::theme::Theme;
13
14pub fn render_ui(frame: &mut Frame, app: &App) {
16 let theme = Theme::default_theme();
17 let chunks = calculate_layout(frame.area());
18
19 render_header(frame, app, &theme, chunks[0]);
20 render_pipeline(frame, app, &theme, chunks[1]);
21 render_footer(frame, app, &theme, chunks[2]);
22}
23
24pub fn render_compact(frame: &mut Frame, app: &App) {
26 let theme = Theme::default_theme();
27 let chunks = calculate_layout(frame.area());
28
29 render_header(frame, app, &theme, chunks[0]);
30 render_pipeline_compact(frame, app, &theme, chunks[1]);
31 render_footer(frame, app, &theme, chunks[2]);
32}
33
34pub fn render_minimal(frame: &mut Frame, app: &App) {
36 let theme = Theme::default_theme();
37 let area = frame.area();
38
39 let chunks = Layout::default()
41 .direction(Direction::Vertical)
42 .margin(1)
43 .constraints([Constraint::Percentage(50), Constraint::Length(3)])
44 .split(area);
45
46 let progress_text = format!(
47 "{} stage {}/{} - {:.1}s",
48 render_progress_bar(app.overall_progress, 30),
49 app.current_stage + 1,
50 app.stages.len(),
51 app.elapsed_time.as_secs_f64()
52 );
53
54 frame.render_widget(
55 Paragraph::new(progress_text).style(theme.progress_bar_style()),
56 chunks[1],
57 );
58}
59
60fn render_header(frame: &mut Frame, app: &App, theme: &Theme, area: ratatui::layout::Rect) {
62 let header_chunks = Layout::default()
63 .direction(Direction::Vertical)
64 .constraints([
65 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
70 .split(area);
71
72 let title_line = Line::from(vec![
74 Span::raw("debtmap"),
75 Span::raw(" "),
76 Span::styled(
77 format!("{:.1}s", app.elapsed_time.as_secs_f64()),
78 theme.time_style(),
79 ),
80 ]);
81 frame.render_widget(Paragraph::new(title_line), header_chunks[0]);
82
83 let progress_text = format!(
85 "{} {}%",
86 render_progress_bar(app.overall_progress, area.width.saturating_sub(6) as usize),
87 (app.overall_progress * 100.0) as u32
88 );
89 frame.render_widget(
90 Paragraph::new(progress_text).style(theme.progress_bar_style()),
91 header_chunks[2],
92 );
93
94 let stage_info = format!("stage {}/{}", app.current_stage + 1, app.stages.len());
96 frame.render_widget(
97 Paragraph::new(stage_info).style(theme.metric_style()),
98 header_chunks[3],
99 );
100}
101
102fn render_pipeline(frame: &mut Frame, app: &App, theme: &Theme, area: ratatui::layout::Rect) {
104 let mut lines = Vec::new();
105
106 for stage in &app.stages {
107 lines.push(Line::from(""));
109
110 let (icon, style) = match stage.status {
112 StageStatus::Completed => ("✓", theme.completed_style()),
113 StageStatus::Active => ("▸", theme.active_style()),
114 StageStatus::Pending => ("·", theme.pending_style()),
115 };
116
117 let mut spans = vec![
118 Span::styled(icon, style),
119 Span::raw(" "),
120 Span::styled(
121 &stage.name,
122 theme.stage_name_style(stage.status == StageStatus::Active),
123 ),
124 ];
125
126 if let Some(metric) = &stage.metric {
128 let icon_width = 1; let spacing_width = 2; let alignment_offset = 5; let remaining = area.width.saturating_sub(
136 (icon_width
137 + spacing_width
138 + stage.name.chars().count()
139 + metric.chars().count()
140 + alignment_offset) as u16,
141 );
142 spans.push(Span::raw(" ".repeat(remaining as usize)));
143 spans.push(Span::styled(metric, theme.metric_style()));
144 }
145
146 lines.push(Line::from(spans));
147
148 if stage.status == StageStatus::Active && !stage.sub_tasks.is_empty() {
150 for subtask in &stage.sub_tasks {
151 lines.push(render_subtask_line(
152 subtask,
153 app.animation_frame,
154 theme,
155 area.width,
156 ));
157 }
158 }
159 }
160
161 frame.render_widget(Paragraph::new(lines), area);
162}
163
164fn render_pipeline_compact(
166 frame: &mut Frame,
167 app: &App,
168 theme: &Theme,
169 area: ratatui::layout::Rect,
170) {
171 let mut lines = Vec::new();
172
173 for stage in &app.stages {
174 let (icon, style) = match stage.status {
175 StageStatus::Completed => ("✓", theme.completed_style()),
176 StageStatus::Active => ("▸", theme.active_style()),
177 StageStatus::Pending => ("·", theme.pending_style()),
178 };
179
180 let mut spans = vec![
181 Span::styled(icon, style),
182 Span::raw(" "),
183 Span::styled(
184 &stage.name,
185 theme.stage_name_style(stage.status == StageStatus::Active),
186 ),
187 ];
188
189 if let Some(metric) = &stage.metric {
190 spans.push(Span::raw(" "));
191 spans.push(Span::styled(metric, theme.metric_style()));
192 }
193
194 lines.push(Line::from(spans));
195 }
196
197 frame.render_widget(Paragraph::new(lines), area);
198}
199
200fn render_subtask_line(
212 subtask: &SubTask,
213 _frame: usize, theme: &Theme,
215 width: u16,
216) -> Line<'static> {
217 const INDENT: &str = " ";
218 let name_with_indent = format!("{}{}", INDENT, subtask.name);
219
220 match subtask.status {
221 StageStatus::Completed => {
222 let metric = "done";
224 let alignment_offset = 5; let spacing_needed = width.saturating_sub(
226 (name_with_indent.chars().count() + metric.len() + alignment_offset) as u16,
227 ) as usize;
228
229 Line::from(vec![
230 Span::raw(name_with_indent),
231 Span::raw(" ".repeat(spacing_needed)),
232 Span::styled(metric, theme.completed_style()),
233 ])
234 }
235 StageStatus::Active => {
236 if let Some((current, total)) = subtask.progress {
237 let metric = format!("{}/{}", current, total);
239 let alignment_offset = 5; let spacing_needed = width.saturating_sub(
241 (name_with_indent.chars().count() + metric.chars().count() + alignment_offset)
242 as u16,
243 ) as usize;
244
245 Line::from(vec![
246 Span::raw(name_with_indent),
247 Span::raw(" ".repeat(spacing_needed)),
248 Span::styled(metric, theme.metric_style()),
249 ])
250 } else {
251 Line::from(Span::raw(name_with_indent))
253 }
254 }
255 StageStatus::Pending => {
256 Line::from(Span::raw(name_with_indent))
258 }
259 }
260}
261
262fn render_footer(frame: &mut Frame, app: &App, theme: &Theme, area: ratatui::layout::Rect) {
264 let stats = format!(
265 "functions {} │ debt {} │ coverage {:.1}%",
266 format_number(app.functions_count),
267 app.debt_count,
268 app.coverage_percent
269 );
270 frame.render_widget(Paragraph::new(stats).style(theme.metric_style()), area);
271}
272
273fn render_progress_bar(progress: f64, width: usize) -> String {
275 let filled = (progress * width as f64) as usize;
276 let empty = width.saturating_sub(filled);
277
278 format!("{}{}", "▓".repeat(filled), "░".repeat(empty))
279}
280
281fn format_number(n: usize) -> String {
283 n.to_string()
284 .as_bytes()
285 .rchunks(3)
286 .rev()
287 .map(std::str::from_utf8)
288 .collect::<Result<Vec<&str>, _>>()
289 .unwrap()
290 .join(",")
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_progress_bar_rendering() {
299 let bar = render_progress_bar(0.5, 20);
300 assert_eq!(bar.len(), 20 * "▓".len());
301 assert!(bar.contains("▓"));
302 assert!(bar.contains("░"));
303 }
304
305 #[test]
306 fn test_progress_bar_bounds() {
307 let bar_empty = render_progress_bar(0.0, 10);
308 assert_eq!(bar_empty, "░░░░░░░░░░");
309
310 let bar_full = render_progress_bar(1.0, 10);
311 assert_eq!(bar_full, "▓▓▓▓▓▓▓▓▓▓");
312 }
313
314 #[test]
315 fn test_format_number() {
316 assert_eq!(format_number(0), "0");
317 assert_eq!(format_number(123), "123");
318 assert_eq!(format_number(1234), "1,234");
319 assert_eq!(format_number(1234567), "1,234,567");
320 }
321}