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, 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 layout = MainLayout::split(
35            full,
36            state.sidebar.visible,
37            state.input.line_count(),
38            thinking_lines,
39            delegate_lines,
40        );
41
42        if let Some(sidebar_area) = layout.sidebar {
43            frame.render_widget(Sidebar::new(&state.sidebar), sidebar_area);
44        }
45
46        frame.render_widget(
47            MessageList::new(&mut state.conversation, state.spinner_frame)
48                .with_tool_details(state.tool_details),
49            layout.messages,
50        );
51        if let Some(thinking_area) = layout.thinking {
52            frame.render_widget(
53                ThinkingPanel::new(&state.live_thinking, state.spinner_frame),
54                thinking_area,
55            );
56        }
57        if let Some(delegate_area) = layout.delegate {
58            frame.render_widget(
59                DelegateBar::new(&state.sub_agents, state.spinner_frame),
60                delegate_area,
61            );
62        }
63        frame.render_widget(InputBox::new(&state.input, !state.is_streaming), layout.input);
64        if !state.input.slash_matches.is_empty() {
65            frame.render_widget(SlashPopover::new(&state.input, layout.input), full);
66        }
67        frame.render_widget(
68            Footer {
69                cwd: &state.cwd,
70                model: &state.model_name,
71                mode: &state.permission_mode,
72                cost: state.total_cost,
73                git_branch: state.git_branch.as_deref(),
74                is_streaming: state.is_streaming,
75                spinner_frame: state.spinner_frame,
76            },
77            layout.footer,
78        );
79
80        frame.render_widget(ToastStack::new(&state.toasts), full);
81
82        match &state.modal.active {
83            Some(ModalKind::Permission { tool_name, description, choice }) => {
84                frame.render_widget(
85                    PermissionDialog::new(tool_name, description, *choice),
86                    full,
87                );
88            }
89            Some(ModalKind::Select {
90                title,
91                query,
92                options,
93                selected,
94                current_value,
95                footer_hint,
96                ..
97            }) => {
98                frame.render_widget(
99                    DialogSelect::new(title, query, options, *selected)
100                        .with_current(current_value.as_deref())
101                        .with_footer(footer_hint.as_deref()),
102                    full,
103                );
104            }
105            Some(ModalKind::Info { title, rows }) => {
106                frame.render_widget(InfoDialog::new(title, rows), full);
107            }
108            Some(ModalKind::Input { title, prompt, buffer, .. }) => {
109                frame.render_widget(InputDialog::new(title, prompt, buffer), full);
110            }
111            Some(ModalKind::QuitConfirm) => {}
112            None => {}
113        }
114    }
115}
116
117impl Default for Renderer {
118    fn default() -> Self { Self }
119}