From 6e81fbeebf7de91bc319b2c90cd25f18fdf288ea Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Fri, 1 May 2020 23:53:29 -0400 Subject: [PATCH] change: more advanced searching and filtering --- CHANGELOG.md | 8 +- CONTRIBUTING.md | 3 + Cargo.lock | 18 +- Cargo.toml | 7 +- src/app.rs | 116 +++-------- src/app/query.rs | 267 ++++++++++++++++++++++++ src/app/states.rs | 310 ++++++++++++++++++++++++---- src/canvas.rs | 3 +- src/canvas/widgets/cpu_graph.rs | 3 +- src/canvas/widgets/process_table.rs | 69 ++----- src/data_conversion.rs | 48 ++++- src/main.rs | 91 +++----- src/utils/error.rs | 13 +- src/utils/logging.rs | 1 + 14 files changed, 675 insertions(+), 282 deletions(-) create mode 100644 src/app/query.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b604aa..563b0efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - Unreleased + +### Features + +- [#114](https://github.com/ClementTsang/bottom/pull/114): Process state per process (originally in 0.4.0, moved to later). + ## [0.4.0] - Unreleased ### Features @@ -13,8 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#55](https://github.com/ClementTsang/bottom/issues/55): Battery monitoring widget. -- [#114](https://github.com/ClementTsang/bottom/pull/114): Process state per process. - - [#134](https://github.com/ClementTsang/bottom/pull/134): `hjkl` movement to delete dialog (credit to [andys8](https://github.com/andys8)). - [#59](https://github.com/ClementTsang/bottom/issues/59): `Alt-h` and `Alt-l` to move left/right in query (and rest of the app actually). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c616a359..4058e944 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,9 @@ If you want to help contribute by submitting a PR, by all means, I'm open! In re - You can check clippy using `cargo +nightly clippy`. +- You may notice that I have fern and log as dependencies; this is mostly for easy debugging via the `debug!()` macro. It writes to the + `debug.log` file that will automatically be created if you run in debug mode (so `cargo run`). + And in regards to the pull request process: - Create a personal fork of the process and PR that, as per the [fork and pull method](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-collaborative-development-models). diff --git a/Cargo.lock b/Cargo.lock index 72666d85..ae20694c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,7 +63,7 @@ name = "backtrace" version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "backtrace-sys 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace-sys 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "backtrace-sys" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)", @@ -422,7 +422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -938,7 +938,7 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1063,7 +1063,7 @@ version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1107,7 +1107,7 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1191,7 +1191,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1310,7 +1310,7 @@ dependencies = [ "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" "checksum backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)" = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" -"checksum backtrace-sys 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "78848718ee1255a2485d1309ad9cdecfc2e7d0362dd11c6829364c6b35ae1bc7" +"checksum backtrace-sys 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399" "checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" "checksum battery 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "36a698e449024a5d18994a815998bf5e2e4bc1883e35a7d7ba95b6b69ee45907" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" @@ -1403,7 +1403,7 @@ dependencies = [ "checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" "checksum proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" "checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" -"checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" +"checksum quote 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4c1f4b0efa5fc5e8ceb705136bfee52cfdb6a4e3509f770b478cd6ed434232a7" "checksum raw-cpuid 7.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b4a349ca83373cfa5d6dbb66fd76e58b2cca08da71a5f6400de0a0a6a9bceeaf" "checksum rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" "checksum rayon-core 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" diff --git a/Cargo.toml b/Cargo.toml index 58b5766a..ab5a1a70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,11 +27,9 @@ crossterm = "0.17" chrono = "0.4.11" clap = "2.33.0" dirs = "2.0.2" -fern = "0.6.0" futures = "0.3.4" heim = "0.0.10" itertools = "0.9.0" -log = "0.4.8" regex = "1.3" sysinfo = "0.14" toml = "0.5.6" @@ -42,9 +40,14 @@ serde = {version = "1.0", features = ["derive"] } unicode-segmentation = "1.6.0" unicode-width = "0.1.7" +# For debugging only... +fern = "0.6.0" +log = "0.4.8" + tui = {version = "0.9", features = ["crossterm"], default-features = false } # tui = {git = "https://github.com/ClementTsang/tui-rs", features = ["crossterm"], default-features = false } + [target.'cfg(windows)'.dependencies] winapi = "0.3.8" diff --git a/src/app.rs b/src/app.rs index de9e8145..ff39f06e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,6 +19,7 @@ pub mod data_farmer; pub mod data_harvester; pub mod layout_manager; mod process_killer; +pub mod query; pub mod states; const MAX_SEARCH_LENGTH: usize = 200; @@ -307,19 +308,6 @@ impl App { let is_in_search_widget = self.is_in_search_widget(); if !self.is_in_dialog() { if is_in_search_widget { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get_mut(&(self.current_widget.widget_id - 1)) - { - if !proc_widget_state.is_grouped { - if proc_widget_state.process_search_state.is_searching_with_pid { - self.search_with_name(); - } else { - self.search_with_pid(); - } - } - } } else if let Some(proc_widget_state) = self .proc_state .widget_states @@ -327,11 +315,7 @@ impl App { { // Toggles process widget grouping state proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); - if proc_widget_state.is_grouped { - self.search_with_name(); - } else { - self.proc_state.force_update = Some(self.current_widget.widget_id); - } + self.proc_state.force_update = Some(self.current_widget.widget_id); } } } @@ -387,9 +371,6 @@ impl App { .process_search_state .search_state .is_enabled = true; - if proc_widget_state.is_grouped { - self.search_with_name(); - } self.move_widget_selection_down(); } } @@ -426,44 +407,6 @@ impl App { } } - pub fn search_with_pid(&mut self) { - if !self.is_in_dialog() { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get_mut(&(self.current_widget.widget_id - 1)) - { - if proc_widget_state - .process_search_state - .search_state - .is_enabled - { - proc_widget_state.process_search_state.is_searching_with_pid = true; - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - } - } - } - } - - pub fn search_with_name(&mut self) { - if !self.is_in_dialog() { - if let Some(proc_widget_state) = self - .proc_state - .widget_states - .get_mut(&(self.current_widget.widget_id - 1)) - { - if proc_widget_state - .process_search_state - .search_state - .is_enabled - { - proc_widget_state.process_search_state.is_searching_with_pid = false; - self.proc_state.force_update = Some(self.current_widget.widget_id - 1); - } - } - } - } - pub fn toggle_ignore_case(&mut self) { let is_in_search_widget = self.is_in_search_widget(); if let Some(proc_widget_state) = self @@ -475,7 +418,7 @@ impl App { proc_widget_state .process_search_state .search_toggle_ignore_case(); - proc_widget_state.update_regex(); + proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } @@ -492,7 +435,7 @@ impl App { proc_widget_state .process_search_state .search_toggle_whole_word(); - proc_widget_state.update_regex(); + proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } @@ -507,7 +450,7 @@ impl App { { if is_in_search_widget && proc_widget_state.is_search_enabled() { proc_widget_state.process_search_state.search_toggle_regex(); - proc_widget_state.update_regex(); + proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } @@ -575,7 +518,7 @@ impl App { true, ); - proc_widget_state.update_regex(); + proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } else { @@ -630,24 +573,18 @@ impl App { .search_state .cursor_direction = CursorDirection::LEFT; - proc_widget_state.update_regex(); + proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); } } } } - pub fn get_current_regex_matcher( - &self, widget_id: u64, - ) -> &Option> { - match self.proc_state.widget_states.get(&widget_id) { - Some(proc_widget_state) => { - &proc_widget_state - .process_search_state - .search_state - .current_regex - } - None => &None, + pub fn get_process_filter(&self, widget_id: u64) -> &Option { + if let Some(process_widget_state) = self.proc_state.widget_states.get(&widget_id) { + &process_widget_state.process_search_state.search_state.query + } else { + &None } } @@ -872,6 +809,8 @@ impl App { } pub fn start_dd(&mut self) { + self.reset_multi_tap_keys(); + if let Some(proc_widget_state) = self .proc_state .widget_states @@ -885,32 +824,25 @@ impl App { if proc_widget_state.scroll_state.current_scroll_position < corresponding_filtered_process_list.len() as u64 { - let current_process = if self.is_grouped(self.current_widget.widget_id) { - let group_pids = &corresponding_filtered_process_list - [proc_widget_state.scroll_state.current_scroll_position as usize] - .group_pids; - - let mut ret = ("".to_string(), group_pids.clone()); - - for pid in group_pids { - if let Some(process) = self.canvas_data.process_data.get(&pid) { - ret.0 = process.name.clone(); - break; - } + let current_process: (String, Vec); + if self.is_grouped(self.current_widget.widget_id) { + if let Some(process) = &corresponding_filtered_process_list + .get(proc_widget_state.scroll_state.current_scroll_position as usize) + { + current_process = (process.name.to_string(), process.group_pids.clone()) + } else { + return; } - ret } else { let process = corresponding_filtered_process_list [proc_widget_state.scroll_state.current_scroll_position as usize] .clone(); - (process.name.clone(), vec![process.pid]) + current_process = (process.name.clone(), vec![process.pid]) }; self.to_delete_process_list = Some(current_process); self.delete_dialog_state.is_showing_dd = true; } - - self.reset_multi_tap_keys(); } } } @@ -977,7 +909,7 @@ impl App { .char_cursor_position += UnicodeWidthChar::width(caught_char).unwrap_or(0); - proc_widget_state.update_regex(); + proc_widget_state.update_query(); self.proc_state.force_update = Some(self.current_widget.widget_id - 1); proc_widget_state .process_search_state diff --git a/src/app/query.rs b/src/app/query.rs new file mode 100644 index 00000000..cfb8a571 --- /dev/null +++ b/src/app/query.rs @@ -0,0 +1,267 @@ +use crate::{ + data_conversion::ConvertedProcessData, + utils::error::{BottomError, Result}, +}; + +#[derive(Debug)] +pub struct Query { + pub query: And, +} + +impl Query { + pub fn process_regexes( + &mut self, is_searching_whole_word: bool, is_ignoring_case: bool, + is_searching_with_regex: bool, + ) -> Result<()> { + self.query.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + ) + } + + pub fn check(&self, process: &ConvertedProcessData) -> bool { + self.query.check(process) + } +} + +#[derive(Debug)] +pub struct And { + pub lhs: Or, + pub rhs: Option>, +} + +impl And { + pub fn process_regexes( + &mut self, is_searching_whole_word: bool, is_ignoring_case: bool, + is_searching_with_regex: bool, + ) -> Result<()> { + self.lhs.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + )?; + if let Some(rhs) = &mut self.rhs { + rhs.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + )?; + } + + Ok(()) + } + + pub fn check(&self, process: &ConvertedProcessData) -> bool { + if let Some(rhs) = &self.rhs { + self.lhs.check(process) && rhs.check(process) + } else { + self.lhs.check(process) + } + } +} + +#[derive(Debug)] +pub struct Or { + pub lhs: Prefix, + pub rhs: Option>, +} + +impl Or { + pub fn process_regexes( + &mut self, is_searching_whole_word: bool, is_ignoring_case: bool, + is_searching_with_regex: bool, + ) -> Result<()> { + self.lhs.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + )?; + if let Some(rhs) = &mut self.rhs { + rhs.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + )?; + } + + Ok(()) + } + + pub fn check(&self, process: &ConvertedProcessData) -> bool { + if let Some(rhs) = &self.rhs { + self.lhs.check(process) || rhs.check(process) + } else { + self.lhs.check(process) + } + } +} + +#[derive(Debug)] +pub enum PrefixType { + Pid, + Cpu, + Mem, + Rps, + Wps, + TRead, + TWrite, + Name, + __Nonexhaustive, +} + +impl std::str::FromStr for PrefixType { + type Err = BottomError; + + fn from_str(s: &str) -> Result { + use PrefixType::*; + + let lower_case = s.to_lowercase(); + match lower_case.as_str() { + "cpu" => Ok(Cpu), + "mem" => Ok(Mem), + "r" => Ok(Rps), + "w" => Ok(Wps), + "read" => Ok(TRead), + "write" => Ok(TWrite), + "pid" => Ok(Pid), + _ => Ok(Name), + } + } +} + +#[derive(Debug)] +pub struct Prefix { + pub and: Option>, + pub regex_prefix: Option<(PrefixType, StringQuery)>, + pub compare_prefix: Option<(PrefixType, NumericalQuery)>, +} + +impl Prefix { + pub fn process_regexes( + &mut self, is_searching_whole_word: bool, is_ignoring_case: bool, + is_searching_with_regex: bool, + ) -> Result<()> { + if let Some(and) = &mut self.and { + return and.process_regexes( + is_searching_whole_word, + is_ignoring_case, + is_searching_with_regex, + ); + } else if let Some((prefix_type, query_content)) = &mut self.regex_prefix { + if let StringQuery::Value(regex_string) = query_content { + match prefix_type { + PrefixType::Pid | PrefixType::Name => { + let escaped_regex: String; + let final_regex_string = &format!( + "{}{}{}{}", + if is_searching_whole_word { "^" } else { "" }, + if is_ignoring_case { "(?i)" } else { "" }, + if !is_searching_with_regex { + escaped_regex = regex::escape(regex_string); + &escaped_regex + } else { + regex_string + }, + if is_searching_whole_word { "$" } else { "" }, + ); + + let taken_pwc = self.regex_prefix.take(); + if let Some((taken_pt, _)) = taken_pwc { + self.regex_prefix = Some(( + taken_pt, + StringQuery::Regex(regex::Regex::new(final_regex_string)?), + )); + } + } + _ => {} + } + } + } + + Ok(()) + } + + pub fn check(&self, process: &ConvertedProcessData) -> bool { + fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool { + match condition { + QueryComparison::Equal => lhs == rhs, + QueryComparison::Less => lhs < rhs, + QueryComparison::Greater => lhs > rhs, + QueryComparison::LessOrEqual => lhs <= rhs, + QueryComparison::GreaterOrEqual => lhs >= rhs, + } + } + + if let Some(and) = &self.and { + and.check(process) + } else if let Some((prefix_type, query_content)) = &self.regex_prefix { + if let StringQuery::Regex(r) = query_content { + match prefix_type { + PrefixType::Name => r.is_match(process.name.as_str()), + PrefixType::Pid => r.is_match(process.pid.to_string().as_str()), + _ => true, + } + } else { + true + } + } else if let Some((prefix_type, numerical_query)) = &self.compare_prefix { + match prefix_type { + PrefixType::Cpu => matches_condition( + &numerical_query.condition, + process.cpu_usage, + numerical_query.value, + ), + PrefixType::Mem => matches_condition( + &numerical_query.condition, + process.mem_usage, + numerical_query.value, + ), + PrefixType::Rps => matches_condition( + &numerical_query.condition, + process.rps_f64, + numerical_query.value, + ), + PrefixType::Wps => matches_condition( + &numerical_query.condition, + process.wps_f64, + numerical_query.value, + ), + PrefixType::TRead => matches_condition( + &numerical_query.condition, + process.tr_f64, + numerical_query.value, + ), + PrefixType::TWrite => matches_condition( + &numerical_query.condition, + process.tw_f64, + numerical_query.value, + ), + _ => true, + } + } else { + true + } + } +} + +#[derive(Debug)] +pub enum QueryComparison { + Equal, + Less, + Greater, + LessOrEqual, + GreaterOrEqual, +} + +#[derive(Debug)] +pub enum StringQuery { + Value(String), + Regex(regex::Regex), +} + +#[derive(Debug)] +pub struct NumericalQuery { + pub condition: QueryComparison, + pub value: f64, +} diff --git a/src/app/states.rs b/src/app/states.rs index ca181502..825a08fa 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -1,10 +1,18 @@ -use std::{collections::HashMap, time::Instant}; +use std::{ + collections::{HashMap, VecDeque}, + time::Instant, +}; use unicode_segmentation::GraphemeCursor; use tui::widgets::TableState; -use crate::{app::layout_manager::BottomWidgetType, constants, data_harvester::processes}; +use crate::{ + app::{layout_manager::BottomWidgetType, query::*}, + constants, + data_harvester::processes, + utils::error::{BottomError::*, Result}, +}; #[derive(Debug)] pub enum ScrollDirection { @@ -61,7 +69,6 @@ impl Default for AppHelpDialogState { pub struct AppSearchState { pub is_enabled: bool, pub current_search_query: String, - pub current_regex: Option>, pub is_blank_search: bool, pub is_invalid_search: bool, pub grapheme_cursor: GraphemeCursor, @@ -69,6 +76,8 @@ pub struct AppSearchState { pub cursor_bar: usize, /// This represents the position in terms of CHARACTERS, not graphemes pub char_cursor_position: usize, + /// The query + pub query: Option, } impl Default for AppSearchState { @@ -76,13 +85,13 @@ impl Default for AppSearchState { AppSearchState { is_enabled: false, current_search_query: String::default(), - current_regex: None, is_invalid_search: false, is_blank_search: true, grapheme_cursor: GraphemeCursor::new(0, 0, true), cursor_direction: CursorDirection::RIGHT, cursor_bar: 0, char_cursor_position: 0, + query: None, } } } @@ -104,7 +113,6 @@ impl AppSearchState { /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { pub search_state: AppSearchState, - pub is_searching_with_pid: bool, pub is_ignoring_case: bool, pub is_searching_whole_word: bool, pub is_searching_with_regex: bool, @@ -114,7 +122,6 @@ impl Default for ProcessSearchState { fn default() -> Self { ProcessSearchState { search_state: AppSearchState::default(), - is_searching_with_pid: false, is_ignoring_case: true, is_searching_whole_word: false, is_searching_with_regex: false, @@ -188,7 +195,7 @@ impl ProcWidgetState { &self.process_search_state.search_state.current_search_query } - pub fn update_regex(&mut self) { + pub fn update_query(&mut self) { if self .process_search_state .search_state @@ -197,39 +204,13 @@ impl ProcWidgetState { { self.process_search_state.search_state.is_invalid_search = false; self.process_search_state.search_state.is_blank_search = true; - } else { - let regex_string = &self.process_search_state.search_state.current_search_query; - let escaped_regex: String; - let final_regex_string = &format!( - "{}{}{}{}", - if self.process_search_state.is_searching_whole_word { - "^" - } else { - "" - }, - if self.process_search_state.is_ignoring_case { - "(?i)" - } else { - "" - }, - if !self.process_search_state.is_searching_with_regex { - escaped_regex = regex::escape(regex_string); - &escaped_regex - } else { - regex_string - }, - if self.process_search_state.is_searching_whole_word { - "$" - } else { - "" - }, - ); - - let new_regex = regex::Regex::new(final_regex_string); + } else if let Ok(parsed_query) = self.parse_query() { + self.process_search_state.search_state.query = Some(parsed_query); self.process_search_state.search_state.is_blank_search = false; - self.process_search_state.search_state.is_invalid_search = new_regex.is_err(); - - self.process_search_state.search_state.current_regex = Some(new_regex); + self.process_search_state.search_state.is_invalid_search = false; + } else { + self.process_search_state.search_state.is_blank_search = false; + self.process_search_state.search_state.is_invalid_search = true; } self.scroll_state.previous_scroll_position = 0; self.scroll_state.current_scroll_position = 0; @@ -260,6 +241,257 @@ impl ProcWidgetState { ) .unwrap(); } + + /// The filtering function. Based on the results of the query. + pub fn matches_filter(&self) -> bool { + // The way this will have to work is that given a "query" structure, we have + // to filter based on it. + + false + } + + /// In charge of parsing the given query. + /// We are defining the following language for a query (case-insensitive prefixes): + /// + /// - Process names: No prefix required, can use regex, match word, or case. + /// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process + /// rather than a prefix. + /// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant). + /// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare. + /// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare. + /// - STATE: Use prefix `state`, TODO when we update how state looks in 0.5 probably. + /// - Read/s: Use prefix `r`. Can compare. + /// - Write/s: Use prefix `w`. Can compare. + /// - Total read: Use prefix `read`. Can compare. + /// - Total write: Use prefix `write`. Can compare. + /// + /// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed + /// or quoted elements after splitting to treat as process names. + /// Furthermore, we want to support boolean joiners like AND and OR, and brackets. + fn parse_query(&self) -> Result { + fn process_string_to_filter(query: &mut VecDeque) -> Result { + Ok(Query { + query: process_and(query)?, + }) + } + + fn process_and(query: &mut VecDeque) -> Result { + let mut lhs = process_or(query)?; + let mut rhs: Option> = None; + + while let Some(queue_top) = query.front() { + if queue_top.to_lowercase() == "and" { + query.pop_front(); + rhs = Some(Box::new(process_or(query)?)); + + if let Some(queue_next) = query.front() { + if queue_next.to_lowercase() == "and" { + // Must merge LHS and RHS + lhs = Or { + lhs: Prefix { + and: Some(Box::new(And { lhs, rhs })), + regex_prefix: None, + compare_prefix: None, + }, + rhs: None, + }; + rhs = None; + } + } else { + break; + } + } else { + break; + } + } + + Ok(And { lhs, rhs }) + } + + fn process_or(query: &mut VecDeque) -> Result { + let mut lhs = process_prefix(query)?; + let mut rhs: Option> = None; + + while let Some(queue_top) = query.front() { + if queue_top.to_lowercase() == "or" { + query.pop_front(); + rhs = Some(Box::new(process_prefix(query)?)); + + if let Some(queue_next) = query.front() { + if queue_next.to_lowercase() == "or" { + // Must merge LHS and RHS + lhs = Prefix { + and: Some(Box::new(And { + lhs: Or { lhs, rhs }, + rhs: None, + })), + regex_prefix: None, + compare_prefix: None, + }; + rhs = None; + } + } else { + break; + } + } else { + break; + } + } + + Ok(Or { lhs, rhs }) + } + + fn process_prefix(query: &mut VecDeque) -> Result { + if let Some(queue_top) = query.pop_front() { + if queue_top == "(" { + // Get content within bracket; and check if paren is complete + let and = process_and(query)?; + if let Some(close_paren) = query.pop_front() { + if close_paren.to_lowercase() == ")" { + return Ok(Prefix { + and: Some(Box::new(and)), + regex_prefix: None, + compare_prefix: None, + }); + } else { + return Err(QueryError("Missing closing parentheses".into())); + } + } else { + return Err(QueryError("Missing closing parentheses".into())); + } + } else if queue_top == ")" { + // This is actually caught by the regex creation, but it seems a bit + // sloppy to leave that up to that to do so... + + return Err(QueryError("Missing opening parentheses".into())); + } else { + // Get prefix type... + let prefix_type = queue_top.parse::()?; + let content = if let PrefixType::Name = prefix_type { + Some(queue_top) + } else { + query.pop_front() + }; + + if let Some(content) = content { + match &prefix_type { + PrefixType::Name => { + return Ok(Prefix { + and: None, + regex_prefix: Some((prefix_type, StringQuery::Value(content))), + compare_prefix: None, + }) + } + PrefixType::Pid => { + // We have to check if someone put an "="... + if content == "=" { + // Check next string if possible + if let Some(queue_next) = query.pop_front() { + return Ok(Prefix { + and: None, + regex_prefix: Some(( + prefix_type, + StringQuery::Value(queue_next), + )), + compare_prefix: None, + }); + } + } else { + return Ok(Prefix { + and: None, + regex_prefix: Some(( + prefix_type, + StringQuery::Value(content), + )), + compare_prefix: None, + }); + } + } + _ => { + // Now we gotta parse the content... yay. + + let mut condition: Option = None; + let mut value: Option = None; + + if content == "=" { + // TODO: Do we want to allow just an empty space to work here too? ie: cpu 5? + condition = Some(QueryComparison::Equal); + if let Some(queue_next) = query.pop_front() { + value = queue_next.parse::().ok(); + } + } else if content == ">" || content == "<" { + // We also have to check if the next string is an "="... + if let Some(queue_next) = query.pop_front() { + if queue_next == "=" { + condition = Some(if content == ">" { + QueryComparison::GreaterOrEqual + } else { + QueryComparison::LessOrEqual + }); + if let Some(queue_next_next) = query.pop_front() { + value = queue_next_next.parse::().ok(); + } + } else { + condition = Some(if content == ">" { + QueryComparison::Greater + } else { + QueryComparison::Less + }); + value = queue_next.parse::().ok(); + } + } + } + + if let Some(condition) = condition { + if let Some(value) = value { + return Ok(Prefix { + and: None, + regex_prefix: None, + compare_prefix: Some(( + prefix_type, + NumericalQuery { condition, value }, + )), + }); + } + } + } + } + } + } + } + + Err(QueryError("Failed to parse comparator.".into())) + } + + let mut split_query = VecDeque::new(); + + self.get_current_search_query() + .split_whitespace() + .for_each(|s| { + // From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses + let mut last = 0; + for (index, matched) in s.match_indices(|x| ['=', '>', '<', '(', ')'].contains(&x)) + { + if last != index { + split_query.push_back(s[last..index].to_owned()); + } + split_query.push_back(matched.to_owned()); + last = index + matched.len(); + } + if last < s.len() { + split_query.push_back(s[last..].to_owned()); + } + }); + + let mut process_filter = process_string_to_filter(&mut split_query)?; + process_filter.process_regexes( + self.process_search_state.is_searching_whole_word, + self.process_search_state.is_ignoring_case, + self.process_search_state.is_searching_with_regex, + )?; + + Ok(process_filter) + } } pub struct ProcState { diff --git a/src/canvas.rs b/src/canvas.rs index 5f243852..a7df9ea9 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -16,7 +16,6 @@ use widgets::*; use crate::{ app::{ self, - data_harvester::processes::ProcessHarvest, layout_manager::{BottomColRow, BottomLayout, BottomWidgetType}, App, }, @@ -41,7 +40,7 @@ pub struct DisplayableData { pub disk_data: Vec>, pub temp_sensor_data: Vec>, // Not the final value - pub process_data: HashMap, + pub process_data: Vec, // Not the final value pub grouped_process_data: Vec, // What's actually displayed diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 2ced7d6c..1ee9fa5a 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -160,8 +160,7 @@ impl CpuGraphWidget for Painter { self.colours.cpu_colour_styles [itx % self.colours.cpu_colour_styles.len()] }) - .data(&cpu.cpu_data[..]) - // .graph_type(tui::widgets::GraphType::Line), + .data(&cpu.cpu_data[..]), // .graph_type(tui::widgets::GraphType::Line), ) } else { None diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index 651c5d2f..63fb45c3 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -1,7 +1,7 @@ use std::cmp::max; use crate::{ - app::{self, App, ProcWidgetState}, + app::{self, App}, canvas::{ drawing_utils::{ get_search_start_position, get_start_position, get_variable_intrinsic_widths, @@ -44,7 +44,7 @@ impl ProcessTableWidget for Painter { widget_id: u64, ) { if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { - let search_height = if draw_border { 4 } else { 3 }; + let search_height = if draw_border { 3 } else { 2 }; if process_widget_state.is_search_enabled() { let processes_chunk = Layout::default() .direction(Direction::Vertical) @@ -148,7 +148,7 @@ impl ProcessTableWidget for Painter { let wps = "W/s".to_string(); let total_read = "Read".to_string(); let total_write = "Write".to_string(); - let process_state = "State".to_string(); + // let process_state = "State".to_string(); let direction_val = if proc_widget_state.process_sorting_reverse { "▼".to_string() @@ -172,7 +172,7 @@ impl ProcessTableWidget for Painter { wps, total_read, total_write, - process_state, + // process_state, ]; let process_headers_lens: Vec = process_headers .iter() @@ -181,7 +181,7 @@ impl ProcessTableWidget for Painter { // Calculate widths let width = f64::from(draw_loc.width); - let width_ratios = [0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]; + let width_ratios = [0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.15, 0.15]; let variable_intrinsic_results = get_variable_intrinsic_widths( width as u16, &width_ratios, @@ -278,20 +278,6 @@ impl ProcessTableWidget for Painter { &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool, widget_id: u64, ) { - fn get_prompt_text<'a>(proc_widget_state: &ProcWidgetState) -> &'a str { - let pid_search_text = "Search by PID (Tab for Name): "; - let name_search_text = "Search by Name (Tab for PID): "; - let grouped_search_text = "Search by Name: "; - - if proc_widget_state.is_grouped { - grouped_search_text - } else if proc_widget_state.process_search_state.is_searching_with_pid { - pid_search_text - } else { - name_search_text - } - } - fn build_query<'a>( is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style, @@ -342,24 +328,11 @@ impl ProcessTableWidget for Painter { if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) { - let chosen_text = get_prompt_text(&proc_widget_state); - let is_on_widget = widget_id == app_state.current_widget.widget_id; let is_on_processes = is_on_widget || (widget_id - 1 == app_state.current_widget.widget_id); let num_columns = draw_loc.width as usize; - let small_mode = num_columns < 70; - let search_title: &str = if !small_mode { - chosen_text - } else if chosen_text.is_empty() { - "" - } else if proc_widget_state.process_search_state.is_searching_with_pid - && !proc_widget_state.is_grouped - { - "p> " - } else { - "n> " - }; + let search_title = "> "; let num_chars_for_text = search_title.len(); @@ -421,34 +394,16 @@ impl ProcessTableWidget for Painter { }; let mut option_text = vec![]; - let case_text = format!( - "{}({})", - if small_mode { "Case" } else { "Match Case " }, - if self.is_mac_os { "F1" } else { "Alt+C" }, - ); - - let whole_text = format!( - "{}({})", - if small_mode { - "Whole" - } else { - "Match Whole Word " - }, - if self.is_mac_os { "F2" } else { "Alt+W" }, - ); - - let regex_text = format!( - "{}({})", - if small_mode { "Regex" } else { "Use Regex " }, - if self.is_mac_os { "F3" } else { "Alt+R" }, - ); + let case_text = format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" },); + let whole_text = format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" },); + let regex_text = format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" },); let option_row = vec![ - Text::raw("\n\n"), + Text::raw("\n"), Text::styled(&case_text, case_style), - Text::raw(if small_mode { " " } else { " " }), + Text::raw(" "), Text::styled(&whole_text, whole_word_style), - Text::raw(if small_mode { " " } else { " " }), + Text::raw(" "), Text::styled(®ex_text, regex_style), ]; option_text.extend(option_row); diff --git a/src/data_conversion.rs b/src/data_conversion.rs index c2492c9d..7f28f35d 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -4,11 +4,7 @@ use std::collections::HashMap; use crate::{ - app::{ - data_farmer, - data_harvester::{self, processes::ProcessHarvest}, - App, - }, + app::{data_farmer, data_harvester, App}, utils::gen_util::{get_exact_byte_values, get_simple_byte_values}, }; @@ -45,6 +41,10 @@ pub struct ConvertedProcessData { pub write_per_sec: String, pub total_read: String, pub total_write: String, + pub rps_f64: f64, + pub wps_f64: f64, + pub tr_f64: f64, + pub tw_f64: f64, pub process_states: String, } @@ -399,8 +399,8 @@ pub fn convert_network_data_points( pub fn convert_process_data( current_data: &data_farmer::DataCollection, -) -> (HashMap, Vec) { - let mut single_list: HashMap = HashMap::new(); +) -> (Vec, Vec) { + let mut single_list = Vec::new(); // cpu, mem, pids let mut grouped_hashmap: HashMap = std::collections::HashMap::new(); @@ -423,7 +423,35 @@ pub fn convert_process_data( (*entry).total_write += process.total_write_bytes; (*entry).process_state.push(process.process_state_char); - single_list.insert(process.pid, process.clone()); + let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false); + let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false); + let converted_total_read = get_exact_byte_values(process.total_read_bytes, false); + let converted_total_write = get_exact_byte_values(process.total_write_bytes, false); + + let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); + let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); + let total_read = format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1); + let total_write = format!( + "{:.*}{}", + 0, converted_total_write.0, converted_total_write.1 + ); + + single_list.push(ConvertedProcessData { + pid: process.pid, + name: process.name.to_string(), + cpu_usage: process.cpu_usage_percent, + mem_usage: process.mem_usage_percent, + group_pids: vec![process.pid], + read_per_sec, + write_per_sec, + total_read, + total_write, + rps_f64: converted_rps.0, + wps_f64: converted_wps.0, + tr_f64: converted_total_read.0, + tw_f64: converted_total_write.0, + process_states: process.process_state.to_owned(), + }); } let grouped_list: Vec = grouped_hashmap @@ -453,6 +481,10 @@ pub fn convert_process_data( write_per_sec, total_read, total_write, + rps_f64: converted_rps.0, + wps_f64: converted_wps.0, + tr_f64: converted_total_read.0, + tw_f64: converted_total_write.0, process_states: p.process_state, } }) diff --git a/src/main.rs b/src/main.rs index 7d31f718..1c99bb84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,7 +98,10 @@ fn get_matches() -> clap::ArgMatches<'static> { } fn main() -> error::Result<()> { - create_logger()?; + #[cfg(debug_assertions)] + { + utils::logging::init_logger()?; + } let matches = get_matches(); let config: Config = create_config(matches.value_of("CONFIG_LOCATION"))?; @@ -314,15 +317,9 @@ fn handle_key_event_or_break( // Otherwise, track the modifier as well... if let KeyModifiers::ALT = event.modifiers { match event.code { - KeyCode::Char('c') | KeyCode::Char('C') => { - app.toggle_ignore_case(); - } - KeyCode::Char('w') | KeyCode::Char('W') => { - app.toggle_search_whole_word(); - } - KeyCode::Char('r') | KeyCode::Char('R') => { - app.toggle_search_regex(); - } + KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(), + KeyCode::Char('w') | KeyCode::Char('W') => app.toggle_search_whole_word(), + KeyCode::Char('r') | KeyCode::Char('R') => app.toggle_search_regex(), KeyCode::Char('h') => app.on_left_key(), KeyCode::Char('l') => app.on_right_key(), _ => {} @@ -370,13 +367,6 @@ fn handle_key_event_or_break( false } -fn create_logger() -> error::Result<()> { - if cfg!(debug_assertions) { - utils::logging::init_logger()?; - } - Ok(()) -} - fn create_config(flag_config_location: Option<&str>) -> error::Result { use std::{ffi::OsString, fs}; let config_path = if let Some(conf_loc) = flag_config_location { @@ -594,7 +584,6 @@ fn update_all_process_lists(app: &mut App) { } fn update_final_process_list(app: &mut App, widget_id: u64) { - use utils::gen_util::get_exact_byte_values; let is_invalid_or_blank = match app.proc_state.widget_states.get(&widget_id) { Some(process_state) => process_state .process_search_state @@ -603,77 +592,43 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { None => false, }; + let process_filter = app.get_process_filter(widget_id); let filtered_process_data: Vec = if app.is_grouped(widget_id) { app.canvas_data .grouped_process_data .iter() .filter(|process| { if is_invalid_or_blank { - return true; - } else if let Some(matcher_result) = app.get_current_regex_matcher(widget_id) { - if let Ok(matcher) = matcher_result { - return matcher.is_match(&process.name); - } + true + } else if let Some(process_filter) = process_filter { + process_filter.check(process) + } else { + true } - - true }) .cloned() .collect::>() } else { - let is_searching_with_pid = match app.proc_state.widget_states.get(&widget_id) { - Some(process_state) => process_state.process_search_state.is_searching_with_pid, - None => false, - }; - app.canvas_data .process_data .iter() - .filter_map(|(_pid, process)| { + .filter_map(|process| { let mut result = true; if !is_invalid_or_blank { - if let Some(matcher_result) = app.get_current_regex_matcher(widget_id) { - if let Ok(matcher) = matcher_result { - if is_searching_with_pid { - result = matcher.is_match(&process.pid.to_string()); - } else { - result = matcher.is_match(&process.name); - } - } - } + result = if let Some(process_filter) = process_filter { + process_filter.check(&process) + } else { + true + }; } - let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false); - let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false); - let converted_total_read = get_exact_byte_values(process.total_read_bytes, false); - let converted_total_write = get_exact_byte_values(process.total_write_bytes, false); - - let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); - let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); - let total_read = - format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1); - let total_write = format!( - "{:.*}{}", - 0, converted_total_write.0, converted_total_write.1 - ); - if result { - return Some(ConvertedProcessData { - pid: process.pid, - name: process.name.clone(), - cpu_usage: process.cpu_usage_percent, - mem_usage: process.mem_usage_percent, - group_pids: vec![process.pid], - read_per_sec, - write_per_sec, - total_read, - total_write, - process_states: process.process_state.clone(), - }); + return Some(process); + } else { + None } - - None }) + .cloned() .collect::>() }; diff --git a/src/utils/error.rs b/src/utils/error.rs index 41b3d065..22c217bc 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -1,4 +1,4 @@ -use std::result; +use std::{borrow::Cow, result}; /// A type alias for handling errors related to Bottom. pub type Result = result::Result; @@ -22,6 +22,8 @@ pub enum BottomError { ConfigError(String), /// An error to represent errors with converting between data types. ConversionError(String), + /// An error to represent errors with querying. + QueryError(Cow<'static, str>), } impl std::fmt::Display for BottomError { @@ -47,6 +49,9 @@ impl std::fmt::Display for BottomError { BottomError::ConversionError(ref message) => { write!(f, "unable to convert: {}", message) } + BottomError::QueryError(ref _message) => { + write!(f, "invalid query - this should not be shown!") + } } } } @@ -98,3 +103,9 @@ impl From for BottomError { BottomError::ConversionError(err.to_string()) } } + +impl From for BottomError { + fn from(err: regex::Error) -> Self { + BottomError::QueryError(err.to_string().into()) + } +} diff --git a/src/utils/logging.rs b/src/utils/logging.rs index 36619587..72822897 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -1,3 +1,4 @@ +#[cfg(debug_assertions)] pub fn init_logger() -> Result<(), fern::InitError> { fern::Dispatch::new() .format(|out, message, record| {