terminal_ui/
lib.rs

1pub mod app;
2pub mod styles;
3pub mod ui;
4pub mod data;
5
6use app::App;
7use crossterm::{
8    event::{self, DisableMouseCapture, Event, KeyCode, KeyModifiers, MouseEventKind},
9    execute,
10    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
11};
12use log_analyzer::{
13    models::settings::Settings,
14    services::log_service::{LogAnalyzer, LogService},
15    stores::{
16        analysis_store::InMemmoryAnalysisStore, log_store::InMemmoryLogStore,
17        processing_store::InMemmoryProcessingStore,
18    },
19};
20
21use std::{
22    error::Error,
23    fs, io,
24    sync::Arc,
25    time::{Duration, Instant},
26};
27use tui::{
28    backend::{Backend, CrosstermBackend},
29    Frame, Terminal, style::Color,
30};
31use ui::{
32    ui_error_message::draw_error_popup, ui_filter_popup::draw_filter_popup,
33    ui_loading_popup::draw_loading_popup, ui_log_analyzer::draw_log_analyzer_view,
34    ui_navigation_popup::draw_navigation_popup, ui_source_popup::draw_source_popup,
35};
36
37
38pub async fn async_main(settings_path: Option<String>) -> Result<(), Box<dyn Error>> {
39    // setup terminal
40    enable_raw_mode()?;
41    let mut stdout = io::stdout();
42    execute!(stdout, EnterAlternateScreen)?;
43    let backend = CrosstermBackend::new(stdout);
44    let mut terminal = Terminal::new(backend)?;
45
46    // Create
47    let log_store = Arc::new(InMemmoryLogStore::new());
48    let processing_store = Arc::new(InMemmoryProcessingStore::new());
49    let analysis_store = Arc::new(InMemmoryAnalysisStore::new());
50
51    let log_service = LogService::new(log_store, processing_store, analysis_store);
52    let mut color = Color::LightBlue;
53
54    if let Some(settings) = settings_path {
55        if let Ok(file) = fs::read_to_string(settings) {
56            if let Ok(settings) = Settings::from_json(&file) {
57                if let Some(formats) = settings.formats {
58                    for format in formats {
59                        log_service.add_format(&format.alias, &format.regex)?;
60                    }
61                }
62                if let Some(filters) = settings.filters {
63                    for filter in filters {
64                        log_service.add_filter(filter);
65                    }
66                }
67                if let Some((r, g, b)) = settings.primary_color {
68                    color = Color::Rgb(r, g, b)
69                }
70            }
71        }
72    }
73
74    // create app and run it
75    let tick_rate = Duration::from_millis(150);
76    let app = App::new(Box::new(log_service), color).await;
77    let res = run_app(&mut terminal, app, tick_rate).await;
78
79    // restore terminal
80    disable_raw_mode()?;
81    execute!(
82        terminal.backend_mut(),
83        LeaveAlternateScreen,
84        DisableMouseCapture
85    )?;
86    terminal.show_cursor()?;
87
88    if let Err(err) = res {
89        println!("{:?}", err);
90    }
91    Ok(())
92}
93
94
95async fn run_app<B: Backend>(
96    terminal: &mut Terminal<B>,
97    mut app: App,
98    tick_rate: Duration,
99) -> io::Result<()> {
100    let mut last_tick = Instant::now();
101
102    loop {
103        terminal.draw(|f| ui(f, &mut app))?;
104
105        let timeout = tick_rate
106            .checked_sub(last_tick.elapsed())
107            .unwrap_or_else(|| Duration::from_secs(0));
108        if crossterm::event::poll(timeout)? {
109            let event = event::read()?;
110
111            match event {
112                Event::Key(key) => {
113                    match key.modifiers {
114                        // Quit
115                        KeyModifiers::CONTROL => match key.code {
116                            KeyCode::Char('c') => return Ok(()),
117                            _ => async_std::task::block_on(app.handle_input(key)),
118                        },
119                        // Navigate
120                        KeyModifiers::SHIFT => match key.code {
121                            KeyCode::Char(_) => async_std::task::block_on(app.handle_input(key)),
122                            KeyCode::Up | KeyCode::BackTab => app.navigate(KeyCode::Up),
123                            KeyCode::Down | KeyCode::Tab => app.navigate(KeyCode::Down),
124                            KeyCode::Left => app.navigate(KeyCode::Left),
125                            KeyCode::Right => app.navigate(KeyCode::Right),
126                            _ => {}
127                        },
128                        // Handle in widget
129                        _ => match key.code {
130                            KeyCode::Tab => app.navigate(KeyCode::Down),
131                            _ => app.handle_input(key).await,
132                        },
133                    }
134                }
135                Event::Mouse(mouse) => match mouse.kind {
136                    MouseEventKind::ScrollUp => {}
137                    MouseEventKind::ScrollDown => {}
138                    MouseEventKind::Down(button) => match button {
139                        crossterm::event::MouseButton::Left => {}
140                        crossterm::event::MouseButton::Right => {}
141                        _ => {}
142                    },
143                    _ => {}
144                },
145                _ => {}
146            }
147        }
148        if last_tick.elapsed() >= tick_rate {
149            app.on_tick().await;
150            last_tick = Instant::now();
151        }
152    }
153}
154
155fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
156    draw_log_analyzer_view(f, app);
157
158    if app.show_source_popup {
159        draw_source_popup(f, app)
160    } else if app.show_filter_popup {
161        draw_filter_popup(f, app)
162    } else if app.show_navigation_popup {
163        draw_navigation_popup(f, app)
164    }
165
166    if app.show_error_message {
167        draw_error_popup(f, app)
168    }
169
170    if app.processing.is_processing {
171        draw_loading_popup(f, app)
172    }
173}