theater_cli/tui/event_explorer/
app.rs1use chrono::{DateTime, Utc};
2use ratatui::widgets::ListState;
3use std::collections::HashMap;
4use theater::chain::ChainEvent;
5
6#[derive(Debug)]
7pub struct EventExplorerApp {
8 pub actor_id: String,
10 pub events: Vec<ChainEvent>,
11 pub filtered_events: Vec<usize>, pub list_state: ListState,
15 pub selected_event_index: Option<usize>,
16 pub should_quit: bool,
17
18 pub detail_mode: DetailMode,
20 pub show_help: bool,
21
22 pub detail_scroll_offset: usize,
24
25 pub active_filters: EventFilters,
27 pub search_query: String,
28 pub filter_input_mode: bool,
29 pub search_input_mode: bool,
30
31 pub page_size: usize,
33 pub current_page: usize,
34
35 pub live_mode: bool,
37 pub follow_mode: bool, pub paused: bool,
39
40 pub event_cache: HashMap<String, ProcessedEventData>,
42}
43
44#[derive(Debug, Clone)]
45pub enum DetailMode {
46 Overview, JsonData, RawData, ChainView, }
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, LeafEvents, WithChildren, OrphanEvents, }
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 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 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 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 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 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 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 }
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 }
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 }
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 }
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}