diff --git a/assets/icons8-add-48-black.png b/assets/icons8-add-48-black.png deleted file mode 100644 index b75bb8e..0000000 Binary files a/assets/icons8-add-48-black.png and /dev/null differ diff --git a/assets/icons8-add-48-white.png b/assets/icons8-add-48-white.png deleted file mode 100644 index 7a17ec6..0000000 Binary files a/assets/icons8-add-48-white.png and /dev/null differ diff --git a/assets/icons8-copy-48-white.png b/assets/icons8-copy-48-white.png deleted file mode 100644 index 7ce7951..0000000 Binary files a/assets/icons8-copy-48-white.png and /dev/null differ diff --git a/assets/icons8-duplicate-32-white.png b/assets/icons8-duplicate-32-white.png new file mode 100644 index 0000000..7f1c042 Binary files /dev/null and b/assets/icons8-duplicate-32-white.png differ diff --git a/assets/icons8-link-24-white.png b/assets/icons8-link-24-white.png new file mode 100644 index 0000000..bcc6a8b Binary files /dev/null and b/assets/icons8-link-24-white.png differ diff --git a/assets/icons8-minus-24-white.png b/assets/icons8-minus-24-white.png new file mode 100644 index 0000000..330c7ce Binary files /dev/null and b/assets/icons8-minus-24-white.png differ diff --git a/assets/icons8-minus-48-black.png b/assets/icons8-minus-48-black.png deleted file mode 100644 index c5eeabf..0000000 Binary files a/assets/icons8-minus-48-black.png and /dev/null differ diff --git a/assets/icons8-minus-48-white.png b/assets/icons8-minus-48-white.png deleted file mode 100644 index af1534d..0000000 Binary files a/assets/icons8-minus-48-white.png and /dev/null differ diff --git a/assets/icons8-plus-24-white.png b/assets/icons8-plus-24-white.png new file mode 100644 index 0000000..efb8dde Binary files /dev/null and b/assets/icons8-plus-24-white.png differ diff --git a/assets/icons8-repeat-24-white.png b/assets/icons8-repeat-24-white.png new file mode 100644 index 0000000..0ea5bf9 Binary files /dev/null and b/assets/icons8-repeat-24-white.png differ diff --git a/assets/icons8-search-24-white.png b/assets/icons8-search-24-white.png new file mode 100644 index 0000000..656a403 Binary files /dev/null and b/assets/icons8-search-24-white.png differ diff --git a/assets/icons8-update-50-white.png b/assets/icons8-update-50-white.png new file mode 100644 index 0000000..8d2d889 Binary files /dev/null and b/assets/icons8-update-50-white.png differ diff --git a/src/app/filtered_log_entries_tab.rs b/src/app/filtered_log_entries_tab.rs index fcc9b05..2f9ef6d 100644 --- a/src/app/filtered_log_entries_tab.rs +++ b/src/app/filtered_log_entries_tab.rs @@ -1,18 +1,19 @@ +use std::time::SystemTime; use std::{ fs::File, io, path::{Path, PathBuf}, }; -use egui::Ui; -use grep::searcher::{Searcher, sinks::Lossy}; +use egui::{include_image, Button, CursorIcon, Ui}; +use grep::searcher::{sinks::Lossy, Searcher}; use grep_regex::RegexMatcherBuilder; use log::error; use super::{ log_entries_table::LogEntriesTable, log_file_reader::{LineNumber, LogFileReader}, - log_view::{LogViewerState, LogViewTabTrait}, + log_view::{LogViewTabTrait, LogViewerState}, }; #[derive(Debug)] @@ -57,7 +58,9 @@ pub struct FilteredLogEntriesTab { search_term: String, search_results: Vec, search_options: SearchOptions, - selected_line: Option + log_entries_table: LogEntriesTable, + repeat_search: bool, + last_search_time: Option, } impl FilteredLogEntriesTab { @@ -68,7 +71,9 @@ impl FilteredLogEntriesTab { search_results: vec![], search_options: Default::default(), editable_search_term: Default::default(), - selected_line: None + log_entries_table: LogEntriesTable::new(), + repeat_search: true, + last_search_time: None, }) } @@ -112,6 +117,12 @@ impl FilteredLogEntriesTab { fn execute_search(&mut self) { self.search_term = self.editable_search_term.clone(); + self.last_search_time = Some(SystemTime::now()); + + if self.search_term.is_empty() { + self.search_results.clear(); + return; + } match Self::search(&self.search_options, &self.log_file_path, &self.search_term) { Ok(results) => { @@ -183,17 +194,40 @@ impl LogViewTabTrait for FilteredLogEntriesTab { log_reader: &mut LogFileReader, viewer_state: &mut LogViewerState, ) { - self.ui_search(ui); + let mut repeat_search = self.repeat_search; - let mut log_entries_table = LogEntriesTable::new() - .filtered_lines(&self.search_results) - .select_line(viewer_state.selected_line_num); + self.ui_search(ui); - if self.selected_line != viewer_state.selected_line_num { - self.selected_line = viewer_state.selected_line_num; - log_entries_table = log_entries_table.scroll_to_selected(); + if repeat_search && log_reader.load_time_point().is_some() { + let search_needed = match self.last_search_time { + None => true, + Some(last_search_time) => last_search_time < log_reader.load_time_point().unwrap(), + }; + if search_needed { + self.execute_search(); + } } - log_entries_table.ui(ui, log_reader, viewer_state); + self.log_entries_table.ui( + ui, + log_reader, + viewer_state, + Some(&self.search_results), + |ui| { + if ui + .add( + Button::image(include_image!("../../assets/icons8-repeat-24-white.png")) + .selected(repeat_search), + ) + .on_hover_cursor(CursorIcon::PointingHand) + .on_hover_text("Repeat Search on Change") + .clicked() + { + repeat_search = !repeat_search; + }; + }, + ); + + self.repeat_search = repeat_search; } } diff --git a/src/app/log_entries_tab.rs b/src/app/log_entries_tab.rs index 0e40db7..19684fa 100644 --- a/src/app/log_entries_tab.rs +++ b/src/app/log_entries_tab.rs @@ -1,17 +1,18 @@ -use crate::app::log_file_reader::LineNumber; use super::{ log_entries_table::LogEntriesTable, - log_file_reader::{LogFileReader}, - log_view::{LogViewerState, LogViewTabTrait}, + log_file_reader::LogFileReader, + log_view::{LogViewTabTrait, LogViewerState}, }; pub struct LogEntriesTab { - selected_line: Option + log_entries_table: LogEntriesTable, } impl LogEntriesTab { pub fn new() -> Box { - Box::new(Self { selected_line: None }) + Box::new(Self { + log_entries_table: LogEntriesTable::new(), + }) } } @@ -24,17 +25,9 @@ impl LogViewTabTrait for LogEntriesTab { &mut self, ui: &mut egui::Ui, log_reader: &mut LogFileReader, - viewer_state: &mut LogViewerState, + viewer_state: &mut LogViewerState, ) { - - let mut log_entries_table = - LogEntriesTable::new().select_line(viewer_state.selected_line_num); - - if self.selected_line != viewer_state.selected_line_num { - self.selected_line = viewer_state.selected_line_num; - log_entries_table = log_entries_table.scroll_to_selected(); - } - - log_entries_table.ui(ui, log_reader, viewer_state); + self.log_entries_table + .ui(ui, log_reader, viewer_state, None, |_| {}); } } diff --git a/src/app/log_entries_table.rs b/src/app/log_entries_table.rs index 19059b1..f3a5b24 100644 --- a/src/app/log_entries_table.rs +++ b/src/app/log_entries_table.rs @@ -9,65 +9,56 @@ use crate::app::log_view::{ColumnTextColor, LogViewerState}; use super::log_file_reader::{LineNumber, LogFileReader}; -pub struct LogEntriesTable<'a> { - filtered_lines: Option<&'a [LineNumber]>, +pub struct LogEntriesTable { selected_line: Option, scroll_to_selected: bool, + sync_line_selection: bool, + tail_log: bool, } -impl<'a> LogEntriesTable<'a> { +impl LogEntriesTable { fn add_tool_button( - ui: &mut egui::Ui, + ui: &mut Ui, image_source: ImageSource<'_>, hover_text: &str, ) -> Response { - ui.add_sized(Vec2::new(18.0, 18.0), Button::image(image_source)) + ui.add_sized(Vec2::new(16.0, 16.0), Button::image(image_source)) .on_hover_cursor(CursorIcon::PointingHand) .on_hover_text(hover_text) } pub fn new() -> Self { Self { - filtered_lines: None, selected_line: None, scroll_to_selected: false, + sync_line_selection: true, + tail_log: false, } } - pub fn scroll_to_selected(mut self) -> Self { - self.scroll_to_selected = true; - self - } - - pub fn select_line(mut self, row: Option) -> Self { - self.selected_line = row; - self - } - - pub fn filtered_lines(mut self, lines: &'a [LineNumber]) -> Self { - self.filtered_lines = Some(lines); - self - } - pub fn ui( &mut self, ui: &mut Ui, log_file_reader: &mut LogFileReader, viewer_state: &mut LogViewerState, + filtered_entries: Option<&[LineNumber]>, + add_toolbar_contents: impl FnOnce(&mut Ui) ) { - let total_rows = match self.filtered_lines { + self.toolbar_ui(ui, log_file_reader, viewer_state, add_toolbar_contents); + + let total_rows = match filtered_entries { Some(lines) => lines.len(), - None => log_file_reader.line_count() as usize, + None => log_file_reader.line_count(), }; let mut table_builder = TableBuilder::new(ui) .max_scroll_height(f32::INFINITY) - .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) + .cell_layout(egui::Layout::left_to_right(Align::Center)) .striped(true) .auto_shrink(false) .min_scrolled_height(0.0) .sense(egui::Sense::click()); - + let mut col_iter = viewer_state.displayed_columns.iter().peekable(); while let Some(col_key) = col_iter.next() { let is_last_col = col_iter.peek().is_none(); @@ -85,10 +76,15 @@ impl<'a> LogEntriesTable<'a> { table_builder = table_builder.column(col_desc); } - if self.scroll_to_selected { - if let Some(selected_line) = self.selected_line { - if let Some(selected_row) = self.find_row_for_line(selected_line) { + if self.tail_log { + if let Some(row) = self.last_row_index(log_file_reader, filtered_entries) { + table_builder = table_builder.scroll_to_row(row, Some(Align::BOTTOM)); + } + } else if self.sync_line_selection && self.selected_line != viewer_state.selected_line_num { + if let Some(selected_line) = viewer_state.selected_line_num { + if let Some(selected_row) = self.find_row_for_line(selected_line, filtered_entries) { table_builder = table_builder.scroll_to_row(selected_row, Some(Align::Center)); + self.selected_line = viewer_state.selected_line_num; } } self.scroll_to_selected = false; @@ -102,7 +98,7 @@ impl<'a> LogEntriesTable<'a> { row.col(|ui| { ui.label(RichText::new(displayed_column).strong()); if columns_displayed_count > 1 { - let rm_icon = include_image!("../../assets/icons8-minus-48-white.png"); + let rm_icon = include_image!("../../assets/icons8-minus-24-white.png"); if Self::add_tool_button(ui, rm_icon, "Remove Column").clicked() { columns_to_remove.push(displayed_column.clone()); } @@ -121,7 +117,7 @@ impl<'a> LogEntriesTable<'a> { .body(|body| { body.rows(16.0, total_rows, |mut row| { let row_idx = row.index(); - let line_number = match self.filtered_lines { + let line_number = match filtered_entries { Some(lines) => lines[row_idx], None => row_idx, }; @@ -132,7 +128,9 @@ impl<'a> LogEntriesTable<'a> { if row.response().clicked() { self.selected_line = Some(line_number); - viewer_state.selected_line_num = self.selected_line; + if self.sync_line_selection { + viewer_state.selected_line_num = self.selected_line; + } } }); }); @@ -141,13 +139,26 @@ impl<'a> LogEntriesTable<'a> { /// Maps a line number to a table row. /// If there is a set of filtered lines set, a binary search is performed to /// find the correct row. Otherwise, the line number is returned as the row. - fn find_row_for_line(&self, line_number: LineNumber) -> Option { - match self.filtered_lines { + fn find_row_for_line(&self, line_number: LineNumber, filtered_entries: Option<&[LineNumber]>) -> Option { + match filtered_entries { Some(lines) => Some(lines.binary_search(&line_number).ok()?), None => Some(line_number), } } + fn last_row_index(&self, log_file_reader: &LogFileReader, filtered_entries: Option<&[LineNumber]>) -> Option { + match filtered_entries { + Some(lines) => { + if lines.is_empty() { + None + } else { + Some(lines.len() - 1) + } + } + None => Some(log_file_reader.line_count() - 1), + } + } + fn ui_logline( log_file_reader: &mut LogFileReader, viewer_state: &mut LogViewerState, @@ -217,6 +228,41 @@ impl<'a> LogEntriesTable<'a> { Some(()) } + fn toolbar_ui( + &mut self, + ui: &mut Ui, + _log_file_reader: &mut LogFileReader, + _log_viewer_state: &mut LogViewerState, + add_toolbar_contents: impl FnOnce(&mut Ui) + Sized, + ) { + ui.horizontal(|ui| { + if ui + .add( + Button::image(include_image!("../../assets/icons8-update-50-white.png")) + .selected(self.tail_log), + ) + .on_hover_cursor(CursorIcon::PointingHand) + .on_hover_text("Tail Log") + .clicked() + { + self.tail_log = !self.tail_log; + }; + if ui + .add( + Button::image(include_image!("../../assets/icons8-link-24-white.png")) + .selected(self.sync_line_selection), + ) + .on_hover_cursor(CursorIcon::PointingHand) + .on_hover_text("Sync Selection") + .clicked() + { + self.sync_line_selection = !self.sync_line_selection; + }; + + add_toolbar_contents(ui); + }); + ui.separator(); + } } fn color_from_loglevel(level: &str) -> Color32 { diff --git a/src/app/log_entry_context_tab.rs b/src/app/log_entry_context_tab.rs index 72bfa9c..0b09fa0 100644 --- a/src/app/log_entry_context_tab.rs +++ b/src/app/log_entry_context_tab.rs @@ -21,7 +21,7 @@ impl LogEntryContextTab { image_source: ImageSource<'_>, hover_text: &str, ) -> Response { - ui.add_sized(Vec2::new(18.0, 18.0), Button::image(image_source)) + ui.add_sized(Vec2::new(16.0, 16.0), Button::image(image_source)) .on_hover_text(hover_text) .on_hover_cursor(CursorIcon::PointingHand) } @@ -78,7 +78,7 @@ impl LogViewTabTrait for LogEntryContextTab { viewer_state.displayed_columns.iter().any(|s| s == key_str); if !column_is_shown { let add_icon = - include_image!("../../assets/icons8-add-48-white.png"); + include_image!("../../assets/icons8-plus-24-white.png"); if Self::add_tool_button(ui, add_icon, "Add Column").clicked() { viewer_state.displayed_columns.push(key_str.to_string()); @@ -95,7 +95,7 @@ impl LogViewTabTrait for LogEntryContextTab { }); row.col(|ui| { let copy_icon = - include_image!("../../assets/icons8-copy-48-white.png"); + include_image!("../../assets/icons8-duplicate-32-white.png"); if Self::add_tool_button(ui, copy_icon, "Copy Value").clicked() { ui.output_mut(|o| { o.copied_text = value_str.clone(); diff --git a/src/app/log_file_reader.rs b/src/app/log_file_reader.rs index e1ffc48..1c2e229 100644 --- a/src/app/log_file_reader.rs +++ b/src/app/log_file_reader.rs @@ -4,6 +4,7 @@ use std::{ io::{self, BufReader, Read, Seek, SeekFrom}, path::Path, }; +use std::time::SystemTime; use crossbeam_channel::Receiver; use grep::searcher::{Searcher, Sink, SinkMatch}; @@ -44,6 +45,7 @@ pub struct LogFileReader { buf_reader: BufReader, line_map: Vec, file_size: FileOffset, + load_time_point: Option, _watcher: Box, watcher_recv: Receiver>, } @@ -63,6 +65,7 @@ impl LogFileReader { buf_reader: BufReader::new(file), line_map: Vec::new(), file_size: 0, + load_time_point: None, _watcher: Box::new(watcher), watcher_recv: rx, }) @@ -96,6 +99,7 @@ impl LogFileReader { self.file_size = self.buf_reader.stream_position()?; self.line_map.push(self.file_size); + self.load_time_point = Some(SystemTime::now()); Ok(self.line_count()) } @@ -103,6 +107,10 @@ impl LogFileReader { self.watcher_recv.try_recv().is_ok() } + pub fn load_time_point(&self) -> Option { + self.load_time_point + } + /// Returns the total number of lines counted in the file /// Only valid after a successful load. pub fn line_count(&self) -> usize {