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
19pub 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;
26pub 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;
40pub const INDEX_SEARCH: usize = INDEX_FILTER_OK_BUTTON + 1;
42pub const INDEX_NAVIGATION: usize = INDEX_SEARCH + 1;
44pub const INDEX_MAX: usize = INDEX_NAVIGATION + 1;
46pub 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
127pub struct App {
131 pub log_analyzer: Box<Arc<dyn LogAnalyzer>>,
133
134 pub color: Color,
136
137 pub selected_module: Module,
139
140 pub show_source_popup: bool,
142 pub show_filter_popup: bool,
144 pub show_error_message: bool,
146 pub show_navigation_popup: bool,
148 pub show_log_options_popup: bool,
150
151 pub input_buffers: Vec<Input>,
154 pub input_buffer_index: usize,
156 pub formats: StatefulList<String>,
158
159 pub source_type: usize,
161 pub filter_type: usize,
163 pub filter_color: usize,
165
166 pub sources: StatefulTable<(bool, String, Option<String>)>,
168 pub filters: StatefulTable<(bool, String)>,
170
171 pub log_lines: LazyStatefulTable<LogLine>,
173 pub search_lines: LazyStatefulTable<LogLineStyled>,
175 pub horizontal_offset: usize,
177
178 pub side_main_size_percentage: u16,
180 pub log_filter_size_percentage: u16,
182 pub log_search_size_percentage: u16,
184
185 pub log_columns: Vec<(String, bool)>,
187
188 pub auto_scroll: bool,
190
191 pub popup: PopupInteraction,
193 pub processing: Processing,
195 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(); let alias = match selected_format_index {
273 0 => {
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, ®ex)?;
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 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 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 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 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 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 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 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 KeyCode::Up => {
462 self.sources.previous();
463 }
464 KeyCode::Down => {
466 self.sources.next();
467 }
468 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 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 KeyCode::Char('-') | KeyCode::Char('d') | KeyCode::Delete | KeyCode::Backspace => {}
485 _ => {}
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 KeyCode::Up => {
511 self.filters.previous();
512 }
513 KeyCode::Down => {
515 self.filters.next();
516 }
517 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 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 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 KeyCode::Char('-') | KeyCode::Char('d') | KeyCode::Delete => {}
577 _ => {}
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 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 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 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 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 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 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, r if r < 0 => max as usize, 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 KeyCode::Up => {
884 if self.input_buffer_index > INDEX_SOURCE_TYPE {
885 self.input_buffer_index -= 1;
886 }
887 }
888 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 KeyCode::Up => {
901 if self.input_buffer_index > INDEX_FILTER_NAME {
902 self.input_buffer_index -= 1;
903 }
904 }
905 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 KeyCode::Up => {
986 let steps = multiplier;
987 for _ in 0..steps {
988 self.log_lines.previous();
989 }
990 }
991 KeyCode::Down => {
993 let steps = multiplier;
994 for _ in 0..steps {
995 self.log_lines.next();
996 }
997 }
998 KeyCode::PageUp => {
1000 let steps = 100 * multiplier;
1001 for _ in 0..steps {
1002 self.log_lines.previous();
1003 }
1004 }
1005 KeyCode::PageDown => {
1007 let steps = 100 * multiplier;
1008 for _ in 0..steps {
1009 self.log_lines.next();
1010 }
1011 }
1012 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 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 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 _ => {}
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 KeyCode::Up => {
1085 let steps = multiplier;
1086 for _ in 0..steps {
1087 self.search_lines.previous();
1088 }
1089 }
1090 KeyCode::Down => {
1092 let steps = multiplier;
1093 for _ in 0..steps {
1094 self.search_lines.next();
1095 }
1096 }
1097 KeyCode::PageUp => {
1099 let steps = 100 * multiplier;
1100 for _ in 0..steps {
1101 self.search_lines.previous();
1102 }
1103 }
1104 KeyCode::PageDown => {
1106 let steps = 100 * multiplier;
1107 for _ in 0..steps {
1108 self.search_lines.next();
1109 }
1110 }
1111 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 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 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 _ => {}
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}