terminal_ui/ui/
ui_log_analyzer.rs

1use log_analyzer::models::{log_line::LogLine, log_line_styled::LogLineStyled};
2use tui::{
3    backend::Backend,
4    layout::{Alignment, Constraint, Direction, Layout, Rect},
5    style::{Color, Modifier, Style},
6    text::{Span, Spans, Text},
7    widgets::{Block, Borders, Cell, Gauge, Paragraph, Row, Table},
8    Frame,
9};
10
11use crate::{
12    app::{App, Module, INDEX_SEARCH},
13    styles::selected_style,
14};
15
16use super::ui_shared::display_cursor;
17
18trait Convert<T> {
19    fn from_str(s: &str) -> Option<T>;
20}
21
22impl Convert<Color> for Color {
23    fn from_str(s: &str) -> Option<Self> {
24        match s {
25            "BLACK" | "Black" | "black" => Some(Color::Black),
26            "WHITE" | "White" | "white" => Some(Color::White),
27            "RED" | "Red" | "red" => Some(Color::Red),
28            "GREEN" | "Green" | "green" => Some(Color::Green),
29            "YELLOW" | "Yellow" | "yellow" => Some(Color::Yellow),
30            "BLUE" | "Blue" | "blue" => Some(Color::Blue),
31            "MAGENTA" | "Magenta" | "magenta" => Some(Color::Magenta),
32            "CYAN" | "Cyan" | "cyan" => Some(Color::Cyan),
33            "GRAY" | "Gray" | "gray" => Some(Color::Gray),
34            "DARKGRAY" | "DarkGray" | "darkgray" => Some(Color::DarkGray),
35            "LIGHTRED" | "LightRed" | "lightred" => Some(Color::LightRed),
36            "LIGHTGREEN" | "LightGreen" | "lightgreen" => Some(Color::LightGreen),
37            "LIGHTYELLOW" | "LightYellow" | "lightyellow" => Some(Color::LightYellow),
38            "LIGHTBLUE" | "LightBlue" | "lightblue" => Some(Color::LightBlue),
39            "LIGHTMAGENTA" | "LightMagenta" | "lightmagenta" => Some(Color::LightMagenta),
40            "LIGHTCYAN" | "LightCyan" | "lightcyan" => Some(Color::LightCyan),
41            _ => None,
42        }
43    }
44}
45
46fn draw_sources<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
47where
48    B: Backend,
49{
50    let sources_widget = Block::default()
51        .title("Sources")
52        .borders(Borders::ALL)
53        .border_style(match app.selected_module {
54            Module::Sources => selected_style(app.color),
55            _ => Style::default(),
56        });
57
58    let selected_style = Style::default().add_modifier(Modifier::REVERSED);
59    let normal_style = Style::default().bg(app.color).add_modifier(Modifier::BOLD);
60
61    let header_cells = ["Enabled", "Log", "Format"]
62        .iter()
63        .map(|h| Cell::from(*h).style(Style::default().fg(Color::Black)));
64    let header = Row::new(header_cells).style(normal_style).bottom_margin(1);
65    let rows = app.sources.items.iter().map(|item| {
66        let get_enabled_widget = |enabled: bool| match enabled {
67            true => Span::styled("V", Style::default().fg(app.color)),
68            false => Span::styled("X", Style::default().fg(Color::Gray)),
69        };
70
71        let format = match &item.2 {
72            Some(format) => format.as_str(),
73            _ => "",
74        };
75
76        let cells = vec![
77            Cell::from(get_enabled_widget(item.0)),
78            Cell::from(Text::from(item.1.as_str())),
79            Cell::from(Text::from(format)),
80        ];
81        Row::new(cells).bottom_margin(0)
82    });
83    let t = Table::new(rows)
84        .header(header)
85        .block(sources_widget)
86        .highlight_style(selected_style)
87        .widths(&[
88            Constraint::Percentage(20),
89            Constraint::Percentage(50),
90            Constraint::Percentage(30),
91        ]);
92    f.render_stateful_widget(t, area, &mut app.sources.state);
93}
94
95fn draw_filters<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
96where
97    B: Backend,
98{
99    let filters_widget = Block::default()
100        .title("Filters")
101        .borders(Borders::ALL)
102        .border_style(match app.selected_module {
103            Module::Filters => selected_style(app.color),
104            _ => Style::default(),
105        });
106    let selected_style = Style::default().add_modifier(Modifier::REVERSED);
107    let normal_style = Style::default().bg(app.color).add_modifier(Modifier::BOLD);
108
109    let header_cells = ["Enabled", "Filter"]
110        .iter()
111        .map(|h| Cell::from(*h).style(Style::default().fg(Color::Black)));
112    let header = Row::new(header_cells).style(normal_style).bottom_margin(1);
113
114    let rows = app.filters.items.iter().map(|item| {
115        let get_enabled_widget = |enabled: bool| match enabled {
116            true => Span::styled("V", Style::default().fg(app.color)),
117            false => Span::styled("X", Style::default().fg(Color::Gray)),
118        };
119
120        let cells = vec![
121            Cell::from(get_enabled_widget(item.0)),
122            Cell::from(Text::from(item.1.as_str())),
123        ];
124        Row::new(cells).bottom_margin(0)
125    });
126    let t = Table::new(rows)
127        .header(header)
128        .block(filters_widget)
129        .highlight_style(selected_style)
130        .widths(&[Constraint::Percentage(20), Constraint::Percentage(80)]);
131    f.render_stateful_widget(t, area, &mut app.filters.state);
132}
133
134fn draw_sidebar<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
135where
136    B: Backend,
137{
138    let left_modules = Layout::default()
139        .direction(Direction::Vertical)
140        .constraints(
141            [
142                Constraint::Percentage(app.log_filter_size_percentage),
143                Constraint::Percentage(100 - app.log_filter_size_percentage),
144            ]
145            .as_ref(),
146        )
147        .split(area);
148
149    draw_sources(f, app, left_modules[0]);
150    draw_filters(f, app, left_modules[1]);
151}
152
153fn log_line_cell_builder<'a>(line: &'a LogLine, column: &'a str, offset: usize) -> Cell<'a> {
154    Cell::from(Span::styled(
155        line.get(column).unwrap().get(offset..).unwrap_or_default(),
156        Style::default().fg(if line.color.is_some() {
157            Color::Rgb(
158                line.color.unwrap().0,
159                line.color.unwrap().1,
160                line.color.unwrap().2,
161            )
162        } else {
163            Color::Reset
164        }),
165    ))
166}
167
168fn log_search_cell_builder<'a>(line: &'a LogLineStyled, column: &'a str, mut offset: usize) -> Cell<'a> {
169    let groups = line.get(column).unwrap();
170
171    Cell::from(Spans::from(
172        groups
173            .into_iter()
174            .filter_map(|(highlight, content)| {
175                let style = match (line.color.is_some(), highlight.as_ref().map(|c| Color::from_str(c))) {
176                    (_, Some(Some(color))) => {
177                        Style::default().fg(color).add_modifier(Modifier::BOLD)
178                    }
179                    (true, _) => Style::default().fg(Color::Rgb(
180                        line.color.unwrap().0,
181                        line.color.unwrap().1,
182                        line.color.unwrap().2,
183                    )),
184                    _ => Style::default(),
185                };
186
187                if highlight.is_some() {
188                    style.add_modifier(Modifier::BOLD);
189                }
190                let retval = content.get(offset..).map(|str| Span::styled(str, style));
191
192                offset = offset.saturating_sub(content.len());
193                retval
194            })
195            .collect::<Vec<Span<'a>>>(),
196    ))
197}
198
199fn draw_log<'a, 's, B>(
200    f: &mut Frame<B>,
201    app: &'s mut App,
202    module: Module,
203    title: &str,
204    area: Rect,
205) where
206    B: Backend,
207{
208    let is_selected = app.selected_module == module;
209    let items = &app.log_lines.items;
210
211    let log_widget = Block::default()
212        .title(title)
213        .borders(Borders::ALL)
214        .border_style(match is_selected {
215            true => selected_style(app.color),
216            _ => Style::default(),
217        });
218
219    let selected_style = Style::default().add_modifier(Modifier::REVERSED);
220    let normal_style = Style::default().bg(app.color).add_modifier(Modifier::BOLD);
221
222    let enabled_columns: Vec<&(String, bool)> = app
223        .log_columns
224        .iter()
225        .filter(|(_, enabled)| *enabled)
226        .collect();
227
228    let header_cells = enabled_columns
229        .iter()
230        .map(|(column, _)| Cell::from(column.clone()).style(Style::default().fg(Color::Black)));
231    let header = Row::new(header_cells).style(normal_style).bottom_margin(1);
232
233    let rows = items.iter().map(|item| {
234        let cells = enabled_columns
235            .iter()
236            .map(|(column, _)| log_line_cell_builder(item, column, app.horizontal_offset));
237        Row::new(cells).bottom_margin(0)
238    });
239
240    let constraints: Vec<Constraint> = enabled_columns
241        .iter()
242        .map(|(name, _)| Constraint::Length(app.get_column_lenght(name)))
243        .collect();
244
245    let t = Table::new(rows)
246        .header(header)
247        .block(log_widget)
248        .highlight_style(selected_style)
249        .widths(&constraints);
250
251    let state = &mut app.log_lines.state;
252    f.render_stateful_widget(t, area, state);
253}
254
255fn draw_search<'a, 's, B>(
256    f: &mut Frame<B>,
257    app: &'s mut App,
258    module: Module,
259    title: &str,
260    area: Rect,
261) where
262    B: Backend,
263{
264    let is_selected = app.selected_module == module;
265    let items = &app.search_lines.items;
266
267    let log_widget = Block::default()
268        .title(title)
269        .borders(Borders::ALL)
270        .border_style(match is_selected {
271            true => selected_style(app.color),
272            _ => Style::default(),
273        });
274
275    let selected_style = Style::default().add_modifier(Modifier::REVERSED);
276    let normal_style = Style::default().bg(app.color).add_modifier(Modifier::BOLD);
277
278    let enabled_columns: Vec<&(String, bool)> = app
279        .log_columns
280        .iter()
281        .filter(|(_, enabled)| *enabled)
282        .collect();
283
284    let header_cells = enabled_columns
285        .iter()
286        .map(|(column, _)| Cell::from(column.clone()).style(Style::default().fg(Color::Black)));
287    let header = Row::new(header_cells).style(normal_style).bottom_margin(1);
288
289    let rows = items.iter().map(|item| {
290        let cells = enabled_columns
291            .iter()
292            .map(|(column, _)| log_search_cell_builder(item, column, app.horizontal_offset));
293        Row::new(cells).bottom_margin(0)
294    });
295
296    let constraints: Vec<Constraint> = enabled_columns
297        .iter()
298        .map(|(name, _)| Constraint::Length(app.get_column_lenght(name)))
299        .collect();
300
301    let t = Table::new(rows)
302        .header(header)
303        .block(log_widget)
304        .highlight_style(selected_style)
305        .widths(&constraints);
306
307    let state = &mut app.search_lines.state;
308    f.render_stateful_widget(t, area, state);
309}
310
311fn draw_search_box<B>(f: &mut Frame<B>, app: &mut App, area: Rect, index: usize, title: &str)
312where
313    B: Backend,
314{
315    let input_widget = Paragraph::new(app.input_buffers[index].value())
316        .style(match app.selected_module {
317            Module::Search => selected_style(app.color),
318            _ => Style::default(),
319        })
320        .block(Block::default().borders(Borders::ALL).title(title));
321
322    f.render_widget(input_widget, area);
323
324    if app.selected_module == Module::Search {
325        display_cursor(f, area, app.input_buffers[index].cursor())
326    }
327}
328
329fn draw_bottom_bar<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
330where
331    B: Backend,
332{
333    let bottom_bar_layout = Layout::default()
334        .direction(Direction::Horizontal)
335        .constraints([
336            Constraint::Percentage(33),
337            Constraint::Percentage(33),
338            Constraint::Percentage(33),
339        ])
340        .split(area);
341
342    let auto_scroll = Paragraph::new("AUTO SCROLL")
343        .style(match app.auto_scroll {
344            false => Style::default().add_modifier(Modifier::DIM),
345            true => selected_style(app.color),
346        })
347        .alignment(Alignment::Center)
348        .block(Block::default().borders(Borders::ALL));
349
350    f.render_widget(auto_scroll, bottom_bar_layout[0]);
351
352    let total = app.log_analyzer.get_total_raw_lines();
353    let filtered = app.log_analyzer.get_total_filtered_lines();
354    let label = format!(" {}/{}", filtered, total);
355    let gauge = Gauge::default()
356        .block(Block::default().borders(Borders::ALL))
357        .gauge_style(Style::default().fg(app.color))
358        .percent((if total > 0 { (filtered * 100 / total).min(100) } else { 0 }) as u16)
359        .label(label);
360    f.render_widget(gauge, bottom_bar_layout[1]);
361
362    let searched = app.log_analyzer.get_total_searched_lines();
363    let label = format!(" {}/{}", searched, total);
364    let gauge = Gauge::default()
365        .block(Block::default().borders(Borders::ALL))
366        .gauge_style(Style::default().fg(app.color))
367        .percent((if total > 0 { (searched * 100 / total).min(100) } else { 0 }) as u16)
368        .label(label);
369
370    f.render_widget(gauge, bottom_bar_layout[2]);
371}
372
373fn draw_main_panel<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
374where
375    B: Backend,
376{
377    let expandable = area.height - 3;
378    let log_lenght = expandable * (app.log_search_size_percentage) as u16 / 100;
379    let search_lenght = expandable * (100 - app.log_search_size_percentage) as u16 / 100;
380
381    let main_modules = Layout::default()
382        .direction(Direction::Vertical)
383        .constraints(
384            [
385                Constraint::Length(log_lenght),
386                Constraint::Length(3),
387                Constraint::Length(search_lenght),
388            ]
389            .as_ref(),
390        )
391        .split(area);
392
393    draw_log(
394        f,
395        app,
396        Module::Logs,
397        "Log",
398        main_modules[0],
399    );
400    draw_search_box(f, app, main_modules[1], INDEX_SEARCH, "Search");
401    draw_search(
402        f,
403        app,
404        Module::SearchResult,
405        "Search results",
406        main_modules[2],
407    );
408}
409
410pub fn draw_log_analyzer_view<B>(f: &mut Frame<B>, app: &mut App)
411where
412    B: Backend,
413{
414    let ui = Layout::default()
415        .direction(Direction::Vertical)
416        .constraints(
417            [
418                Constraint::Length(f.size().height - 3),
419                Constraint::Length(3),
420            ]
421            .as_ref(),
422        )
423        .split(f.size());
424
425    // Create two chunks with equal horizontal screen space
426    let panels = Layout::default()
427        .direction(Direction::Horizontal)
428        .constraints([
429            Constraint::Percentage(app.side_main_size_percentage),
430            Constraint::Percentage(100 - app.side_main_size_percentage),
431        ])
432        .split(ui[0]);
433
434    draw_sidebar(f, app, panels[0]);
435    draw_main_panel(f, app, panels[1]);
436    draw_bottom_bar(f, app, ui[1]);
437}