stynx_code_tui/render/
renderer.rs1use 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_rows, HistoryRow, 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 rows = flat_rows(state);
92 if let Some(row) = rows.get(idx) {
93 let (mi, ti) = match row {
94 HistoryRow::Tool { msg, tool } => (*msg, *tool),
95 HistoryRow::Sub { msg, tool, .. } => (*msg, *tool),
96 };
97 let tool = &state.conversation.messages[mi].tool_uses[ti];
98 frame.render_widget(ToolDetail::new(tool), full);
99 }
100 }
101 }
102
103 match &state.modal.active {
104 Some(ModalKind::Permission { tool_name, description, choice }) => {
105 frame.render_widget(
106 PermissionDialog::new(tool_name, description, *choice),
107 full,
108 );
109 }
110 Some(ModalKind::Select {
111 title,
112 query,
113 options,
114 selected,
115 current_value,
116 footer_hint,
117 ..
118 }) => {
119 frame.render_widget(
120 DialogSelect::new(title, query, options, *selected)
121 .with_current(current_value.as_deref())
122 .with_footer(footer_hint.as_deref()),
123 full,
124 );
125 }
126 Some(ModalKind::Info { title, rows }) => {
127 frame.render_widget(InfoDialog::new(title, rows), full);
128 }
129 Some(ModalKind::Input { title, prompt, buffer, .. }) => {
130 frame.render_widget(InputDialog::new(title, prompt, buffer), full);
131 }
132 Some(ModalKind::QuitConfirm) => {}
133 None => {}
134 }
135 }
136}
137
138impl Default for Renderer {
139 fn default() -> Self { Self }
140}