1mod cache;
17mod frame;
18mod history;
19mod layout;
20mod stats;
21mod streaming;
22mod transcript;
23
24use std::collections::HashMap;
25
26use crate::app::App;
27use crate::conversation::{ConversationId, ConversationItemId};
28use crate::error::RenderError;
29use crate::theme::Theme;
30use crate::view::generation::Generation;
31use crate::view::syntax::SyntaxHighlighter;
32use frame::draw_frame;
33use ratatui::Terminal;
34use ratatui::backend::Backend;
35
36use cache::RenderCache;
37use history::NativeHistoryCursor;
38use layout::FrameLayout;
39use stats::Lap;
40pub use stats::RenderStats;
41use streaming::StreamEntry;
42
43pub struct DrawContext<'a> {
48 pub theme: &'a Theme,
49 pub highlighter: &'a mut SyntaxHighlighter,
50 pub theme_generation: Generation,
51}
52
53pub struct Renderer {
60 theme: Theme,
61 highlighter: SyntaxHighlighter,
62 theme_generation: Option<Generation>,
63 render_cache: RenderCache,
64 native_history: NativeHistoryCursor,
65 stream_cache: HashMap<ConversationItemId, StreamEntry>,
66 stats: RenderStats,
67}
68
69impl Default for Renderer {
70 fn default() -> Self {
71 Self::new()
72 }
73}
74
75impl Renderer {
76 pub fn new() -> Self {
77 Self {
78 theme: Theme::default(),
79 highlighter: SyntaxHighlighter::new(),
80 theme_generation: None,
81 render_cache: RenderCache::default(),
82 native_history: NativeHistoryCursor::default(),
83 stream_cache: HashMap::new(),
84 stats: RenderStats::default(),
85 }
86 }
87
88 pub fn take_stats(&mut self) -> RenderStats {
89 let highlight = self.highlighter.take_stats();
90 RenderStats { highlight, ..std::mem::take(&mut self.stats) }
91 }
92
93 pub fn theme(&self) -> &Theme {
94 &self.theme
95 }
96
97 fn sync_theme(&mut self, app: &App) {
100 if self.theme_generation != Some(app.theme_generation()) {
101 self.theme = app.theme().clone();
102 self.theme_generation = Some(app.theme_generation());
103 self.render_cache.clear();
104 }
105 }
106
107 fn generation(&self) -> Generation {
108 self.theme_generation.unwrap_or_default()
109 }
110
111 pub fn draw<B: Backend>(&mut self, terminal: &mut Terminal<B>, app: &mut App) -> Result<(), RenderError<B::Error>> {
112 self.sync_theme(app);
113 terminal.autoresize().map_err(RenderError::Backend)?;
114 let area = terminal.get_frame().area();
115 self.sync_conversation(app.conversation_id());
116 self.reconcile_history(terminal, app)?;
117
118 let lap = Lap::start();
119 let layout = FrameLayout::new(area, app, self);
120 let capacity = usize::from(layout.transcript_height).saturating_sub(usize::from(layout.progress_height));
121 self.stats.ns_layout += lap.ns();
122
123 let lap = Lap::start();
124 let live = self.commit_overflow(terminal, app, area.width, capacity)?;
125 self.stats.ns_live += lap.ns();
126
127 let live = if app.full_screen_active() { Vec::new() } else { live };
128 let lap = Lap::start();
129 terminal.draw(|frame| draw_frame(frame, app, self, &layout, &live)).map_err(RenderError::Backend)?;
130 self.stats.ns_draw += lap.ns();
131 self.stats.frames += 1;
132 self.render_cache.current = std::mem::take(&mut self.render_cache.frame);
133 Ok(())
134 }
135
136 fn context(&mut self) -> DrawContext<'_> {
138 let theme_generation = self.generation();
139 DrawContext { theme: &self.theme, highlighter: &mut self.highlighter, theme_generation }
140 }
141
142 fn sync_conversation(&mut self, conversation_id: ConversationId) {
143 if self.native_history.conversation_id != Some(conversation_id) {
144 self.native_history =
145 NativeHistoryCursor { conversation_id: Some(conversation_id), ..NativeHistoryCursor::default() };
146 self.render_cache.clear();
147 self.stream_cache.clear();
148 }
149 }
150}