cli_tutor/ui/
content_pane.rs1use 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
11pub 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
20fn 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 let para = Paragraph::new(lines)
27 .block(
28 Block::default()
29 .borders(Borders::ALL)
30 .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 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 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
89fn 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 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 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 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 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 .scroll((app.examples_scroll, 0));
139
140 frame.render_widget(para, area);
141}