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