Skip to main content

cli_tutor/ui/
content_pane.rs

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