diff --git a/Cargo.lock b/Cargo.lock index b55534b..70c6b0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,6 +661,19 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clipboard-win" version = "5.4.1" @@ -1520,6 +1533,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.58.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -2671,6 +2708,7 @@ dependencies = [ name = "rlogg" version = "0.2.0" dependencies = [ + "chrono", "eframe", "rayon", "regex", diff --git a/Cargo.toml b/Cargo.toml index 70321bc..5e7cc21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ regex = "1.11" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" rayon = "1.10" +chrono = "0.4" diff --git a/src/config.rs b/src/config.rs index f6d9342..ab7f034 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,6 +12,18 @@ pub struct AppConfig { pub last_search_query: String, #[serde(default)] pub highlight_rules: Vec, + #[serde(default)] + pub date_range_enabled: bool, + #[serde(default = "default_date_format")] + pub date_format: String, + #[serde(default)] + pub date_from: String, + #[serde(default)] + pub date_to: String, +} + +fn default_date_format() -> String { + String::from("%Y-%m-%d %H:%M:%S") } impl AppConfig { diff --git a/src/main.rs b/src/main.rs index c7e1bbe..2fb6014 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,9 +67,10 @@ 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(), + date_range_enabled: config.date_range_enabled, + date_format: config.date_format, + date_from: config.date_from, + date_to: config.date_to, }, indexing_state: IndexingState::new(), search_state: SearchState::new(), @@ -93,6 +94,10 @@ impl LogViewerApp { use_regex: self.search_panel_state.use_regex, last_search_query: self.search_panel_state.query.clone(), highlight_rules: self.highlight_manager.rules.clone(), + date_range_enabled: self.search_panel_state.date_range_enabled, + date_format: self.search_panel_state.date_format.clone(), + date_from: self.search_panel_state.date_from.clone(), + date_to: self.search_panel_state.date_to.clone(), }; config.save(); } @@ -110,11 +115,40 @@ impl LogViewerApp { fn handle_search(&mut self) { if let Some(tab) = self.active_tab() { + // Validate date format if date range is enabled + if self.search_panel_state.date_range_enabled { + use chrono::NaiveDateTime; + + // Validate format by trying to parse example dates + let test_date = "2025-01-01 00:00:00"; + if NaiveDateTime::parse_from_str(test_date, &self.search_panel_state.date_format).is_err() { + eprintln!("Invalid date format: {}", self.search_panel_state.date_format); + eprintln!("Expected format like: %Y-%m-%d %H:%M:%S"); + return; + } + + // Validate date_from and date_to can be parsed + if !self.search_panel_state.date_from.is_empty() { + if NaiveDateTime::parse_from_str(&self.search_panel_state.date_from, &self.search_panel_state.date_format).is_err() { + eprintln!("Invalid date_from format. Expected format: {}", self.search_panel_state.date_format); + return; + } + } + + if !self.search_panel_state.date_to.is_empty() { + if NaiveDateTime::parse_from_str(&self.search_panel_state.date_to, &self.search_panel_state.date_format).is_err() { + eprintln!("Invalid date_to format. Expected format: {}", self.search_panel_state.date_format); + return; + } + } + } + let params = SearchParams { 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_format: self.search_panel_state.date_format.clone(), date_from: self.search_panel_state.date_from.clone(), date_to: self.search_panel_state.date_to.clone(), }; diff --git a/src/search.rs b/src/search.rs index e2e9bef..3ab37ac 100644 --- a/src/search.rs +++ b/src/search.rs @@ -5,6 +5,7 @@ use std::io::BufReader; use std::path::Path; use std::sync::{Arc, Mutex}; use std::thread; +use chrono::NaiveDateTime; use crate::line_index::LineIndex; use crate::types::FilteredLine; @@ -44,6 +45,7 @@ pub struct SearchParams { pub case_sensitive: bool, pub use_regex: bool, pub date_range_enabled: bool, + pub date_format: String, pub date_from: String, pub date_to: String, } @@ -102,6 +104,107 @@ pub fn start_search( }); } +fn format_to_regex(format: &str) -> Option { + // Convert chrono format to regex pattern + let mut regex = format.to_string(); + regex = regex.replace("%Y", r"\d{4}"); // 4-digit year + regex = regex.replace("%m", r"\d{2}"); // 2-digit month + regex = regex.replace("%d", r"\d{2}"); // 2-digit day + regex = regex.replace("%H", r"\d{2}"); // 2-digit hour + regex = regex.replace("%M", r"\d{2}"); // 2-digit minute + regex = regex.replace("%S", r"\d{2}"); // 2-digit second + regex = regex.replace("%I", r"\d{2}"); // 2-digit hour (12h) + regex = regex.replace("%p", r"(AM|PM)"); // AM/PM + Some(regex) +} + +fn extract_and_parse_date(content: &str, format: &str) -> Option { + // Convert format to regex pattern + let pattern = format_to_regex(format)?; + let regex = Regex::new(&pattern).ok()?; + let date_str = regex.captures(content)?.get(0)?.as_str(); + + // Parse using the provided format + NaiveDateTime::parse_from_str(date_str, format).ok() +} + +fn find_line_with_date( + line_index: &LineIndex, + file_handle: &mut BufReader, + start_line: usize, + format: &str, + move_down: bool, +) -> Option<(usize, NaiveDateTime)> { + let max_search = 1000; // Search up to 10 lines + let range: Box> = if move_down { + Box::new(start_line..std::cmp::min(start_line + max_search, line_index.total_lines)) + } else { + Box::new((start_line.saturating_sub(max_search)..=start_line).rev()) + }; + + for line_num in range { + if let Some(content) = line_index.read_line(file_handle, line_num) { + if let Some(date) = extract_and_parse_date(&content, format) { + return Some((line_num, date)); + } + } + } + None +} + +fn binary_search_date( + line_index: &LineIndex, + file_handle: &mut BufReader, + target_date: &NaiveDateTime, + format: &str, + find_first: bool, // true = find first occurrence, false = find last +) -> Option { + let mut left = 0; + let mut right = line_index.total_lines; + let mut result = None; + + while left < right { + let mid = (left + right) / 2; + + // Find a line with a date starting from mid, moving down + match find_line_with_date(line_index, file_handle, mid, format, true) { + Some((line_with_date, date)) => { + if find_first { + // Finding first line >= target_date + if date >= *target_date { + result = Some(line_with_date); + right = mid; + } else { + left = mid + 1; + } + } else { + // Finding last line <= target_date + if date <= *target_date { + result = Some(line_with_date); + // Ensure we make progress in the binary search + left = mid + 1; + } else { + right = mid; + } + } + } + None => { + // Can't find a date near mid, try searching in a larger range + // or skip this section + if find_first { + // When finding first, if we can't find a date, try the right half + left = mid + 1; + } else { + // When finding last, if we can't find a date, try the left half + right = mid; + } + } + } + } + + result +} + fn find_date_range( params: &SearchParams, line_index: &LineIndex, @@ -111,35 +214,23 @@ fn find_date_range( return None; } + // Parse the target dates using the provided format + let date_from = NaiveDateTime::parse_from_str(¶ms.date_from, ¶ms.date_format).ok()?; + let date_to = NaiveDateTime::parse_from_str(¶ms.date_to, ¶ms.date_format).ok()?; + let file = File::open(file_path).ok()?; let mut file_handle = BufReader::new(file); - let mut start_line = None; - let mut end_line = None; + // Binary search for first line >= date_from + let start_line = binary_search_date(line_index, &mut file_handle, &date_from, ¶ms.date_format, true)?; - // 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; - } - } - } + // Binary search for last line <= date_to + let end_line = binary_search_date(line_index, &mut file_handle, &date_to, ¶ms.date_format, false)?; - // 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, + if start_line < end_line { + Some((start_line, end_line + 1)) // +1 to include the end line + } else { + None } } diff --git a/src/ui/search_panel.rs b/src/ui/search_panel.rs index 42575d0..3054173 100644 --- a/src/ui/search_panel.rs +++ b/src/ui/search_panel.rs @@ -9,6 +9,7 @@ pub struct SearchPanelState { pub use_regex: bool, pub history: Vec, pub date_range_enabled: bool, + pub date_format: String, pub date_from: String, pub date_to: String, } @@ -110,6 +111,16 @@ 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("Format:"); + ui.add( + egui::TextEdit::singleline(&mut state.date_format) + .desired_width(200.0) + .hint_text("%Y-%m-%d %H:%M:%S") + ); + }); + + ui.add_space(4.0); ui.horizontal(|ui| { ui.label("From:"); ui.add(