toon_format/tui/components/
repl_panel.rs

1use ratatui::{
2    layout::{
3        Constraint,
4        Direction,
5        Layout,
6        Margin,
7        Rect,
8    },
9    style::{
10        Color,
11        Modifier,
12        Style,
13    },
14    text::{
15        Line,
16        Span,
17    },
18    widgets::{
19        Block,
20        Borders,
21        Paragraph,
22        Scrollbar,
23        ScrollbarOrientation,
24        ScrollbarState,
25        Wrap,
26    },
27    Frame,
28};
29
30use crate::tui::state::{
31    AppState,
32    ReplLineKind,
33};
34
35pub struct ReplPanel;
36
37impl ReplPanel {
38    pub fn render(f: &mut Frame, area: Rect, app: &mut AppState) {
39        let chunks = Layout::default()
40            .direction(Direction::Vertical)
41            .constraints([Constraint::Min(10), Constraint::Length(3)])
42            .split(area);
43
44        Self::render_output(f, chunks[0], app);
45        Self::render_input(f, chunks[1], app);
46    }
47
48    fn render_output(f: &mut Frame, area: Rect, app: &AppState) {
49        let lines: Vec<Line> = app
50            .repl
51            .output
52            .iter()
53            .skip(app.repl.scroll_offset)
54            .map(|line| {
55                let style = match line.kind {
56                    ReplLineKind::Prompt => Style::default().fg(Color::Cyan),
57                    ReplLineKind::Success => Style::default().fg(Color::Green),
58                    ReplLineKind::Error => Style::default().fg(Color::Red),
59                    ReplLineKind::Info => Style::default().fg(Color::Yellow),
60                };
61                Line::from(Span::styled(&line.content, style))
62            })
63            .collect();
64
65        let block = Block::default()
66            .borders(Borders::ALL)
67            .border_style(Style::default().fg(Color::Cyan))
68            .title(" REPL Session (Ctrl+R to toggle, Esc to close) ");
69
70        let paragraph = Paragraph::new(lines)
71            .block(block)
72            .wrap(Wrap { trim: false });
73
74        f.render_widget(paragraph, area);
75
76        if app.repl.output.len() > (area.height as usize - 2) {
77            let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
78                .begin_symbol(Some("↑"))
79                .end_symbol(Some("↓"));
80
81            let mut scrollbar_state =
82                ScrollbarState::new(app.repl.output.len()).position(app.repl.scroll_offset);
83
84            f.render_stateful_widget(
85                scrollbar,
86                area.inner(Margin {
87                    vertical: 1,
88                    horizontal: 0,
89                }),
90                &mut scrollbar_state,
91            );
92        }
93    }
94
95    fn render_input(f: &mut Frame, area: Rect, app: &AppState) {
96        let prompt = Span::styled(
97            "> ",
98            Style::default()
99                .fg(Color::Cyan)
100                .add_modifier(Modifier::BOLD),
101        );
102
103        let input_text = Span::raw(&app.repl.input);
104        let cursor = Span::styled("█", Style::default().fg(Color::White));
105
106        let line = Line::from(vec![prompt, input_text, cursor]);
107
108        let block = Block::default()
109            .borders(Borders::ALL)
110            .border_style(Style::default().fg(Color::Cyan));
111
112        let paragraph = Paragraph::new(line).block(block);
113
114        f.render_widget(paragraph, area);
115    }
116}