1use crate::api_client::QueryResponse;
2use crate::buffer::SortOrder;
3use crate::buffer::{AppMode, BufferAPI};
4use crate::config::config::Config;
5use crate::sql_highlighter::SqlHighlighter;
6use crate::tui_state::SelectionMode;
7use ratatui::{
8 layout::{Constraint, Rect},
9 style::{Color, Modifier, Style},
10 text::{Line, Text},
11 widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table, TableState},
12 Frame,
13};
14use regex::Regex;
15
16pub struct TuiRenderer {
18 config: Config,
19 sql_highlighter: SqlHighlighter,
20}
21
22impl Default for TuiRenderer {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28pub struct RenderContext<'a> {
30 pub buffer: &'a dyn BufferAPI,
31 pub config: &'a Config,
32 pub table_state: &'a TableState,
33 pub selection_mode: SelectionMode,
34 pub last_yanked: Option<(&'a str, &'a str)>,
35 pub filter_active: bool,
36 pub filter_pattern: &'a str,
37 pub filter_regex: Option<&'a Regex>,
38 pub sort_column: Option<usize>,
39 pub sort_order: SortOrder,
40 pub help_scroll: u16,
41 pub debug_content: &'a str,
42 pub debug_scroll: u16,
43 pub history_matches: &'a [HistoryMatch],
44 pub history_selected: usize,
45 pub jump_input: &'a str,
46 pub input_text: &'a str,
47 pub cursor_token_pos: (usize, usize),
48 pub current_token: Option<&'a str>,
49 pub parser_error: Option<&'a str>,
50}
51
52#[derive(Clone)]
53pub struct HistoryMatch {
54 pub entry: HistoryEntry,
55 pub score: i64,
56 pub indices: Vec<usize>,
57}
58
59#[derive(Clone)]
60pub struct HistoryEntry {
61 pub command: String,
62 pub success: bool,
63 pub timestamp: chrono::DateTime<chrono::Utc>,
64 pub execution_count: usize,
65 pub duration_ms: Option<u64>,
66}
67
68impl TuiRenderer {
69 pub fn new() -> Self {
70 Self {
71 config: Config::load().unwrap_or_default(),
72 sql_highlighter: SqlHighlighter::new(),
73 }
74 }
75 pub fn render_status_line(
77 f: &mut Frame,
78 area: Rect,
79 mode: AppMode,
80 buffer: &dyn BufferAPI,
81 message: &str,
82 ) {
83 let status_text = format!(
87 "[{}] {} | {}",
88 format!("{:?}", mode),
89 buffer.get_status_message(),
90 message
91 );
92
93 let status = Paragraph::new(status_text)
94 .style(Style::default().fg(Color::White).bg(Color::DarkGray))
95 .block(Block::default().borders(Borders::NONE));
96
97 f.render_widget(status, area);
98 }
99
100 pub fn render_table(
102 f: &mut Frame,
103 area: Rect,
104 results: &QueryResponse,
105 _selected_row: Option<usize>,
106 _current_column: usize,
107 _pinned_columns: &[usize],
108 ) {
109 if results.data.is_empty() {
113 let empty_msg = Paragraph::new("No results to display")
114 .style(Style::default().fg(Color::Gray))
115 .block(Block::default().borders(Borders::ALL).title("Results"));
116 f.render_widget(empty_msg, area);
117 return;
118 }
119
120 }
122
123 pub fn render_help(f: &mut Frame, area: Rect, scroll_offset: u16) {
125 let help_text = vec![
126 "=== SQL CLI Help ===",
127 "",
128 "NAVIGATION:",
129 " ↑/↓/←/→ or hjkl - Move cursor",
130 " PgUp/PgDn - Page up/down",
131 " Home/End - Go to start/end",
132 " g/G - Go to first/last row",
133 "",
134 "MODES:",
135 " i - Enter edit mode",
136 " Esc - Exit to command mode",
137 " Enter - Execute query",
138 " Tab - Autocomplete",
139 "",
140 "OPERATIONS:",
141 " / - Search",
142 " Ctrl+F - Filter",
143 " s/S - Sort ascending/descending",
144 " y/Y - Yank cell/row",
145 " Ctrl+E - Export to CSV",
146 " Ctrl+J - Export to JSON",
147 "",
148 "FUNCTION KEYS:",
149 " F1 - This help",
150 " F5 - Debug mode",
151 " F6 - Pretty query",
152 " F7 - History",
153 " F8 - Cache",
154 " F9 - Statistics",
155 "",
156 "Press q or Esc to close help",
157 ];
158
159 let visible_height = area.height.saturating_sub(2) as usize;
160 let start = scroll_offset as usize;
161 let end = (start + visible_height).min(help_text.len());
162
163 let visible_text: Vec<Line> = help_text[start..end]
164 .iter()
165 .map(|&line| Line::from(line))
166 .collect();
167
168 let help_widget = Paragraph::new(visible_text)
169 .block(Block::default().borders(Borders::ALL).title(format!(
170 "Help - Lines {}-{} of {}",
171 start + 1,
172 end,
173 help_text.len()
174 )))
175 .style(Style::default().fg(Color::White));
176
177 f.render_widget(help_widget, area);
178 }
179
180 pub fn render_debug(f: &mut Frame, area: Rect, debug_content: &str, scroll_offset: u16) {
182 let visible_height = area.height.saturating_sub(2) as usize;
183 let lines: Vec<&str> = debug_content.lines().collect();
184 let total_lines = lines.len();
185 let start = scroll_offset as usize;
186 let end = (start + visible_height).min(total_lines);
187
188 let visible_lines: Vec<Line> = lines[start..end]
189 .iter()
190 .map(|&line| Line::from(line.to_string()))
191 .collect();
192
193 let debug_text = Text::from(visible_lines);
194 let has_error = debug_content.contains("ERROR");
195
196 let (border_color, title) = if has_error {
197 (Color::Red, "Debug Info [ERROR]")
198 } else {
199 (Color::Yellow, "Debug Info")
200 };
201
202 let debug_widget = Paragraph::new(debug_text)
203 .block(
204 Block::default()
205 .borders(Borders::ALL)
206 .title(format!(
207 "{} - Lines {}-{} of {}",
208 title,
209 start + 1,
210 end,
211 total_lines
212 ))
213 .border_style(Style::default().fg(border_color)),
214 )
215 .style(Style::default().fg(Color::White));
216
217 f.render_widget(debug_widget, area);
218 }
219
220 pub fn render_history(
222 f: &mut Frame,
223 area: Rect,
224 history_items: &[String],
225 selected_index: usize,
226 ) {
227 let items: Vec<ListItem> = history_items
228 .iter()
229 .enumerate()
230 .map(|(i, item)| {
231 let style = if i == selected_index {
232 Style::default()
233 .fg(Color::Yellow)
234 .add_modifier(Modifier::BOLD)
235 } else {
236 Style::default()
237 };
238 ListItem::new(item.as_str()).style(style)
239 })
240 .collect();
241
242 let history_list = List::new(items)
243 .block(
244 Block::default()
245 .borders(Borders::ALL)
246 .title("Command History"),
247 )
248 .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
249
250 f.render_widget(history_list, area);
251 }
252
253 pub fn render_cache(
255 f: &mut Frame,
256 area: Rect,
257 cache_entries: &[String],
258 selected_index: usize,
259 ) {
260 let items: Vec<ListItem> = cache_entries
261 .iter()
262 .enumerate()
263 .map(|(i, entry)| {
264 let style = if i == selected_index {
265 Style::default()
266 .fg(Color::Green)
267 .add_modifier(Modifier::BOLD)
268 } else {
269 Style::default()
270 };
271 ListItem::new(entry.as_str()).style(style)
272 })
273 .collect();
274
275 let cache_list = List::new(items)
276 .block(Block::default().borders(Borders::ALL).title("Query Cache"))
277 .highlight_style(Style::default().add_modifier(Modifier::REVERSED));
278
279 f.render_widget(cache_list, area);
280 }
281
282 pub fn render_column_stats(f: &mut Frame, area: Rect, stats: &[(String, String)]) {
284 let rows: Vec<Row> = stats
285 .iter()
286 .map(|(name, value)| {
287 Row::new(vec![Cell::from(name.as_str()), Cell::from(value.as_str())])
288 })
289 .collect();
290
291 let table = Table::new(
292 rows,
293 [Constraint::Percentage(50), Constraint::Percentage(50)],
294 )
295 .block(
296 Block::default()
297 .borders(Borders::ALL)
298 .title("Column Statistics"),
299 )
300 .style(Style::default());
301
302 f.render_widget(table, area);
303 }
304}