Skip to main content

cli_tutor/ui/
content_pane.rs

1use crate::app::{App, ContentView};
2use crate::ui::{exercise_view, free_practice_view};
3use ratatui::{
4    layout::Rect,
5    style::{Color, Modifier, Style},
6    text::{Line, Span},
7    widgets::{Block, Borders, Paragraph, Wrap},
8    Frame,
9};
10
11// content_display.ENG.2 — each view in its own file
12pub fn render(app: &App, frame: &mut Frame, area: Rect) {
13    match app.current_view {
14        ContentView::Intro => render_intro(app, frame, area),
15        ContentView::Examples => render_examples(app, frame, area),
16        ContentView::Exercise => exercise_view::render(app, frame, area),
17        // free_practice.VIEW.1
18        ContentView::FreePractice => free_practice_view::render(app, frame, area),
19    }
20}
21
22// content_display.INTRO_VIEW
23fn render_intro(app: &App, frame: &mut Frame, area: Rect) {
24    let text = &app.current_module().intro.text;
25    let lines = render_intro_text(text);
26
27    // content_display.INTRO_VIEW.1 — scrollable paragraph
28    let para = Paragraph::new(lines)
29        .block(
30            Block::default()
31                .borders(Borders::ALL)
32                // content_display.INTRO_VIEW.4
33                .title("Introduction"),
34        )
35        .wrap(Wrap { trim: false })
36        .scroll((app.intro_scroll, 0));
37
38    frame.render_widget(para, area);
39}
40
41fn render_intro_text(text: &str) -> Vec<Line<'static>> {
42    text.lines()
43        .map(|line| {
44            let owned = line.to_string();
45            // content_display.INTRO_VIEW.3 — headings bold + underlined
46            if owned.starts_with("## ") {
47                return Line::from(Span::styled(
48                    owned,
49                    Style::default()
50                        .add_modifier(Modifier::BOLD)
51                        .add_modifier(Modifier::UNDERLINED),
52                ));
53            }
54            // content_display.INTRO_VIEW.2 — inline code with distinct style
55            if owned.contains('`') {
56                return Line::from(render_inline_code(owned));
57            }
58            Line::from(Span::raw(owned))
59        })
60        .collect()
61}
62
63fn render_inline_code(line: String) -> Vec<Span<'static>> {
64    let mut spans = Vec::new();
65    let mut remaining = line.as_str();
66    let mut in_code = false;
67
68    while let Some(pos) = remaining.find('`') {
69        let before = remaining[..pos].to_string();
70        if !before.is_empty() {
71            spans.push(if in_code {
72                Span::styled(
73                    before,
74                    Style::default()
75                        .fg(Color::Cyan)
76                        .add_modifier(Modifier::BOLD),
77                )
78            } else {
79                Span::raw(before)
80            });
81        }
82        in_code = !in_code;
83        remaining = &remaining[pos + 1..];
84    }
85    if !remaining.is_empty() {
86        spans.push(Span::raw(remaining.to_string()));
87    }
88    spans
89}
90
91// content_display.EXAMPLES_VIEW
92fn render_examples(app: &App, frame: &mut Frame, area: Rect) {
93    let module = app.current_module();
94    let count = module.examples.len();
95
96    let mut lines: Vec<Line<'static>> = Vec::new();
97
98    for (i, ex) in module.examples.iter().enumerate() {
99        // content_display.EXAMPLES_VIEW.1 — titled block
100        lines.push(Line::from(Span::styled(
101            ex.title.clone(),
102            Style::default().add_modifier(Modifier::BOLD),
103        )));
104        if !ex.description.is_empty() {
105            lines.push(Line::from(Span::raw(ex.description.clone())));
106        }
107        // content_display.EXAMPLES_VIEW.2,3,4 — command with $ prefix, bold
108        lines.push(Line::from(vec![
109            Span::styled("$ ", Style::default().fg(Color::Green)),
110            Span::styled(
111                ex.command.clone(),
112                Style::default()
113                    .fg(Color::Cyan)
114                    .add_modifier(Modifier::BOLD),
115            ),
116        ]));
117        for out_line in ex.output.lines() {
118            lines.push(Line::from(Span::styled(
119                out_line.to_string(),
120                Style::default().fg(Color::DarkGray),
121            )));
122        }
123        // content_display.EXAMPLES_VIEW.5 — divider between examples
124        if i + 1 < count {
125            lines.push(Line::from(""));
126            lines.push(Line::from(Span::styled(
127                "─".repeat(40),
128                Style::default().fg(Color::DarkGray),
129            )));
130            lines.push(Line::from(""));
131        }
132    }
133
134    // content_display.EXAMPLES_VIEW.7 — count in title
135    let title = format!("Examples ({})", count);
136    let para = Paragraph::new(lines)
137        .block(Block::default().borders(Borders::ALL).title(title))
138        .wrap(Wrap { trim: false })
139        // content_display.EXAMPLES_VIEW.6 — scrollable list
140        .scroll((app.examples_scroll, 0));
141
142    frame.render_widget(para, area);
143}