diff --git a/src/communication/handlers/highlight.rs b/src/communication/handlers/highlight.rs index 9a4100d..e3fac60 100644 --- a/src/communication/handlers/highlight.rs +++ b/src/communication/handlers/highlight.rs @@ -1,66 +1,19 @@ use std::io::Result; use crossterm::event::KeyCode; -use regex::bytes::Regex; -use super::{handler::Handler, processor::ProcessorMethods}; +use super::{handler::Handler, processor::ProcessorMethods, search::SearchState}; use crate::{ - communication::{ - handlers::{processor::update_progress, user_input::UserInputHandler}, - input::InputType::Normal, - reader::MainWindow, - }, - constants::cli::{ - cli_chars::{COMMAND_CHAR, HIGHLIGHT_CHAR, NORMAL_STR, TOGGLE_HIGHLIGHT_CHAR}, - patterns::ANSI_COLOR_PATTERN, - }, + communication::reader::MainWindow, + constants::cli::cli_chars::{COMMAND_CHAR, HIGHLIGHT_CHAR, TOGGLE_HIGHLIGHT_CHAR}, ui::scroll::{self, ScrollState, update_current_match_index}, }; pub struct HighlightHandler { - color_pattern: Regex, - current_pattern: Option, - input_handler: UserInputHandler, + search: SearchState, } impl HighlightHandler { - /// Test a message to see if it matches the pattern while also escaping the color code - fn test(&self, message: &str) -> bool { - let clean_message = self - .color_pattern - .replace_all(message.as_bytes(), "".as_bytes()); - match &self.current_pattern { - Some(pattern) => pattern.is_match(&clean_message), - None => panic!("Match called with no pattern!"), - } - } - - /// Save the user input pattern to the main window config - fn set_pattern(&mut self, window: &mut MainWindow) -> Result<()> { - let pattern = match self.input_handler.gather(window) { - Ok(pattern) => pattern, - Err(why) => panic!("Unable to gather text: {why:?}"), - }; - - self.current_pattern = match Regex::new(&pattern) { - Ok(regex) => { - window.config.current_status = Some(format!("Highlight with pattern /{pattern}/")); - window.write_status()?; - - // Update the main window's regex - window.config.regex_pattern = Some(regex.clone()); - Some(regex) - } - Err(e) => { - window.write_to_command_line(&format!("Invalid regex: /{pattern}/ ({e})"))?; - None - } - }; - window.set_cli_cursor(Some(NORMAL_STR))?; - window.config.highlight_match = true; - Ok(()) - } - /// Internal implementation of pg_up to skip to the previous match fn pg_up(&self, window: &mut MainWindow) { update_current_match_index(window, true); @@ -73,55 +26,21 @@ impl HighlightHandler { } impl ProcessorMethods for HighlightHandler { - /// Process matches, loading the buffer of indexes to matched messages in the main buffer fn process_matches(&mut self, window: &mut MainWindow) -> Result<()> { - let mut wrote_progress = false; - if self.current_pattern.is_some() { - // Start from where we left off to the most recent message - let start = window.config.last_index_regexed; - let end = window.messages().len(); - - for index in start..end { - if self.test(&window.messages()[index]) { - window.config.matched_rows.push(index); - } - - // Update the user interface with the current state - wrote_progress = update_progress(window, start, end, index)?; - - // Update the last spot so we know where to start next time - window.config.last_index_regexed = index + 1; - } - if wrote_progress { - window.write_status()?; - } - } - Ok(()) + self.search.process_matches(window) } - /// Return the app to a normal input state fn return_to_normal(&mut self, window: &mut MainWindow) -> Result<()> { self.clear_matches(window)?; window.config.current_matched_row = 0; - window.config.current_status = None; - window.update_input_type(Normal)?; - window.set_cli_cursor(None)?; - self.input_handler.gather(window)?; - window.redraw()?; - Ok(()) + self.search.finish_return_to_normal(window) } - /// Clear the matched messages from the message buffer fn clear_matches(&mut self, window: &mut MainWindow) -> Result<()> { - self.current_pattern = None; - window.config.regex_pattern = None; - window.config.matched_rows.clear(); - window.config.last_index_regexed = 0; - window.config.highlight_match = false; + self.search.clear_matches(window)?; if matches!(window.config.scroll_state, ScrollState::Centered) { window.config.scroll_state = ScrollState::Free; } - window.reset_command_line()?; Ok(()) } } @@ -129,14 +48,12 @@ impl ProcessorMethods for HighlightHandler { impl Handler for HighlightHandler { fn new() -> HighlightHandler { HighlightHandler { - color_pattern: Regex::new(ANSI_COLOR_PATTERN).unwrap(), - current_pattern: None, - input_handler: UserInputHandler::new(), + search: SearchState::new(), } } fn receive_input(&mut self, window: &mut MainWindow, key: KeyCode) -> Result<()> { - match &self.current_pattern { + match &self.search.current_pattern { Some(_) => match key { // Scroll KeyCode::Down => scroll::down(window), @@ -170,15 +87,15 @@ impl Handler for HighlightHandler { }, None => match key { KeyCode::Enter => { - self.set_pattern(window)?; - if self.current_pattern.is_some() { + self.search.set_pattern(window, "Highlight")?; + if self.search.current_pattern.is_some() { window.reset_output()?; self.process_matches(window)?; } window.redraw()?; } KeyCode::Esc => self.return_to_normal(window)?, - key => self.input_handler.receive_input(window, key)?, + key => self.search.input_handler.receive_input(window, key)?, }, } window.redraw()?; @@ -213,7 +130,7 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); assert_eq!( vec![0, 10, 20, 30, 40, 50, 60, 70, 80, 90], @@ -231,7 +148,7 @@ mod tests { // Set regex pattern let pattern = "a"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); logria.config.regex_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); assert_eq!(0, logria.config.matched_rows.len()); @@ -247,11 +164,11 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); handler.return_to_normal(&mut logria).unwrap(); - assert!(handler.current_pattern.is_none()); + assert!(handler.search.current_pattern.is_none()); assert!(logria.config.regex_pattern.is_none()); assert_eq!(logria.config.matched_rows.len(), 0); assert_eq!(logria.config.last_index_regexed, 0); @@ -267,7 +184,7 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); assert_eq!(100, logria.config.last_index_regexed); } @@ -292,7 +209,7 @@ mod tests { // Set state to highlight mode logria.input_type = InputType::Highlight; - handler.test("test"); + handler.search.test("test"); } #[test] @@ -305,7 +222,7 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); // Normally this is set by `set_pattern()` but that requires user input logria.config.regex_pattern = Some(Regex::new(pattern).unwrap()); @@ -334,7 +251,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for page up @@ -375,7 +292,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for page up @@ -405,7 +322,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for scroll up to change to free scrolling, then page up @@ -449,7 +366,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for scroll up, then page up @@ -491,7 +408,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for page down @@ -536,7 +453,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for page down @@ -583,7 +500,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for scroll up to change to free scrolling, then page down @@ -631,7 +548,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for scroll up, then page up @@ -679,7 +596,7 @@ mod tests { // Set regex pattern let pattern = "0"; // Matches every 10th message - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); // Simulate keystroke for scroll up, then page up diff --git a/src/communication/handlers/mod.rs b/src/communication/handlers/mod.rs index c29d80f..a9e3667 100644 --- a/src/communication/handlers/mod.rs +++ b/src/communication/handlers/mod.rs @@ -6,5 +6,6 @@ pub mod normal; pub mod parser; pub mod processor; pub mod regex; +pub mod search; pub mod startup; pub mod user_input; diff --git a/src/communication/handlers/regex.rs b/src/communication/handlers/regex.rs index b9ac7e0..0806b30 100644 --- a/src/communication/handlers/regex.rs +++ b/src/communication/handlers/regex.rs @@ -1,130 +1,41 @@ use std::io::Result; use crossterm::event::KeyCode; -use regex::bytes::Regex; -use super::{handler::Handler, processor::ProcessorMethods}; +use super::{handler::Handler, processor::ProcessorMethods, search::SearchState}; use crate::{ - communication::{ - handlers::{processor::update_progress, user_input::UserInputHandler}, - input::InputType::Normal, - reader::MainWindow, - }, - constants::cli::{ - cli_chars::{COMMAND_CHAR, NORMAL_STR, REGEX_CHAR, TOGGLE_HIGHLIGHT_CHAR}, - patterns::ANSI_COLOR_PATTERN, - }, + communication::reader::MainWindow, + constants::cli::cli_chars::{COMMAND_CHAR, REGEX_CHAR, TOGGLE_HIGHLIGHT_CHAR}, ui::scroll, }; pub struct RegexHandler { - color_pattern: Regex, - current_pattern: Option, - input_handler: UserInputHandler, -} - -impl RegexHandler { - /// Test a message to see if it matches the pattern while also escaping the color code - fn test(&self, message: &str) -> bool { - // TODO: Possibly without the extra allocation here? - let clean_message = self - .color_pattern - .replace_all(message.as_bytes(), "".as_bytes()); - match &self.current_pattern { - Some(pattern) => pattern.is_match(&clean_message), - None => panic!("Match called with no pattern!"), - } - } - - /// Save the user input pattern to the main window config - fn set_pattern(&mut self, window: &mut MainWindow) -> Result<()> { - let pattern = match self.input_handler.gather(window) { - Ok(pattern) => pattern, - Err(why) => panic!("Unable to gather text: {why:?}"), - }; - - self.current_pattern = match Regex::new(&pattern) { - Ok(regex) => { - window.config.current_status = Some(format!("Regex with pattern /{pattern}/")); - window.write_status()?; - - // Update the main window's regex - window.config.regex_pattern = Some(regex.clone()); - Some(regex) - } - Err(e) => { - window.write_to_command_line(&format!("Invalid regex: /{pattern}/ ({e})"))?; - None - } - }; - window.set_cli_cursor(Some(NORMAL_STR))?; - window.config.highlight_match = true; - Ok(()) - } + search: SearchState, } impl ProcessorMethods for RegexHandler { - /// Process matches, loading the buffer of indexes to matched messages in the main buffer fn process_matches(&mut self, window: &mut MainWindow) -> Result<()> { - let mut wrote_progress = false; - if self.current_pattern.is_some() { - // Start from where we left off to the most recent message - // Start from where we left off to the most recent message - let start = window.config.last_index_regexed; - let end = window.messages().len(); - - for index in start..end { - if self.test(&window.messages()[index]) { - window.config.matched_rows.push(index); - } - - // Update the user interface with the current state - wrote_progress = update_progress(window, start, end, index)?; - - // Update the last spot so we know where to start next time - window.config.last_index_regexed = index + 1; - } - if wrote_progress { - window.write_status()?; - } - } - Ok(()) + self.search.process_matches(window) } - /// Return the app to a normal input state fn return_to_normal(&mut self, window: &mut MainWindow) -> Result<()> { - self.clear_matches(window)?; - window.config.current_status = None; - window.update_input_type(Normal)?; - window.set_cli_cursor(None)?; - self.input_handler.gather(window)?; - window.redraw()?; - Ok(()) + self.search.return_to_normal(window) } - /// Clear the matched messages from the message buffer fn clear_matches(&mut self, window: &mut MainWindow) -> Result<()> { - self.current_pattern = None; - window.config.regex_pattern = None; - window.config.matched_rows.clear(); - window.config.last_index_regexed = 0; - window.config.highlight_match = false; - window.reset_command_line()?; - Ok(()) + self.search.clear_matches(window) } } impl Handler for RegexHandler { fn new() -> RegexHandler { RegexHandler { - color_pattern: Regex::new(ANSI_COLOR_PATTERN).unwrap(), - current_pattern: None, - input_handler: UserInputHandler::new(), + search: SearchState::new(), } } fn receive_input(&mut self, window: &mut MainWindow, key: KeyCode) -> Result<()> { - match &self.current_pattern { + match &self.search.current_pattern { Some(_) => match key { // Scroll KeyCode::Down => scroll::down(window), @@ -158,15 +69,15 @@ impl Handler for RegexHandler { }, None => match key { KeyCode::Enter => { - self.set_pattern(window)?; - if self.current_pattern.is_some() { + self.search.set_pattern(window, "Regex")?; + if self.search.current_pattern.is_some() { window.reset_output()?; self.process_matches(window)?; } window.redraw()?; } KeyCode::Esc => self.return_to_normal(window)?, - key => self.input_handler.receive_input(window, key)?, + key => self.search.input_handler.receive_input(window, key)?, }, } window.redraw()?; @@ -198,7 +109,7 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); assert_eq!( vec![0, 10, 20, 30, 40, 50, 60, 70, 80, 90], @@ -216,7 +127,7 @@ mod tests { // Set regex pattern let pattern = "a"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); logria.config.regex_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); assert_eq!(0, logria.config.matched_rows.len()); @@ -232,11 +143,11 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); handler.return_to_normal(&mut logria).unwrap(); - assert!(handler.current_pattern.is_none()); + assert!(handler.search.current_pattern.is_none()); assert!(logria.config.regex_pattern.is_none()); assert_eq!(logria.config.matched_rows.len(), 0); assert_eq!(logria.config.last_index_regexed, 0); @@ -252,7 +163,7 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); handler.process_matches(&mut logria).unwrap(); assert_eq!(100, logria.config.last_index_regexed); } @@ -277,7 +188,7 @@ mod tests { // Set state to regex mode logria.input_type = InputType::Regex; - handler.test("test"); + handler.search.test("test"); } #[test] @@ -290,7 +201,7 @@ mod tests { // Set regex pattern let pattern = "0"; - handler.current_pattern = Some(Regex::new(pattern).unwrap()); + handler.search.current_pattern = Some(Regex::new(pattern).unwrap()); // Normally this is set by `set_pattern()` but that requires user input logria.config.regex_pattern = Some(Regex::new(pattern).unwrap()); diff --git a/src/communication/handlers/search.rs b/src/communication/handlers/search.rs new file mode 100644 index 0000000..6587f22 --- /dev/null +++ b/src/communication/handlers/search.rs @@ -0,0 +1,122 @@ +use std::io::Result; + +use regex::bytes::Regex; + +use super::{handler::Handler, processor::update_progress, user_input::UserInputHandler}; +use crate::{ + communication::{input::InputType::Normal, reader::MainWindow}, + constants::cli::{cli_chars::NORMAL_STR, patterns::ANSI_COLOR_PATTERN}, +}; + +/// Shared state and logic for regex filtering and highlight searching. +/// +/// Both `RegexHandler` and `HighlightHandler` compose this struct +/// to avoid duplicating pattern matching, match processing, and +/// cleanup logic. +pub struct SearchState { + color_pattern: Regex, + pub current_pattern: Option, + pub input_handler: UserInputHandler, +} + +impl SearchState { + pub fn new() -> Self { + SearchState { + color_pattern: Regex::new(ANSI_COLOR_PATTERN).unwrap(), + current_pattern: None, + input_handler: UserInputHandler::new(), + } + } + + /// Test a message to see if it matches the pattern while also escaping the color code + pub fn test(&self, message: &str) -> bool { + // TODO: Possibly without the extra allocation here? + let clean_message = self + .color_pattern + .replace_all(message.as_bytes(), "".as_bytes()); + match &self.current_pattern { + Some(pattern) => pattern.is_match(&clean_message), + None => panic!("Match called with no pattern!"), + } + } + + /// Save the user input pattern to the main window config + pub fn set_pattern(&mut self, window: &mut MainWindow, mode_name: &str) -> Result<()> { + let pattern = match self.input_handler.gather(window) { + Ok(pattern) => pattern, + Err(why) => panic!("Unable to gather text: {why:?}"), + }; + + self.current_pattern = match Regex::new(&pattern) { + Ok(regex) => { + window.config.current_status = + Some(format!("{mode_name} with pattern /{pattern}/")); + window.write_status()?; + + // Update the main window's regex + window.config.regex_pattern = Some(regex.clone()); + Some(regex) + } + Err(e) => { + window.write_to_command_line(&format!("Invalid regex: /{pattern}/ ({e})"))?; + None + } + }; + window.set_cli_cursor(Some(NORMAL_STR))?; + window.config.highlight_match = true; + Ok(()) + } + + /// Process matches, loading the buffer of indexes to matched messages in the main buffer + pub fn process_matches(&self, window: &mut MainWindow) -> Result<()> { + let mut wrote_progress = false; + if self.current_pattern.is_some() { + // Start from where we left off to the most recent message + let start = window.config.last_index_regexed; + let end = window.messages().len(); + + for index in start..end { + if self.test(&window.messages()[index]) { + window.config.matched_rows.push(index); + } + + // Update the user interface with the current state + wrote_progress = update_progress(window, start, end, index)?; + + // Update the last spot so we know where to start next time + window.config.last_index_regexed = index + 1; + } + if wrote_progress { + window.write_status()?; + } + } + Ok(()) + } + + /// Clear the matched messages from the message buffer + pub fn clear_matches(&mut self, window: &mut MainWindow) -> Result<()> { + self.current_pattern = None; + window.config.regex_pattern = None; + window.config.matched_rows.clear(); + window.config.last_index_regexed = 0; + window.config.highlight_match = false; + window.reset_command_line()?; + Ok(()) + } + + /// Finish returning to normal mode after clear_matches has been called + pub fn finish_return_to_normal(&mut self, window: &mut MainWindow) -> Result<()> { + window.config.current_status = None; + window.update_input_type(Normal)?; + window.set_cli_cursor(None)?; + self.input_handler.gather(window)?; + window.redraw()?; + Ok(()) + } + + /// Return the app to a normal input state + pub fn return_to_normal(&mut self, window: &mut MainWindow) -> Result<()> { + self.clear_matches(window)?; + self.finish_return_to_normal(window) + } +} diff --git a/src/communication/reader.rs b/src/communication/reader.rs index ae8a795..4cce33c 100644 --- a/src/communication/reader.rs +++ b/src/communication/reader.rs @@ -337,7 +337,7 @@ impl MainWindow { // If have fewer messages than lines, just render it all end = message_pointer_length - 1; } else if (self.config.current_end < self.config.last_row as usize) - | (self.config.current_end < message_pointer_length) + || (self.config.current_end < message_pointer_length) { // If the last row we rendered comes before the last row we can render, // use all of the available rows diff --git a/src/extensions/parser.rs b/src/extensions/parser.rs index b86e0eb..db51397 100644 --- a/src/extensions/parser.rs +++ b/src/extensions/parser.rs @@ -167,7 +167,7 @@ impl Parser { } AggregationMethod::Mode => { self.aggregator_map - .insert(method_name.to_string(), Box::new(Counter::mean())); + .insert(method_name.to_string(), Box::new(Counter::mode())); } AggregationMethod::Sum => { self.aggregator_map diff --git a/src/util/aggregators/counter.rs b/src/util/aggregators/counter.rs index 105c3e0..67c4467 100644 --- a/src/util/aggregators/counter.rs +++ b/src/util/aggregators/counter.rs @@ -51,7 +51,7 @@ impl Counter { } /// Creates a new `Counter` configured to return only the top message. - pub fn mean() -> Counter { + pub fn mode() -> Counter { Counter { counts: HashMap::new(), total_count: 0,