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 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}