From 2bb1333d04d8d8afddc8cc63fddc48e129f5cc22 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 12 Jan 2020 15:41:27 -0500 Subject: [PATCH] Mostly done the base display and control logic for search, now need to implement search filter logic --- README.md | 4 +- src/app.rs | 303 ++++++++++++++++++++++++++++++++------------------ src/canvas.rs | 44 +++++--- src/main.rs | 15 ++- 4 files changed, 239 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 7f3e2f82..8678a15a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Run using `btm`. #### General -- `Ctrl-q`, `Ctrl-c` to quit. +- `q`, `Ctrl-c` to quit. Note if you are currently in the search widget, `q` will not work so you can still type. - `Ctrl-r` to reset the screen and reset all collected data. @@ -102,7 +102,7 @@ Run using `btm`. - `Tab` to group together processes with the same name. Disables PID sorting. `dd` will now kill all processes covered by that name. -- `Ctrl-f` or `/` to toggle a search box for finding a process. By default this searches for process name, press `p` or `n` within the search bar to switch between searching for PID and name respectively. +- `Ctrl-f` or `/` to toggle a search box for finding a process. Press `Ctrl-p` or `Ctrl-n` within the search bar widget to switch between searching for PID and name respectively. Press `Esc` or `Ctrl-f` to close it. ### Mouse actions diff --git a/src/app.rs b/src/app.rs index 2edf05d4..075a5254 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ pub enum ApplicationPosition { Temp, Network, Process, + ProcessSearch, } #[derive(Debug)] @@ -58,7 +59,8 @@ pub struct App { pub canvas_data: canvas::CanvasData, enable_grouping: bool, enable_searching: bool, - current_search_phrase: String, + current_search_query: String, + searching_pid: bool, } impl App { @@ -99,7 +101,8 @@ impl App { canvas_data: canvas::CanvasData::default(), enable_grouping: false, enable_searching: false, - current_search_phrase: String::default(), + current_search_query: String::default(), + searching_pid: false, } } @@ -107,11 +110,29 @@ impl App { self.reset_multi_tap_keys(); self.show_help = false; self.show_dd = false; - self.enable_searching = false; + if self.enable_searching { + self.current_application_position = ApplicationPosition::Process; + self.enable_searching = false; + } + self.current_search_query = String::new(); + self.searching_pid = false; self.to_delete_process_list = None; self.dd_err = None; } + pub fn on_esc(&mut self) { + self.reset_multi_tap_keys(); + if self.is_in_dialog() { + self.show_help = false; + self.show_dd = false; + self.to_delete_process_list = None; + self.dd_err = None; + } else if self.enable_searching { + self.current_application_position = ApplicationPosition::Process; + self.enable_searching = false; + } + } + fn reset_multi_tap_keys(&mut self) { self.awaiting_second_char = false; self.second_char = ' '; @@ -144,8 +165,19 @@ impl App { pub fn toggle_searching(&mut self) { if !self.is_in_dialog() { - if let ApplicationPosition::Process = self.current_application_position { - self.enable_searching = !(self.enable_searching); + match self.current_application_position { + ApplicationPosition::Process | ApplicationPosition::ProcessSearch => { + if self.enable_searching { + // Toggle off + self.enable_searching = false; + self.current_application_position = ApplicationPosition::Process; + } else { + // Toggle on + self.enable_searching = true; + self.current_application_position = ApplicationPosition::ProcessSearch; + } + } + _ => {} } } } @@ -154,6 +186,38 @@ impl App { self.enable_searching } + pub fn is_in_search_widget(&self) -> bool { + if let ApplicationPosition::ProcessSearch = self.current_application_position { + true + } else { + false + } + } + + pub fn search_with_pid(&mut self) { + if !self.is_in_dialog() && self.is_searching() { + if let ApplicationPosition::ProcessSearch = self.current_application_position { + self.searching_pid = true; + } + } + } + + pub fn search_with_name(&mut self) { + if !self.is_in_dialog() && self.is_searching() { + if let ApplicationPosition::ProcessSearch = self.current_application_position { + self.searching_pid = false; + } + } + } + + pub fn is_searching_with_pid(&self) -> bool { + self.searching_pid + } + + pub fn get_current_search_phrase(&self) -> &String { + &self.current_search_query + } + /// One of two functions allowed to run while in a dialog... pub fn on_enter(&mut self) { if self.show_dd { @@ -171,6 +235,12 @@ impl App { } } + pub fn on_backspace(&mut self) { + if let ApplicationPosition::ProcessSearch = self.current_application_position { + self.current_search_query.pop(); + } + } + pub fn on_char_key(&mut self, caught_char: char) { // Forbid any char key presses when showing a dialog box... if !self.is_in_dialog() { @@ -183,128 +253,127 @@ impl App { } self.last_key_press = current_key_press_inst; - match caught_char { - '/' => { - if let ApplicationPosition::Process = self.current_application_position { + if let ApplicationPosition::ProcessSearch = self.current_application_position { + self.current_search_query.push(caught_char); + } else { + match caught_char { + '/' => { self.toggle_searching(); } - } - 'd' => { - if let ApplicationPosition::Process = self.current_application_position { - if self.awaiting_second_char && self.second_char == 'd' { + 'd' => { + if let ApplicationPosition::Process = self.current_application_position { + if self.awaiting_second_char && self.second_char == 'd' { + self.awaiting_second_char = false; + self.second_char = ' '; + let current_process = if self.is_grouped() { + let mut res: Vec = Vec::new(); + for pid in &self.canvas_data.grouped_process_data + [self.currently_selected_process_position as usize] + .group + { + let result = self + .canvas_data + .process_data + .iter() + .find(|p| p.pid == *pid); + if let Some(process) = result { + res.push((*process).clone()); + } + } + res + } else { + vec![self.canvas_data.process_data + [self.currently_selected_process_position as usize] + .clone()] + }; + self.to_delete_process_list = Some(current_process); + self.show_dd = true; + self.reset_multi_tap_keys(); + } else { + self.awaiting_second_char = true; + self.second_char = 'd'; + } + } + } + 'g' => { + if self.awaiting_second_char && self.second_char == 'g' { self.awaiting_second_char = false; self.second_char = ' '; - - let current_process = if self.is_grouped() { - let mut res: Vec = Vec::new(); - for pid in &self.canvas_data.grouped_process_data - [self.currently_selected_process_position as usize] - .group - { - let result = self - .canvas_data - .process_data - .iter() - .find(|p| p.pid == *pid); - - if let Some(process) = result { - res.push((*process).clone()); - } - } - res - } else { - vec![self.canvas_data.process_data - [self.currently_selected_process_position as usize] - .clone()] - }; - self.to_delete_process_list = Some(current_process); - self.show_dd = true; - self.reset_multi_tap_keys(); + self.skip_to_first(); } else { self.awaiting_second_char = true; - self.second_char = 'd'; + self.second_char = 'g'; } } - } - 'g' => { - if self.awaiting_second_char && self.second_char == 'g' { - self.awaiting_second_char = false; - self.second_char = ' '; - self.skip_to_first(); - } else { - self.awaiting_second_char = true; - self.second_char = 'g'; + 'G' => self.skip_to_last(), + 'k' => self.decrement_position_count(), + 'j' => self.increment_position_count(), + 'f' => { + self.is_frozen = !self.is_frozen; } - } - 'G' => self.skip_to_last(), - 'k' => self.decrement_position_count(), - 'j' => self.increment_position_count(), - 'f' => { - self.is_frozen = !self.is_frozen; - } - 'c' => { - match self.process_sorting_type { - processes::ProcessSorting::CPU => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::CPU; - self.process_sorting_reverse = true; - } - } - self.to_be_resorted = true; - self.currently_selected_process_position = 0; - } - 'm' => { - match self.process_sorting_type { - processes::ProcessSorting::MEM => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::MEM; - self.process_sorting_reverse = true; - } - } - self.to_be_resorted = true; - self.currently_selected_process_position = 0; - } - 'p' => { - // Disable if grouping - if !self.enable_grouping { + 'c' => { match self.process_sorting_type { - processes::ProcessSorting::PID => { + processes::ProcessSorting::CPU => { self.process_sorting_reverse = !self.process_sorting_reverse } _ => { - self.process_sorting_type = processes::ProcessSorting::PID; + self.process_sorting_type = processes::ProcessSorting::CPU; + self.process_sorting_reverse = true; + } + } + self.to_be_resorted = true; + self.currently_selected_process_position = 0; + } + 'm' => { + match self.process_sorting_type { + processes::ProcessSorting::MEM => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::MEM; + self.process_sorting_reverse = true; + } + } + self.to_be_resorted = true; + self.currently_selected_process_position = 0; + } + 'p' => { + // Disable if grouping + if !self.enable_grouping { + match self.process_sorting_type { + processes::ProcessSorting::PID => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::PID; + self.process_sorting_reverse = false; + } + } + self.to_be_resorted = true; + self.currently_selected_process_position = 0; + } + } + 'n' => { + match self.process_sorting_type { + processes::ProcessSorting::NAME => { + self.process_sorting_reverse = !self.process_sorting_reverse + } + _ => { + self.process_sorting_type = processes::ProcessSorting::NAME; self.process_sorting_reverse = false; } } self.to_be_resorted = true; self.currently_selected_process_position = 0; } - } - 'n' => { - match self.process_sorting_type { - processes::ProcessSorting::NAME => { - self.process_sorting_reverse = !self.process_sorting_reverse - } - _ => { - self.process_sorting_type = processes::ProcessSorting::NAME; - self.process_sorting_reverse = false; - } + '?' => { + self.show_help = true; } - self.to_be_resorted = true; - self.currently_selected_process_position = 0; + _ => {} } - '?' => { - self.show_help = true; + if self.awaiting_second_char && caught_char != self.second_char { + self.awaiting_second_char = false; } - _ => {} - } - - if self.awaiting_second_char && caught_char != self.second_char { - self.awaiting_second_char = false; } } } @@ -332,13 +401,15 @@ impl App { // CPU -(down)> MEM // MEM -(down)> Network, -(right)> TEMP // TEMP -(down)> Disk, -(left)> MEM, -(up)> CPU - // Disk -(down)> Processes, -(left)> MEM, -(up)> TEMP + // Disk -(down)> Processes OR PROC_SEARCH, -(left)> MEM, -(up)> TEMP // Network -(up)> MEM, -(right)> PROC - // PROC -(up)> Disk, -(left)> Network + // PROC -(up)> Disk OR PROC_SEARCH if enabled, -(left)> Network + // PROC_SEARCH -(up)> Disk, -(down)> PROC, -(left)> Network pub fn on_left(&mut self) { if !self.is_in_dialog() { self.current_application_position = match self.current_application_position { ApplicationPosition::Process => ApplicationPosition::Network, + ApplicationPosition::ProcessSearch => ApplicationPosition::Network, ApplicationPosition::Disk => ApplicationPosition::Mem, ApplicationPosition::Temp => ApplicationPosition::Mem, _ => self.current_application_position, @@ -363,7 +434,14 @@ impl App { self.current_application_position = match self.current_application_position { ApplicationPosition::Mem => ApplicationPosition::Cpu, ApplicationPosition::Network => ApplicationPosition::Mem, - ApplicationPosition::Process => ApplicationPosition::Disk, + ApplicationPosition::Process => { + if self.is_searching() { + ApplicationPosition::ProcessSearch + } else { + ApplicationPosition::Disk + } + } + ApplicationPosition::ProcessSearch => ApplicationPosition::Disk, ApplicationPosition::Temp => ApplicationPosition::Cpu, ApplicationPosition::Disk => ApplicationPosition::Temp, _ => self.current_application_position, @@ -378,7 +456,14 @@ impl App { ApplicationPosition::Cpu => ApplicationPosition::Mem, ApplicationPosition::Mem => ApplicationPosition::Network, ApplicationPosition::Temp => ApplicationPosition::Disk, - ApplicationPosition::Disk => ApplicationPosition::Process, + ApplicationPosition::Disk => { + if self.is_searching() { + ApplicationPosition::ProcessSearch + } else { + ApplicationPosition::Process + } + } + ApplicationPosition::ProcessSearch => ApplicationPosition::Process, _ => self.current_application_position, }; self.reset_multi_tap_keys(); diff --git a/src/canvas.rs b/src/canvas.rs index e6b29eab..417b4835 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -7,7 +7,7 @@ use std::cmp::max; use tui::{ backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Color, Style}, terminal::Frame, widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget}, Terminal, @@ -17,6 +17,7 @@ const TEXT_COLOUR: Color = Color::Gray; const GRAPH_COLOUR: Color = Color::Gray; const BORDER_STYLE_COLOUR: Color = Color::Gray; const HIGHLIGHTED_BORDER_STYLE_COLOUR: Color = Color::LightBlue; +const TABLE_HEADER_COLOUR: Color = Color::LightBlue; const GOLDEN_RATIO: f32 = 0.618_034; // Approx, good enough for use (also Clippy gets mad if it's too long) // Headers @@ -30,7 +31,7 @@ const FORCE_MIN_THRESHOLD: usize = 5; lazy_static! { static ref HELP_TEXT: [Text<'static>; 17] = [ Text::raw("\nGeneral Keybindings\n"), - Text::raw("Ctrl-q, Ctrl-c to quit.\n"), + Text::raw("q, Ctrl-c to quit. Note if you are currently in the search widget, `q` will not work.\n"), Text::raw("Ctrl-r to reset all data.\n"), Text::raw("f to toggle freezing and unfreezing the display.\n"), Text::raw( @@ -46,7 +47,7 @@ lazy_static! { Text::raw("p to sort by PID.\n"), Text::raw("n to sort by process name.\n"), Text::raw("Tab to group together processes with the same name.\n"), - Text::raw("Ctrl-f or / to toggle searching for a process. Use p and n to toggle between searching for PID and name.\n"), + Text::raw("Ctrl-f or / to toggle searching for a process. Use Ctrl-p and Ctrl-n to toggle between searching for PID and name.\n"), Text::raw("\nFor startup flags, type in \"btm -h\".") ]; static ref COLOUR_LIST: Vec = gen_n_colours(constants::NUM_COLOURS); @@ -500,7 +501,7 @@ fn draw_cpu_legend( _ => *CANVAS_BORDER_STYLE, }, )) - .header_style(Style::default().fg(Color::LightBlue)) + .header_style(Style::default().fg(TABLE_HEADER_COLOUR)) .widths( &(intrinsic_widths .into_iter() @@ -692,7 +693,7 @@ fn draw_network_labels( _ => *CANVAS_BORDER_STYLE, }, )) - .header_style(Style::default().fg(Color::LightBlue)) + .header_style(Style::default().fg(TABLE_HEADER_COLOUR)) .widths( &(intrinsic_widths .into_iter() @@ -759,7 +760,7 @@ fn draw_temp_table( _ => *CANVAS_BORDER_STYLE, }), ) - .header_style(Style::default().fg(Color::LightBlue)) + .header_style(Style::default().fg(TABLE_HEADER_COLOUR)) .widths( &(intrinsic_widths .into_iter() @@ -824,11 +825,7 @@ fn draw_disk_table( _ => *CANVAS_BORDER_STYLE, }), ) - .header_style( - Style::default() - .fg(Color::LightBlue) - .modifier(Modifier::BOLD), - ) + .header_style(Style::default().fg(TABLE_HEADER_COLOUR)) .widths( &(intrinsic_widths .into_iter() @@ -841,7 +838,28 @@ fn draw_disk_table( fn draw_search_field( f: &mut Frame, app_state: &mut app::App, draw_loc: Rect, ) { - // TODO: Search field + let search_text = [ + if app_state.is_searching_with_pid() { + Text::styled("\nPID: ", Style::default().fg(TABLE_HEADER_COLOUR)) + } else { + Text::styled("\nName: ", Style::default().fg(TABLE_HEADER_COLOUR)) + }, + Text::raw(app_state.get_current_search_phrase()), + ]; + Paragraph::new(search_text.iter()) + .block( + Block::default() + .title("Search (Ctrl-p and Ctrl-n to switch search types, Esc or Ctrl-f to close)") + .borders(Borders::ALL) + .border_style(match app_state.current_application_position { + app::ApplicationPosition::ProcessSearch => *CANVAS_HIGHLIGHTED_BORDER_STYLE, + _ => *CANVAS_BORDER_STYLE, + }), + ) + .style(Style::default().fg(Color::Gray)) + .alignment(Alignment::Left) + .wrap(true) // TODO: We want this to keep going right... slicing? + .render(f, draw_loc); } fn draw_processes_table( @@ -952,7 +970,7 @@ fn draw_processes_table( _ => *CANVAS_BORDER_STYLE, }), ) - .header_style(Style::default().fg(Color::LightBlue)) + .header_style(Style::default().fg(TABLE_HEADER_COLOUR)) .widths( &(intrinsic_widths .into_iter() diff --git a/src/main.rs b/src/main.rs index f7a9de21..ef346e78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -212,27 +212,36 @@ fn main() -> error::Result<()> { Event::KeyInput(event) => { if event.modifiers.is_empty() { // If only a code, and no modifiers, don't bother... + + // Required to catch for while typing + if event.code == KeyCode::Char('q') && !app.is_in_search_widget() { + break; + } + match event.code { KeyCode::End => app.skip_to_last(), KeyCode::Home => app.skip_to_first(), KeyCode::Up => app.decrement_position_count(), KeyCode::Down => app.increment_position_count(), KeyCode::Char(character) => app.on_char_key(character), - KeyCode::Esc => app.reset(), + KeyCode::Esc => app.on_esc(), KeyCode::Enter => app.on_enter(), KeyCode::Tab => app.on_tab(), + KeyCode::Backspace => app.on_backspace(), _ => {} } } else { // Otherwise, track the modifier as well... if let KeyModifiers::CONTROL = event.modifiers { match event.code { - KeyCode::Char('c') | KeyCode::Char('q') => break, - KeyCode::Char('f') => app.on_char_key('/'), // Note that this is fine for now, assuming '/' does not do anything other than search. + KeyCode::Char('c') => break, + KeyCode::Char('f') => app.toggle_searching(), // Note that this is fine for now, assuming '/' does not do anything other than search. KeyCode::Left | KeyCode::Char('h') => app.on_left(), KeyCode::Right | KeyCode::Char('l') => app.on_right(), KeyCode::Up | KeyCode::Char('k') => app.on_up(), KeyCode::Down | KeyCode::Char('j') => app.on_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();