terminal_ui/
app.rs

1use anyhow::Result;
2use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3use log_analyzer::models::filter::FilterAction;
4use log_analyzer::models::log_line_styled::LogLineStyled;
5use log_analyzer::models::{filter::Filter, log_line::LogLine};
6use log_analyzer::services::log_service::{Event as LogEvent, LogAnalyzer};
7use tui::style::Color;
8
9use std::sync::Arc;
10
11use tui_input::backend::crossterm as input_backend;
12use tui_input::Input;
13
14use crate::data::lazy_stateful_table::{LazySource, LazyStatefulTable, CAPACITY};
15use crate::data::stateful_list::StatefulList;
16use crate::data::stateful_table::StatefulTable;
17use crate::data::Stateful;
18
19/* ------ NEW SOURCE INDEXES ------- */
20pub const INDEX_SOURCE_TYPE: usize = 0;
21pub const INDEX_SOURCE_PATH: usize = INDEX_SOURCE_TYPE + 1;
22pub const INDEX_SOURCE_FORMAT: usize = INDEX_SOURCE_PATH + 1;
23pub const INDEX_SOURCE_NEW_FORMAT_ALIAS: usize = INDEX_SOURCE_FORMAT + 1;
24pub const INDEX_SOURCE_NEW_FORMAT_REGEX: usize = INDEX_SOURCE_NEW_FORMAT_ALIAS + 1;
25pub const INDEX_SOURCE_OK_BUTTON: usize = INDEX_SOURCE_NEW_FORMAT_REGEX + 1;
26/* ------ FILTER INDEXES ------- */
27pub const INDEX_FILTER_NAME: usize = INDEX_SOURCE_OK_BUTTON + 1;
28pub const INDEX_FILTER_TYPE: usize = INDEX_FILTER_NAME + 1;
29pub const INDEX_FILTER_LOG: usize = INDEX_FILTER_TYPE + 1;
30pub const INDEX_FILTER_DATETIME: usize = INDEX_FILTER_LOG + 1;
31pub const INDEX_FILTER_TIMESTAMP: usize = INDEX_FILTER_DATETIME + 1;
32pub const INDEX_FILTER_APP: usize = INDEX_FILTER_TIMESTAMP + 1;
33pub const INDEX_FILTER_SEVERITY: usize = INDEX_FILTER_APP + 1;
34pub const INDEX_FILTER_FUNCTION: usize = INDEX_FILTER_SEVERITY + 1;
35pub const INDEX_FILTER_PAYLOAD: usize = INDEX_FILTER_FUNCTION + 1;
36pub const INDEX_FILTER_RED_COLOR: usize = INDEX_FILTER_PAYLOAD + 1;
37pub const INDEX_FILTER_GREEN_COLOR: usize = INDEX_FILTER_RED_COLOR + 1;
38pub const INDEX_FILTER_BLUE_COLOR: usize = INDEX_FILTER_GREEN_COLOR + 1;
39pub const INDEX_FILTER_OK_BUTTON: usize = INDEX_FILTER_BLUE_COLOR + 1;
40/* ------ SEARCH INDEXES ------- */
41pub const INDEX_SEARCH: usize = INDEX_FILTER_OK_BUTTON + 1;
42/* ------ NAVIGATION INDEXES ------- */
43pub const INDEX_NAVIGATION: usize = INDEX_SEARCH + 1;
44/* ----------------------------------- */
45pub const INDEX_MAX: usize = INDEX_NAVIGATION + 1;
46/* ----------------------------------- */
47
48pub struct PopupInteraction {
49    pub response: bool,
50    pub message: String,
51    pub calling_module: Module,
52}
53
54pub struct Processing {
55    pub is_processing: bool,
56    pub focus_on: usize,
57}
58
59impl Processing {
60    fn set_focus(&mut self, focus: Option<usize>) {
61        self.focus_on = match focus {
62            Some(focus) => focus,
63            None => 0,
64        }
65    }
66}
67
68impl Default for Processing {
69    fn default() -> Self {
70        Self {
71            is_processing: false,
72            focus_on: 0,
73        }
74    }
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum Module {
79    Sources,
80    Filters,
81    Logs,
82    Search,
83    SearchResult,
84    SourcePopup,
85    FilterPopup,
86    NavigationPopup,
87    ErrorPopup,
88    None,
89}
90
91struct LogSourcer {
92    log_analyzer: Box<Arc<dyn LogAnalyzer>>,
93}
94
95impl LazySource<LogLine> for LogSourcer {
96    fn source(&self, from: usize, to: usize) -> Vec<LogLine> {
97        self.log_analyzer.get_log_lines(from, to)
98    }
99
100    fn source_elements_containing(
101        &self,
102        index: usize,
103        quantity: usize,
104    ) -> (Vec<LogLine>, usize, usize) {
105        self.log_analyzer.get_log_lines_containing(index, quantity)
106    }
107}
108struct SearchSourcer {
109    log_analyzer: Box<Arc<dyn LogAnalyzer>>,
110}
111
112impl LazySource<LogLineStyled> for SearchSourcer {
113    fn source(&self, from: usize, to: usize) -> Vec<LogLineStyled> {
114        self.log_analyzer.get_search_lines(from, to)
115    }
116
117    fn source_elements_containing(
118        &self,
119        index: usize,
120        quantity: usize,
121    ) -> (Vec<LogLineStyled>, usize, usize) {
122        self.log_analyzer
123            .get_search_lines_containing(index, quantity)
124    }
125}
126
127/// This struct holds the current state of the app. In particular, it has the `items` field which is a wrapper
128/// around `ListState`. Keeping track of the items state let us render the associated widget with its state
129/// and have access to features such as natural scrolling.
130pub struct App {
131    /// Api to the backend processor
132    pub log_analyzer: Box<Arc<dyn LogAnalyzer>>,
133
134    /// Primary color
135    pub color: Color,
136
137    /// Currently selected module. Used to manage inputs and highlight focus
138    pub selected_module: Module,
139
140    /// Display the new source popup
141    pub show_source_popup: bool,
142    /// Display the new filter popup
143    pub show_filter_popup: bool,
144    /// Display an error message
145    pub show_error_message: bool,
146    /// Display the navigation popup
147    pub show_navigation_popup: bool,
148    /// Display the navigation popup
149    pub show_log_options_popup: bool,
150
151    /// Vector of user input. Entries are uniquely assigned to each UI input, and the selection is
152    /// performed with the `input_buffer_index`
153    pub input_buffers: Vec<Input>,
154    /// Currently selected input buffer
155    pub input_buffer_index: usize,
156    /// Stateful list of all the current formats to be displayed in the source popup
157    pub formats: StatefulList<String>,
158
159    /// Tab selector index for Source Type
160    pub source_type: usize,
161    /// Tab selector index for Filter Type
162    pub filter_type: usize,
163    /// Tab selector index for Filter Type
164    pub filter_color: usize,
165
166    // Display all log sources in the sources panel
167    pub sources: StatefulTable<(bool, String, Option<String>)>,
168    // Display all filters in the filters panel
169    pub filters: StatefulTable<(bool, String)>,
170
171    /// Lazy widget for the main view of the logs
172    pub log_lines: LazyStatefulTable<LogLine>,
173    /// Lazy widget for the main view of the search
174    pub search_lines: LazyStatefulTable<LogLineStyled>,
175    /// Apply an offset to the logs to simulate horizontal scrolling
176    pub horizontal_offset: usize,
177
178    /// Resizing of the side_menu to the main view
179    pub side_main_size_percentage: u16,
180    /// Resizing on the side_menu between sources and filters
181    pub log_filter_size_percentage: u16,
182    /// Resizing on the main view between logs and searchs
183    pub log_search_size_percentage: u16,
184
185    /// Active log columns to display in the log and the search
186    pub log_columns: Vec<(String, bool)>,
187
188    /// Auto scroll to the last receive elements. Used for live logs
189    pub auto_scroll: bool,
190
191    /// Manage the popup interaction
192    pub popup: PopupInteraction,
193    /// Manage the processing popup
194    pub processing: Processing,
195    /// Receive state events from the backed to kwow when it's busy or when new elements are available
196    event_receiver: tokio::sync::broadcast::Receiver<LogEvent>,
197}
198
199impl App {
200    pub async fn new(log_analyzer: Box<Arc<dyn LogAnalyzer>>, primary_color: Color) -> App {
201        let mut formats = vec!["New".to_string()];
202        formats.extend(
203            log_analyzer
204                .get_formats()
205                .into_iter()
206                .map(|format| format.alias),
207        );
208
209        let sources = log_analyzer.get_logs();
210        let filters = log_analyzer
211            .get_filters()
212            .iter()
213            .map(|(enabled, filter)| (*enabled, filter.alias.clone()))
214            .collect();
215
216        let log_sourcer = LogSourcer {
217            log_analyzer: log_analyzer.clone(),
218        };
219        let search_sourcer = SearchSourcer {
220            log_analyzer: log_analyzer.clone(),
221        };
222
223        let event_receiver = log_analyzer.on_event();
224
225        App {
226            log_analyzer,
227            color: primary_color,
228            selected_module: Module::Sources,
229            show_source_popup: false,
230            show_filter_popup: false,
231            show_navigation_popup: false,
232            show_error_message: false,
233            show_log_options_popup: false,
234
235            input_buffers: vec![Input::default(); INDEX_MAX],
236            input_buffer_index: 0,
237
238            formats: StatefulList::with_items(formats),
239
240            source_type: 0,
241            filter_type: 0,
242            filter_color: 0,
243
244            sources: StatefulTable::with_items(sources),
245            filters: StatefulTable::with_items(filters),
246
247            log_lines: LazyStatefulTable::new(Box::new(log_sourcer)),
248            search_lines: LazyStatefulTable::new(Box::new(search_sourcer)),
249            horizontal_offset: 0,
250            log_filter_size_percentage: 50,
251            log_search_size_percentage: 75,
252            side_main_size_percentage: 25,
253            log_columns: LogLine::columns()
254                .into_iter()
255                .map(|column| (column, true))
256                .collect(),
257            auto_scroll: false,
258
259            popup: PopupInteraction {
260                response: true,
261                calling_module: Module::None,
262                message: String::new(),
263            },
264            processing: Processing::default(),
265            event_receiver,
266        }
267    }
268
269    pub async fn add_log(&mut self) -> Result<()> {
270        let selected_format_index = self.formats.state.selected().unwrap(); // There is always one item selected
271
272        let alias = match selected_format_index {
273            0 /* NEW */ => {
274                let alias = self.input_buffers[INDEX_SOURCE_NEW_FORMAT_ALIAS]
275                    .value()
276                    .to_string();
277                let regex = self.input_buffers[INDEX_SOURCE_NEW_FORMAT_REGEX]
278                    .value()
279                    .to_string();
280
281                if !alias.is_empty() {
282                    self.log_analyzer.add_format(&alias, &regex)?;
283                    self.update_formats().await;
284                    Some(alias)
285                } else {
286                    None
287                }
288
289            },
290            _ => Some(self.formats.items[selected_format_index].clone())
291        };
292
293        let path = self.input_buffers[INDEX_SOURCE_PATH].value().to_string();
294        self.log_analyzer
295            .add_log(self.source_type, &path, alias.as_ref())?;
296
297        Ok(())
298    }
299
300    pub async fn update_formats(&mut self) {
301        let mut formats = vec!["New".to_string()];
302        formats.extend(
303            self.log_analyzer
304                .get_formats()
305                .into_iter()
306                .map(|format| format.alias),
307        );
308
309        self.formats = StatefulList::with_items(formats);
310        self.formats.state.select(Some(0));
311    }
312
313    pub async fn update_sources(&mut self) {
314        let index = self.sources.state.selected();
315        let sources = self.log_analyzer.get_logs();
316        self.sources = StatefulTable::with_items(sources);
317
318        if index.is_some() && self.sources.items.len() >= index.unwrap() {
319            self.sources.state.select(index)
320        }
321    }
322
323    pub async fn update_filters(&mut self) {
324        let filters: Vec<(bool, String)> = self
325            .log_analyzer
326            .get_filters()
327            .iter()
328            .map(|(enabled, filter)| (*enabled, filter.alias.clone()))
329            .collect();
330
331        let index = self.filters.state.selected();
332        let length: usize = filters.len();
333        self.filters = StatefulTable::with_items(filters);
334
335        if index.is_some() && length >= index.unwrap() {
336            self.filters.state.select(index)
337        }
338    }
339
340    async fn pull_events(&mut self) {
341        let mut events = Vec::new();
342        while let Ok(event) = self.event_receiver.try_recv() {
343            events.push(event);
344        }
345
346        // Reload logs when some lines are received and there are no items displayed
347        if !self.processing.is_processing
348            && self.log_lines.items.len() < CAPACITY
349            && events.iter().any(|e| matches!(e, LogEvent::NewLines(_, _)))
350        {
351            self.log_lines.reload();
352        }
353
354        // Reload search logs when some search lines are received and there are no items displayed
355        if !self.processing.is_processing
356            && self.search_lines.items.len() < CAPACITY
357            && events
358                .iter()
359                .any(|e| matches!(e, LogEvent::NewSearchLines(_, _)))
360        {
361            self.search_lines.reload();
362        }
363
364        // Auto scroll
365        if self.auto_scroll && events.iter().any(|e| matches!(e, LogEvent::NewLines(_, _))) {
366            self.log_lines.navigate_to_bottom();
367        }
368
369        if self.auto_scroll
370            && events
371                .iter()
372                .any(|e| matches!(e, LogEvent::NewSearchLines(_, _)))
373        {
374            self.search_lines.navigate_to_bottom();
375        }
376
377        // Handle enter filtering
378        if events.iter().any(|e| matches!(e, LogEvent::Filtering)) {
379            self.processing.is_processing = true;
380
381            self.processing.set_focus(
382                self.log_lines
383                    .get_selected_item()
384                    .map(|l| l.index.parse().unwrap()),
385            );
386            self.log_lines.clear();
387            self.search_lines.clear();
388        }
389
390        // Handle exit filtering
391        if self.processing.is_processing
392            && events.iter().any(|e| matches!(e, LogEvent::FilterFinished))
393        {
394            self.log_lines.navigate_to(self.processing.focus_on);
395            self.search_lines.navigate_to(self.processing.focus_on);
396
397            self.processing.is_processing = false;
398            self.processing = Processing::default();
399        }
400
401        // Handle enter searching
402        if events.iter().any(|e| matches!(e, LogEvent::Searching)) {
403            self.processing.is_processing = true;
404            self.processing.set_focus(
405                self.search_lines
406                    .get_selected_item()
407                    .map(|l| l.unformat().index.parse().unwrap()),
408            );
409            self.search_lines.clear();
410        }
411
412        // Handle exit searching
413        if events.iter().any(|e| matches!(e, LogEvent::SearchFinished)) {
414            self.processing.is_processing = false;
415
416            self.search_lines.navigate_to(self.processing.focus_on);
417            self.processing = Processing::default();
418        }
419    }
420
421    pub async fn on_tick(&mut self) {
422        self.pull_events().await;
423    }
424
425    pub async fn handle_input(&mut self, key: KeyEvent) {
426        match self.selected_module {
427            Module::Sources => self.handle_sources_input(key).await,
428            Module::Filters => self.handle_filters_input(key).await,
429            Module::Logs => self.handle_log_input(key).await,
430            Module::Search => self.handle_search_input(key).await,
431            Module::SearchResult => self.handle_search_result_input(key).await,
432            Module::SourcePopup => self.handle_source_popup_input(key).await,
433            Module::FilterPopup => self.handle_filter_popup_input(key).await,
434            Module::NavigationPopup => self.handle_navigation_popup_input(key).await,
435            Module::ErrorPopup => self.handle_error_popup_input(key).await,
436            _ => {}
437        }
438    }
439
440    async fn handle_sources_input(&mut self, key: KeyEvent) {
441        if key.modifiers == KeyModifiers::SHIFT {
442            match key.code {
443                KeyCode::Char('W') => {
444                    App::decrease_ratio(&mut self.log_filter_size_percentage, 5, 20)
445                }
446                KeyCode::Char('S') => {
447                    App::increase_ratio(&mut self.log_filter_size_percentage, 5, 80)
448                }
449                KeyCode::Char('A') => {
450                    App::decrease_ratio(&mut self.side_main_size_percentage, 5, 0)
451                }
452                KeyCode::Char('D') => {
453                    App::increase_ratio(&mut self.side_main_size_percentage, 5, 50)
454                }
455                _ => {}
456            };
457        }
458
459        match key.code {
460            // Navigate up sources
461            KeyCode::Up => {
462                self.sources.previous();
463            }
464            // Navigate down sources
465            KeyCode::Down => {
466                self.sources.next();
467            }
468            // Toggle enabled/disabled source
469            KeyCode::Enter => {
470                if let Some(i) = self.sources.state.selected() {
471                    let (_, id, _) = &self.sources.items[i];
472                    self.log_analyzer.toggle_source(id);
473                    self.update_sources().await;
474                }
475            }
476            // Add new source -> Popup window
477            KeyCode::Char('i') | KeyCode::Char('+') | KeyCode::Char('a') => {
478                self.formats.state.select(Some(0));
479                self.show_source_popup = true;
480                self.input_buffer_index = INDEX_SOURCE_TYPE;
481                self.selected_module = Module::SourcePopup;
482            }
483            // Delete source
484            KeyCode::Char('-') | KeyCode::Char('d') | KeyCode::Delete | KeyCode::Backspace => {}
485            // Nothing
486            _ => {}
487        }
488    }
489
490    async fn handle_filters_input(&mut self, key: KeyEvent) {
491        if key.modifiers == KeyModifiers::SHIFT {
492            match key.code {
493                KeyCode::Char('W') => {
494                    App::decrease_ratio(&mut self.log_filter_size_percentage, 5, 20)
495                }
496                KeyCode::Char('S') => {
497                    App::increase_ratio(&mut self.log_filter_size_percentage, 5, 80)
498                }
499                KeyCode::Char('A') => {
500                    App::decrease_ratio(&mut self.side_main_size_percentage, 5, 0)
501                }
502                KeyCode::Char('D') => {
503                    App::increase_ratio(&mut self.side_main_size_percentage, 5, 50)
504                }
505                _ => {}
506            };
507        }
508        match key.code {
509            // Navigate up filters
510            KeyCode::Up => {
511                self.filters.previous();
512            }
513            // Navigate down filters
514            KeyCode::Down => {
515                self.filters.next();
516            }
517            // Toggle enabled/disabled source
518            KeyCode::Enter => {
519                if let Some(index) = self.filters.state.selected() {
520                    let (_, alias) = &self.filters.items[index];
521                    self.log_analyzer.toggle_filter(alias);
522                }
523                self.update_filters().await;
524            }
525            // Add new filter -> Popup window
526            KeyCode::Char('i') | KeyCode::Char('+') | KeyCode::Char('a') => {
527                self.show_filter_popup = true;
528                self.input_buffer_index = INDEX_FILTER_NAME;
529                self.selected_module = Module::FilterPopup;
530            }
531            // Edit filter -> Popup window
532            KeyCode::Char('e') => {
533                self.show_filter_popup = true;
534                self.input_buffer_index = INDEX_FILTER_NAME;
535                self.selected_module = Module::FilterPopup;
536
537                if let Some(i) = self.filters.state.selected() {
538                    let (_, alias) = &self.filters.items[i];
539                    if let Some((_, filter)) = self
540                        .log_analyzer
541                        .get_filters()
542                        .into_iter()
543                        .find(|(_, filter)| filter.alias == *alias)
544                    {
545                        self.filter_type = filter.action.into();
546                        self.input_buffers[INDEX_FILTER_NAME] =
547                            Input::default().with_value(alias.clone());
548                        self.input_buffers[INDEX_FILTER_TYPE] =
549                            Input::default().with_value("".into());
550                        self.input_buffers[INDEX_FILTER_LOG] =
551                            Input::default().with_value(filter.filter.log);
552                        self.input_buffers[INDEX_FILTER_DATETIME] =
553                            Input::default().with_value(filter.filter.date);
554                        self.input_buffers[INDEX_FILTER_TIMESTAMP] =
555                            Input::default().with_value(filter.filter.timestamp);
556                        self.input_buffers[INDEX_FILTER_APP] =
557                            Input::default().with_value(filter.filter.app);
558                        self.input_buffers[INDEX_FILTER_SEVERITY] =
559                            Input::default().with_value(filter.filter.severity);
560                        self.input_buffers[INDEX_FILTER_FUNCTION] =
561                            Input::default().with_value(filter.filter.function);
562                        self.input_buffers[INDEX_FILTER_PAYLOAD] =
563                            Input::default().with_value(filter.filter.payload);
564                        if let Some((r, g, b)) = filter.filter.color {
565                            self.input_buffers[INDEX_FILTER_RED_COLOR] =
566                                Input::default().with_value(r.to_string());
567                            self.input_buffers[INDEX_FILTER_GREEN_COLOR] =
568                                Input::default().with_value(g.to_string());
569                            self.input_buffers[INDEX_FILTER_BLUE_COLOR] =
570                                Input::default().with_value(b.to_string());
571                        }
572                    }
573                }
574            }
575            // Delete filter
576            KeyCode::Char('-') | KeyCode::Char('d') | KeyCode::Delete => {}
577            // Nothing
578            _ => {}
579        }
580    }
581
582    async fn handle_log_input(&mut self, key: KeyEvent) {
583        self.handle_table_log_input(key).await;
584    }
585
586    async fn handle_search_result_input(&mut self, key: KeyEvent) {
587        self.handle_table_search_input(key).await;
588    }
589
590    async fn handle_search_input(&mut self, key: KeyEvent) {
591        match key.code {
592            KeyCode::Enter => {
593                self.search_lines.clear();
594                self.log_analyzer
595                    .add_search(self.input_buffers[INDEX_SEARCH].value());
596            }
597            _ => {
598                input_backend::to_input_request(Event::Key(key))
599                    .map(|req| self.input_buffers[INDEX_SEARCH].handle(req));
600            }
601        }
602    }
603
604    async fn handle_source_popup_input(&mut self, key: KeyEvent) {
605        let mut fill_format = |_: usize, current_format: &str| match current_format {
606            "New" => {
607                self.input_buffers[INDEX_SOURCE_NEW_FORMAT_ALIAS] = Input::default();
608                self.input_buffers[INDEX_SOURCE_NEW_FORMAT_REGEX] = Input::default();
609            }
610            alias => {
611                let format = self
612                    .log_analyzer
613                    .get_formats()
614                    .iter()
615                    .find(|format| format.alias == alias)
616                    .unwrap()
617                    .clone();
618                self.input_buffers[INDEX_SOURCE_NEW_FORMAT_ALIAS] =
619                    Input::default().with_value(format.alias);
620                self.input_buffers[INDEX_SOURCE_NEW_FORMAT_REGEX] =
621                    Input::default().with_value(format.regex);
622            }
623        };
624        // Add new source -> Popup window
625        if key.code == KeyCode::Esc {
626            self.show_source_popup = false;
627            self.source_type = 0;
628            self.selected_module = Module::Sources;
629            self.formats.state.select(Some(0));
630            self.input_buffers[INDEX_SOURCE_TYPE..INDEX_SOURCE_NEW_FORMAT_REGEX]
631                .iter_mut()
632                .for_each(|b| *b = Input::default().with_value("".into()));
633            return;
634        }
635
636        match self.input_buffer_index {
637            INDEX_SOURCE_TYPE => {
638                // Switch between file and ws
639                if key.code == KeyCode::Right || key.code == KeyCode::Left {
640                    self.source_type = !self.source_type & 1;
641                }
642            }
643            INDEX_SOURCE_FORMAT => match key.code {
644                // Navigate up sources
645                KeyCode::Up => {
646                    if self.input_buffer_index == INDEX_SOURCE_FORMAT {
647                        let i = self.formats.previous();
648                        fill_format(i, self.formats.items[i].as_str());
649                    }
650                }
651                // Navigate down sources
652                KeyCode::Down => {
653                    if self.input_buffer_index == INDEX_SOURCE_FORMAT {
654                        let i = self.formats.next();
655                        fill_format(i, self.formats.items[i].as_str());
656                    }
657                }
658                _ => {}
659            },
660            index @ (INDEX_SOURCE_PATH
661            | INDEX_SOURCE_NEW_FORMAT_ALIAS
662            | INDEX_SOURCE_NEW_FORMAT_REGEX) => {
663                input_backend::to_input_request(Event::Key(key))
664                    .map(|req| self.input_buffers[index].handle(req));
665            }
666            INDEX_SOURCE_OK_BUTTON => {
667                if key.code == KeyCode::Enter {
668                    match self.add_log().await {
669                        Ok(_) => {
670                            self.show_source_popup = false;
671                            self.source_type = 0;
672                            self.selected_module = Module::Sources;
673                            self.update_sources().await;
674                            self.input_buffers[INDEX_SOURCE_TYPE..INDEX_SOURCE_NEW_FORMAT_REGEX]
675                                .iter_mut()
676                                .for_each(|b| *b = Input::default().with_value("".into()));
677                        }
678                        Err(err) => {
679                            self.selected_module = Module::ErrorPopup;
680                            self.show_error_message = true;
681                            self.popup.message = format!("{:?}", err);
682                            self.popup.calling_module = Module::SourcePopup;
683                        }
684                    }
685                }
686            }
687            _ => {}
688        }
689    }
690
691    async fn handle_filter_popup_input(&mut self, key: KeyEvent) {
692        // Add new filter -> Popup window
693        if key.code == KeyCode::Esc {
694            self.show_filter_popup = false;
695            self.selected_module = Module::Filters;
696            self.filter_type = 0;
697            self.input_buffers[INDEX_FILTER_NAME..INDEX_FILTER_BLUE_COLOR]
698                .iter_mut()
699                .for_each(|b| *b = Input::default().with_value("".into()));
700            return;
701        }
702
703        match self.input_buffer_index {
704            index @ (INDEX_FILTER_NAME
705            | INDEX_FILTER_LOG
706            | INDEX_FILTER_DATETIME
707            | INDEX_FILTER_TIMESTAMP
708            | INDEX_FILTER_APP
709            | INDEX_FILTER_SEVERITY
710            | INDEX_FILTER_FUNCTION
711            | INDEX_FILTER_PAYLOAD
712            | INDEX_FILTER_RED_COLOR
713            | INDEX_FILTER_GREEN_COLOR
714            | INDEX_FILTER_BLUE_COLOR) => {
715                input_backend::to_input_request(Event::Key(key))
716                    .map(|req| self.input_buffers[index].handle(req));
717            }
718            INDEX_FILTER_TYPE => {
719                // Switch tabs
720                if key.code == KeyCode::Right || key.code == KeyCode::Left {
721                    let circular_choice = |i: &mut usize, max, add: i32| {
722                        *i = match (*i as i32 + add) as i32 {
723                            r if r > max => 0_usize,    // if adding overflows -> set to 0
724                            r if r < 0 => max as usize, // if adding underflows -> set to 0
725                            r => r as usize,
726                        }
727                    };
728
729                    let sum = if key.code == KeyCode::Right { 1 } else { -1 };
730                    if self.input_buffer_index == INDEX_FILTER_TYPE {
731                        circular_choice(&mut self.filter_type, 2, sum)
732                    }
733                }
734            }
735
736            INDEX_FILTER_OK_BUTTON => {
737                if key.code == KeyCode::Enter {
738                    let filter = Filter {
739                        alias: self.input_buffers[INDEX_FILTER_NAME].value().to_string(),
740                        action: FilterAction::from(self.filter_type),
741                        filter: LogLine {
742                            log: self.input_buffers[INDEX_FILTER_LOG].value().to_string(),
743                            date: self.input_buffers[INDEX_FILTER_DATETIME]
744                                .value()
745                                .to_string(),
746                            timestamp: self.input_buffers[INDEX_FILTER_TIMESTAMP]
747                                .value()
748                                .to_string(),
749                            app: self.input_buffers[INDEX_FILTER_APP].value().to_string(),
750                            severity: self.input_buffers[INDEX_FILTER_SEVERITY]
751                                .value()
752                                .to_string(),
753                            function: self.input_buffers[INDEX_FILTER_FUNCTION]
754                                .value()
755                                .to_string(),
756                            payload: self.input_buffers[INDEX_FILTER_PAYLOAD].value().to_string(),
757                            color: parse_color(
758                                self.input_buffers[INDEX_FILTER_RED_COLOR].value(),
759                                self.input_buffers[INDEX_FILTER_GREEN_COLOR].value(),
760                                self.input_buffers[INDEX_FILTER_BLUE_COLOR].value(),
761                            ),
762                            ..Default::default()
763                        },
764                    };
765                    self.log_analyzer.add_filter(filter);
766                    self.show_filter_popup = false;
767                    self.selected_module = Module::Filters;
768                    self.filter_type = 0;
769                    self.update_filters().await;
770                    self.input_buffers[INDEX_FILTER_NAME..INDEX_FILTER_BLUE_COLOR]
771                        .iter_mut()
772                        .for_each(|b| *b = Input::default().with_value("".into()));
773                }
774            }
775            _ => {}
776        }
777    }
778
779    async fn handle_navigation_popup_input(&mut self, key: KeyEvent) {
780        match key.code {
781            KeyCode::Enter => {
782                match self.input_buffers[INDEX_NAVIGATION]
783                    .value()
784                    .parse::<usize>()
785                {
786                    Ok(index) => {
787                        self.show_navigation_popup = false;
788                        self.selected_module = self.popup.calling_module;
789                        self.input_buffers[INDEX_NAVIGATION] =
790                            Input::default().with_value("".into());
791
792                        match self.selected_module {
793                            Module::Logs => {
794                                self.log_lines.navigate_to(index);
795                            }
796                            Module::SearchResult => {
797                                self.search_lines.navigate_to(index);
798                            }
799                            _ => {}
800                        }
801                    }
802                    Err(err) => {
803                        self.selected_module = Module::ErrorPopup;
804                        self.show_error_message = true;
805                        self.popup.message = err.to_string();
806                    }
807                }
808            }
809            KeyCode::Esc => {
810                self.show_navigation_popup = false;
811                self.selected_module = self.popup.calling_module;
812                self.input_buffers[INDEX_NAVIGATION] = Input::default().with_value("".into());
813            }
814            _ => {
815                input_backend::to_input_request(Event::Key(key))
816                    .map(|req| self.input_buffers[INDEX_NAVIGATION].handle(req));
817            }
818        }
819    }
820
821    async fn handle_error_popup_input(&mut self, key: KeyEvent) {
822        match key.code {
823            KeyCode::Enter | KeyCode::Esc => {
824                self.show_error_message = false;
825                self.popup.response = true;
826                self.selected_module = self.popup.calling_module;
827            }
828            _ => {}
829        }
830    }
831
832    pub fn navigate(&mut self, direction: KeyCode) {
833        match self.selected_module {
834            Module::Sources => {
835                match direction {
836                    KeyCode::Up | KeyCode::Down => self.selected_module = Module::Filters,
837                    KeyCode::Left | KeyCode::Right => self.selected_module = Module::Logs,
838                    _ => {}
839                };
840                self.sources.unselect()
841            }
842            Module::Filters => {
843                match direction {
844                    KeyCode::Up | KeyCode::Down => self.selected_module = Module::Sources,
845                    KeyCode::Left | KeyCode::Right => self.selected_module = Module::Search,
846                    _ => {}
847                };
848                self.filters.unselect()
849            }
850            Module::Logs => match direction {
851                KeyCode::Up => self.selected_module = Module::SearchResult,
852                KeyCode::Down => self.selected_module = Module::Search,
853                KeyCode::Left | KeyCode::Right => {
854                    if self.side_main_size_percentage > 0 {
855                        self.selected_module = Module::Sources
856                    }
857                }
858                _ => {}
859            },
860            Module::Search => match direction {
861                KeyCode::Up => self.selected_module = Module::Logs,
862                KeyCode::Down => self.selected_module = Module::SearchResult,
863                KeyCode::Left | KeyCode::Right => {
864                    if self.side_main_size_percentage > 0 {
865                        self.selected_module = Module::Filters
866                    }
867                }
868                _ => {}
869            },
870            Module::SearchResult => match direction {
871                KeyCode::Up => self.selected_module = Module::Search,
872                KeyCode::Down => self.selected_module = Module::Logs,
873                KeyCode::Left | KeyCode::Right => {
874                    if self.side_main_size_percentage > 0 {
875                        self.selected_module = Module::Filters
876                    }
877                }
878                _ => {}
879            },
880            Module::SourcePopup => {
881                match direction {
882                    // Navigate up sources
883                    KeyCode::Up => {
884                        if self.input_buffer_index > INDEX_SOURCE_TYPE {
885                            self.input_buffer_index -= 1;
886                        }
887                    }
888                    // Navigate down sources
889                    KeyCode::Down => {
890                        if self.input_buffer_index < INDEX_SOURCE_OK_BUTTON {
891                            self.input_buffer_index += 1;
892                        }
893                    }
894                    _ => {}
895                }
896            }
897            Module::FilterPopup => {
898                match direction {
899                    // Navigate up sources
900                    KeyCode::Up => {
901                        if self.input_buffer_index > INDEX_FILTER_NAME {
902                            self.input_buffer_index -= 1;
903                        }
904                    }
905                    // Navigate down sources
906                    KeyCode::Down => {
907                        if self.input_buffer_index < INDEX_FILTER_OK_BUTTON {
908                            self.input_buffer_index += 1;
909                        }
910                    }
911                    _ => {}
912                }
913            }
914            Module::ErrorPopup => (),
915            Module::NavigationPopup => (),
916            Module::None => self.selected_module = Module::Logs,
917        }
918    }
919
920    fn increase_ratio(ratio: &mut u16, step: u16, max: u16) {
921        *ratio = (*ratio + step).min(max)
922    }
923
924    fn decrease_ratio(ratio: &mut u16, step: u16, min: u16) {
925        *ratio = if *ratio > min { *ratio - step } else { *ratio }
926    }
927
928    pub fn get_column_lenght(&self, column: &str) -> u16 {
929        let lenght = |log_lines: &Vec<LogLine>| {
930            log_lines
931                .iter()
932                .map(|l| l.get(column).unwrap())
933                .max_by_key(|l| l.len())
934                .map(|l| l.len().clamp(0, u16::MAX as usize) as u16)
935        };
936
937        let max_log_lenght = lenght(&self.log_lines.items);
938        let max_search_lenght = lenght(
939            &self
940                .search_lines
941                .items
942                .iter()
943                .map(|line| line.unformat())
944                .collect(),
945        );
946
947        match (max_log_lenght, max_search_lenght) {
948            (Some(l), Some(s)) => l.max(s),
949            (Some(l), None) => l,
950            (None, Some(s)) => s,
951            _ => 15,
952        }
953    }
954
955    async fn handle_table_log_input(&mut self, key: KeyEvent) {
956        let multiplier = if key.modifiers == KeyModifiers::ALT {
957            10
958        } else {
959            1
960        };
961        match key.modifiers {
962            KeyModifiers::SHIFT => match key.code {
963                KeyCode::Char('W') => {
964                    App::decrease_ratio(&mut self.log_search_size_percentage, 5, 10)
965                }
966                KeyCode::Char('S') => {
967                    App::increase_ratio(&mut self.log_search_size_percentage, 5, 90)
968                }
969                KeyCode::Char('A') => {
970                    App::decrease_ratio(&mut self.side_main_size_percentage, 5, 0)
971                }
972                KeyCode::Char('D') => {
973                    App::increase_ratio(&mut self.side_main_size_percentage, 5, 50)
974                }
975                KeyCode::Char('G') => {
976                    self.input_buffer_index = INDEX_NAVIGATION;
977                    self.show_navigation_popup = true;
978                    self.popup.calling_module = Module::Logs;
979                    self.selected_module = Module::NavigationPopup;
980                }
981                _ => {}
982            },
983            _ => match key.code {
984                // Navigate up log_lines
985                KeyCode::Up => {
986                    let steps = multiplier;
987                    for _ in 0..steps {
988                        self.log_lines.previous();
989                    }
990                }
991                // Navigate down log_lines
992                KeyCode::Down => {
993                    let steps = multiplier;
994                    for _ in 0..steps {
995                        self.log_lines.next();
996                    }
997                }
998                // Navigate up log_lines
999                KeyCode::PageUp => {
1000                    let steps = 100 * multiplier;
1001                    for _ in 0..steps {
1002                        self.log_lines.previous();
1003                    }
1004                }
1005                // Navigate down log_lines
1006                KeyCode::PageDown => {
1007                    let steps = 100 * multiplier;
1008                    for _ in 0..steps {
1009                        self.log_lines.next();
1010                    }
1011                }
1012                // Navigate up log_lines
1013                KeyCode::Left => {
1014                    if self.horizontal_offset > 0 {
1015                        self.horizontal_offset -= if self.horizontal_offset == 0 { 0 } else { 10 };
1016                        return;
1017                    }
1018                    for (i, (column, enabled)) in self.log_columns.iter().enumerate().rev() {
1019                        if !*enabled && self.get_column_lenght(column) != 0 {
1020                            self.log_columns[i].1 = true;
1021                            return;
1022                        }
1023                    }
1024                }
1025                // Navigate down log_lines
1026                KeyCode::Right => {
1027                    for (i, (column, enabled)) in self.log_columns.iter().enumerate() {
1028                        if i != (self.log_columns.len() - 1)
1029                            && *enabled
1030                            && self.get_column_lenght(column) != 0
1031                        {
1032                            self.log_columns[i].1 = false;
1033                            return;
1034                        }
1035                    }
1036                    self.horizontal_offset += 10
1037                }
1038                // Toogle columns
1039                KeyCode::Char('l') => self.log_columns[0].1 = !self.log_columns[0].1,
1040                KeyCode::Char('i') => self.log_columns[1].1 = !self.log_columns[1].1,
1041                KeyCode::Char('d') => self.log_columns[2].1 = !self.log_columns[2].1,
1042                KeyCode::Char('t') => self.log_columns[3].1 = !self.log_columns[3].1,
1043                KeyCode::Char('a') => self.log_columns[4].1 = !self.log_columns[4].1,
1044                KeyCode::Char('s') => self.log_columns[5].1 = !self.log_columns[5].1,
1045                KeyCode::Char('f') => self.log_columns[6].1 = !self.log_columns[6].1,
1046                KeyCode::Char('p') => self.log_columns[7].1 = !self.log_columns[7].1,
1047                KeyCode::Char('r') => self.auto_scroll = !self.auto_scroll,
1048                // Nothing
1049                _ => {}
1050            },
1051        }
1052    }
1053
1054    async fn handle_table_search_input(&mut self, key: KeyEvent){
1055        let multiplier = if key.modifiers == KeyModifiers::ALT {
1056            10
1057        } else {
1058            1
1059        };
1060        match key.modifiers {
1061            KeyModifiers::SHIFT => match key.code {
1062                KeyCode::Char('W') => {
1063                    App::decrease_ratio(&mut self.log_search_size_percentage, 5, 10)
1064                }
1065                KeyCode::Char('S') => {
1066                    App::increase_ratio(&mut self.log_search_size_percentage, 5, 90)
1067                }
1068                KeyCode::Char('A') => {
1069                    App::decrease_ratio(&mut self.side_main_size_percentage, 5, 0)
1070                }
1071                KeyCode::Char('D') => {
1072                    App::increase_ratio(&mut self.side_main_size_percentage, 5, 50)
1073                }
1074                KeyCode::Char('G') => {
1075                    self.input_buffer_index = INDEX_NAVIGATION;
1076                    self.show_navigation_popup = true;
1077                    self.popup.calling_module = Module::SearchResult;
1078                    self.selected_module = Module::NavigationPopup;
1079                }
1080                _ => {}
1081            },
1082            _ => match key.code {
1083                // Navigate up log_lines
1084                KeyCode::Up => {
1085                    let steps = multiplier;
1086                    for _ in 0..steps {
1087                        self.search_lines.previous();
1088                    }
1089                }
1090                // Navigate down log_lines
1091                KeyCode::Down => {
1092                    let steps = multiplier;
1093                    for _ in 0..steps {
1094                        self.search_lines.next();
1095                    }
1096                }
1097                // Navigate up log_lines
1098                KeyCode::PageUp => {
1099                    let steps = 100 * multiplier;
1100                    for _ in 0..steps {
1101                        self.search_lines.previous();
1102                    }
1103                }
1104                // Navigate down log_lines
1105                KeyCode::PageDown => {
1106                    let steps = 100 * multiplier;
1107                    for _ in 0..steps {
1108                        self.search_lines.next();
1109                    }
1110                }
1111                // Navigate up log_lines
1112                KeyCode::Left => {
1113                    if self.horizontal_offset > 0 {
1114                        self.horizontal_offset -= if self.horizontal_offset == 0 { 0 } else { 10 };
1115                        return;
1116                    }
1117                    for (i, (column, enabled)) in self.log_columns.iter().enumerate().rev() {
1118                        if !*enabled && self.get_column_lenght(column) != 0 {
1119                            self.log_columns[i].1 = true;
1120                            return;
1121                        }
1122                    }
1123                }
1124                // Navigate down log_lines
1125                KeyCode::Right => {
1126                    for (i, (column, enabled)) in self.log_columns.iter().enumerate() {
1127                        if i != (self.log_columns.len() - 1)
1128                            && *enabled
1129                            && self.get_column_lenght(column) != 0
1130                        {
1131                            self.log_columns[i].1 = false;
1132                            return;
1133                        }
1134                    }
1135                    self.horizontal_offset += 10
1136                }
1137                // Toogle columns
1138                KeyCode::Char('l') => self.log_columns[0].1 = !self.log_columns[0].1,
1139                KeyCode::Char('i') => self.log_columns[1].1 = !self.log_columns[1].1,
1140                KeyCode::Char('d') => self.log_columns[2].1 = !self.log_columns[2].1,
1141                KeyCode::Char('t') => self.log_columns[3].1 = !self.log_columns[3].1,
1142                KeyCode::Char('a') => self.log_columns[4].1 = !self.log_columns[4].1,
1143                KeyCode::Char('s') => self.log_columns[5].1 = !self.log_columns[5].1,
1144                KeyCode::Char('f') => self.log_columns[6].1 = !self.log_columns[6].1,
1145                KeyCode::Char('p') => self.log_columns[7].1 = !self.log_columns[7].1,
1146                KeyCode::Char('r') => self.auto_scroll = !self.auto_scroll,
1147                KeyCode::Enter => {
1148                    if let Some(current_line) = self.search_lines.get_selected_item() {
1149                            self.log_lines.navigate_to(current_line.unformat().index.parse().unwrap());
1150                    }
1151                }
1152                // Nothing
1153                _ => {}
1154            },
1155        }
1156    }
1157}
1158
1159pub fn parse_color(r: &str, g: &str, b: &str) -> Option<(u8, u8, u8)> {
1160    match (r.parse::<u8>(), g.parse::<u8>(), b.parse::<u8>()) {
1161        parse
1162            if [&parse.0, &parse.1, &parse.2]
1163                .into_iter()
1164                .any(|p| p.is_ok()) =>
1165        {
1166            Some((
1167                parse.0.unwrap_or_default(),
1168                parse.1.unwrap_or_default(),
1169                parse.2.unwrap_or_default(),
1170            ))
1171        }
1172        _ => None,
1173    }
1174}