Merge pull request #139 from ClementTsang/improve_searching

Improve searching
This commit is contained in:
Clement Tsang 2020-05-02 23:54:10 -04:00 committed by GitHub
commit 39d7450aad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1003 additions and 374 deletions

View File

@ -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,7 +19,9 @@ 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).
### Changes
@ -42,7 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#70](https://github.com/ClementTsang/bottom/issues/70): Redesigned help menu to allow for scrolling.
- [#134](https://github.com/ClementTsang/bottom/pull/134): Added `hjkl` movement to delete dialog.
- [#59](https://github.com/ClementTsang/bottom/issues/59): Moved maximization key to `e`, renamed feature to _expanding_ the widget. Done to allow for the `<Enter>` key to be used later for a more intuitive usage.
- [#59](https://github.com/ClementTsang/bottom/issues/59): Redesigned search menu and query.

View File

@ -42,6 +42,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).

20
Cargo.lock generated
View File

@ -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)",
@ -116,7 +116,7 @@ dependencies = [
[[package]]
name = "bottom"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"assert_cmd 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.46 (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"

View File

@ -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"

106
README.md
View File

@ -3,13 +3,13 @@
[![Build Status](https://travis-ci.com/ClementTsang/bottom.svg?token=1wvzVgp94E1TZyPNs8JF&branch=master)](https://travis-ci.com/ClementTsang/bottom)
[![crates.io link](https://img.shields.io/crates/v/bottom.svg)](https://crates.io/crates/bottom)
[![tokei](https://tokei.rs/b1/github/ClementTsang/bottom?category=code)](https://github.com/ClementTsang/bottom)
[![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
A cross-platform graphical process/system monitor with a customizable interface and a multitude of features. Supports Linux, macOS, and Windows. Inspired by both [gtop](https://github.com/aksakalli/gtop) and [gotop](https://github.com/cjbassi/gotop).
<!--TODO: Update recording for 0.4-->
![Quick demo recording showing off searching, maximizing, and process killing.](assets/summary_and_search.gif) _Theme based on [gruvbox](https://github.com/morhetz/gruvbox) (see [sample config](./sample_configs/demo_config.toml))._ Recorded on version 0.2.0.
![Quick demo recording showing off searching, expanding, and process killing.](assets/summary_and_search.gif) _Theme based on [gruvbox](https://github.com/morhetz/gruvbox) (see [sample config](./sample_configs/demo_config.toml))._ Recorded on version 0.2.0.
**Note**: This documentation is relevant to version 0.4.0 and may refer to in-development or unreleased features, especially if you are reading this on the master branch. Please refer to [release branch](https://github.com/ClementTsang/bottom/tree/release/README.md) or [crates.io](https://crates.io/crates/bottom) for the most up-to-date _release_ documentation.
@ -32,10 +32,15 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Process bindings](#process-bindings)
- [Process search bindings](#process-search-bindings)
- [Battery bindings](#battery-bindings)
- [Process searching keywords](#process-searching-keywords)
- [Supported keywords](#supported-keywords)
- [Supported comparison operators](#supported-comparison-operators)
- [Supported logical operators](#supported-logical-operators)
- [Supported units](#supported-units)
- [Features](#features)
- [Process filtering](#process-filtering)
- [Process searching](#process-searching)
- [Zoom](#zoom)
- [Maximizing](#maximizing)
- [Expanding](#expanding)
- [Basic mode](#basic-mode)
- [Config files](#config-files)
- [Config flags](#config-flags)
@ -44,6 +49,7 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Battery](#battery)
- [Compatibility](#compatibility)
- [Contribution](#contribution)
- [Contributors](#contributors)
- [Thanks](#thanks)
## Installation
@ -169,7 +175,7 @@ Run using `btm`.
| | |
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
| `q`, `Ctrl-c` | Quit |
| `Esc` | Close dialog windows, search, widgets, or exit maximized mode |
| `Esc` | Close dialog windows, search, widgets, or exit expanded mode |
| `Ctrl-r` | Reset display and any collected data |
| `f` | Freeze/unfreeze updating with new data |
| `Ctrl`-arrow key<br>`Shift`-arrow key<br>`H/J/K/L` | Move to a different widget (on macOS some keybindings may conflict) |
@ -180,7 +186,7 @@ Run using `btm`.
| `?` | Open help menu |
| `gg`, `Home` | Jump to the first entry |
| `Shift-g`, `End` | Jump to the last entry |
| `Enter` | Maximize the currently selected widget |
| `e` | Expand the currently selected widget |
| `+` | Zoom in on chart (decrease time range) |
| `-` | Zoom out on chart (increase time range) |
| `=` | Reset zoom |
@ -225,10 +231,57 @@ Run using `btm`.
#### Battery bindings
| | |
| ------- | -------------------------- |
| `Left` | Go to the next battery |
| `Right` | Go to the previous battery |
| | |
| -------------- | -------------------------- |
| `Left, Alt-h` | Go to the next battery |
| `Right, Alt-l` | Go to the previous battery |
### Process searching keywords
Note none of the keywords are case sensitive. Furthermore, if you want to search a reserved keyword, surround the text in quotes - for example, `"And" or "Or"` would be a valid search.
#### Supported keywords
| | | |
| -------- | --------------- | ------------------------------------------------------------------------------- |
| `pid` | `pid: 1044` | Matches by PID; supports regex and requiring matching the entire PID |
| `cpu` | `cpu > 0.5` | Matches the condition for the CPU column; supports comparison operators |
| `mem` | `mem < 0.5` | Matches the condition for the memory column; supports comparison operators |
| `read` | `read = 1` | Matches the condition for the read/s column; supports comparison operators |
| `write` | `write >= 1` | Matches the condition for the write/s column; supports comparison operators |
| `tread` | `tread <= 1024` | Matches the condition for the total read column; supports comparison operators |
| `twrite` | `twrite > 1024` | Matches the condition for the total write column; supports comparison operators |
#### Supported comparison operators
| | |
| ---- | -------------------------------------------------------------- |
| `=` | Checks if the values are equal |
| `>` | Checks if the left value is strictly greater than the right |
| `<` | Checks if the left value is strictly less than the right |
| `>=` | Checks if the left value is greater than or equal to the right |
| `<=` | Checks if the left value is less than or equal to the right |
#### Supported logical operators
| | | |
| ------------------ | -------------------------------------------- | ----------------------------------------------------|
| `and, &&, <Space>` | `<CONDITION 1> and/&&/<Space> <CONDITION 2>` | Requires both conditions to be true to match |
| `or, \|\|` | `<CONDITION 1> or/\|\| <CONDITION 2>` | Requires at least one condition to be true to match |
#### Supported units
| | |
| ----- | --------- |
| `B` | Bytes |
| `KB` | Kilobytes |
| `MB` | Megabytes |
| `GB` | Gigabytes |
| `TB` | Terabytes |
| `KiB` | Kibibytes |
| `MiB` | Mebibytes |
| `GiB` | Gibibytes |
| `TiB` | Tebibytes |
## Features
@ -252,12 +305,29 @@ It also aims to be:
In addition, bottom also currently has the following features:
### Process filtering
### Process searching
On any process widget, hit `/` to bring up a search bar. If the layout has
multiple process widgets, note this search is independent of other widgets. Searching
supports regex, matching case, and matching entire words. Use `Tab` to toggle between
searching by PID and by process name.
On any process widget, hit `/` to bring up a search bar. If the layout has multiple process widgets, note this search is independent of other widgets.
![search bar image](assets/search_empty.png)
By default, just typing in something will search by process name:
![a simple search](assets/simple_search.png)
This simple search can be refined by matching by case, matching the entire word, or by using regex:
![a slightly better search](assets/simple_advanced_search.png)
Now let's say you want to search for two things: luckily, we have the `AND` and `OR` logical operators:
![logical operator demo](assets/or_search.png)
Furthermore, one is able to refine their searches by CPU usage, memory usage, PID, and more. For example:
![using cpu filter](assets/search_cpu_filter.png)
One can see all available keywords and query options [here](#process-searching-keywords).
### Zoom
@ -265,9 +335,9 @@ Using the `+`/`-` keys or the scroll wheel will move the current time intervals
Widgets can hold different time intervals independently. These time intervals can be adjusted using the
`-t`/`--default_time_value` and `-d`/`--time_delta` options, or their corresponding config options.
### Maximizing
### Expand
Only care about one specific widget? You can go to that widget and hit `Enter` to make that widget take
Only care about one specific widget? You can go to that widget and hit `e` to make that widget expand and take
up the entire drawing area.
### Basic mode
@ -464,6 +534,8 @@ The current compatibility of widgets with operating systems from personal testin
Contribution is always welcome - please take a look at [CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to help.
### Contributors
Thanks to all contributors ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/or_search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/search_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
assets/simple_search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -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);
}
}
@ -533,16 +476,6 @@ impl App {
} else {
self.delete_dialog_state.is_showing_dd = false;
}
} else if !self.is_in_dialog() && !self.app_config_fields.use_basic_mode {
// Pop-out mode. We ignore if in process search.
match self.current_widget.widget_type {
BottomWidgetType::ProcSearch => {}
_ => {
self.is_expanded = true;
self.is_force_redraw = true;
}
}
}
}
@ -585,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 {
@ -640,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<std::result::Result<regex::Regex, regex::Error>> {
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<query::Query> {
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
}
}
@ -882,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
@ -895,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<u32>);
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();
}
}
}
@ -987,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
@ -1178,6 +1100,7 @@ impl App {
'+' => self.zoom_in(),
'-' => self.zoom_out(),
'=' => self.reset_zoom(),
'e' => self.expand_widget(),
_ => {}
}
@ -1209,6 +1132,20 @@ impl App {
self.to_delete_process_list.clone()
}
fn expand_widget(&mut self) {
if !self.is_in_dialog() && !self.app_config_fields.use_basic_mode {
// Pop-out mode. We ignore if in process search.
match self.current_widget.widget_type {
BottomWidgetType::ProcSearch => {}
_ => {
self.is_expanded = true;
self.is_force_redraw = true;
}
}
}
}
pub fn move_widget_selection_left(&mut self) {
if !self.is_in_dialog() && !self.is_expanded {
if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) {

615
src/app/query.rs Normal file
View File

@ -0,0 +1,615 @@
use super::ProcWidgetState;
use crate::{
data_conversion::ConvertedProcessData,
utils::error::{
BottomError::{self, QueryError},
Result,
},
};
use std::collections::VecDeque;
const DELIMITER_LIST: [char; 5] = ['=', '>', '<', '(', ')'];
const AND_LIST: [&str; 2] = ["and", "&&"];
const OR_LIST: [&str; 2] = ["or", "||"];
/// I only separated this as otherwise, the states.rs file gets huge... and this should
/// belong in another file anyways, IMO.
pub trait ProcessQuery {
/// 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<Query>;
}
impl ProcessQuery for ProcWidgetState {
fn parse_query(&self) -> Result<Query> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
let mut lhs: And = process_and(query)?;
while query.front().is_some() {
let rhs = Some(Box::new(process_or(query)?));
lhs = And {
lhs: Or {
lhs: Prefix {
and: Some(Box::from(lhs)),
compare_prefix: None,
regex_prefix: None,
},
rhs: None,
},
rhs,
};
}
Ok(Query { query: lhs })
}
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
let mut lhs = process_or(query)?;
let mut rhs: Option<Box<Or>> = None;
while let Some(queue_top) = query.front() {
if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_or(query)?));
if let Some(queue_next) = query.front() {
if AND_LIST.contains(&queue_next.to_lowercase().as_str()) {
// 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<String>) -> Result<Or> {
let mut lhs = process_prefix(query)?;
let mut rhs: Option<Box<Prefix>> = None;
while let Some(queue_top) = query.front() {
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_prefix(query)?));
if let Some(queue_next) = query.front() {
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
// 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<String>) -> Result<Prefix> {
if let Some(queue_top) = query.pop_front() {
if queue_top == "(" {
if query.front().is_none() {
return Err(QueryError("Missing closing parentheses".into()));
}
// 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::<PrefixType>()?;
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.trim_matches('\"').to_owned()),
)),
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<QueryComparison> = None;
let mut value: Option<f64> = 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::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} 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::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} else {
condition = Some(if content == ">" {
QueryComparison::Greater
} else {
QueryComparison::Less
});
value = queue_next.parse::<f64>().ok();
}
} else {
return Err(QueryError("Missing value".into()));
}
}
if let Some(condition) = condition {
if let Some(read_value) = value {
// Now we want to check one last thing - is there a unit?
// If no unit, assume base.
// Furthermore, base must be PEEKED at initially, and will
// require (likely) prefix_type specific checks
// Lastly, if it *is* a unit, remember to POP!
let mut value = read_value;
match prefix_type {
PrefixType::Rps
| PrefixType::Wps
| PrefixType::TRead
| PrefixType::TWrite => {
if let Some(potential_unit) = query.front() {
match potential_unit.as_str() {
"TB" => {
value *= 1_000_000_000_000.0;
query.pop_front();
}
"TiB" => {
value *= 1_099_511_627_776.0;
query.pop_front();
}
"GB" => {
value *= 1_000_000_000.0;
query.pop_front();
}
"GiB" => {
value *= 1_073_741_824.0;
query.pop_front();
}
"MB" => {
value *= 1_000_000.0;
query.pop_front();
}
"MiB" => {
value *= 1_048_576.0;
query.pop_front();
}
"KB" => {
value *= 1000.0;
query.pop_front();
}
"KiB" => {
value *= 1024.0;
query.pop_front();
}
"B" => {
// Just gotta pop.
query.pop_front();
}
_ => {}
}
}
}
_ => {}
}
return Ok(Prefix {
and: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
NumericalQuery { condition, value },
)),
});
}
}
}
}
} else {
return Err(QueryError("Missing argument for search prefix".into()));
}
}
}
Err(QueryError("Invalid search".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| DELIMITER_LIST.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)
}
}
#[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<Box<Or>>,
}
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<Box<Prefix>>,
}
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<Self> {
use PrefixType::*;
let lower_case = s.to_lowercase();
match lower_case.as_str() {
"cpu" => Ok(Cpu),
"mem" => Ok(Mem),
"read" => Ok(Rps),
"write" => Ok(Wps),
"tread" => Ok(TRead),
"twrite" => Ok(TWrite),
"pid" => Ok(Pid),
_ => Ok(Name),
}
}
}
#[derive(Debug)]
pub struct Prefix {
pub and: Option<Box<And>>,
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).abs() < f64::EPSILON,
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,
}

View File

@ -4,7 +4,11 @@ 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,
};
#[derive(Debug)]
pub enum ScrollDirection {
@ -61,7 +65,6 @@ impl Default for AppHelpDialogState {
pub struct AppSearchState {
pub is_enabled: bool,
pub current_search_query: String,
pub current_regex: Option<std::result::Result<regex::Regex, regex::Error>>,
pub is_blank_search: bool,
pub is_invalid_search: bool,
pub grapheme_cursor: GraphemeCursor,
@ -69,6 +72,9 @@ 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<Query>,
pub error_message: Option<String>,
}
impl Default for AppSearchState {
@ -76,13 +82,14 @@ 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,
error_message: None,
}
}
}
@ -104,7 +111,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 +120,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,48 +193,28 @@ 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
.current_search_query
.is_empty()
{
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.is_blank_search = true;
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
} 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);
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);
let parsed_query = self.parse_query();
if let Ok(parsed_query) = parsed_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 = false;
self.process_search_state.search_state.error_message = None;
} else if let Err(err) = parsed_query {
self.process_search_state.search_state.is_blank_search = false;
self.process_search_state.search_state.is_invalid_search = true;
self.process_search_state.search_state.error_message = Some(err.to_string());
}
}
self.scroll_state.previous_scroll_position = 0;
self.scroll_state.current_scroll_position = 0;

View File

@ -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<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>,
// Not the final value
pub process_data: HashMap<u32, ProcessHarvest>,
pub process_data: Vec<ConvertedProcessData>,
// Not the final value
pub grouped_process_data: Vec<ConvertedProcessData>,
// What's actually displayed
@ -386,9 +385,18 @@ impl Painter {
self.draw_basic_cpu(&mut f, app_state, vertical_chunks[0], 1);
self.draw_basic_memory(&mut f, app_state, middle_chunks[0], 2);
self.draw_basic_network(&mut f, app_state, middle_chunks[1], 3);
self.draw_basic_table_arrows(&mut f, app_state, vertical_chunks[3]);
if let Some(basic_table_widget_state) = &app_state.basic_table_widget_state {
let widget_id = basic_table_widget_state.currently_displayed_widget_id;
if let Some(current_table) = app_state.widget_map.get(&widget_id) {
self.draw_basic_table_arrows(
&mut f,
app_state,
vertical_chunks[3],
current_table,
);
}
match basic_table_widget_state.currently_displayed_widget_type {
Disk => self.draw_disk_table(
&mut f,

View File

@ -26,6 +26,7 @@ pub struct CanvasColours {
pub graph_style: Style,
// Full, Medium, Low
pub battery_bar_styles: Vec<Style>,
pub invalid_query_style: Style,
}
impl Default for CanvasColours {
@ -60,6 +61,7 @@ impl Default for CanvasColours {
Style::default().fg(Color::Green),
Style::default().fg(Color::Green),
],
invalid_query_style: tui::style::Style::default().fg(tui::style::Color::Red),
}
}
}

View File

@ -1 +0,0 @@

View File

@ -1,7 +1,10 @@
use std::cmp::max;
use crate::{
app::{layout_manager::BottomWidgetType, App},
app::{
layout_manager::{BottomWidget, BottomWidgetType},
App,
},
canvas::Painter,
};
@ -14,19 +17,18 @@ use tui::{
pub trait BasicTableArrows {
fn draw_basic_table_arrows<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
&self, f: &mut Frame<'_, B>, app_state: &App, draw_loc: Rect, current_table: &BottomWidget,
);
}
impl BasicTableArrows for Painter {
fn draw_basic_table_arrows<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect,
&self, f: &mut Frame<'_, B>, app_state: &App, draw_loc: Rect, current_table: &BottomWidget,
) {
// Effectively a paragraph with a ton of spacing
let (left_table, right_table) = (
{
app_state
.current_widget
current_table
.left_neighbour
.map(|left_widget_id| {
app_state
@ -38,8 +40,7 @@ impl BasicTableArrows for Painter {
.unwrap_or_else(|| &BottomWidgetType::Temp)
},
{
app_state
.current_widget
current_table
.right_neighbour
.map(|right_widget_id| {
app_state

View File

@ -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

View File

@ -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,11 +44,11 @@ impl ProcessTableWidget for Painter {
widget_id: u64,
) {
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
let search_width = if draw_border { 5 } else { 3 };
let search_height = if draw_border { 4 } else { 3 };
if process_widget_state.is_search_enabled() {
let processes_chunk = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(search_width)].as_ref())
.constraints([Constraint::Min(0), Constraint::Length(search_height)].as_ref())
.split(draw_loc);
self.draw_processes_table(f, app_state, processes_chunk[0], draw_border, widget_id);
@ -85,6 +85,9 @@ impl ProcessTableWidget for Painter {
// do so by hiding some elements!
let num_rows = max(0, i64::from(draw_loc.height) - self.table_height_offset) as u64;
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 is_search_enabled = proc_widget_state.is_search_enabled();
let position = get_start_position(
num_rows,
@ -145,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()
@ -169,7 +172,7 @@ impl ProcessTableWidget for Painter {
wps,
total_read,
total_write,
process_state,
// process_state,
];
let process_headers_lens: Vec<usize> = process_headers
.iter()
@ -178,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,
@ -212,7 +215,7 @@ impl ProcessTableWidget for Painter {
String::default()
};
let (border_and_title_style, highlight_style) = if is_on_widget {
let (border_and_title_style, highlight_style) = if is_on_processes {
(
self.colours.highlighted_border_style,
self.colours.currently_selected_text_style,
@ -229,7 +232,11 @@ impl ProcessTableWidget for Painter {
} else {
self.colours.widget_title_style
})
.borders(Borders::ALL)
.borders(if is_search_enabled {
*TOP_LEFT_RIGHT
} else {
Borders::ALL
})
.border_style(border_and_title_style)
} else if is_on_widget {
Block::default()
@ -271,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,
@ -335,29 +328,16 @@ 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();
let mut search_text = vec![Text::styled(search_title, self.colours.table_header_style)];
let cursor_position = proc_widget_state.get_cursor_position();
let current_cursor_position = proc_widget_state.get_char_cursor_position();
let is_search_enabled = proc_widget_state.is_search_enabled();
let start_position: usize = get_search_start_position(
num_columns - num_chars_for_text - 5,
@ -373,6 +353,14 @@ impl ProcessTableWidget for Painter {
app_state.is_force_redraw,
);
let mut search_text = vec![Text::styled(
search_title,
if is_on_widget {
self.colours.table_header_style
} else {
self.colours.text_style
},
)];
let query = proc_widget_state.get_current_search_query().as_str();
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
let query_with_cursor: Vec<Text<'_>> = build_query(
@ -385,6 +373,8 @@ impl ProcessTableWidget for Painter {
self.colours.text_style,
);
// TODO: [QUERY] Make text/border go red if error?
// Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
self.colours.currently_selected_text_style
@ -410,71 +400,55 @@ impl ProcessTableWidget for Painter {
self.colours.text_style
};
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 option_row = vec![
Text::raw("\n\n"),
Text::styled(&case_text, case_style),
Text::raw(if small_mode { " " } else { " " }),
Text::styled(&whole_text, whole_word_style),
Text::raw(if small_mode { " " } else { " " }),
Text::styled(&regex_text, regex_style),
let option_text = vec![
Text::raw("\n"),
Text::styled(
format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }),
case_style,
),
Text::raw(" "),
Text::styled(
format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" }),
whole_word_style,
),
Text::raw(" "),
Text::styled(
format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" }),
regex_style,
),
];
option_text.extend(option_row);
search_text.extend(query_with_cursor);
search_text.push(Text::styled(
format!(
"\n{}",
if let Some(err) = &proc_widget_state
.process_search_state
.search_state
.error_message
{
err.as_str()
} else {
""
}
),
self.colours.invalid_query_style,
));
search_text.extend(option_text);
let current_border_style = if proc_widget_state
.process_search_state
.search_state
.is_invalid_search
{
*INVALID_REGEX_STYLE
} else if is_on_widget {
let current_border_style = if is_on_processes {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
let title = if draw_border {
const TITLE_BASE: &str = " Esc to close ";
let repeat_num = max(
0,
draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2,
);
format!("{} Esc to close ", "".repeat(repeat_num as usize))
} else {
String::new()
};
let process_search_block = if draw_border {
Block::default()
.title(&title)
.title_style(current_border_style)
.borders(Borders::ALL)
.borders(if is_search_enabled {
*BOTTOM_LEFT_RIGHT
} else {
Borders::ALL
})
.border_style(current_border_style)
} else if is_on_widget {
Block::default()

View File

@ -27,28 +27,30 @@ pub const FORCE_MIN_THRESHOLD: usize = 5;
lazy_static! {
pub static ref SIDE_BORDERS: tui::widgets::Borders =
tui::widgets::Borders::from_bits_truncate(20);
pub static ref TOP_LEFT_RIGHT: tui::widgets::Borders =
tui::widgets::Borders::from_bits_truncate(22);
pub static ref BOTTOM_LEFT_RIGHT: tui::widgets::Borders =
tui::widgets::Borders::from_bits_truncate(28);
pub static ref DEFAULT_TEXT_STYLE: tui::style::Style =
tui::style::Style::default().fg(tui::style::Color::Gray);
pub static ref DEFAULT_HEADER_STYLE: tui::style::Style =
tui::style::Style::default().fg(tui::style::Color::LightBlue);
pub static ref INVALID_REGEX_STYLE: tui::style::Style =
tui::style::Style::default().fg(tui::style::Color::Red);
}
// Help text
pub const HELP_CONTENTS_TEXT: [&str; 6] = [
"Press the corresponding numbers to jump to the section, or scroll:\n",
"1 - General bindings\n",
"2 - CPU bindings\n",
"3 - Process bindings\n",
"4 - Process search bindings\n",
"5 - Battery bindings",
"1 - General\n",
"2 - CPU widget\n",
"3 - Process widget\n",
"4 - Process search widget\n",
"5 - Battery widget",
];
pub const GENERAL_HELP_TEXT: [&str; 20] = [
"1 - General bindings\n",
"1 - General\n",
"q, Ctrl-c Quit\n",
"Esc Close dialog windows, search, widgets, or exit maximized mode\n",
"Esc Close dialog windows, search, widgets, or exit expanded mode\n",
"Ctrl-r Reset display and any collected data\n",
"f Freeze/unfreeze updating with new data\n",
"Ctrl-Arrow \n",
@ -61,7 +63,7 @@ pub const GENERAL_HELP_TEXT: [&str; 20] = [
"? Open help menu\n",
"gg Jump to the first entry\n",
"G Jump to the last entry\n",
"Enter Maximize the currently selected widget\n",
"e Expand the currently selected widget\n",
"+ Zoom in on chart (decrease time range)\n",
"- Zoom out on chart (increase time range)\n",
"= Reset zoom\n",
@ -69,14 +71,14 @@ pub const GENERAL_HELP_TEXT: [&str; 20] = [
];
pub const CPU_HELP_TEXT: [&str; 4] = [
"2 - CPU bindings\n",
"2 - CPU widget\n",
"/ Open filtering for showing certain CPU cores\n",
"Space Toggle enabled/disabled cores\n",
"Esc Exit filtering mode",
];
pub const PROCESS_HELP_TEXT: [&str; 8] = [
"3 - Process bindings\n",
"3 - Process widget\n",
"dd Kill the selected process\n",
"c Sort by memory usage, press again to reverse sorting order\n",
"m Sort by memory usage\n",
@ -86,8 +88,8 @@ pub const PROCESS_HELP_TEXT: [&str; 8] = [
"Ctrl-f, / Open process search widget",
];
pub const SEARCH_HELP_TEXT: [&str; 13] = [
"4 - Process search bindings\n",
pub const SEARCH_HELP_TEXT: [&str; 40] = [
"4 - Process search widget\n",
"Tab Toggle between searching for PID and name\n",
"Esc Close the search widget (retains the filter)\n",
"Ctrl-a Skip to the start of the search query\n",
@ -98,12 +100,39 @@ pub const SEARCH_HELP_TEXT: [&str; 13] = [
"Alt-c/F1 Toggle matching case\n",
"Alt-w/F2 Toggle matching the entire word\n",
"Alt-r/F3 Toggle using regex\n",
"Left Move cursor left\n",
"Right Move cursor right",
"Left, Alt-h Move cursor left\n",
"Right, Alt-l Move cursor right\n",
"Search keywords\n",
"pid\n",
"cpu\n",
"mem\n",
"pid\n",
"read\n",
"write\n",
"tread\n",
"twrite\n\n",
"\nComparison operators\n",
"=\n",
">\n",
"<\n",
">=\n",
"<=\n",
"\nLogical operators\n",
"and/&&\n",
"or/||\n",
"\nSupported units\n",
"B\n",
"KB\n",
"MB\n",
"TB\n",
"KiB\n",
"MiB\n",
"GiB\n",
"TiB\n",
];
pub const BATTERY_HELP_TEXT: [&str; 3] = [
"5 - Battery bindings\n",
"5 - Battery widget\n",
"Left Go to previous battery\n",
"Right Go to next battery",
];

View File

@ -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<u32, ProcessHarvest>, Vec<ConvertedProcessData>) {
let mut single_list: HashMap<u32, ProcessHarvest> = HashMap::new();
) -> (Vec<ConvertedProcessData>, Vec<ConvertedProcessData>) {
let mut single_list = Vec::new();
// cpu, mem, pids
let mut grouped_hashmap: HashMap<String, SingleProcessData> = 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: process.read_bytes_per_sec as f64,
wps_f64: process.write_bytes_per_sec as f64,
tr_f64: process.total_read_bytes as f64,
tw_f64: process.total_write_bytes as f64,
process_states: process.process_state.to_owned(),
});
}
let grouped_list: Vec<ConvertedProcessData> = grouped_hashmap
@ -453,6 +481,10 @@ pub fn convert_process_data(
write_per_sec,
total_read,
total_write,
rps_f64: p.read_per_sec as f64,
wps_f64: p.write_per_sec as f64,
tr_f64: p.total_read as f64,
tw_f64: p.total_write as f64,
process_states: p.process_state,
}
})

View File

@ -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,11 @@ 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(),
_ => {}
}
} else if let KeyModifiers::CONTROL = event.modifiers {
@ -368,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<Config> {
use std::{ffi::OsString, fs};
let config_path = if let Some(conf_loc) = flag_config_location {
@ -592,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
@ -601,77 +592,38 @@ 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<ConvertedProcessData> = 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::<Vec<_>>()
} 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)| {
let mut result = true;
.filter(|process| {
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);
}
}
if let Some(process_filter) = process_filter {
process_filter.check(&process)
} else {
true
}
} 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(),
});
}
None
})
.cloned()
.collect::<Vec<_>>()
};

View File

@ -1,4 +1,4 @@
use std::result;
use std::{borrow::Cow, result};
/// A type alias for handling errors related to Bottom.
pub type Result<T> = result::Result<T, BottomError>;
@ -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,7 @@ impl std::fmt::Display for BottomError {
BottomError::ConversionError(ref message) => {
write!(f, "unable to convert: {}", message)
}
BottomError::QueryError(ref message) => write!(f, "{}", message),
}
}
}
@ -98,3 +101,9 @@ impl From<std::str::Utf8Error> for BottomError {
BottomError::ConversionError(err.to_string())
}
}
impl From<regex::Error> for BottomError {
fn from(err: regex::Error) -> Self {
BottomError::QueryError(err.to_string().into())
}
}

View File

@ -1,3 +1,4 @@
#[cfg(debug_assertions)]
pub fn init_logger() -> Result<(), fern::InitError> {
fern::Dispatch::new()
.format(|out, message, record| {