theater_cli/tui/event_explorer/
app.rs

1use chrono::{DateTime, Utc};
2use ratatui::widgets::ListState;
3use std::collections::HashMap;
4use theater::chain::ChainEvent;
5
6#[derive(Debug)]
7pub struct EventExplorerApp {
8    // Core data
9    pub actor_id: String,
10    pub events: Vec<ChainEvent>,
11    pub filtered_events: Vec<usize>, // Indices into events vec
12
13    // UI state
14    pub list_state: ListState,
15    pub selected_event_index: Option<usize>,
16    pub should_quit: bool,
17
18    // Display modes
19    pub detail_mode: DetailMode,
20    pub show_help: bool,
21
22    // Detail panel scrolling
23    pub detail_scroll_offset: usize,
24
25    // Filtering
26    pub active_filters: EventFilters,
27    pub search_query: String,
28    pub filter_input_mode: bool,
29    pub search_input_mode: bool,
30
31    // Pagination
32    pub page_size: usize,
33    pub current_page: usize,
34
35    // Real-time mode
36    pub live_mode: bool,
37    pub follow_mode: bool, // Auto-scroll to new events
38    pub paused: bool,
39
40    // Performance
41    pub event_cache: HashMap<String, ProcessedEventData>,
42}
43
44#[derive(Debug, Clone)]
45pub enum DetailMode {
46    Overview,  // Basic event info
47    JsonData,  // Pretty-printed JSON data
48    RawData,   // Hex dump of raw bytes
49    ChainView, // Show event in chain context
50}
51
52#[derive(Debug, Clone)]
53pub struct EventFilters {
54    pub event_type: Option<String>,
55    pub time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
56    pub level_filter: Option<EventLevel>,
57    pub has_error: Option<bool>,
58    pub chain_filter: Option<ChainFilter>,
59}
60
61#[derive(Debug, Clone)]
62pub enum ChainFilter {
63    RootEvents,   // Events with no parent
64    LeafEvents,   // Events with no children
65    WithChildren, // Events that have children
66    OrphanEvents, // Events with missing parents
67}
68
69#[derive(Debug, Clone)]
70pub struct ProcessedEventData {
71    pub formatted_data: String,
72    pub data_type: DataType,
73    pub size_bytes: usize,
74    pub is_utf8: bool,
75}
76
77#[derive(Debug, Clone)]
78pub enum DataType {
79    Json,
80    Binary,
81    Text,
82    Empty,
83}
84
85#[derive(Debug, Clone)]
86pub enum EventLevel {
87    Info,
88    Warning,
89    Error,
90}
91
92impl EventExplorerApp {
93    pub fn new(actor_id: String, live_mode: bool, follow_mode: bool) -> Self {
94        Self {
95            actor_id,
96            events: Vec::new(),
97            filtered_events: Vec::new(),
98            list_state: ListState::default(),
99            selected_event_index: None,
100            should_quit: false,
101            detail_mode: DetailMode::Overview,
102            show_help: false,
103            detail_scroll_offset: 0,
104            active_filters: EventFilters::default(),
105            search_query: String::new(),
106            filter_input_mode: false,
107            search_input_mode: false,
108            page_size: 50,
109            current_page: 0,
110            live_mode,
111            follow_mode,
112            paused: false,
113            event_cache: HashMap::new(),
114        }
115    }
116
117    pub fn load_events(&mut self, events: Vec<ChainEvent>) {
118        self.events = events;
119        self.apply_filters();
120        self.update_selection();
121    }
122
123    pub fn add_live_event(&mut self, event: ChainEvent) {
124        if self.live_mode && !self.paused {
125            self.events.push(event);
126            self.apply_filters();
127
128            if self.follow_mode {
129                // Auto-scroll to newest event
130                self.select_last_event();
131            }
132        }
133    }
134
135    pub fn apply_filters(&mut self) {
136        self.filtered_events.clear();
137
138        for (index, event) in self.events.iter().enumerate() {
139            if self.event_matches_filters(event) {
140                self.filtered_events.push(index);
141            }
142        }
143    }
144
145    pub fn event_matches_filters(&self, event: &ChainEvent) -> bool {
146        // Apply all active filters
147        if let Some(ref type_filter) = self.active_filters.event_type {
148            if !event.event_type.contains(type_filter) {
149                return false;
150            }
151        }
152
153        if !self.search_query.is_empty() {
154            let searchable = format!(
155                "{} {}",
156                event.event_type,
157                event.description.as_deref().unwrap_or("")
158            );
159            if !searchable
160                .to_lowercase()
161                .contains(&self.search_query.to_lowercase())
162            {
163                return false;
164            }
165        }
166
167        // Add more filter logic here in future phases
168        true
169    }
170
171    pub fn get_selected_event(&self) -> Option<&ChainEvent> {
172        self.selected_event_index
173            .and_then(|idx| self.filtered_events.get(idx))
174            .and_then(|&event_idx| self.events.get(event_idx))
175    }
176
177    pub fn select_next(&mut self) {
178        if self.filtered_events.is_empty() {
179            return;
180        }
181
182        let next_idx = match self.selected_event_index {
183            Some(idx) => (idx + 1).min(self.filtered_events.len() - 1),
184            None => 0,
185        };
186
187        self.selected_event_index = Some(next_idx);
188        self.list_state.select(Some(next_idx));
189        // Reset detail scroll when selecting different event
190        self.detail_scroll_offset = 0;
191    }
192
193    pub fn select_previous(&mut self) {
194        if self.filtered_events.is_empty() {
195            return;
196        }
197
198        let prev_idx = match self.selected_event_index {
199            Some(idx) => idx.saturating_sub(1),
200            None => 0,
201        };
202
203        self.selected_event_index = Some(prev_idx);
204        self.list_state.select(Some(prev_idx));
205        // Reset detail scroll when selecting different event
206        self.detail_scroll_offset = 0;
207    }
208
209    pub fn page_up(&mut self) {
210        if self.filtered_events.is_empty() {
211            return;
212        }
213
214        let current = self.selected_event_index.unwrap_or(0);
215        let new_idx = current.saturating_sub(self.page_size);
216
217        self.selected_event_index = Some(new_idx);
218        self.list_state.select(Some(new_idx));
219    }
220
221    pub fn page_down(&mut self) {
222        if self.filtered_events.is_empty() {
223            return;
224        }
225
226        let current = self.selected_event_index.unwrap_or(0);
227        let new_idx = (current + self.page_size).min(self.filtered_events.len() - 1);
228
229        self.selected_event_index = Some(new_idx);
230        self.list_state.select(Some(new_idx));
231    }
232
233    pub fn select_last_event(&mut self) {
234        if !self.filtered_events.is_empty() {
235            let last_idx = self.filtered_events.len() - 1;
236            self.selected_event_index = Some(last_idx);
237            self.list_state.select(Some(last_idx));
238        }
239    }
240
241    pub fn cycle_detail_mode(&mut self) {
242        self.detail_mode = match self.detail_mode {
243            DetailMode::Overview => DetailMode::JsonData,
244            DetailMode::JsonData => DetailMode::RawData,
245            DetailMode::RawData => DetailMode::ChainView,
246            DetailMode::ChainView => DetailMode::Overview,
247        };
248        // Reset scroll when changing modes
249        self.detail_scroll_offset = 0;
250    }
251
252    pub fn scroll_detail_up(&mut self) {
253        self.detail_scroll_offset = self.detail_scroll_offset.saturating_sub(1);
254    }
255
256    pub fn scroll_detail_down(&mut self) {
257        self.detail_scroll_offset += 1;
258        // Note: We'll clamp this in the UI based on available content
259    }
260
261    pub fn scroll_detail_page_up(&mut self, page_size: usize) {
262        self.detail_scroll_offset = self.detail_scroll_offset.saturating_sub(page_size);
263    }
264
265    pub fn scroll_detail_page_down(&mut self, page_size: usize) {
266        self.detail_scroll_offset += page_size;
267        // Note: We'll clamp this in the UI based on available content
268    }
269
270    pub fn reset_detail_scroll(&mut self) {
271        self.detail_scroll_offset = 0;
272    }
273
274    pub fn toggle_help(&mut self) {
275        self.show_help = !self.show_help;
276    }
277
278    pub fn toggle_pause(&mut self) {
279        if self.live_mode {
280            self.paused = !self.paused;
281        }
282    }
283
284    pub fn toggle_follow(&mut self) {
285        if self.live_mode {
286            self.follow_mode = !self.follow_mode;
287            if self.follow_mode {
288                self.select_last_event();
289            }
290        }
291    }
292
293    pub fn enter_search_mode(&mut self) {
294        self.search_input_mode = true;
295        // In future phases, this will enable input capture for search
296    }
297
298    pub fn clear_search(&mut self) {
299        self.search_query.clear();
300        self.search_input_mode = false;
301        self.apply_filters();
302    }
303
304    pub fn enter_filter_mode(&mut self) {
305        self.filter_input_mode = true;
306        // In future phases, this will open filter dialog
307    }
308
309    pub fn set_event_type_filter(&mut self, event_type: String) {
310        self.active_filters.event_type = Some(event_type);
311        self.apply_filters();
312    }
313
314    pub fn set_search_query(&mut self, query: String) {
315        self.search_query = query;
316        self.apply_filters();
317    }
318
319    fn update_selection(&mut self) {
320        if self.filtered_events.is_empty() {
321            self.selected_event_index = None;
322            self.list_state.select(None);
323        } else if self.selected_event_index.is_none() {
324            self.selected_event_index = Some(0);
325            self.list_state.select(Some(0));
326        }
327    }
328
329    pub fn quit(&mut self) {
330        self.should_quit = true;
331    }
332}
333
334impl Default for EventFilters {
335    fn default() -> Self {
336        Self {
337            event_type: None,
338            time_range: None,
339            level_filter: None,
340            has_error: None,
341            chain_filter: None,
342        }
343    }
344}
345
346impl DetailMode {
347    pub fn display_name(&self) -> &'static str {
348        match self {
349            DetailMode::Overview => "Overview",
350            DetailMode::JsonData => "Data",
351            DetailMode::RawData => "Raw",
352            DetailMode::ChainView => "Chain",
353        }
354    }
355
356    pub fn next_mode_name(&self) -> &'static str {
357        match self {
358            DetailMode::Overview => "Data",
359            DetailMode::JsonData => "Raw",
360            DetailMode::RawData => "Chain",
361            DetailMode::ChainView => "Overview",
362        }
363    }
364}