diff --git a/src/main.rs b/src/main.rs index 03b159a..c7e1bbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,6 +67,9 @@ impl LogViewerApp { case_sensitive: config.case_sensitive, use_regex: config.use_regex, history: config.search_history, + date_range_enabled: false, + date_from: String::new(), + date_to: String::new(), }, indexing_state: IndexingState::new(), search_state: SearchState::new(), @@ -111,6 +114,9 @@ impl LogViewerApp { query: self.search_panel_state.query.clone(), case_sensitive: self.search_panel_state.case_sensitive, use_regex: self.search_panel_state.use_regex, + date_range_enabled: self.search_panel_state.date_range_enabled, + date_from: self.search_panel_state.date_from.clone(), + date_to: self.search_panel_state.date_to.clone(), }; let line_index = Arc::clone(&tab.line_index); @@ -306,7 +312,8 @@ impl eframe::App for LogViewerApp { } // Render filtered view with improved styling - let show_filtered = !self.search_panel_state.query.is_empty(); + // Show filtered view if there's a query OR if date range is enabled + let show_filtered = !self.search_panel_state.query.is_empty() || self.search_panel_state.date_range_enabled; let highlight_rules = self.highlight_manager.rules.clone(); let mut export_clicked = false; diff --git a/src/search.rs b/src/search.rs index a849d35..e2e9bef 100644 --- a/src/search.rs +++ b/src/search.rs @@ -43,6 +43,9 @@ pub struct SearchParams { pub query: String, pub case_sensitive: bool, pub use_regex: bool, + pub date_range_enabled: bool, + pub date_from: String, + pub date_to: String, } impl SearchParams { @@ -99,6 +102,47 @@ pub fn start_search( }); } +fn find_date_range( + params: &SearchParams, + line_index: &LineIndex, + file_path: &Path, +) -> Option<(usize, usize)> { + if !params.date_range_enabled || params.date_from.is_empty() || params.date_to.is_empty() { + return None; + } + + let file = File::open(file_path).ok()?; + let mut file_handle = BufReader::new(file); + + let mut start_line = None; + let mut end_line = None; + + // Find first line containing date_from text + for line_num in 0..line_index.total_lines { + if let Some(content) = line_index.read_line(&mut file_handle, line_num) { + if content.contains(¶ms.date_from) { + start_line = Some(line_num); + break; + } + } + } + + // Find last line containing date_to text + for line_num in (0..line_index.total_lines).rev() { + if let Some(content) = line_index.read_line(&mut file_handle, line_num) { + if content.contains(¶ms.date_to) { + end_line = Some(line_num + 1); // +1 to include this line + break; + } + } + } + + match (start_line, end_line) { + (Some(start), Some(end)) if start < end => Some((start, end)), + _ => None, + } +} + fn search_lines( params: &SearchParams, line_index: &LineIndex, @@ -110,17 +154,28 @@ fn search_lines( return Vec::new(); } - // Determine optimal chunk size based on total lines - // Aim for enough chunks to utilize all cores, but not too many to avoid overhead - let num_threads = rayon::current_num_threads(); - let min_chunk_size = 1000; // Process at least 1000 lines per chunk - let chunk_size = (total_lines / (num_threads * 4)).max(min_chunk_size); + // Determine the line range to search (all lines or date range) + let (search_start, search_end) = if let Some((start, end)) = find_date_range(params, line_index, file_path) { + (start, end) + } else { + (0, total_lines) + }; - // Split line numbers into chunks - let chunks: Vec<(usize, usize)> = (0..total_lines) + let lines_to_search = search_end - search_start; + if lines_to_search == 0 { + return Vec::new(); + } + + // Determine optimal chunk size based on lines to search + let num_threads = rayon::current_num_threads(); + let min_chunk_size = 1000; + let chunk_size = (lines_to_search / (num_threads * 4)).max(min_chunk_size); + + // Split line numbers into chunks within the search range + let chunks: Vec<(usize, usize)> = (search_start..search_end) .step_by(chunk_size) .map(|start| { - let end = (start + chunk_size).min(total_lines); + let end = (start + chunk_size).min(search_end); (start, end) }) .collect(); @@ -143,7 +198,14 @@ fn search_lines( // Process each line - only store line numbers, not content for (line_number, content) in lines { - if params.matches_line(&content, ®ex_matcher) { + // If date range is enabled and query is empty, include all lines in range + let should_include = if params.date_range_enabled && params.query.is_empty() { + true + } else { + params.matches_line(&content, ®ex_matcher) + }; + + if should_include { chunk_results.push(FilteredLine { line_number }); } } diff --git a/src/ui/search_panel.rs b/src/ui/search_panel.rs index 8fa4964..42575d0 100644 --- a/src/ui/search_panel.rs +++ b/src/ui/search_panel.rs @@ -8,6 +8,9 @@ pub struct SearchPanelState { pub case_sensitive: bool, pub use_regex: bool, pub history: Vec, + pub date_range_enabled: bool, + pub date_from: String, + pub date_to: String, } pub struct SearchPanelActions { @@ -75,8 +78,9 @@ pub fn render_search_panel( .checkbox(&mut state.case_sensitive, "Case sensitive") .changed(); let regex_changed = ui.checkbox(&mut state.use_regex, "Regex").changed(); + let date_range_changed = ui.checkbox(&mut state.date_range_enabled, "Date range").changed(); - if case_changed || regex_changed { + if case_changed || regex_changed || date_range_changed { actions.config_changed = true; } @@ -103,6 +107,28 @@ pub fn render_search_panel( } }); + // Date range fields (show when date_range_enabled is true) + if state.date_range_enabled { + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label("From:"); + ui.add( + egui::TextEdit::singleline(&mut state.date_from) + .desired_width(180.0) + .hint_text("2025-01-01 00:00:00") + ); + + ui.add_space(8.0); + + ui.label("To:"); + ui.add( + egui::TextEdit::singleline(&mut state.date_to) + .desired_width(180.0) + .hint_text("2025-01-01 01:00:00") + ); + }); + } + // Progress bar if search_state.is_searching() { ui.add_space(6.0);