cli_tutor/ui/
content_pane.rs1use 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
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 ContentView::FreePractice => free_practice_view::render(app, frame, area),
19 }
20}
21
22fn 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 let para = Paragraph::new(lines)
29 .block(
30 Block::default()
31 .borders(Borders::ALL)
32 .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 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 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
91fn 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 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 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 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 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 .scroll((app.examples_scroll, 0));
141
142 frame.render_widget(para, area);
143}