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/), 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). 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 ## [0.4.0] - Unreleased
### Features ### 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. - [#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 ### 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. - [#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. - [#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 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: 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). - 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" version = "0.3.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "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)", "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
@ -71,7 +71,7 @@ dependencies = [
[[package]] [[package]]
name = "backtrace-sys" name = "backtrace-sys"
version = "0.1.36" version = "0.1.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)", "cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
@ -116,7 +116,7 @@ dependencies = [
[[package]] [[package]]
name = "bottom" name = "bottom"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"assert_cmd 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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 = [ dependencies = [
"proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
@ -938,7 +938,7 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"proc-macro2 1.0.10 (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)", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"proc-macro2 1.0.10 (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)",
"unicode-xid 0.2.0 (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" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"proc-macro2 1.0.10 (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)", "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 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 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 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 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 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" "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-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-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 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 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 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" "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" chrono = "0.4.11"
clap = "2.33.0" clap = "2.33.0"
dirs = "2.0.2" dirs = "2.0.2"
fern = "0.6.0"
futures = "0.3.4" futures = "0.3.4"
heim = "0.0.10" heim = "0.0.10"
itertools = "0.9.0" itertools = "0.9.0"
log = "0.4.8"
regex = "1.3" regex = "1.3"
sysinfo = "0.14" sysinfo = "0.14"
toml = "0.5.6" toml = "0.5.6"
@ -42,9 +40,14 @@ serde = {version = "1.0", features = ["derive"] }
unicode-segmentation = "1.6.0" unicode-segmentation = "1.6.0"
unicode-width = "0.1.7" 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 = {version = "0.9", features = ["crossterm"], default-features = false }
# tui = {git = "https://github.com/ClementTsang/tui-rs", features = ["crossterm"], default-features = false } # tui = {git = "https://github.com/ClementTsang/tui-rs", features = ["crossterm"], default-features = false }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winapi = "0.3.8" winapi = "0.3.8"

104
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) [![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) [![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) [![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). 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--> <!--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. **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 bindings](#process-bindings)
- [Process search bindings](#process-search-bindings) - [Process search bindings](#process-search-bindings)
- [Battery bindings](#battery-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) - [Features](#features)
- [Process filtering](#process-filtering) - [Process searching](#process-searching)
- [Zoom](#zoom) - [Zoom](#zoom)
- [Maximizing](#maximizing) - [Expanding](#expanding)
- [Basic mode](#basic-mode) - [Basic mode](#basic-mode)
- [Config files](#config-files) - [Config files](#config-files)
- [Config flags](#config-flags) - [Config flags](#config-flags)
@ -44,6 +49,7 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Battery](#battery) - [Battery](#battery)
- [Compatibility](#compatibility) - [Compatibility](#compatibility)
- [Contribution](#contribution) - [Contribution](#contribution)
- [Contributors](#contributors)
- [Thanks](#thanks) - [Thanks](#thanks)
## Installation ## Installation
@ -169,7 +175,7 @@ Run using `btm`.
| | | | | |
| -------------------------------------------------- | ---------------------------------------------------------------------------- | | -------------------------------------------------- | ---------------------------------------------------------------------------- |
| `q`, `Ctrl-c` | Quit | | `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 | | `Ctrl-r` | Reset display and any collected data |
| `f` | Freeze/unfreeze updating with new 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) | | `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 | | `?` | Open help menu |
| `gg`, `Home` | Jump to the first entry | | `gg`, `Home` | Jump to the first entry |
| `Shift-g`, `End` | Jump to the last 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 in on chart (decrease time range) |
| `-` | Zoom out on chart (increase time range) | | `-` | Zoom out on chart (increase time range) |
| `=` | Reset zoom | | `=` | Reset zoom |
@ -226,9 +232,56 @@ Run using `btm`.
#### Battery bindings #### Battery bindings
| | | | | |
| ------- | -------------------------- | | -------------- | -------------------------- |
| `Left` | Go to the next battery | | `Left, Alt-h` | Go to the next battery |
| `Right` | Go to the previous 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 ## Features
@ -252,12 +305,29 @@ It also aims to be:
In addition, bottom also currently has the following features: 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 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.
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 ![search bar image](assets/search_empty.png)
searching by PID and by process name.
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 ### 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 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. `-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. up the entire drawing area.
### Basic mode ### 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. 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)): Thanks to all contributors ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- 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 data_harvester;
pub mod layout_manager; pub mod layout_manager;
mod process_killer; mod process_killer;
pub mod query;
pub mod states; pub mod states;
const MAX_SEARCH_LENGTH: usize = 200; const MAX_SEARCH_LENGTH: usize = 200;
@ -307,19 +308,6 @@ impl App {
let is_in_search_widget = self.is_in_search_widget(); let is_in_search_widget = self.is_in_search_widget();
if !self.is_in_dialog() { if !self.is_in_dialog() {
if is_in_search_widget { 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 } else if let Some(proc_widget_state) = self
.proc_state .proc_state
.widget_states .widget_states
@ -327,14 +315,10 @@ impl App {
{ {
// Toggles process widget grouping state // Toggles process widget grouping state
proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); 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);
} }
} }
} }
}
/// I don't like this, but removing it causes a bunch of breakage. /// I don't like this, but removing it causes a bunch of breakage.
/// Use ``proc_widget_state.is_grouped`` if possible! /// Use ``proc_widget_state.is_grouped`` if possible!
@ -387,9 +371,6 @@ impl App {
.process_search_state .process_search_state
.search_state .search_state
.is_enabled = true; .is_enabled = true;
if proc_widget_state.is_grouped {
self.search_with_name();
}
self.move_widget_selection_down(); 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) { pub fn toggle_ignore_case(&mut self) {
let is_in_search_widget = self.is_in_search_widget(); let is_in_search_widget = self.is_in_search_widget();
if let Some(proc_widget_state) = self if let Some(proc_widget_state) = self
@ -475,7 +418,7 @@ impl App {
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_toggle_ignore_case(); .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); self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
} }
} }
@ -492,7 +435,7 @@ impl App {
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_toggle_whole_word(); .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); 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() { if is_in_search_widget && proc_widget_state.is_search_enabled() {
proc_widget_state.process_search_state.search_toggle_regex(); 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); self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
} }
} }
@ -533,16 +476,6 @@ impl App {
} else { } else {
self.delete_dialog_state.is_showing_dd = false; 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, true,
); );
proc_widget_state.update_regex(); proc_widget_state.update_query();
self.proc_state.force_update = Some(self.current_widget.widget_id - 1); self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
} }
} else { } else {
@ -640,24 +573,18 @@ impl App {
.search_state .search_state
.cursor_direction = CursorDirection::LEFT; .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); self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
} }
} }
} }
} }
pub fn get_current_regex_matcher( pub fn get_process_filter(&self, widget_id: u64) -> &Option<query::Query> {
&self, widget_id: u64, if let Some(process_widget_state) = self.proc_state.widget_states.get(&widget_id) {
) -> &Option<std::result::Result<regex::Regex, regex::Error>> { &process_widget_state.process_search_state.search_state.query
match self.proc_state.widget_states.get(&widget_id) { } else {
Some(proc_widget_state) => { &None
&proc_widget_state
.process_search_state
.search_state
.current_regex
}
None => &None,
} }
} }
@ -882,6 +809,8 @@ impl App {
} }
pub fn start_dd(&mut self) { pub fn start_dd(&mut self) {
self.reset_multi_tap_keys();
if let Some(proc_widget_state) = self if let Some(proc_widget_state) = self
.proc_state .proc_state
.widget_states .widget_states
@ -895,32 +824,25 @@ impl App {
if proc_widget_state.scroll_state.current_scroll_position if proc_widget_state.scroll_state.current_scroll_position
< corresponding_filtered_process_list.len() as u64 < corresponding_filtered_process_list.len() as u64
{ {
let current_process = if self.is_grouped(self.current_widget.widget_id) { let current_process: (String, Vec<u32>);
let group_pids = &corresponding_filtered_process_list if self.is_grouped(self.current_widget.widget_id) {
[proc_widget_state.scroll_state.current_scroll_position as usize] if let Some(process) = &corresponding_filtered_process_list
.group_pids; .get(proc_widget_state.scroll_state.current_scroll_position as usize)
{
let mut ret = ("".to_string(), group_pids.clone()); current_process = (process.name.to_string(), process.group_pids.clone())
} else {
for pid in group_pids { return;
if let Some(process) = self.canvas_data.process_data.get(&pid) {
ret.0 = process.name.clone();
break;
} }
}
ret
} else { } else {
let process = corresponding_filtered_process_list let process = corresponding_filtered_process_list
[proc_widget_state.scroll_state.current_scroll_position as usize] [proc_widget_state.scroll_state.current_scroll_position as usize]
.clone(); .clone();
(process.name.clone(), vec![process.pid]) current_process = (process.name.clone(), vec![process.pid])
}; };
self.to_delete_process_list = Some(current_process); self.to_delete_process_list = Some(current_process);
self.delete_dialog_state.is_showing_dd = true; self.delete_dialog_state.is_showing_dd = true;
} }
self.reset_multi_tap_keys();
} }
} }
} }
@ -987,7 +909,7 @@ impl App {
.char_cursor_position += .char_cursor_position +=
UnicodeWidthChar::width(caught_char).unwrap_or(0); 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); self.proc_state.force_update = Some(self.current_widget.widget_id - 1);
proc_widget_state proc_widget_state
.process_search_state .process_search_state
@ -1178,6 +1100,7 @@ impl App {
'+' => self.zoom_in(), '+' => self.zoom_in(),
'-' => self.zoom_out(), '-' => self.zoom_out(),
'=' => self.reset_zoom(), '=' => self.reset_zoom(),
'e' => self.expand_widget(),
_ => {} _ => {}
} }
@ -1209,6 +1132,20 @@ impl App {
self.to_delete_process_list.clone() 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) { pub fn move_widget_selection_left(&mut self) {
if !self.is_in_dialog() && !self.is_expanded { if !self.is_in_dialog() && !self.is_expanded {
if let Some(current_widget) = self.widget_map.get(&self.current_widget.widget_id) { 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 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)] #[derive(Debug)]
pub enum ScrollDirection { pub enum ScrollDirection {
@ -61,7 +65,6 @@ impl Default for AppHelpDialogState {
pub struct AppSearchState { pub struct AppSearchState {
pub is_enabled: bool, pub is_enabled: bool,
pub current_search_query: String, pub current_search_query: String,
pub current_regex: Option<std::result::Result<regex::Regex, regex::Error>>,
pub is_blank_search: bool, pub is_blank_search: bool,
pub is_invalid_search: bool, pub is_invalid_search: bool,
pub grapheme_cursor: GraphemeCursor, pub grapheme_cursor: GraphemeCursor,
@ -69,6 +72,9 @@ pub struct AppSearchState {
pub cursor_bar: usize, pub cursor_bar: usize,
/// This represents the position in terms of CHARACTERS, not graphemes /// This represents the position in terms of CHARACTERS, not graphemes
pub char_cursor_position: usize, pub char_cursor_position: usize,
/// The query
pub query: Option<Query>,
pub error_message: Option<String>,
} }
impl Default for AppSearchState { impl Default for AppSearchState {
@ -76,13 +82,14 @@ impl Default for AppSearchState {
AppSearchState { AppSearchState {
is_enabled: false, is_enabled: false,
current_search_query: String::default(), current_search_query: String::default(),
current_regex: None,
is_invalid_search: false, is_invalid_search: false,
is_blank_search: true, is_blank_search: true,
grapheme_cursor: GraphemeCursor::new(0, 0, true), grapheme_cursor: GraphemeCursor::new(0, 0, true),
cursor_direction: CursorDirection::RIGHT, cursor_direction: CursorDirection::RIGHT,
cursor_bar: 0, cursor_bar: 0,
char_cursor_position: 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. /// ProcessSearchState only deals with process' search's current settings and state.
pub struct ProcessSearchState { pub struct ProcessSearchState {
pub search_state: AppSearchState, pub search_state: AppSearchState,
pub is_searching_with_pid: bool,
pub is_ignoring_case: bool, pub is_ignoring_case: bool,
pub is_searching_whole_word: bool, pub is_searching_whole_word: bool,
pub is_searching_with_regex: bool, pub is_searching_with_regex: bool,
@ -114,7 +120,6 @@ impl Default for ProcessSearchState {
fn default() -> Self { fn default() -> Self {
ProcessSearchState { ProcessSearchState {
search_state: AppSearchState::default(), search_state: AppSearchState::default(),
is_searching_with_pid: false,
is_ignoring_case: true, is_ignoring_case: true,
is_searching_whole_word: false, is_searching_whole_word: false,
is_searching_with_regex: false, is_searching_with_regex: false,
@ -188,48 +193,28 @@ impl ProcWidgetState {
&self.process_search_state.search_state.current_search_query &self.process_search_state.search_state.current_search_query
} }
pub fn update_regex(&mut self) { pub fn update_query(&mut self) {
if self if self
.process_search_state .process_search_state
.search_state .search_state
.current_search_query .current_search_query
.is_empty() .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_blank_search = true;
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
} else { } else {
let regex_string = &self.process_search_state.search_state.current_search_query; let parsed_query = self.parse_query();
let escaped_regex: String; if let Ok(parsed_query) = parsed_query {
let final_regex_string = &format!( self.process_search_state.search_state.query = Some(parsed_query);
"{}{}{}{}",
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_blank_search = false;
self.process_search_state.search_state.is_invalid_search = new_regex.is_err(); self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
self.process_search_state.search_state.current_regex = Some(new_regex); } 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.previous_scroll_position = 0;
self.scroll_state.current_scroll_position = 0; self.scroll_state.current_scroll_position = 0;

View File

@ -16,7 +16,6 @@ use widgets::*;
use crate::{ use crate::{
app::{ app::{
self, self,
data_harvester::processes::ProcessHarvest,
layout_manager::{BottomColRow, BottomLayout, BottomWidgetType}, layout_manager::{BottomColRow, BottomLayout, BottomWidgetType},
App, App,
}, },
@ -41,7 +40,7 @@ pub struct DisplayableData {
pub disk_data: Vec<Vec<String>>, pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>, pub temp_sensor_data: Vec<Vec<String>>,
// Not the final value // Not the final value
pub process_data: HashMap<u32, ProcessHarvest>, pub process_data: Vec<ConvertedProcessData>,
// Not the final value // Not the final value
pub grouped_process_data: Vec<ConvertedProcessData>, pub grouped_process_data: Vec<ConvertedProcessData>,
// What's actually displayed // 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_cpu(&mut f, app_state, vertical_chunks[0], 1);
self.draw_basic_memory(&mut f, app_state, middle_chunks[0], 2); 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_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 { if let Some(basic_table_widget_state) = &app_state.basic_table_widget_state {
let widget_id = basic_table_widget_state.currently_displayed_widget_id; 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 { match basic_table_widget_state.currently_displayed_widget_type {
Disk => self.draw_disk_table( Disk => self.draw_disk_table(
&mut f, &mut f,

View File

@ -26,6 +26,7 @@ pub struct CanvasColours {
pub graph_style: Style, pub graph_style: Style,
// Full, Medium, Low // Full, Medium, Low
pub battery_bar_styles: Vec<Style>, pub battery_bar_styles: Vec<Style>,
pub invalid_query_style: Style,
} }
impl Default for CanvasColours { impl Default for CanvasColours {
@ -60,6 +61,7 @@ impl Default for CanvasColours {
Style::default().fg(Color::Green), Style::default().fg(Color::Green),
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 std::cmp::max;
use crate::{ use crate::{
app::{layout_manager::BottomWidgetType, App}, app::{
layout_manager::{BottomWidget, BottomWidgetType},
App,
},
canvas::Painter, canvas::Painter,
}; };
@ -14,19 +17,18 @@ use tui::{
pub trait BasicTableArrows { pub trait BasicTableArrows {
fn draw_basic_table_arrows<B: Backend>( 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 { impl BasicTableArrows for Painter {
fn draw_basic_table_arrows<B: Backend>( 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 // Effectively a paragraph with a ton of spacing
let (left_table, right_table) = ( let (left_table, right_table) = (
{ {
app_state current_table
.current_widget
.left_neighbour .left_neighbour
.map(|left_widget_id| { .map(|left_widget_id| {
app_state app_state
@ -38,8 +40,7 @@ impl BasicTableArrows for Painter {
.unwrap_or_else(|| &BottomWidgetType::Temp) .unwrap_or_else(|| &BottomWidgetType::Temp)
}, },
{ {
app_state current_table
.current_widget
.right_neighbour .right_neighbour
.map(|right_widget_id| { .map(|right_widget_id| {
app_state app_state

View File

@ -160,8 +160,7 @@ impl CpuGraphWidget for Painter {
self.colours.cpu_colour_styles self.colours.cpu_colour_styles
[itx % self.colours.cpu_colour_styles.len()] [itx % self.colours.cpu_colour_styles.len()]
}) })
.data(&cpu.cpu_data[..]) .data(&cpu.cpu_data[..]), // .graph_type(tui::widgets::GraphType::Line),
// .graph_type(tui::widgets::GraphType::Line),
) )
} else { } else {
None None

View File

@ -1,7 +1,7 @@
use std::cmp::max; use std::cmp::max;
use crate::{ use crate::{
app::{self, App, ProcWidgetState}, app::{self, App},
canvas::{ canvas::{
drawing_utils::{ drawing_utils::{
get_search_start_position, get_start_position, get_variable_intrinsic_widths, get_search_start_position, get_start_position, get_variable_intrinsic_widths,
@ -44,11 +44,11 @@ impl ProcessTableWidget for Painter {
widget_id: u64, widget_id: u64,
) { ) {
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { 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() { if process_widget_state.is_search_enabled() {
let processes_chunk = Layout::default() let processes_chunk = Layout::default()
.direction(Direction::Vertical) .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); .split(draw_loc);
self.draw_processes_table(f, app_state, processes_chunk[0], draw_border, widget_id); 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! // do so by hiding some elements!
let num_rows = max(0, i64::from(draw_loc.height) - self.table_height_offset) as u64; 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_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( let position = get_start_position(
num_rows, num_rows,
@ -145,7 +148,7 @@ impl ProcessTableWidget for Painter {
let wps = "W/s".to_string(); let wps = "W/s".to_string();
let total_read = "Read".to_string(); let total_read = "Read".to_string();
let total_write = "Write".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 { let direction_val = if proc_widget_state.process_sorting_reverse {
"".to_string() "".to_string()
@ -169,7 +172,7 @@ impl ProcessTableWidget for Painter {
wps, wps,
total_read, total_read,
total_write, total_write,
process_state, // process_state,
]; ];
let process_headers_lens: Vec<usize> = process_headers let process_headers_lens: Vec<usize> = process_headers
.iter() .iter()
@ -178,7 +181,7 @@ impl ProcessTableWidget for Painter {
// Calculate widths // Calculate widths
let width = f64::from(draw_loc.width); 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( let variable_intrinsic_results = get_variable_intrinsic_widths(
width as u16, width as u16,
&width_ratios, &width_ratios,
@ -212,7 +215,7 @@ impl ProcessTableWidget for Painter {
String::default() 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.highlighted_border_style,
self.colours.currently_selected_text_style, self.colours.currently_selected_text_style,
@ -229,7 +232,11 @@ impl ProcessTableWidget for Painter {
} else { } else {
self.colours.widget_title_style 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) .border_style(border_and_title_style)
} else if is_on_widget { } else if is_on_widget {
Block::default() 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, &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64, 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>( fn build_query<'a>(
is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize,
cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style, 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) = if let Some(proc_widget_state) =
app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) 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_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 num_columns = draw_loc.width as usize;
let small_mode = num_columns < 70; let search_title = "> ";
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 num_chars_for_text = search_title.len(); 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 cursor_position = proc_widget_state.get_cursor_position();
let current_cursor_position = proc_widget_state.get_char_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( let start_position: usize = get_search_start_position(
num_columns - num_chars_for_text - 5, num_columns - num_chars_for_text - 5,
@ -373,6 +353,14 @@ impl ProcessTableWidget for Painter {
app_state.is_force_redraw, 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 query = proc_widget_state.get_current_search_query().as_str();
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
let query_with_cursor: Vec<Text<'_>> = build_query( let query_with_cursor: Vec<Text<'_>> = build_query(
@ -385,6 +373,8 @@ impl ProcessTableWidget for Painter {
self.colours.text_style, self.colours.text_style,
); );
// TODO: [QUERY] Make text/border go red if error?
// Text options shamelessly stolen from VS Code. // Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case { let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
@ -410,71 +400,55 @@ impl ProcessTableWidget for Painter {
self.colours.text_style self.colours.text_style
}; };
let mut option_text = vec![]; let option_text = vec![
let case_text = format!( Text::raw("\n"),
"{}({})", Text::styled(
if small_mode { "Case" } else { "Match Case " }, format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }),
if self.is_mac_os { "F1" } else { "Alt+C" }, case_style,
); ),
Text::raw(" "),
let whole_text = format!( Text::styled(
"{}({})", format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" }),
if small_mode { whole_word_style,
"Whole" ),
} else { Text::raw(" "),
"Match Whole Word " Text::styled(
}, format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" }),
if self.is_mac_os { "F2" } else { "Alt+W" }, regex_style,
); ),
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),
]; ];
option_text.extend(option_row);
search_text.extend(query_with_cursor); search_text.extend(query_with_cursor);
search_text.extend(option_text); search_text.push(Text::styled(
format!(
let current_border_style = if proc_widget_state "\n{}",
if let Some(err) = &proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.is_invalid_search .error_message
{ {
*INVALID_REGEX_STYLE err.as_str()
} else if is_on_widget { } else {
""
}
),
self.colours.invalid_query_style,
));
search_text.extend(option_text);
let current_border_style = if is_on_processes {
self.colours.highlighted_border_style self.colours.highlighted_border_style
} else { } else {
self.colours.border_style 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 { let process_search_block = if draw_border {
Block::default() Block::default()
.title(&title) .borders(if is_search_enabled {
.title_style(current_border_style) *BOTTOM_LEFT_RIGHT
.borders(Borders::ALL) } else {
Borders::ALL
})
.border_style(current_border_style) .border_style(current_border_style)
} else if is_on_widget { } else if is_on_widget {
Block::default() Block::default()

View File

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

View File

@ -4,11 +4,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
app::{ app::{data_farmer, data_harvester, App},
data_farmer,
data_harvester::{self, processes::ProcessHarvest},
App,
},
utils::gen_util::{get_exact_byte_values, get_simple_byte_values}, utils::gen_util::{get_exact_byte_values, get_simple_byte_values},
}; };
@ -45,6 +41,10 @@ pub struct ConvertedProcessData {
pub write_per_sec: String, pub write_per_sec: String,
pub total_read: String, pub total_read: String,
pub total_write: 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, pub process_states: String,
} }
@ -399,8 +399,8 @@ pub fn convert_network_data_points(
pub fn convert_process_data( pub fn convert_process_data(
current_data: &data_farmer::DataCollection, current_data: &data_farmer::DataCollection,
) -> (HashMap<u32, ProcessHarvest>, Vec<ConvertedProcessData>) { ) -> (Vec<ConvertedProcessData>, Vec<ConvertedProcessData>) {
let mut single_list: HashMap<u32, ProcessHarvest> = HashMap::new(); let mut single_list = Vec::new();
// cpu, mem, pids // cpu, mem, pids
let mut grouped_hashmap: HashMap<String, SingleProcessData> = std::collections::HashMap::new(); 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).total_write += process.total_write_bytes;
(*entry).process_state.push(process.process_state_char); (*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 let grouped_list: Vec<ConvertedProcessData> = grouped_hashmap
@ -453,6 +481,10 @@ pub fn convert_process_data(
write_per_sec, write_per_sec,
total_read, total_read,
total_write, 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, process_states: p.process_state,
} }
}) })

View File

@ -98,7 +98,10 @@ fn get_matches() -> clap::ArgMatches<'static> {
} }
fn main() -> error::Result<()> { fn main() -> error::Result<()> {
create_logger()?; #[cfg(debug_assertions)]
{
utils::logging::init_logger()?;
}
let matches = get_matches(); let matches = get_matches();
let config: Config = create_config(matches.value_of("CONFIG_LOCATION"))?; 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... // Otherwise, track the modifier as well...
if let KeyModifiers::ALT = event.modifiers { if let KeyModifiers::ALT = event.modifiers {
match event.code { match event.code {
KeyCode::Char('c') | KeyCode::Char('C') => { KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(),
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('w') | KeyCode::Char('W') => { KeyCode::Char('h') => app.on_left_key(),
app.toggle_search_whole_word(); KeyCode::Char('l') => app.on_right_key(),
}
KeyCode::Char('r') | KeyCode::Char('R') => {
app.toggle_search_regex();
}
_ => {} _ => {}
} }
} else if let KeyModifiers::CONTROL = event.modifiers { } else if let KeyModifiers::CONTROL = event.modifiers {
@ -368,13 +367,6 @@ fn handle_key_event_or_break(
false 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> { fn create_config(flag_config_location: Option<&str>) -> error::Result<Config> {
use std::{ffi::OsString, fs}; use std::{ffi::OsString, fs};
let config_path = if let Some(conf_loc) = flag_config_location { 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) { 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) { let is_invalid_or_blank = match app.proc_state.widget_states.get(&widget_id) {
Some(process_state) => process_state Some(process_state) => process_state
.process_search_state .process_search_state
@ -601,77 +592,38 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
None => false, None => false,
}; };
let process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = if app.is_grouped(widget_id) { let filtered_process_data: Vec<ConvertedProcessData> = if app.is_grouped(widget_id) {
app.canvas_data app.canvas_data
.grouped_process_data .grouped_process_data
.iter() .iter()
.filter(|process| { .filter(|process| {
if is_invalid_or_blank { 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 true
} else if let Some(process_filter) = process_filter {
process_filter.check(process)
} else {
true
}
}) })
.cloned() .cloned()
.collect::<Vec<_>>() .collect::<Vec<_>>()
} else { } 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 app.canvas_data
.process_data .process_data
.iter() .iter()
.filter_map(|(_pid, process)| { .filter(|process| {
let mut result = true;
if !is_invalid_or_blank { if !is_invalid_or_blank {
if let Some(matcher_result) = app.get_current_regex_matcher(widget_id) { if let Some(process_filter) = process_filter {
if let Ok(matcher) = matcher_result { process_filter.check(&process)
if is_searching_with_pid {
result = matcher.is_match(&process.pid.to_string());
} else { } else {
result = matcher.is_match(&process.name); 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<_>>() .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. /// A type alias for handling errors related to Bottom.
pub type Result<T> = result::Result<T, BottomError>; pub type Result<T> = result::Result<T, BottomError>;
@ -22,6 +22,8 @@ pub enum BottomError {
ConfigError(String), ConfigError(String),
/// An error to represent errors with converting between data types. /// An error to represent errors with converting between data types.
ConversionError(String), ConversionError(String),
/// An error to represent errors with querying.
QueryError(Cow<'static, str>),
} }
impl std::fmt::Display for BottomError { impl std::fmt::Display for BottomError {
@ -47,6 +49,7 @@ impl std::fmt::Display for BottomError {
BottomError::ConversionError(ref message) => { BottomError::ConversionError(ref message) => {
write!(f, "unable to convert: {}", 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()) 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> { pub fn init_logger() -> Result<(), fern::InitError> {
fern::Dispatch::new() fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {