Skip to main content

stynx_code_tui/render/
renderer.rs

1use ratatui::Frame;
2use ratatui::style::Style;
3use ratatui::widgets::Block;
4
5use crate::layout::MainLayout;
6use crate::state::{AppState, ModalKind};
7use crate::theme;
8use crate::widgets::delegate_bar::DelegateBar;
9use crate::widgets::{DialogSelect, Footer, InfoDialog, InputBox, InputDialog, MessageList, PermissionDialog, Sidebar, SlashPopover, SummaryBar, ThinkingPanel, ToastStack};
10
11pub struct Renderer;
12
13impl Renderer {
14    pub fn new() -> Self { Self }
15
16    pub fn draw(frame: &mut Frame, state: &mut AppState) {
17        let full = frame.area();
18        frame.render_widget(
19            Block::default().style(Style::default().bg(theme::BACKGROUND())),
20            full,
21        );
22
23        let thinking_lines = if state.is_streaming && !state.live_thinking.trim().is_empty() {
24            state
25                .live_thinking
26                .lines()
27                .filter(|l| !l.trim().is_empty())
28                .count()
29        } else {
30            0
31        };
32
33        let delegate_lines = state.sub_agents.len();
34        let has_summary = state.last_summary.is_some();
35        let layout = MainLayout::split(
36            full,
37            state.sidebar.visible,
38            state.input.line_count(),
39            thinking_lines,
40            delegate_lines,
41            has_summary,
42        );
43
44        if let Some(sidebar_area) = layout.sidebar {
45            frame.render_widget(Sidebar::new(&state.sidebar), sidebar_area);
46        }
47
48        frame.render_widget(
49            MessageList::new(&mut state.conversation, state.spinner_frame)
50                .with_tool_details(state.tool_details),
51            layout.messages,
52        );
53        if let Some(thinking_area) = layout.thinking {
54            frame.render_widget(
55                ThinkingPanel::new(&state.live_thinking, state.spinner_frame),
56                thinking_area,
57            );
58        }
59        if let Some(delegate_area) = layout.delegate {
60            frame.render_widget(
61                DelegateBar::new(&state.sub_agents, state.spinner_frame),
62                delegate_area,
63            );
64        }
65        if let (Some(summary_area), Some(text)) = (layout.summary, state.last_summary.as_deref()) {
66            frame.render_widget(SummaryBar::new(text), summary_area);
67        }
68        frame.render_widget(InputBox::new(&state.input, !state.is_streaming), layout.input);
69        if !state.input.slash_matches.is_empty() {
70            frame.render_widget(SlashPopover::new(&state.input, layout.input), full);
71        }
72        frame.render_widget(
73            Footer {
74                cwd: &state.cwd,
75                model: &state.model_name,
76                mode: &state.permission_mode,
77                cost: state.total_cost,
78                git_branch: state.git_branch.as_deref(),
79                is_streaming: state.is_streaming,
80                spinner_frame: state.spinner_frame,
81            },
82            layout.footer,
83        );
84
85        frame.render_widget(ToastStack::new(&state.toasts), full);
86
87        match &state.modal.active {
88            Some(ModalKind::Permission { tool_name, description, choice }) => {
89                frame.render_widget(
90                    PermissionDialog::new(tool_name, description, *choice),
91                    full,
92                );
93            }
94            Some(ModalKind::Select {
95                title,
96                query,
97                options,
98                selected,
99                current_value,
100                footer_hint,
101                ..
102            }) => {
103                frame.render_widget(
104                    DialogSelect::new(title, query, options, *selected)
105                        .with_current(current_value.as_deref())
106                        .with_footer(footer_hint.as_deref()),
107                    full,
108                );
109            }
110            Some(ModalKind::Info { title, rows }) => {
111                frame.render_widget(InfoDialog::new(title, rows), full);
112            }
113            Some(ModalKind::Input { title, prompt, buffer, .. }) => {
114                frame.render_widget(InputDialog::new(title, prompt, buffer), full);
115            }
116            Some(ModalKind::QuitConfirm) => {}
117            None => {}
118        }
119    }
120}
121
122impl Default for Renderer {
123    fn default() -> Self { Self }
124}