From fc3a2e69ec834b209ee6f4e1dbf97b4d1cc96c81 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 2 Feb 2020 23:15:24 -0500 Subject: Made search look prettier and organized it a bit... also added match whole word functionality. --- src/app.rs | 152 +++++++++++++++++++++++++++++++++++++++++----------------- src/canvas.rs | 94 ++++++++++++++++++++++++++---------- src/main.rs | 28 +++++++++-- 3 files changed, 202 insertions(+), 72 deletions(-) (limited to 'src') diff --git a/src/app.rs b/src/app.rs index 57dbaf89..455b3437 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,8 +46,64 @@ pub struct AppScrollWidgetState { pub widget_scroll_position: i64, } -/// AppSearchState only deals with the search's state. -pub struct AppSearchState {} +/// AppSearchState only deals with the search's current settings and state. +pub struct AppSearchState { + current_search_query: String, + searching_pid: bool, + ignore_case: bool, + current_regex: std::result::Result, + current_cursor_position: usize, + match_word: bool, + use_regex: bool, +} + +impl Default for AppSearchState { + fn default() -> Self { + AppSearchState { + current_search_query: String::default(), + searching_pid: false, + ignore_case: false, + current_regex: BASE_REGEX.clone(), + current_cursor_position: 0, + match_word: false, + use_regex: false, + } + } +} + +impl AppSearchState { + pub fn toggle_ignore_case(&mut self) { + self.ignore_case = !self.ignore_case; + } + + pub fn toggle_search_whole_word(&mut self) { + self.match_word = !self.match_word; + } + + pub fn toggle_search_regex(&mut self) { + self.use_regex = !self.use_regex; + } + + pub fn toggle_search_with_pid(&mut self) { + self.searching_pid = !self.searching_pid; + } + + pub fn is_ignoring_case(&self) -> bool { + self.ignore_case + } + + pub fn is_searching_whole_word(&self) -> bool { + self.match_word + } + + pub fn is_searching_with_regex(&self) -> bool { + self.use_regex + } + + pub fn is_searching_with_pid(&self) -> bool { + self.searching_pid + } +} // TODO: [OPT] Group like fields together... this is kinda gross to step through pub struct App { @@ -84,12 +140,8 @@ pub struct App { pub canvas_data: canvas::DisplayableData, enable_grouping: bool, enable_searching: bool, - current_search_query: String, - searching_pid: bool, - pub ignore_case: bool, - current_regex: std::result::Result, - current_cursor_position: usize, pub data_collection: DataCollection, + pub search_state: AppSearchState, } impl App { @@ -130,12 +182,8 @@ impl App { canvas_data: canvas::DisplayableData::default(), enable_grouping: false, enable_searching: false, - current_search_query: String::default(), - searching_pid: false, - ignore_case: false, - current_regex: BASE_REGEX.clone(), //TODO: [OPT] seems like a thing we can switch to lifetimes to avoid cloning - current_cursor_position: 0, data_collection: DataCollection::default(), + search_state: AppSearchState::default(), } } @@ -147,8 +195,8 @@ impl App { self.current_widget_selected = WidgetPosition::Process; self.enable_searching = false; } - self.current_search_query = String::new(); - self.searching_pid = false; + self.search_state.current_search_query = String::new(); + self.search_state.searching_pid = false; self.to_delete_process_list = None; self.dd_err = None; } @@ -189,7 +237,13 @@ impl App { match self.current_widget_selected { WidgetPosition::Process => self.toggle_grouping(), WidgetPosition::Disk => {} - WidgetPosition::ProcessSearch => self.toggle_ignore_case(), + WidgetPosition::ProcessSearch => { + if self.search_state.is_searching_with_pid() { + self.search_with_name(); + } else { + self.search_with_pid(); + } + } _ => {} } } @@ -226,7 +280,7 @@ impl App { pub fn search_with_pid(&mut self) { if !self.is_in_dialog() && self.is_searching() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.searching_pid = true; + self.search_state.searching_pid = true; } } } @@ -234,43 +288,50 @@ impl App { pub fn search_with_name(&mut self) { if !self.is_in_dialog() && self.is_searching() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.searching_pid = false; + self.search_state.searching_pid = false; } } } - pub fn is_searching_with_pid(&self) -> bool { - self.searching_pid - } - pub fn get_current_search_query(&self) -> &String { - &self.current_search_query + &self.search_state.current_search_query } pub fn toggle_ignore_case(&mut self) { if !self.is_in_dialog() && self.is_searching() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.ignore_case = !self.ignore_case; + self.search_state.toggle_ignore_case(); self.update_regex(); self.update_process_gui = true; } } } - fn update_regex(&mut self) { - self.current_regex = if self.current_search_query.is_empty() { + pub fn update_regex(&mut self) { + self.search_state.current_regex = if self.search_state.current_search_query.is_empty() { BASE_REGEX.clone() - } else if self.ignore_case { - regex::Regex::new(&(format!("(?i){}", self.current_search_query))) } else { - regex::Regex::new(&(self.current_search_query)) + let mut final_regex_string = self.search_state.current_search_query.clone(); + + if !self.search_state.is_searching_with_regex() { + final_regex_string = regex::escape(&final_regex_string); + } + + if self.search_state.is_searching_whole_word() { + final_regex_string = format!("^{}$", final_regex_string); + } + if self.search_state.is_ignoring_case() { + final_regex_string = format!("(?i){}", final_regex_string); + } + + regex::Regex::new(&final_regex_string) }; self.previous_process_position = 0; self.currently_selected_process_position = 0; } pub fn get_cursor_position(&self) -> usize { - self.current_cursor_position + self.search_state.current_cursor_position } /// One of two functions allowed to run while in a dialog... @@ -292,10 +353,11 @@ impl App { pub fn on_backspace(&mut self) { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - if self.current_cursor_position > 0 { - self.current_cursor_position -= 1; - self.current_search_query - .remove(self.current_cursor_position); + if self.search_state.current_cursor_position > 0 { + self.search_state.current_cursor_position -= 1; + self.search_state + .current_search_query + .remove(self.search_state.current_cursor_position); self.update_regex(); self.update_process_gui = true; @@ -304,7 +366,7 @@ impl App { } pub fn get_current_regex_matcher(&self) -> &std::result::Result { - &self.current_regex + &self.search_state.current_regex } pub fn on_up_key(&mut self) { @@ -328,8 +390,8 @@ impl App { pub fn on_left_key(&mut self) { if !self.is_in_dialog() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - if self.current_cursor_position > 0 { - self.current_cursor_position -= 1; + if self.search_state.current_cursor_position > 0 { + self.search_state.current_cursor_position -= 1; } } } @@ -338,8 +400,10 @@ impl App { pub fn on_right_key(&mut self) { if !self.is_in_dialog() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - if self.current_cursor_position < self.current_search_query.len() { - self.current_cursor_position += 1; + if self.search_state.current_cursor_position + < self.search_state.current_search_query.len() + { + self.search_state.current_cursor_position += 1; } } } @@ -348,7 +412,7 @@ impl App { pub fn skip_cursor_beginning(&mut self) { if !self.is_in_dialog() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.current_cursor_position = 0; + self.search_state.current_cursor_position = 0; } } } @@ -356,7 +420,8 @@ impl App { pub fn skip_cursor_end(&mut self) { if !self.is_in_dialog() { if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.current_cursor_position = self.current_search_query.len(); + self.search_state.current_cursor_position = + self.search_state.current_search_query.len(); } } } @@ -374,9 +439,10 @@ impl App { self.last_key_press = current_key_press_inst; if let WidgetPosition::ProcessSearch = self.current_widget_selected { - self.current_search_query - .insert(self.current_cursor_position, caught_char); - self.current_cursor_position += 1; + self.search_state + .current_search_query + .insert(self.search_state.current_cursor_position, caught_char); + self.search_state.current_cursor_position += 1; self.update_regex(); diff --git a/src/canvas.rs b/src/canvas.rs index 321d9ddd..962d6d40 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -376,10 +376,24 @@ pub fn draw_data( let processes_chunk = Layout::default() .direction(Direction::Vertical) .margin(0) - .constraints([Constraint::Percentage(25), Constraint::Percentage(75)].as_ref()) + .constraints( + if (bottom_chunks[1].height as f64 * 0.25) as u16 >= 4 { + [Constraint::Percentage(75), Constraint::Percentage(25)] + } else { + let required = if bottom_chunks[1].height < 10 { + bottom_chunks[1].height / 2 + } else { + 5 + }; + let remaining = bottom_chunks[1].height - required; + [Constraint::Length(remaining), Constraint::Length(required)] + } + .as_ref(), + ) .split(bottom_chunks[1]); - draw_search_field(&mut f, app_state, processes_chunk[0]); - draw_processes_table(&mut f, app_state, processes_chunk[1]); + + draw_processes_table(&mut f, app_state, processes_chunk[0]); + draw_search_field(&mut f, app_state, processes_chunk[1]); } else { draw_processes_table(&mut f, app_state, bottom_chunks[1]); } @@ -873,7 +887,7 @@ fn draw_disk_table( fn draw_search_field( f: &mut Frame, app_state: &mut app::App, draw_loc: Rect, ) { - let width = draw_loc.width - 18; // TODO [SEARCH] this is hard-coded... ew + let width = max(0, draw_loc.width as i64 - 20) as u64; // TODO [SEARCH] this is hard-coded... ew let query = app_state.get_current_search_query(); let shrunk_query = if query.len() < width as usize { query @@ -909,36 +923,66 @@ fn draw_search_field( } } - let mut search_text = vec![ - if app_state.is_searching_with_pid() { - Text::styled("\nPID", Style::default().fg(TABLE_HEADER_COLOUR)) + let mut search_text = vec![if app_state.search_state.is_searching_with_pid() { + Text::styled( + "Search by PID (Tab for Name): ", + Style::default().fg(TABLE_HEADER_COLOUR), + ) + } else { + Text::styled( + "Search by Name (Tab for PID): ", + Style::default().fg(TABLE_HEADER_COLOUR), + ) + }]; + + // Text options shamelessly stolen from VS Code. + let option_text = vec![ + Text::styled("\n\n", Style::default().fg(TABLE_HEADER_COLOUR)), + Text::styled( + "Match Case (Alt+C)", + Style::default().fg(TABLE_HEADER_COLOUR), + ), + if !app_state.search_state.is_ignoring_case() { + Text::styled("[*]", Style::default().fg(TABLE_HEADER_COLOUR)) + } else { + Text::styled("[ ]", Style::default().fg(TABLE_HEADER_COLOUR)) + }, + Text::styled(" ", Style::default().fg(TABLE_HEADER_COLOUR)), + Text::styled( + "Match Whole Word (Alt+W)", + Style::default().fg(TABLE_HEADER_COLOUR), + ), + if app_state.search_state.is_searching_whole_word() { + Text::styled("[*]", Style::default().fg(TABLE_HEADER_COLOUR)) } else { - Text::styled("\nName", Style::default().fg(TABLE_HEADER_COLOUR)) + Text::styled("[ ]", Style::default().fg(TABLE_HEADER_COLOUR)) }, - if app_state.ignore_case { - Text::styled(" (Ignore Case): ", Style::default().fg(TABLE_HEADER_COLOUR)) + Text::styled(" ", Style::default().fg(TABLE_HEADER_COLOUR)), + Text::styled( + "Use Regex (Alt+R)", + Style::default().fg(TABLE_HEADER_COLOUR), + ), + if app_state.search_state.is_searching_with_regex() { + Text::styled("[*]", Style::default().fg(TABLE_HEADER_COLOUR)) } else { - Text::styled(": ", Style::default().fg(TABLE_HEADER_COLOUR)) + Text::styled("[ ]", Style::default().fg(TABLE_HEADER_COLOUR)) }, ]; search_text.extend(query_with_cursor); + search_text.extend(option_text); - // TODO: [SEARCH] Gotta make this easier to understand... it's pretty ugly cramming controls like this Paragraph::new(search_text.iter()) - .block( - Block::default() - .title("Search (Esc or Ctrl-f to close)") - .borders(Borders::ALL) - .border_style(if app_state.get_current_regex_matcher().is_err() { - Style::default().fg(Color::Red) - } else { - match app_state.current_widget_selected { - app::WidgetPosition::ProcessSearch => *CANVAS_HIGHLIGHTED_BORDER_STYLE, - _ => *CANVAS_BORDER_STYLE, - } - }), - ) + .block(Block::default().borders(Borders::ALL).border_style( + if app_state.get_current_regex_matcher().is_err() { + Style::default().fg(Color::Red) + } else { + match app_state.current_widget_selected { + app::WidgetPosition::ProcessSearch => *CANVAS_HIGHLIGHTED_BORDER_STYLE, + _ => *CANVAS_BORDER_STYLE, + } + }, + )) .style(Style::default().fg(Color::Gray)) .alignment(Alignment::Left) .wrap(false) diff --git a/src/main.rs b/src/main.rs index 90454c1c..12c9cc88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,7 +133,7 @@ fn main() -> error::Result<()> { // Set default search method if matches.is_present("CASE_INSENSITIVE_DEFAULT") { - app.ignore_case = true; + app.search_state.toggle_ignore_case(); } // Set up up tui and crossterm @@ -257,8 +257,6 @@ fn main() -> error::Result<()> { KeyCode::Right => app.move_right(), KeyCode::Up => app.move_up(), KeyCode::Down => app.move_down(), - KeyCode::Char('p') => app.search_with_pid(), - KeyCode::Char('n') => app.search_with_name(), KeyCode::Char('r') => { if rtx.send(ResetEvent::Reset).is_ok() { app.reset(); @@ -276,6 +274,28 @@ fn main() -> error::Result<()> { KeyCode::Down => app.move_down(), _ => {} } + } else if let KeyModifiers::ALT = event.modifiers { + match event.code { + KeyCode::Char('c') => { + if app.is_in_search_widget() { + app.search_state.toggle_ignore_case(); + app.update_regex(); + } + } + KeyCode::Char('w') => { + if app.is_in_search_widget() { + app.search_state.toggle_search_whole_word(); + app.update_regex(); + } + } + KeyCode::Char('r') => { + if app.is_in_search_widget() { + app.search_state.toggle_search_regex(); + app.update_regex(); + } + } + _ => {} + } } } @@ -417,7 +437,7 @@ fn update_final_process_list(app: &mut app::App) { .iter() .filter(|(_pid, process)| { if let Ok(matcher) = app.get_current_regex_matcher() { - if app.is_searching_with_pid() { + if app.search_state.is_searching_with_pid() { matcher.is_match(&process.pid.to_string()) } else { matcher.is_match(&process.name) -- cgit v1.2.3