sql_cli/ui/rendering/
tui_renderer.rs

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
16/// Handles all rendering operations for the TUI
17pub 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
28/// Context needed for rendering various TUI components
29pub 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    #[must_use]
70    pub fn new() -> Self {
71        Self {
72            _config: Config::load().unwrap_or_default(),
73            _sql_highlighter: SqlHighlighter::new(),
74        }
75    }
76    /// Render the main status line at the bottom of the screen
77    pub fn render_status_line(
78        f: &mut Frame,
79        area: Rect,
80        mode: AppMode,
81        buffer: &dyn BufferAPI,
82        message: &str,
83    ) {
84        // This will contain the status line rendering logic
85        // Extracted from enhanced_tui.rs render_status_line method
86
87        let status_text = format!(
88            "[{}] {} | {}",
89            format!("{:?}", mode),
90            buffer.get_status_message(),
91            message
92        );
93
94        let status = Paragraph::new(status_text)
95            .style(Style::default().fg(Color::White).bg(Color::DarkGray))
96            .block(Block::default().borders(Borders::NONE));
97
98        f.render_widget(status, area);
99    }
100
101    /// Render the results table
102    pub fn render_table(
103        f: &mut Frame,
104        area: Rect,
105        results: &QueryResponse,
106        _selected_row: Option<usize>,
107        _current_column: usize,
108        _pinned_columns: &[usize],
109    ) {
110        // This will contain the table rendering logic
111        // Extracted from enhanced_tui.rs render_table_immutable method
112
113        if results.data.is_empty() {
114            let empty_msg = Paragraph::new("No results to display")
115                .style(Style::default().fg(Color::Gray))
116                .block(Block::default().borders(Borders::ALL).title("Results"));
117            f.render_widget(empty_msg, area);
118        }
119
120        // Table rendering logic here...
121    }
122
123    /// Render the help screen
124    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    /// Render the debug view
181    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    /// Render the history view
221    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    /// Render the cache view
254    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    /// Render column statistics
283    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}