mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 07:34:27 +02:00
Merge pull request #5 from ClementTsang/process_search_filter
Implement process search filtering
This commit is contained in:
commit
a92ee19346
@ -29,7 +29,7 @@ futures-timer = "2.0.2"
|
|||||||
futures = "0.3.1"
|
futures = "0.3.1"
|
||||||
heim = "0.0.9"
|
heim = "0.0.9"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
regex = "1.3.1"
|
regex = "1.3.3"
|
||||||
sysinfo = "0.9" #0.9 seems to be the last working version for my Ryzen PC...
|
sysinfo = "0.9" #0.9 seems to be the last working version for my Ryzen PC...
|
||||||
tokio = "0.2.9"
|
tokio = "0.2.9"
|
||||||
winapi = "0.3"
|
winapi = "0.3"
|
||||||
|
59
README.md
59
README.md
@ -6,26 +6,21 @@ A graphical top clone, written in Rust. Inspired by both [gtop](https://github.c
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation
|
## Features
|
||||||
|
|
||||||
### Linux
|
Features of bottom include:
|
||||||
|
|
||||||
You can install by cloning and using `cargo build --release`, or use `cargo install bottom`. Other installation methods based on distros are as follows:
|
- CPU widget to show a visual representation of per-core usage. Average CPU display also exists.
|
||||||
|
|
||||||
#### Arch Linux
|
- Memory widget to show a visual representation of both RAM and SWAP usage.
|
||||||
|
|
||||||
You can get it from the AUR by installing `bottom`.
|
- Networks widget to show a log-based visual representation of network usage.
|
||||||
|
|
||||||
### Windows
|
- Sortable and searchable process widget. Searching supports regex, and you can search by PID and process name.
|
||||||
|
|
||||||
You can currently install by cloning and building yourself using `cargo build --release`, or use `cargo install bottom`
|
- Disks widget to display usage and I/O per second.
|
||||||
. You may need to install a font like [FreeMono](https://fonts2u.com/free-monospaced.font) and use a terminal like cmder for font support to work properly, unfortunately.
|
|
||||||
|
|
||||||
### macOS
|
- Temperature widget to monitor detected sensors in your system.
|
||||||
|
|
||||||
macOS support will hopefully come soon<sup>TM</sup>.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
The compatibility of each widget and operating systems are, as of version 0.1.0, as follows:
|
The compatibility of each widget and operating systems are, as of version 0.1.0, as follows:
|
||||||
|
|
||||||
@ -35,6 +30,24 @@ The compatibility of each widget and operating systems are, as of version 0.1.0,
|
|||||||
| Windows | ✓ | ✓ | ✓ | Currently not working | ✓ | Partially supported (total RX/TX unavailable) |
|
| Windows | ✓ | ✓ | ✓ | Currently not working | ✓ | Partially supported (total RX/TX unavailable) |
|
||||||
| macOS | Untested | Untested | Untested | Untested | Untested | Untested |
|
| macOS | Untested | Untested | Untested | Untested | Untested | Untested |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
You can install the in-development version by cloning and using `cargo build --release`. You can get release versions using `cargo install bottom`, or manually building from the Releases page. Other installation methods based on distros are as follows:
|
||||||
|
|
||||||
|
#### Arch Linux
|
||||||
|
|
||||||
|
You can get the release versions from the AUR by installing `bottom`.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
You can install the in-development version by cloning and using `cargo build --release`. You can get release versions using `cargo install bottom`, or manually building from the Releases page. You may need to install a font like [FreeMono](https://fonts2u.com/free-monospaced.font) and use a terminal like cmder for font support to work properly, unfortunately.
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
macOS support will hopefully come soon<sup>TM</sup>.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Run using `btm`.
|
Run using `btm`.
|
||||||
@ -69,13 +82,13 @@ Run using `btm`.
|
|||||||
|
|
||||||
#### General
|
#### General
|
||||||
|
|
||||||
- `q`, `Ctrl-c` to quit.
|
- `q`, `Ctrl-c` to quit. Note if you are currently in the search widget, `q` will not work so you can still type.
|
||||||
|
|
||||||
- `Ctrl-r` to reset the screen and reset all collected data.
|
- `Ctrl-r` to reset the screen and reset all collected data.
|
||||||
|
|
||||||
- `f` to freeze the screen from updating with new data. Press `f` again to unfreeze. Note that monitoring will still continue in the background.
|
- `f` to freeze the screen from updating with new data. Press `f` again to unfreeze. Note that monitoring will still continue in the background.
|
||||||
|
|
||||||
- `Ctrl+Up/k`, `Ctrl+Down/j`, `Ctrl+Left/h`, `Ctrl+Right/l` to navigate between widgets.
|
- `Ctrl-Up` or `Ctrl-k`, `Ctrl-Down` or `Ctrl-j`, `Ctrl-Left` or `Ctrl-h`, `Ctrl-Right` or `Ctrl-l` to navigate between widgets.
|
||||||
|
|
||||||
- `Esc` to close a dialog window.
|
- `Esc` to close a dialog window.
|
||||||
|
|
||||||
@ -83,11 +96,11 @@ Run using `btm`.
|
|||||||
|
|
||||||
#### Scrollable Tables
|
#### Scrollable Tables
|
||||||
|
|
||||||
- `Up` and `Down` scrolls through the list if the widget is a table (Temperature, Disks, Processes).
|
- `Up` or `k` and `Down` or `j` scrolls through the list if the widget is a table (Temperature, Disks, Processes).
|
||||||
|
|
||||||
- `gg` or `Home` to jump to the first entry of the current table.
|
- `gg` or `Home` to jump to the first entry of the current table.
|
||||||
|
|
||||||
- `G` (`Shift+g`) or `End` to jump to the last entry of the current table.
|
- `G` (`Shift-g`) or `End` to jump to the last entry of the current table.
|
||||||
|
|
||||||
#### Processes
|
#### Processes
|
||||||
|
|
||||||
@ -103,9 +116,19 @@ Run using `btm`.
|
|||||||
|
|
||||||
- `Tab` to group together processes with the same name. Disables PID sorting. `dd` will now kill all processes covered by that name.
|
- `Tab` to group together processes with the same name. Disables PID sorting. `dd` will now kill all processes covered by that name.
|
||||||
|
|
||||||
|
- `Ctrl-f` or `/` to open the search widget.
|
||||||
|
|
||||||
|
#### Search Widget
|
||||||
|
|
||||||
|
- `Ctrl-p` or `Ctrl-n` to switch between searching for PID and name respectively.
|
||||||
|
|
||||||
|
- `Esc` or `Ctrl-f` to close.
|
||||||
|
|
||||||
|
Note that `q` is disabled while in the search widget.
|
||||||
|
|
||||||
### Mouse actions
|
### Mouse actions
|
||||||
|
|
||||||
- Scrolling with the mouse will scroll through the currently selected list, similar to using the up/down arrow keys.
|
- Scrolling with the mouse will scroll through the currently selected list.
|
||||||
|
|
||||||
## Thanks, kudos, and all the like
|
## Thanks, kudos, and all the like
|
||||||
|
|
||||||
|
147
src/app.rs
147
src/app.rs
@ -14,6 +14,7 @@ pub enum ApplicationPosition {
|
|||||||
Temp,
|
Temp,
|
||||||
Network,
|
Network,
|
||||||
Process,
|
Process,
|
||||||
|
ProcessSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -24,6 +25,11 @@ pub enum ScrollDirection {
|
|||||||
DOWN,
|
DOWN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref BASE_REGEX: std::result::Result<regex::Regex, regex::Error> =
|
||||||
|
regex::Regex::new(".*");
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
// Sorting
|
// Sorting
|
||||||
pub process_sorting_type: processes::ProcessSorting,
|
pub process_sorting_type: processes::ProcessSorting,
|
||||||
@ -57,6 +63,10 @@ pub struct App {
|
|||||||
last_key_press: Instant,
|
last_key_press: Instant,
|
||||||
pub canvas_data: canvas::CanvasData,
|
pub canvas_data: canvas::CanvasData,
|
||||||
enable_grouping: bool,
|
enable_grouping: bool,
|
||||||
|
enable_searching: bool,
|
||||||
|
current_search_query: String,
|
||||||
|
searching_pid: bool,
|
||||||
|
current_regex: std::result::Result<regex::Regex, regex::Error>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@ -96,6 +106,10 @@ impl App {
|
|||||||
last_key_press: Instant::now(),
|
last_key_press: Instant::now(),
|
||||||
canvas_data: canvas::CanvasData::default(),
|
canvas_data: canvas::CanvasData::default(),
|
||||||
enable_grouping: false,
|
enable_grouping: false,
|
||||||
|
enable_searching: false,
|
||||||
|
current_search_query: String::default(),
|
||||||
|
searching_pid: false,
|
||||||
|
current_regex: BASE_REGEX.clone(), //TODO: [OPT] seems like a thing we can switch to lt for if not fast
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,10 +117,29 @@ impl App {
|
|||||||
self.reset_multi_tap_keys();
|
self.reset_multi_tap_keys();
|
||||||
self.show_help = false;
|
self.show_help = false;
|
||||||
self.show_dd = false;
|
self.show_dd = false;
|
||||||
|
if self.enable_searching {
|
||||||
|
self.current_application_position = ApplicationPosition::Process;
|
||||||
|
self.enable_searching = false;
|
||||||
|
}
|
||||||
|
self.current_search_query = String::new();
|
||||||
|
self.searching_pid = false;
|
||||||
self.to_delete_process_list = None;
|
self.to_delete_process_list = None;
|
||||||
self.dd_err = None;
|
self.dd_err = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_esc(&mut self) {
|
||||||
|
self.reset_multi_tap_keys();
|
||||||
|
if self.is_in_dialog() {
|
||||||
|
self.show_help = false;
|
||||||
|
self.show_dd = false;
|
||||||
|
self.to_delete_process_list = None;
|
||||||
|
self.dd_err = None;
|
||||||
|
} else if self.enable_searching {
|
||||||
|
self.current_application_position = ApplicationPosition::Process;
|
||||||
|
self.enable_searching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn reset_multi_tap_keys(&mut self) {
|
fn reset_multi_tap_keys(&mut self) {
|
||||||
self.awaiting_second_char = false;
|
self.awaiting_second_char = false;
|
||||||
self.second_char = ' ';
|
self.second_char = ' ';
|
||||||
@ -137,6 +170,61 @@ impl App {
|
|||||||
self.enable_grouping
|
self.enable_grouping
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_searching(&mut self) {
|
||||||
|
if !self.is_in_dialog() {
|
||||||
|
match self.current_application_position {
|
||||||
|
ApplicationPosition::Process | ApplicationPosition::ProcessSearch => {
|
||||||
|
if self.enable_searching {
|
||||||
|
// Toggle off
|
||||||
|
self.enable_searching = false;
|
||||||
|
self.current_application_position = ApplicationPosition::Process;
|
||||||
|
} else {
|
||||||
|
// Toggle on
|
||||||
|
self.enable_searching = true;
|
||||||
|
self.current_application_position = ApplicationPosition::ProcessSearch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_searching(&self) -> bool {
|
||||||
|
self.enable_searching
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_in_search_widget(&self) -> bool {
|
||||||
|
if let ApplicationPosition::ProcessSearch = self.current_application_position {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_with_pid(&mut self) {
|
||||||
|
if !self.is_in_dialog() && self.is_searching() {
|
||||||
|
if let ApplicationPosition::ProcessSearch = self.current_application_position {
|
||||||
|
self.searching_pid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_with_name(&mut self) {
|
||||||
|
if !self.is_in_dialog() && self.is_searching() {
|
||||||
|
if let ApplicationPosition::ProcessSearch = self.current_application_position {
|
||||||
|
self.searching_pid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_searching_with_pid(&self) -> bool {
|
||||||
|
self.searching_pid
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_search_query(&self) -> &String {
|
||||||
|
&self.current_search_query
|
||||||
|
}
|
||||||
|
|
||||||
/// One of two functions allowed to run while in a dialog...
|
/// One of two functions allowed to run while in a dialog...
|
||||||
pub fn on_enter(&mut self) {
|
pub fn on_enter(&mut self) {
|
||||||
if self.show_dd {
|
if self.show_dd {
|
||||||
@ -151,9 +239,31 @@ impl App {
|
|||||||
self.show_dd = false;
|
self.show_dd = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if let ApplicationPosition::ProcessSearch = self.current_application_position {
|
||||||
|
// Generate regex.
|
||||||
|
|
||||||
|
// TODO: [OPT] if we can get this to work WITHOUT pressing enter that would be good.
|
||||||
|
// However, this will be a bit hard without a thorough look at optimization to avoid
|
||||||
|
// wasteful regex generation.
|
||||||
|
|
||||||
|
self.current_regex = if self.current_search_query.is_empty() {
|
||||||
|
BASE_REGEX.clone()
|
||||||
|
} else {
|
||||||
|
regex::Regex::new(&(self.current_search_query))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_backspace(&mut self) {
|
||||||
|
if let ApplicationPosition::ProcessSearch = self.current_application_position {
|
||||||
|
self.current_search_query.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_regex_matcher(&self) -> &std::result::Result<regex::Regex, regex::Error> {
|
||||||
|
&self.current_regex
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_char_key(&mut self, caught_char: char) {
|
pub fn on_char_key(&mut self, caught_char: char) {
|
||||||
// Forbid any char key presses when showing a dialog box...
|
// Forbid any char key presses when showing a dialog box...
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
@ -166,13 +276,18 @@ impl App {
|
|||||||
}
|
}
|
||||||
self.last_key_press = current_key_press_inst;
|
self.last_key_press = current_key_press_inst;
|
||||||
|
|
||||||
|
if let ApplicationPosition::ProcessSearch = self.current_application_position {
|
||||||
|
self.current_search_query.push(caught_char);
|
||||||
|
} else {
|
||||||
match caught_char {
|
match caught_char {
|
||||||
|
'/' => {
|
||||||
|
self.toggle_searching();
|
||||||
|
}
|
||||||
'd' => {
|
'd' => {
|
||||||
if let ApplicationPosition::Process = self.current_application_position {
|
if let ApplicationPosition::Process = self.current_application_position {
|
||||||
if self.awaiting_second_char && self.second_char == 'd' {
|
if self.awaiting_second_char && self.second_char == 'd' {
|
||||||
self.awaiting_second_char = false;
|
self.awaiting_second_char = false;
|
||||||
self.second_char = ' ';
|
self.second_char = ' ';
|
||||||
|
|
||||||
let current_process = if self.is_grouped() {
|
let current_process = if self.is_grouped() {
|
||||||
let mut res: Vec<ConvertedProcessData> = Vec::new();
|
let mut res: Vec<ConvertedProcessData> = Vec::new();
|
||||||
for pid in &self.canvas_data.grouped_process_data
|
for pid in &self.canvas_data.grouped_process_data
|
||||||
@ -184,7 +299,6 @@ impl App {
|
|||||||
.process_data
|
.process_data
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.pid == *pid);
|
.find(|p| p.pid == *pid);
|
||||||
|
|
||||||
if let Some(process) = result {
|
if let Some(process) = result {
|
||||||
res.push((*process).clone());
|
res.push((*process).clone());
|
||||||
}
|
}
|
||||||
@ -214,6 +328,9 @@ impl App {
|
|||||||
self.second_char = 'g';
|
self.second_char = 'g';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'G' => self.skip_to_last(),
|
||||||
|
'k' => self.decrement_position_count(),
|
||||||
|
'j' => self.increment_position_count(),
|
||||||
'f' => {
|
'f' => {
|
||||||
self.is_frozen = !self.is_frozen;
|
self.is_frozen = !self.is_frozen;
|
||||||
}
|
}
|
||||||
@ -277,12 +394,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.awaiting_second_char && caught_char != self.second_char {
|
if self.awaiting_second_char && caught_char != self.second_char {
|
||||||
self.awaiting_second_char = false;
|
self.awaiting_second_char = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn kill_highlighted_process(&mut self) -> Result<()> {
|
pub fn kill_highlighted_process(&mut self) -> Result<()> {
|
||||||
// Technically unnecessary but this is a good check...
|
// Technically unnecessary but this is a good check...
|
||||||
@ -307,13 +424,15 @@ impl App {
|
|||||||
// CPU -(down)> MEM
|
// CPU -(down)> MEM
|
||||||
// MEM -(down)> Network, -(right)> TEMP
|
// MEM -(down)> Network, -(right)> TEMP
|
||||||
// TEMP -(down)> Disk, -(left)> MEM, -(up)> CPU
|
// TEMP -(down)> Disk, -(left)> MEM, -(up)> CPU
|
||||||
// Disk -(down)> Processes, -(left)> MEM, -(up)> TEMP
|
// Disk -(down)> Processes OR PROC_SEARCH, -(left)> MEM, -(up)> TEMP
|
||||||
// Network -(up)> MEM, -(right)> PROC
|
// Network -(up)> MEM, -(right)> PROC
|
||||||
// PROC -(up)> Disk, -(left)> Network
|
// PROC -(up)> Disk OR PROC_SEARCH if enabled, -(left)> Network
|
||||||
|
// PROC_SEARCH -(up)> Disk, -(down)> PROC, -(left)> Network
|
||||||
pub fn on_left(&mut self) {
|
pub fn on_left(&mut self) {
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
self.current_application_position = match self.current_application_position {
|
self.current_application_position = match self.current_application_position {
|
||||||
ApplicationPosition::Process => ApplicationPosition::Network,
|
ApplicationPosition::Process => ApplicationPosition::Network,
|
||||||
|
ApplicationPosition::ProcessSearch => ApplicationPosition::Network,
|
||||||
ApplicationPosition::Disk => ApplicationPosition::Mem,
|
ApplicationPosition::Disk => ApplicationPosition::Mem,
|
||||||
ApplicationPosition::Temp => ApplicationPosition::Mem,
|
ApplicationPosition::Temp => ApplicationPosition::Mem,
|
||||||
_ => self.current_application_position,
|
_ => self.current_application_position,
|
||||||
@ -338,7 +457,14 @@ impl App {
|
|||||||
self.current_application_position = match self.current_application_position {
|
self.current_application_position = match self.current_application_position {
|
||||||
ApplicationPosition::Mem => ApplicationPosition::Cpu,
|
ApplicationPosition::Mem => ApplicationPosition::Cpu,
|
||||||
ApplicationPosition::Network => ApplicationPosition::Mem,
|
ApplicationPosition::Network => ApplicationPosition::Mem,
|
||||||
ApplicationPosition::Process => ApplicationPosition::Disk,
|
ApplicationPosition::Process => {
|
||||||
|
if self.is_searching() {
|
||||||
|
ApplicationPosition::ProcessSearch
|
||||||
|
} else {
|
||||||
|
ApplicationPosition::Disk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplicationPosition::ProcessSearch => ApplicationPosition::Disk,
|
||||||
ApplicationPosition::Temp => ApplicationPosition::Cpu,
|
ApplicationPosition::Temp => ApplicationPosition::Cpu,
|
||||||
ApplicationPosition::Disk => ApplicationPosition::Temp,
|
ApplicationPosition::Disk => ApplicationPosition::Temp,
|
||||||
_ => self.current_application_position,
|
_ => self.current_application_position,
|
||||||
@ -353,7 +479,14 @@ impl App {
|
|||||||
ApplicationPosition::Cpu => ApplicationPosition::Mem,
|
ApplicationPosition::Cpu => ApplicationPosition::Mem,
|
||||||
ApplicationPosition::Mem => ApplicationPosition::Network,
|
ApplicationPosition::Mem => ApplicationPosition::Network,
|
||||||
ApplicationPosition::Temp => ApplicationPosition::Disk,
|
ApplicationPosition::Temp => ApplicationPosition::Disk,
|
||||||
ApplicationPosition::Disk => ApplicationPosition::Process,
|
ApplicationPosition::Disk => {
|
||||||
|
if self.is_searching() {
|
||||||
|
ApplicationPosition::ProcessSearch
|
||||||
|
} else {
|
||||||
|
ApplicationPosition::Process
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplicationPosition::ProcessSearch => ApplicationPosition::Process,
|
||||||
_ => self.current_application_position,
|
_ => self.current_application_position,
|
||||||
};
|
};
|
||||||
self.reset_multi_tap_keys();
|
self.reset_multi_tap_keys();
|
||||||
|
@ -7,7 +7,7 @@ use std::cmp::max;
|
|||||||
use tui::{
|
use tui::{
|
||||||
backend,
|
backend,
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Style},
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget},
|
widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget},
|
||||||
Terminal,
|
Terminal,
|
||||||
@ -17,6 +17,7 @@ const TEXT_COLOUR: Color = Color::Gray;
|
|||||||
const GRAPH_COLOUR: Color = Color::Gray;
|
const GRAPH_COLOUR: Color = Color::Gray;
|
||||||
const BORDER_STYLE_COLOUR: Color = Color::Gray;
|
const BORDER_STYLE_COLOUR: Color = Color::Gray;
|
||||||
const HIGHLIGHTED_BORDER_STYLE_COLOUR: Color = Color::LightBlue;
|
const HIGHLIGHTED_BORDER_STYLE_COLOUR: Color = Color::LightBlue;
|
||||||
|
const TABLE_HEADER_COLOUR: Color = Color::LightBlue;
|
||||||
const GOLDEN_RATIO: f32 = 0.618_034; // Approx, good enough for use (also Clippy gets mad if it's too long)
|
const GOLDEN_RATIO: f32 = 0.618_034; // Approx, good enough for use (also Clippy gets mad if it's too long)
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
@ -28,24 +29,26 @@ const WINDOWS_NETWORK_HEADERS: [&str; 2] = ["RX", "TX"];
|
|||||||
const FORCE_MIN_THRESHOLD: usize = 5;
|
const FORCE_MIN_THRESHOLD: usize = 5;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref HELP_TEXT: [Text<'static>; 15] = [
|
static ref HELP_TEXT: [Text<'static>; 17] = [
|
||||||
Text::raw("\nGeneral Keybindings\n"),
|
Text::raw("\nGeneral Keybindings\n"),
|
||||||
Text::raw("q, Ctrl-c to quit.\n"),
|
Text::raw("q, Ctrl-c to quit. Note if you are currently in the search widget, `q` will not work.\n"),
|
||||||
Text::raw("Ctrl-r to reset all data.\n"),
|
Text::raw("Ctrl-r to reset all data.\n"),
|
||||||
Text::raw("f to toggle freezing and unfreezing the display.\n"),
|
Text::raw("f to toggle freezing and unfreezing the display.\n"),
|
||||||
Text::raw(
|
Text::raw(
|
||||||
"Ctrl+Up/k, Ctrl+Down/j, Ctrl+Left/h, Ctrl+Right/l to navigate between panels.\n"
|
"Ctrl-Up or Ctrl-k, Ctrl-Down or Ctrl-j, Ctrl-Left or Ctrl-h, Ctrl-Right or Ctrl-l to navigate between widgets.\n"
|
||||||
),
|
),
|
||||||
Text::raw("Up and Down scrolls through a list.\n"),
|
Text::raw("Up or k and Down or j scrolls through a list.\n"),
|
||||||
Text::raw("Esc to close a dialog window (help or dd confirmation).\n"),
|
Text::raw("Esc to close a dialog window (help or dd confirmation).\n"),
|
||||||
Text::raw("? to get this help screen.\n"),
|
Text::raw("? to get this help screen.\n"),
|
||||||
Text::raw("\n Process Panel Keybindings\n"),
|
Text::raw("\n Process Widget Keybindings\n"),
|
||||||
Text::raw("dd to kill the selected process.\n"),
|
Text::raw("dd to kill the selected process.\n"),
|
||||||
Text::raw("c to sort by CPU usage.\n"),
|
Text::raw("c to sort by CPU usage.\n"),
|
||||||
Text::raw("m to sort by memory usage.\n"),
|
Text::raw("m to sort by memory usage.\n"),
|
||||||
Text::raw("p to sort by PID.\n"),
|
Text::raw("p to sort by PID.\n"),
|
||||||
Text::raw("n to sort by process name.\n"),
|
Text::raw("n to sort by process name.\n"),
|
||||||
Text::raw("`Tab` to group together processes with the same name.\n")
|
Text::raw("Tab to group together processes with the same name.\n"),
|
||||||
|
Text::raw("Ctrl-f to toggle searching for a process. / to just open it. Use Ctrl-p and Ctrl-n to toggle between searching for PID and name.\n"),
|
||||||
|
Text::raw("\nFor startup flags, type in \"btm -h\".")
|
||||||
];
|
];
|
||||||
static ref COLOUR_LIST: Vec<Color> = gen_n_colours(constants::NUM_COLOURS);
|
static ref COLOUR_LIST: Vec<Color> = gen_n_colours(constants::NUM_COLOURS);
|
||||||
static ref CANVAS_BORDER_STYLE: Style = Style::default().fg(BORDER_STYLE_COLOUR);
|
static ref CANVAS_BORDER_STYLE: Style = Style::default().fg(BORDER_STYLE_COLOUR);
|
||||||
@ -149,9 +152,9 @@ pub fn draw_data<B: backend::Backend>(
|
|||||||
.margin(1)
|
.margin(1)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
Constraint::Percentage(32),
|
Constraint::Percentage(27),
|
||||||
Constraint::Percentage(40),
|
Constraint::Percentage(50),
|
||||||
Constraint::Percentage(28),
|
Constraint::Percentage(23),
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
@ -307,14 +310,7 @@ pub fn draw_data<B: backend::Backend>(
|
|||||||
let network_chunk = Layout::default()
|
let network_chunk = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(0)
|
.margin(0)
|
||||||
.constraints(
|
.constraints([Constraint::Percentage(75), Constraint::Percentage(25)].as_ref())
|
||||||
if app_state.left_legend {
|
|
||||||
[Constraint::Percentage(10), Constraint::Percentage(90)]
|
|
||||||
} else {
|
|
||||||
[Constraint::Percentage(75), Constraint::Percentage(10)]
|
|
||||||
}
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(bottom_chunks[0]);
|
.split(bottom_chunks[0]);
|
||||||
|
|
||||||
// Default chunk index based on left or right legend setting
|
// Default chunk index based on left or right legend setting
|
||||||
@ -343,8 +339,18 @@ pub fn draw_data<B: backend::Backend>(
|
|||||||
draw_disk_table(&mut f, app_state, middle_divided_chunk_2[1]);
|
draw_disk_table(&mut f, app_state, middle_divided_chunk_2[1]);
|
||||||
|
|
||||||
// Processes table
|
// Processes table
|
||||||
|
if app_state.is_searching() {
|
||||||
|
let processes_chunk = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(0)
|
||||||
|
.constraints([Constraint::Percentage(25), Constraint::Percentage(75)].as_ref())
|
||||||
|
.split(bottom_chunks[1]);
|
||||||
|
draw_search_field(&mut f, app_state, processes_chunk[0]);
|
||||||
|
draw_processes_table(&mut f, app_state, processes_chunk[1]);
|
||||||
|
} else {
|
||||||
draw_processes_table(&mut f, app_state, bottom_chunks[1]);
|
draw_processes_table(&mut f, app_state, bottom_chunks[1]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -495,7 +501,7 @@ fn draw_cpu_legend<B: backend::Backend>(
|
|||||||
_ => *CANVAS_BORDER_STYLE,
|
_ => *CANVAS_BORDER_STYLE,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.header_style(Style::default().fg(Color::LightBlue))
|
.header_style(Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
.widths(
|
.widths(
|
||||||
&(intrinsic_widths
|
&(intrinsic_widths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -687,7 +693,7 @@ fn draw_network_labels<B: backend::Backend>(
|
|||||||
_ => *CANVAS_BORDER_STYLE,
|
_ => *CANVAS_BORDER_STYLE,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.header_style(Style::default().fg(Color::LightBlue))
|
.header_style(Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
.widths(
|
.widths(
|
||||||
&(intrinsic_widths
|
&(intrinsic_widths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -754,7 +760,7 @@ fn draw_temp_table<B: backend::Backend>(
|
|||||||
_ => *CANVAS_BORDER_STYLE,
|
_ => *CANVAS_BORDER_STYLE,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.header_style(Style::default().fg(Color::LightBlue))
|
.header_style(Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
.widths(
|
.widths(
|
||||||
&(intrinsic_widths
|
&(intrinsic_widths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -819,11 +825,7 @@ fn draw_disk_table<B: backend::Backend>(
|
|||||||
_ => *CANVAS_BORDER_STYLE,
|
_ => *CANVAS_BORDER_STYLE,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.header_style(
|
.header_style(Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
Style::default()
|
|
||||||
.fg(Color::LightBlue)
|
|
||||||
.modifier(Modifier::BOLD),
|
|
||||||
)
|
|
||||||
.widths(
|
.widths(
|
||||||
&(intrinsic_widths
|
&(intrinsic_widths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -833,6 +835,45 @@ fn draw_disk_table<B: backend::Backend>(
|
|||||||
.render(f, draw_loc);
|
.render(f, draw_loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_search_field<B: backend::Backend>(
|
||||||
|
f: &mut Frame<B>, app_state: &mut app::App, draw_loc: Rect,
|
||||||
|
) {
|
||||||
|
let width = draw_loc.width - 10;
|
||||||
|
let query = app_state.get_current_search_query();
|
||||||
|
let shrunk_query = if query.len() < width as usize {
|
||||||
|
query
|
||||||
|
} else {
|
||||||
|
&query[(query.len() - width as usize)..]
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_text = [
|
||||||
|
if app_state.is_searching_with_pid() {
|
||||||
|
Text::styled("\nPID : ", Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
|
} else {
|
||||||
|
Text::styled("\nName: ", Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
|
},
|
||||||
|
Text::raw(shrunk_query),
|
||||||
|
];
|
||||||
|
Paragraph::new(search_text.iter())
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Search (Ctrl-p and Ctrl-n to switch search types, Esc or Ctrl-f to close, Enter to search)")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(if app_state.get_current_regex_matcher().is_err() {
|
||||||
|
Style::default().fg(Color::Red)
|
||||||
|
} else {
|
||||||
|
match app_state.current_application_position {
|
||||||
|
app::ApplicationPosition::ProcessSearch => *CANVAS_HIGHLIGHTED_BORDER_STYLE,
|
||||||
|
_ => *CANVAS_BORDER_STYLE,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.wrap(false)
|
||||||
|
.render(f, draw_loc);
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_processes_table<B: backend::Backend>(
|
fn draw_processes_table<B: backend::Backend>(
|
||||||
f: &mut Frame<B>, app_state: &mut app::App, draw_loc: Rect,
|
f: &mut Frame<B>, app_state: &mut app::App, draw_loc: Rect,
|
||||||
) {
|
) {
|
||||||
@ -941,7 +982,7 @@ fn draw_processes_table<B: backend::Backend>(
|
|||||||
_ => *CANVAS_BORDER_STYLE,
|
_ => *CANVAS_BORDER_STYLE,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.header_style(Style::default().fg(Color::LightBlue))
|
.header_style(Style::default().fg(TABLE_HEADER_COLOUR))
|
||||||
.widths(
|
.widths(
|
||||||
&(intrinsic_widths
|
&(intrinsic_widths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
//! This mainly concerns converting collected data into things that the canvas
|
||||||
|
//! can actually handle.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::data_collection,
|
app::data_collection,
|
||||||
constants,
|
constants,
|
||||||
utils::gen_util::{get_exact_byte_values, get_simple_byte_values},
|
utils::gen_util::{get_exact_byte_values, get_simple_byte_values},
|
||||||
};
|
};
|
||||||
use constants::*;
|
use constants::*;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct ConvertedNetworkData {
|
pub struct ConvertedNetworkData {
|
||||||
@ -137,11 +141,23 @@ pub fn update_disk_row(app_data: &data_collection::Data) -> Vec<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_process_row(
|
pub fn update_process_row(
|
||||||
app_data: &data_collection::Data,
|
app_data: &data_collection::Data, regex_matcher: &std::result::Result<Regex, regex::Error>,
|
||||||
|
use_pid: bool,
|
||||||
) -> (Vec<ConvertedProcessData>, Vec<ConvertedProcessData>) {
|
) -> (Vec<ConvertedProcessData>, Vec<ConvertedProcessData>) {
|
||||||
let process_vector: Vec<ConvertedProcessData> = app_data
|
let process_vector: Vec<ConvertedProcessData> = app_data
|
||||||
.list_of_processes
|
.list_of_processes
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|process| {
|
||||||
|
if let Ok(matcher) = regex_matcher {
|
||||||
|
if use_pid {
|
||||||
|
matcher.is_match(&process.pid.to_string())
|
||||||
|
} else {
|
||||||
|
matcher.is_match(&process.name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(|process| ConvertedProcessData {
|
.map(|process| ConvertedProcessData {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
name: process.name.to_string(),
|
name: process.name.to_string(),
|
||||||
@ -168,6 +184,17 @@ pub fn update_process_row(
|
|||||||
if let Some(grouped_list_of_processes) = &app_data.grouped_list_of_processes {
|
if let Some(grouped_list_of_processes) = &app_data.grouped_list_of_processes {
|
||||||
grouped_process_vector = grouped_list_of_processes
|
grouped_process_vector = grouped_list_of_processes
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|process| {
|
||||||
|
if let Ok(matcher) = regex_matcher {
|
||||||
|
if use_pid {
|
||||||
|
matcher.is_match(&process.pid.to_string())
|
||||||
|
} else {
|
||||||
|
matcher.is_match(&process.name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(|process| ConvertedProcessData {
|
.map(|process| ConvertedProcessData {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
name: process.name.to_string(),
|
name: process.name.to_string(),
|
||||||
|
71
src/main.rs
71
src/main.rs
@ -9,8 +9,8 @@ extern crate lazy_static;
|
|||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{
|
event::{
|
||||||
self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode, KeyEvent,
|
self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode, KeyModifiers,
|
||||||
KeyModifiers, MouseEvent,
|
MouseEvent,
|
||||||
},
|
},
|
||||||
execute,
|
execute,
|
||||||
terminal::LeaveAlternateScreen,
|
terminal::LeaveAlternateScreen,
|
||||||
@ -212,58 +212,45 @@ fn main() -> error::Result<()> {
|
|||||||
Event::KeyInput(event) => {
|
Event::KeyInput(event) => {
|
||||||
if event.modifiers.is_empty() {
|
if event.modifiers.is_empty() {
|
||||||
// If only a code, and no modifiers, don't bother...
|
// If only a code, and no modifiers, don't bother...
|
||||||
|
|
||||||
|
// Required to catch for while typing
|
||||||
|
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Char('q') => break,
|
KeyCode::End => app.skip_to_last(),
|
||||||
KeyCode::Char('G') | KeyCode::End => app.skip_to_last(),
|
|
||||||
KeyCode::Home => app.skip_to_first(),
|
KeyCode::Home => app.skip_to_first(),
|
||||||
KeyCode::Char('h') => app.on_left(),
|
|
||||||
KeyCode::Char('l') => app.on_right(),
|
|
||||||
KeyCode::Char('k') => app.on_up(),
|
|
||||||
KeyCode::Char('j') => app.on_down(),
|
|
||||||
KeyCode::Up => app.decrement_position_count(),
|
KeyCode::Up => app.decrement_position_count(),
|
||||||
KeyCode::Down => app.increment_position_count(),
|
KeyCode::Down => app.increment_position_count(),
|
||||||
KeyCode::Char(uncaught_char) => app.on_char_key(uncaught_char),
|
KeyCode::Char(character) => app.on_char_key(character),
|
||||||
KeyCode::Esc => app.reset(),
|
KeyCode::Esc => app.on_esc(),
|
||||||
KeyCode::Enter => app.on_enter(),
|
KeyCode::Enter => app.on_enter(),
|
||||||
KeyCode::Tab => app.on_tab(),
|
KeyCode::Tab => app.on_tab(),
|
||||||
|
KeyCode::Backspace => app.on_backspace(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, track the modifier as well...
|
// Otherwise, track the modifier as well...
|
||||||
match event {
|
if let KeyModifiers::CONTROL = event.modifiers {
|
||||||
KeyEvent {
|
match event.code {
|
||||||
modifiers: KeyModifiers::CONTROL,
|
KeyCode::Char('c') => break,
|
||||||
code: KeyCode::Char('c'),
|
KeyCode::Char('f') => app.toggle_searching(), // Note that this is fine for now, assuming '/' does not do anything other than search.
|
||||||
} => break,
|
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
|
||||||
KeyEvent {
|
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
|
||||||
code: KeyCode::Left,
|
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
|
||||||
} => app.on_left(),
|
KeyCode::Char('p') => app.search_with_pid(),
|
||||||
KeyEvent {
|
KeyCode::Char('n') => app.search_with_name(),
|
||||||
modifiers: KeyModifiers::CONTROL,
|
KeyCode::Char('r') => {
|
||||||
code: KeyCode::Right,
|
if rtx.send(ResetEvent::Reset).is_ok() {
|
||||||
} => app.on_right(),
|
|
||||||
KeyEvent {
|
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
code: KeyCode::Up,
|
|
||||||
} => app.on_up(),
|
|
||||||
KeyEvent {
|
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
code: KeyCode::Down,
|
|
||||||
} => app.on_down(),
|
|
||||||
KeyEvent {
|
|
||||||
modifiers: KeyModifiers::CONTROL,
|
|
||||||
code: KeyCode::Char('r'),
|
|
||||||
} => {
|
|
||||||
while rtx.send(ResetEvent::Reset).is_err() {
|
|
||||||
debug!("Sent reset message.");
|
|
||||||
}
|
|
||||||
debug!("Resetting begins...");
|
|
||||||
app.reset();
|
app.reset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if app.to_be_resorted {
|
if app.to_be_resorted {
|
||||||
handle_process_sorting(&mut app);
|
handle_process_sorting(&mut app);
|
||||||
@ -397,7 +384,11 @@ fn handle_process_sorting(app: &mut app::App) {
|
|||||||
app.process_sorting_reverse,
|
app.process_sorting_reverse,
|
||||||
);
|
);
|
||||||
|
|
||||||
let tuple_results = update_process_row(&app.data);
|
let tuple_results = update_process_row(
|
||||||
|
&app.data,
|
||||||
|
app.get_current_regex_matcher(),
|
||||||
|
app.is_searching_with_pid(),
|
||||||
|
);
|
||||||
app.canvas_data.process_data = tuple_results.0;
|
app.canvas_data.process_data = tuple_results.0;
|
||||||
app.canvas_data.grouped_process_data = tuple_results.1;
|
app.canvas_data.grouped_process_data = tuple_results.1;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user