Skip to main content

cai_tui/
app.rs

1//! TUI Application state and logic
2
3use cai_core::Entry;
4use cai_storage::Storage;
5use ratatui::style::Color;
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// Application mode
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Mode {
12    /// Normal mode - viewing results
13    Normal,
14    /// Query input mode
15    Query,
16    /// Search mode
17    Search,
18    /// Detail view - showing selected entry
19    Detail,
20    /// Help screen
21    Help,
22}
23
24/// Application state
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum AppState {
27    /// Running normally
28    Running,
29    /// Should quit
30    Quitting,
31}
32
33/// Sort order
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SortOrder {
36    /// Ascending
37    Asc,
38    /// Descending
39    Desc,
40}
41
42/// Sortable column
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Column {
45    /// Timestamp column
46    Timestamp,
47    /// Source column
48    Source,
49    /// Prompt column
50    Prompt,
51}
52
53/// Main TUI application
54pub struct App<S>
55where
56    S: Storage + ?Sized,
57{
58    /// Storage backend
59    storage: Arc<S>,
60    /// Current application mode
61    pub mode: Mode,
62    /// Application state
63    pub state: AppState,
64    /// Current query input
65    pub query_input: String,
66    /// Current search input
67    pub search_input: String,
68    /// Query results
69    pub entries: Vec<Entry>,
70    /// Selected entry index
71    pub selected: usize,
72    /// Scroll offset
73    pub scroll: usize,
74    /// Current sort column
75    pub sort_column: Column,
76    /// Current sort order
77    pub sort_order: SortOrder,
78    /// Status message
79    pub status_message: String,
80    /// Status message color
81    pub status_color: Color,
82    /// Status message timestamp (for auto-clear)
83    pub status_timestamp: u64,
84    /// Query history
85    pub history: Vec<String>,
86    /// History index (for navigation)
87    pub history_index: Option<usize>,
88    /// Detail view scroll offset
89    pub detail_scroll: usize,
90    /// Help scroll offset
91    pub help_scroll: usize,
92}
93
94impl<S> App<S>
95where
96    S: Storage,
97{
98    /// Create a new application
99    pub fn new(storage: Arc<S>) -> Self {
100        Self {
101            storage,
102            mode: Mode::Normal,
103            state: AppState::Running,
104            query_input: String::new(),
105            search_input: String::new(),
106            entries: Vec::new(),
107            selected: 0,
108            scroll: 0,
109            sort_column: Column::Timestamp,
110            sort_order: SortOrder::Desc,
111            status_message: "Press '/' to search, 'i' to enter query mode, 'q' to quit".to_string(),
112            status_color: Color::Gray,
113            status_timestamp: now(),
114            history: Vec::new(),
115            history_index: None,
116            detail_scroll: 0,
117            help_scroll: 0,
118        }
119    }
120
121    /// Execute a query and update entries
122    pub async fn execute_query(&mut self, query: &str) {
123        // Add to history
124        if !query.is_empty() {
125            self.history.push(query.to_string());
126            self.history_index = None;
127        }
128
129        // Parse and execute query (simplified - actual SQL parsing would be in cai-query)
130        // For now, just get all entries
131        match self.storage.query(None).await {
132            Ok(entries) => {
133                self.entries = entries;
134                self.selected = 0;
135                self.scroll = 0;
136                self.sort_entries();
137                self.set_status(
138                    format!("Query returned {} results", self.entries.len()),
139                    Color::Green,
140                );
141            }
142            Err(e) => {
143                self.set_status(format!("Query error: {}", e), Color::Red);
144            }
145        }
146    }
147
148    /// Search entries
149    pub fn search(&mut self) {
150        if self.search_input.is_empty() {
151            return;
152        }
153
154        let query = self.search_input.to_lowercase();
155        self.entries.retain(|entry| {
156            entry.prompt.to_lowercase().contains(&query)
157                || entry.response.to_lowercase().contains(&query)
158                || format!("{:?}", entry.source)
159                    .to_lowercase()
160                    .contains(&query)
161        });
162
163        self.selected = 0;
164        self.scroll = 0;
165        self.set_status(
166            format!("Found {} results", self.entries.len()),
167            Color::Green,
168        );
169    }
170
171    /// Clear search and reload all entries
172    pub async fn clear_search(&mut self) {
173        self.search_input.clear();
174        self.execute_query("").await;
175    }
176
177    /// Set status message
178    pub fn set_status(&mut self, msg: String, color: Color) {
179        self.status_message = msg;
180        self.status_color = color;
181        self.status_timestamp = now();
182    }
183
184    /// Check if status message should be cleared (after 5 seconds)
185    pub fn should_clear_status(&self) -> bool {
186        now() - self.status_timestamp > 5
187    }
188
189    /// Clear status message to default
190    pub fn reset_status(&mut self) {
191        self.status_message =
192            "Press '/' to search, 'i' to enter query mode, 'q' to quit".to_string();
193        self.status_color = Color::Gray;
194    }
195
196    /// Select previous entry
197    pub fn select_previous(&mut self) {
198        if !self.entries.is_empty() && self.selected > 0 {
199            self.selected -= 1;
200            if self.selected < self.scroll {
201                self.scroll = self.selected;
202            }
203        }
204    }
205
206    /// Select next entry
207    pub fn select_next(&mut self, height: usize) {
208        if !self.entries.is_empty() {
209            self.selected = self.selected.saturating_add(1).min(self.entries.len() - 1);
210            // Calculate visible area height (minus header and padding)
211            let visible_height = height.saturating_sub(4);
212            if self.selected >= self.scroll + visible_height && visible_height > 0 {
213                self.scroll = self.selected - visible_height + 1;
214            }
215        }
216    }
217
218    /// Sort entries by current column
219    pub fn sort_entries(&mut self) {
220        match self.sort_column {
221            Column::Timestamp => {
222                self.entries.sort_by(|a, b| {
223                    if self.sort_order == SortOrder::Asc {
224                        a.timestamp.cmp(&b.timestamp)
225                    } else {
226                        b.timestamp.cmp(&a.timestamp)
227                    }
228                });
229            }
230            Column::Source => {
231                self.entries.sort_by(|a, b| {
232                    let source_cmp = format!("{:?}", a.source).cmp(&format!("{:?}", b.source));
233                    if self.sort_order == SortOrder::Asc {
234                        source_cmp
235                    } else {
236                        source_cmp.reverse()
237                    }
238                });
239            }
240            Column::Prompt => {
241                self.entries.sort_by(|a, b| {
242                    let prompt_cmp = a.prompt.cmp(&b.prompt);
243                    if self.sort_order == SortOrder::Asc {
244                        prompt_cmp
245                    } else {
246                        prompt_cmp.reverse()
247                    }
248                });
249            }
250        }
251    }
252
253    /// Toggle sort order for current column
254    pub fn toggle_sort(&mut self, column: Column) {
255        if self.sort_column == column {
256            self.sort_order = match self.sort_order {
257                SortOrder::Asc => SortOrder::Desc,
258                SortOrder::Desc => SortOrder::Asc,
259            };
260        } else {
261            self.sort_column = column;
262            self.sort_order = SortOrder::Asc;
263        }
264        self.sort_entries();
265    }
266
267    /// Navigate history backwards
268    pub fn history_previous(&mut self) {
269        if self.history.is_empty() {
270            return;
271        }
272
273        match self.history_index {
274            None => {
275                self.history_index = Some(self.history.len() - 1);
276            }
277            Some(idx) if idx > 0 => {
278                self.history_index = Some(idx - 1);
279            }
280            _ => {}
281        }
282
283        if let Some(idx) = self.history_index {
284            self.query_input = self.history[idx].clone();
285        }
286    }
287
288    /// Navigate history forwards
289    pub fn history_next(&mut self) {
290        if self.history.is_empty() {
291            return;
292        }
293
294        match self.history_index {
295            Some(idx) if idx < self.history.len() - 1 => {
296                self.history_index = Some(idx + 1);
297                if let Some(idx) = self.history_index {
298                    self.query_input = self.history[idx].clone();
299                }
300            }
301            Some(_) => {
302                self.history_index = None;
303                self.query_input.clear();
304            }
305            None => {}
306        }
307    }
308
309    /// Get selected entry
310    pub fn selected_entry(&self) -> Option<&Entry> {
311        self.entries.get(self.selected)
312    }
313
314    /// Get row style based on selection
315    pub fn row_style(&self, index: usize) -> ratatui::style::Style {
316        use ratatui::style::Style;
317        if index == self.selected {
318            Style::default().bg(ratatui::style::Color::DarkGray)
319        } else {
320            Style::default()
321        }
322    }
323
324    /// Scroll detail view down
325    pub fn detail_scroll_down(&mut self) {
326        self.detail_scroll = self.detail_scroll.saturating_add(1);
327    }
328
329    /// Scroll detail view up
330    pub fn detail_scroll_up(&mut self) {
331        self.detail_scroll = self.detail_scroll.saturating_sub(1);
332    }
333
334    /// Reset detail scroll
335    pub fn detail_scroll_reset(&mut self) {
336        self.detail_scroll = 0;
337    }
338
339    /// Scroll help view down
340    pub fn help_scroll_down(&mut self) {
341        self.help_scroll = self.help_scroll.saturating_add(1);
342    }
343
344    /// Scroll help view up
345    pub fn help_scroll_up(&mut self) {
346        self.help_scroll = self.help_scroll.saturating_sub(1);
347    }
348}
349
350/// Get current timestamp in seconds
351fn now() -> u64 {
352    SystemTime::now()
353        .duration_since(UNIX_EPOCH)
354        .unwrap_or_default()
355        .as_secs()
356}