feature: Adds tree view (#223)

Adds a tree process view to bottom.

Currently uses a pretty jank method of column width setting, should get fixed in #225.
This commit is contained in:
Clement Tsang 2020-09-06 23:03:03 -04:00 committed by GitHub
parent 0d8572c692
commit eb8295c430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 719 additions and 242 deletions

View File

@ -1,5 +1,7 @@
#!/bin/sh #!/bin/sh
set -e
echo "Running pre-push hook:" echo "Running pre-push hook:"
echo "Executing: cargo +nightly clippy -- -D clippy::all" echo "Executing: cargo +nightly clippy -- -D clippy::all"

View File

@ -46,10 +46,13 @@
"fract", "fract",
"gnueabihf", "gnueabihf",
"gotop", "gotop",
"gotop's",
"gtop", "gtop",
"haase", "haase",
"heim", "heim",
"hjkl", "hjkl",
"htop",
"indexmap",
"libc", "libc",
"markdownlint", "markdownlint",
"memb", "memb",
@ -62,6 +65,7 @@
"nvme", "nvme",
"paren", "paren",
"pmem", "pmem",
"ppid",
"prepush", "prepush",
"processthreadsapi", "processthreadsapi",
"regexes", "regexes",

View File

@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#220](https://github.com/ClementTsang/bottom/pull/220): Add ability to hide specific temperature and disk entries via config. - [#220](https://github.com/ClementTsang/bottom/pull/220): Add ability to hide specific temperature and disk entries via config.
- [#223](https://github.com/ClementTsang/bottom/pull/223): Add tree mode for processes.
### Changes ### Changes
- [#213](https://github.com/ClementTsang/bottom/pull/213), [#214](https://github.com/ClementTsang/bottom/pull/214): Updated help descriptions, added auto-complete generation. - [#213](https://github.com/ClementTsang/bottom/pull/213), [#214](https://github.com/ClementTsang/bottom/pull/214): Updated help descriptions, added auto-complete generation.

17
Cargo.lock generated
View File

@ -150,6 +150,7 @@ dependencies = [
"fern", "fern",
"futures", "futures",
"heim", "heim",
"indexmap",
"itertools", "itertools",
"lazy_static", "lazy_static",
"libc", "libc",
@ -534,6 +535,12 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
[[package]]
name = "hashbrown"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7"
[[package]] [[package]]
name = "heim" name = "heim"
version = "0.0.10" version = "0.0.10"
@ -723,6 +730,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" checksum = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e"
[[package]]
name = "indexmap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "iovec" name = "iovec"
version = "0.1.4" version = "0.1.4"

View File

@ -32,6 +32,7 @@ ctrlc = {version = "3.1", features = ["termination"]}
clap = "2.33" clap = "2.33"
dirs = "3.0.1" dirs = "3.0.1"
futures = "0.3.5" futures = "0.3.5"
indexmap = "1.6.0"
itertools = "0.9.0" itertools = "0.9.0"
libc = "0.2" libc = "0.2"
regex = "1.3" regex = "1.3"

View File

@ -41,6 +41,7 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Processes](#processes) - [Processes](#processes)
- [Process searching](#process-searching) - [Process searching](#process-searching)
- [Process sorting](#process-sorting) - [Process sorting](#process-sorting)
- [Tree mode](#tree-mode)
- [Zoom](#zoom) - [Zoom](#zoom)
- [Expanding](#expanding) - [Expanding](#expanding)
- [Basic mode](#basic-mode) - [Basic mode](#basic-mode)
@ -246,23 +247,24 @@ Run using `btm`.
| `s, F6` | Open process sort widget | | `s, F6` | Open process sort widget |
| `I` | Invert current sort | | `I` | Invert current sort |
| `%` | Toggle between values and percentages for memory usage | | `%` | Toggle between values and percentages for memory usage |
| `t`, `F5` | Toggle tree mode |
#### Process search bindings #### Process search bindings
| | | | | |
| ------------ | -------------------------------------------- | | ------------- | -------------------------------------------- |
| `Tab` | Toggle between searching by PID or name | | `Tab` | Toggle between searching by PID or name |
| `Esc` | Close the search widget (retains the filter) | | `Esc` | Close the search widget (retains the filter) |
| `Ctrl-a` | Skip to the start of the search query | | `Ctrl-a` | Skip to the start of the search query |
| `Ctrl-e` | Skip to the end of the search query | | `Ctrl-e` | Skip to the end of the search query |
| `Ctrl-u` | Clear the current search query | | `Ctrl-u` | Clear the current search query |
| `Backspace` | Delete the character behind the cursor | | `Backspace` | Delete the character behind the cursor |
| `Delete` | Delete the character at the cursor | | `Delete` | Delete the character at the cursor |
| `Alt-c`/`F1` | Toggle matching case | | `Alt-c`, `F1` | Toggle matching case |
| `Alt-w`/`F2` | Toggle matching the entire word | | `Alt-w`, `F2` | Toggle matching the entire word |
| `Alt-r`/`F3` | Toggle using regex | | `Alt-r`, `F3` | Toggle using regex |
| `Left` | Move cursor left | | `Left` | Move cursor left |
| `Right` | Move cursor right | | `Right` | Move cursor right |
### Process sort bindings ### Process sort bindings
@ -424,6 +426,23 @@ You can sort the processes list by any column you want by pressing `s` while on
![sorting](assets/sort.png) ![sorting](assets/sort.png)
#### Tree mode
Use `t` or `F5` to toggle tree mode in a process widget. This is somewhat similar to htop's tree
mode.
![Standard tree](assets/trees_1.png)
Sorting works as well, but it is done per groups of siblings. For example, by CPU%:
![Standard tree](assets/trees_2.png)
You can also still filter processes. Branches that entirely do not match the query are pruned out,
but if a branch contains an element that does match the query, any non-matching elements will instead
just be greyed out, so the tree structure is still maintained:
![Standard tree](assets/trees_3.png)
### Zoom ### Zoom
Using the `+`/`-` keys or the scroll wheel will move the current time intervals of the currently selected widget, and `=` to reset the zoom levels to the default. Using the `+`/`-` keys or the scroll wheel will move the current time intervals of the currently selected widget, and `=` to reset the zoom levels to the default.

BIN
assets/trees_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

BIN
assets/trees_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
assets/trees_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -13,6 +13,7 @@ pub use states::*;
use crate::{ use crate::{
canvas, constants, canvas, constants,
utils::error::{BottomError, Result}, utils::error::{BottomError, Result},
Pid,
}; };
pub mod data_farmer; pub mod data_farmer;
@ -67,7 +68,7 @@ pub struct App {
pub dd_err: Option<String>, pub dd_err: Option<String>,
#[builder(default, setter(skip))] #[builder(default, setter(skip))]
to_delete_process_list: Option<(String, Vec<u32>)>, to_delete_process_list: Option<(String, Vec<Pid>)>,
#[builder(default = false, setter(skip))] #[builder(default = false, setter(skip))]
pub is_frozen: bool, pub is_frozen: bool,
@ -265,37 +266,40 @@ impl App {
.proc_state .proc_state
.get_mut_widget_state(self.current_widget.widget_id) .get_mut_widget_state(self.current_widget.widget_id)
{ {
// Toggles process widget grouping state // Do NOT allow when in tree mode!
proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); if !proc_widget_state.is_tree_mode {
// Toggles process widget grouping state
proc_widget_state.is_grouped = !(proc_widget_state.is_grouped);
// Forcefully switch off column if we were on it... // Forcefully switch off column if we were on it...
if (proc_widget_state.is_grouped if (proc_widget_state.is_grouped
&& proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Pid)
|| (!proc_widget_state.is_grouped
&& proc_widget_state.process_sorting_type && proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Count) == data_harvester::processes::ProcessSorting::Pid)
{ || (!proc_widget_state.is_grouped
proc_widget_state.process_sorting_type = && proc_widget_state.process_sorting_type
data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group == data_harvester::processes::ProcessSorting::Count)
proc_widget_state.process_sorting_reverse = true; {
proc_widget_state.process_sorting_type =
data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group
proc_widget_state.is_process_sort_descending = true;
}
proc_widget_state
.columns
.column_mapping
.get_mut(&processes::ProcessSorting::State)
.unwrap()
.enabled = !(proc_widget_state.is_grouped);
proc_widget_state
.columns
.toggle(&processes::ProcessSorting::Count);
proc_widget_state
.columns
.toggle(&processes::ProcessSorting::Pid);
self.proc_state.force_update = Some(self.current_widget.widget_id);
} }
proc_widget_state
.columns
.column_mapping
.get_mut(&processes::ProcessSorting::State)
.unwrap()
.enabled = !(proc_widget_state.is_grouped);
proc_widget_state
.columns
.toggle(&processes::ProcessSorting::Count);
proc_widget_state
.columns
.toggle(&processes::ProcessSorting::Pid);
self.proc_state.force_update = Some(self.current_widget.widget_id);
} }
} }
_ => {} _ => {}
@ -384,8 +388,8 @@ impl App {
}; };
if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) { if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) {
proc_widget_state.process_sorting_reverse = proc_widget_state.is_process_sort_descending =
!proc_widget_state.process_sorting_reverse; !proc_widget_state.is_process_sort_descending;
self.proc_state.force_update = Some(widget_id); self.proc_state.force_update = Some(widget_id);
} }
@ -483,6 +487,24 @@ impl App {
} }
} }
pub fn toggle_tree_mode(&mut self) {
if let Some(proc_widget_state) = self
.proc_state
.widget_states
.get_mut(&(self.current_widget.widget_id))
{
proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode;
if proc_widget_state.is_tree_mode {
// We enabled... set PID sort type to ascending.
proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid;
proc_widget_state.is_process_sort_descending = false;
}
self.proc_state.force_update = Some(self.current_widget.widget_id);
}
}
/// 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.delete_dialog_state.is_showing_dd { if self.delete_dialog_state.is_showing_dd {
@ -889,7 +911,7 @@ 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() < corresponding_filtered_process_list.len()
{ {
let current_process: (String, Vec<u32>); let current_process: (String, Vec<Pid>);
if self.is_grouped(self.current_widget.widget_id) { if self.is_grouped(self.current_widget.widget_id) {
if let Some(process) = &corresponding_filtered_process_list if let Some(process) = &corresponding_filtered_process_list
.get(proc_widget_state.scroll_state.current_scroll_position) .get(proc_widget_state.scroll_state.current_scroll_position)
@ -1069,13 +1091,13 @@ impl App {
{ {
match proc_widget_state.process_sorting_type { match proc_widget_state.process_sorting_type {
processes::ProcessSorting::CpuPercent => { processes::ProcessSorting::CpuPercent => {
proc_widget_state.process_sorting_reverse = proc_widget_state.is_process_sort_descending =
!proc_widget_state.process_sorting_reverse !proc_widget_state.is_process_sort_descending
} }
_ => { _ => {
proc_widget_state.process_sorting_type = proc_widget_state.process_sorting_type =
processes::ProcessSorting::CpuPercent; processes::ProcessSorting::CpuPercent;
proc_widget_state.process_sorting_reverse = true; proc_widget_state.is_process_sort_descending = true;
} }
} }
self.proc_state.force_update = Some(self.current_widget.widget_id); self.proc_state.force_update = Some(self.current_widget.widget_id);
@ -1092,13 +1114,13 @@ impl App {
{ {
match proc_widget_state.process_sorting_type { match proc_widget_state.process_sorting_type {
processes::ProcessSorting::MemPercent => { processes::ProcessSorting::MemPercent => {
proc_widget_state.process_sorting_reverse = proc_widget_state.is_process_sort_descending =
!proc_widget_state.process_sorting_reverse !proc_widget_state.is_process_sort_descending
} }
_ => { _ => {
proc_widget_state.process_sorting_type = proc_widget_state.process_sorting_type =
processes::ProcessSorting::MemPercent; processes::ProcessSorting::MemPercent;
proc_widget_state.process_sorting_reverse = true; proc_widget_state.is_process_sort_descending = true;
} }
} }
self.proc_state.force_update = Some(self.current_widget.widget_id); self.proc_state.force_update = Some(self.current_widget.widget_id);
@ -1116,13 +1138,13 @@ impl App {
if !proc_widget_state.is_grouped { if !proc_widget_state.is_grouped {
match proc_widget_state.process_sorting_type { match proc_widget_state.process_sorting_type {
processes::ProcessSorting::Pid => { processes::ProcessSorting::Pid => {
proc_widget_state.process_sorting_reverse = proc_widget_state.is_process_sort_descending =
!proc_widget_state.process_sorting_reverse !proc_widget_state.is_process_sort_descending
} }
_ => { _ => {
proc_widget_state.process_sorting_type = proc_widget_state.process_sorting_type =
processes::ProcessSorting::Pid; processes::ProcessSorting::Pid;
proc_widget_state.process_sorting_reverse = false; proc_widget_state.is_process_sort_descending = false;
} }
} }
self.proc_state.force_update = Some(self.current_widget.widget_id); self.proc_state.force_update = Some(self.current_widget.widget_id);
@ -1168,8 +1190,8 @@ impl App {
match proc_widget_state.process_sorting_type { match proc_widget_state.process_sorting_type {
processes::ProcessSorting::ProcessName processes::ProcessSorting::ProcessName
| processes::ProcessSorting::Command => { | processes::ProcessSorting::Command => {
proc_widget_state.process_sorting_reverse = proc_widget_state.is_process_sort_descending =
!proc_widget_state.process_sorting_reverse !proc_widget_state.is_process_sort_descending
} }
_ => { _ => {
proc_widget_state.process_sorting_type = proc_widget_state.process_sorting_type =
@ -1178,7 +1200,7 @@ impl App {
} else { } else {
processes::ProcessSorting::ProcessName processes::ProcessSorting::ProcessName
}; };
proc_widget_state.process_sorting_reverse = false; proc_widget_state.is_process_sort_descending = false;
} }
} }
self.proc_state.force_update = Some(self.current_widget.widget_id); self.proc_state.force_update = Some(self.current_widget.widget_id);
@ -1194,6 +1216,7 @@ impl App {
'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right), 'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right),
'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up), 'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up),
'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down), 'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down),
't' => self.toggle_tree_mode(),
'+' => self.zoom_in(), '+' => self.zoom_in(),
'-' => self.zoom_out(), '-' => self.zoom_out(),
'=' => self.reset_zoom(), '=' => self.reset_zoom(),
@ -1228,7 +1251,7 @@ impl App {
} }
} }
pub fn get_to_delete_processes(&self) -> Option<(String, Vec<u32>)> { pub fn get_to_delete_processes(&self) -> Option<(String, Vec<Pid>)> {
self.to_delete_process_list.clone() self.to_delete_process_list.clone()
} }

View File

@ -72,7 +72,7 @@ pub struct DataCollector {
pub data: Data, pub data: Data,
sys: System, sys: System,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pid_mapping: HashMap<u32, processes::PrevProcDetails>, pid_mapping: HashMap<crate::Pid, processes::PrevProcDetails>,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
prev_idle: f64, prev_idle: f64,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]

View File

@ -1,3 +1,4 @@
use crate::Pid;
use std::path::PathBuf; use std::path::PathBuf;
use sysinfo::ProcessStatus; use sysinfo::ProcessStatus;
@ -59,7 +60,8 @@ impl Default for ProcessSorting {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ProcessHarvest { pub struct ProcessHarvest {
pub pid: u32, pub pid: Pid,
pub parent_pid: Option<Pid>, // Remember, parent_pid 0 is root...
pub cpu_usage_percent: f64, pub cpu_usage_percent: f64,
pub mem_usage_percent: f64, pub mem_usage_percent: f64,
pub mem_usage_bytes: u64, pub mem_usage_bytes: u64,
@ -89,7 +91,7 @@ pub struct PrevProcDetails {
} }
impl PrevProcDetails { impl PrevProcDetails {
pub fn new(pid: u32) -> Self { pub fn new(pid: Pid) -> Self {
PrevProcDetails { PrevProcDetails {
proc_io_path: PathBuf::from(format!("/proc/{}/io", pid)), proc_io_path: PathBuf::from(format!("/proc/{}/io", pid)),
proc_exe_path: PathBuf::from(format!("/proc/{}/exe", pid)), proc_exe_path: PathBuf::from(format!("/proc/{}/exe", pid)),
@ -200,7 +202,7 @@ fn read_path_contents(path: &PathBuf) -> std::io::Result<String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn get_linux_process_state(stat: &[&str]) -> (char, String) { fn get_linux_process_state(stat: &[&str]) -> (char, String) {
// The -2 offset is because of us cutting off name + pid // The -2 offset is because of us cutting off name + pid, normally it's 2
if let Some(first_char) = stat[0].chars().collect::<Vec<char>>().first() { if let Some(first_char) = stat[0].chars().collect::<Vec<char>>().first() {
( (
*first_char, *first_char,
@ -241,8 +243,8 @@ fn get_linux_cpu_usage(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn read_proc<S: core::hash::BuildHasher>( fn read_proc<S: core::hash::BuildHasher>(
pid: u32, cpu_usage: f64, cpu_fraction: f64, pid: Pid, cpu_usage: f64, cpu_fraction: f64,
pid_mapping: &mut HashMap<u32, PrevProcDetails, S>, use_current_cpu_total: bool, pid_mapping: &mut HashMap<Pid, PrevProcDetails, S>, use_current_cpu_total: bool,
time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64, time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64,
) -> error::Result<ProcessHarvest> { ) -> error::Result<ProcessHarvest> {
let pid_stat = pid_mapping let pid_stat = pid_mapping
@ -282,6 +284,7 @@ fn read_proc<S: core::hash::BuildHasher>(
&mut pid_stat.cpu_time, &mut pid_stat.cpu_time,
use_current_cpu_total, use_current_cpu_total,
)?; )?;
let parent_pid = stat[1].parse::<Pid>().ok();
let (_vsize, rss) = get_linux_process_vsize_rss(&stat); let (_vsize, rss) = get_linux_process_vsize_rss(&stat);
let mem_usage_kb = rss * page_file_kb; let mem_usage_kb = rss * page_file_kb;
let mem_usage_percent = mem_usage_kb as f64 / mem_total_kb as f64 * 100.0; let mem_usage_percent = mem_usage_kb as f64 / mem_total_kb as f64 * 100.0;
@ -320,6 +323,7 @@ fn read_proc<S: core::hash::BuildHasher>(
Ok(ProcessHarvest { Ok(ProcessHarvest {
pid, pid,
parent_pid,
name, name,
command, command,
mem_usage_percent, mem_usage_percent,
@ -337,14 +341,16 @@ fn read_proc<S: core::hash::BuildHasher>(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn linux_get_processes_list( pub fn linux_get_processes_list(
prev_idle: &mut f64, prev_non_idle: &mut f64, prev_idle: &mut f64, prev_non_idle: &mut f64,
pid_mapping: &mut HashMap<u32, PrevProcDetails, RandomState>, use_current_cpu_total: bool, pid_mapping: &mut HashMap<Pid, PrevProcDetails, RandomState>, use_current_cpu_total: bool,
time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64, time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64,
) -> crate::utils::error::Result<Vec<ProcessHarvest>> { ) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
// TODO: [PROC THREADS] Add threads
if let Ok((cpu_usage, cpu_fraction)) = cpu_usage_calculation(prev_idle, prev_non_idle) { if let Ok((cpu_usage, cpu_fraction)) = cpu_usage_calculation(prev_idle, prev_non_idle) {
let process_vector: Vec<ProcessHarvest> = std::fs::read_dir("/proc")? let process_vector: Vec<ProcessHarvest> = std::fs::read_dir("/proc")?
.filter_map(|dir| { .filter_map(|dir| {
if let Ok(dir) = dir { if let Ok(dir) = dir {
let pid = dir.file_name().to_string_lossy().trim().parse::<u32>(); let pid = dir.file_name().to_string_lossy().trim().parse::<Pid>();
if let Ok(pid) = pid { if let Ok(pid) = pid {
// I skip checking if the path is also a directory, it's not needed I think? // I skip checking if the path is also a directory, it's not needed I think?
if let Ok(process_object) = read_proc( if let Ok(process_object) = read_proc(
@ -424,7 +430,8 @@ pub fn windows_macos_get_processes_list(
let disk_usage = process_val.disk_usage(); let disk_usage = process_val.disk_usage();
process_vector.push(ProcessHarvest { process_vector.push(ProcessHarvest {
pid: process_val.pid() as u32, pid: process_val.pid(),
parent_pid: process_val.parent(),
name, name,
command, command,
mem_usage_percent: if mem_total_kb > 0 { mem_usage_percent: if mem_total_kb > 0 {

View File

@ -10,6 +10,7 @@ use winapi::{
/// This file is meant to house (OS specific) implementations on how to kill processes. /// This file is meant to house (OS specific) implementations on how to kill processes.
use crate::utils::error::BottomError; use crate::utils::error::BottomError;
use crate::Pid;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
struct Process(HANDLE); struct Process(HANDLE);
@ -31,9 +32,9 @@ impl Process {
} }
/// Kills a process, given a PID. /// Kills a process, given a PID.
pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> { pub fn kill_process_given_pid(pid: Pid) -> crate::utils::error::Result<()> {
if cfg!(target_os = "linux") || cfg!(target_os = "macos") { if cfg!(target_family = "unix") {
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_family = "unix"))]
{ {
let output = unsafe { libc::kill(pid as i32, libc::SIGTERM) }; let output = unsafe { libc::kill(pid as i32, libc::SIGTERM) };
if output != 0 { if output != 0 {
@ -59,8 +60,8 @@ pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> {
}; };
} }
} }
} else if cfg!(target_os = "windows") { } else if cfg!(target_family = "windows") {
#[cfg(target_os = "windows")] #[cfg(target_family = "windows")]
{ {
let process = Process::open(pid as DWORD)?; let process = Process::open(pid as DWORD)?;
process.kill()?; process.kill()?;

View File

@ -9,6 +9,7 @@ use crate::{
constants, constants,
data_harvester::processes::{self, ProcessSorting}, data_harvester::processes::{self, ProcessSorting},
}; };
use ProcessSorting::*;
#[derive(Debug)] #[derive(Debug)]
pub enum ScrollDirection { pub enum ScrollDirection {
@ -159,7 +160,6 @@ pub struct ProcColumn {
impl Default for ProcColumn { impl Default for ProcColumn {
fn default() -> Self { fn default() -> Self {
use ProcessSorting::*;
let ordered_columns = vec![ let ordered_columns = vec![
Count, Count,
Pid, Pid,
@ -352,11 +352,12 @@ pub struct ProcWidgetState {
pub is_grouped: bool, pub is_grouped: bool,
pub scroll_state: AppScrollWidgetState, pub scroll_state: AppScrollWidgetState,
pub process_sorting_type: processes::ProcessSorting, pub process_sorting_type: processes::ProcessSorting,
pub process_sorting_reverse: bool, pub is_process_sort_descending: bool,
pub is_using_command: bool, pub is_using_command: bool,
pub current_column_index: usize, pub current_column_index: usize,
pub is_sort_open: bool, pub is_sort_open: bool,
pub columns: ProcColumn, pub columns: ProcColumn,
pub is_tree_mode: bool,
} }
impl ProcWidgetState { impl ProcWidgetState {
@ -390,11 +391,12 @@ impl ProcWidgetState {
is_grouped, is_grouped,
scroll_state: AppScrollWidgetState::default(), scroll_state: AppScrollWidgetState::default(),
process_sorting_type, process_sorting_type,
process_sorting_reverse: true, is_process_sort_descending: true,
is_using_command: false, is_using_command: false,
current_column_index: 0, current_column_index: 0,
is_sort_open: false, is_sort_open: false,
columns, columns,
is_tree_mode: false,
} }
} }
@ -422,7 +424,7 @@ impl ProcWidgetState {
if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) { if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
if *new_sort_type == self.process_sorting_type { if *new_sort_type == self.process_sorting_type {
// Just reverse the search if we're reselecting! // Just reverse the search if we're reselecting!
self.process_sorting_reverse = !(self.process_sorting_reverse); self.is_process_sort_descending = !(self.is_process_sort_descending);
} else { } else {
self.process_sorting_type = new_sort_type.clone(); self.process_sorting_type = new_sort_type.clone();
match self.process_sorting_type { match self.process_sorting_type {
@ -431,7 +433,7 @@ impl ProcWidgetState {
| ProcessSorting::ProcessName | ProcessSorting::ProcessName
| ProcessSorting::Command => { | ProcessSorting::Command => {
// Also invert anything that uses alphabetical sorting by default. // Also invert anything that uses alphabetical sorting by default.
self.process_sorting_reverse = false; self.is_process_sort_descending = false;
} }
_ => {} _ => {}
} }

View File

@ -42,7 +42,6 @@ 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>>,
pub single_process_data: Vec<ConvertedProcessData>, // Contains single process data pub single_process_data: Vec<ConvertedProcessData>, // Contains single process data
pub process_data: Vec<ConvertedProcessData>, // Not the final value, may be grouped or single
pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed
pub mem_label_percent: String, pub mem_label_percent: String,
pub swap_label_percent: String, pub swap_label_percent: String,

View File

@ -28,6 +28,7 @@ pub struct CanvasColours {
// Full, Medium, Low // Full, Medium, Low
pub battery_bar_styles: Vec<Style>, pub battery_bar_styles: Vec<Style>,
pub invalid_query_style: Style, pub invalid_query_style: Style,
pub disabled_text_style: Style,
} }
impl Default for CanvasColours { impl Default for CanvasColours {
@ -63,7 +64,8 @@ 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), invalid_query_style: Style::default().fg(tui::style::Color::Red),
disabled_text_style: Style::default().fg(Color::DarkGray),
} }
} }
} }

View File

@ -349,13 +349,11 @@ impl CpuGraphWidget for Painter {
get_variable_intrinsic_widths(width as u16, &width_ratios, &CPU_LEGEND_HEADER_LENS); get_variable_intrinsic_widths(width as u16, &width_ratios, &CPU_LEGEND_HEADER_LENS);
let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1];
let (border_and_title_style, highlight_style) = if is_on_widget { // Note we don't set highlight_style, as it should always be shown for this widget.
( let border_and_title_style = if is_on_widget {
self.colours.highlighted_border_style, self.colours.highlighted_border_style
self.colours.currently_selected_text_style,
)
} else { } else {
(self.colours.border_style, self.colours.text_style) self.colours.border_style
}; };
// Draw // Draw
@ -367,7 +365,7 @@ impl CpuGraphWidget for Painter {
.border_style(border_and_title_style), .border_style(border_and_title_style),
) )
.header_style(self.colours.table_header_style) .header_style(self.colours.table_header_style)
.highlight_style(highlight_style) .highlight_style(self.colours.currently_selected_text_style)
.widths( .widths(
&(intrinsic_widths &(intrinsic_widths
.iter() .iter()

View File

@ -36,7 +36,7 @@ pub trait ProcessTableWidget {
widget_id: u64, widget_id: u64,
); );
/// Draws the process sort box. /// Draws the process search field.
/// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget /// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget
/// state that is stored. /// state that is stored.
/// ///
@ -173,15 +173,6 @@ impl ProcessTableWidget for Painter {
.finalized_process_data_map .finalized_process_data_map
.get(&widget_id) .get(&widget_id)
{ {
// Admittedly this is kinda a hack... but we need to:
// * Scroll
// * Show/hide elements based on scroll position
//
// As such, we use a process_counter to know when we've
// hit the process we've currently scrolled to.
// We also need to move the list - we can
// do so by hiding some elements!
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0 0
} else { } else {
@ -217,39 +208,52 @@ impl ProcessTableWidget for Painter {
// Draw! // Draw!
let is_proc_widget_grouped = proc_widget_state.is_grouped; let is_proc_widget_grouped = proc_widget_state.is_grouped;
let is_using_command = proc_widget_state.is_using_command; let is_using_command = proc_widget_state.is_using_command;
let is_tree = proc_widget_state.is_tree_mode;
let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem); let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
// FIXME: [PROC OPTIMIZE] This can definitely be optimized; string references work fine here!
let process_rows = sliced_vec.iter().map(|process| { let process_rows = sliced_vec.iter().map(|process| {
Row::Data( let data = vec![
vec![ if is_proc_widget_grouped {
if is_proc_widget_grouped { process.group_pids.len().to_string()
process.group_pids.len().to_string() } else {
process.pid.to_string()
},
if is_tree {
if let Some(prefix) = &process.process_description_prefix {
prefix.clone()
} else { } else {
process.pid.to_string() String::default()
}, }
if is_using_command { } else if is_using_command {
process.command.clone() process.command.clone()
} else { } else {
process.name.clone() process.name.clone()
}, },
format!("{:.1}%", process.cpu_percent_usage), format!("{:.1}%", process.cpu_percent_usage),
if mem_enabled { if mem_enabled {
format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1) format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
} else { } else {
format!("{:.1}%", process.mem_percent_usage) format!("{:.1}%", process.mem_percent_usage)
}, },
process.read_per_sec.to_string(), process.read_per_sec.clone(),
process.write_per_sec.to_string(), process.write_per_sec.clone(),
process.total_read.to_string(), process.total_read.clone(),
process.total_write.to_string(), process.total_write.clone(),
process.process_state.to_string(), process.process_state.clone(),
] ]
.into_iter(), .into_iter();
)
if process.is_disabled_entry {
Row::StyledData(data, self.colours.disabled_text_style)
} else {
Row::Data(data)
}
}); });
let process_headers = proc_widget_state.columns.get_column_headers( let process_headers = proc_widget_state.columns.get_column_headers(
&proc_widget_state.process_sorting_type, &proc_widget_state.process_sorting_type,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
); );
let process_headers_lens: Vec<usize> = process_headers let process_headers_lens: Vec<usize> = process_headers
@ -269,6 +273,8 @@ impl ProcessTableWidget for Painter {
} }
} else if proc_widget_state.is_using_command { } else if proc_widget_state.is_using_command {
vec![0.05, 0.7, 0.05, 0.05, 0.03, 0.03, 0.03, 0.03] vec![0.05, 0.7, 0.05, 0.05, 0.03, 0.03, 0.03, 0.03]
} else if proc_widget_state.is_tree_mode {
vec![0.05, 0.4, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
} else { } else {
vec![0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] vec![0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
}; };
@ -280,6 +286,7 @@ impl ProcessTableWidget for Painter {
let intrinsic_widths = let intrinsic_widths =
&(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1];
// TODO: gotop's "x out of y" thing is really nice to help keep track of the scroll position.
f.render_stateful_widget( f.render_stateful_widget(
Table::new(process_headers.iter(), process_rows) Table::new(process_headers.iter(), process_rows)
.block(process_block) .block(process_block)
@ -592,6 +599,7 @@ impl ProcessTableWidget for Painter {
.iter() .iter()
.map(|column| Row::Data(vec![column].into_iter())); .map(|column| Row::Data(vec![column].into_iter()));
// FIXME: [State] Shorten state to small form if it can't fit...?
let column_state = &mut proc_widget_state.columns.column_state; let column_state = &mut proc_widget_state.columns.column_state;
column_state.select(Some( column_state.select(Some(
proc_widget_state proc_widget_state

View File

@ -92,7 +92,7 @@ pub const CPU_HELP_TEXT: [&str; 2] = [
// TODO [Help]: Search in help? // TODO [Help]: Search in help?
// TODO [Help]: Move to using tables for easier formatting? // TODO [Help]: Move to using tables for easier formatting?
pub const PROCESS_HELP_TEXT: [&str; 12] = [ pub const PROCESS_HELP_TEXT: [&str; 13] = [
"3 - Process widget\n", "3 - Process widget\n",
"dd Kill the selected process\n", "dd Kill the selected process\n",
"c Sort by CPU usage, press again to reverse sorting order\n", "c Sort by CPU usage, press again to reverse sorting order\n",
@ -104,7 +104,8 @@ pub const PROCESS_HELP_TEXT: [&str; 12] = [
"P Toggle between showing the full command or just the process name\n", "P Toggle between showing the full command or just the process name\n",
"s, F6 Open process sort widget\n", "s, F6 Open process sort widget\n",
"I Invert current sort\n", "I Invert current sort\n",
"% Toggle between values and percentages for memory usage", "% Toggle between values and percentages for memory usage\n",
"t, F5 Toggle tree mode",
]; ];
pub const SEARCH_HELP_TEXT: [&str; 46] = [ pub const SEARCH_HELP_TEXT: [&str; 46] = [
@ -116,9 +117,9 @@ pub const SEARCH_HELP_TEXT: [&str; 46] = [
"Ctrl-u Clear the current search query\n", "Ctrl-u Clear the current search query\n",
"Backspace Delete the character behind the cursor\n", "Backspace Delete the character behind the cursor\n",
"Delete Delete the character at the cursor\n", "Delete Delete the character at the cursor\n",
"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, Alt-h Move cursor left\n", "Left, Alt-h Move cursor left\n",
"Right, Alt-l Move cursor right\n", "Right, Alt-l Move cursor right\n",
"\n", "\n",
@ -142,8 +143,8 @@ pub const SEARCH_HELP_TEXT: [&str; 46] = [
"<= ex: cpu <= 1\n", "<= ex: cpu <= 1\n",
"\n", "\n",
"Logical operators:\n", "Logical operators:\n",
"and/&&/<Space> ex: btm and cpu > 1 and mem > 1\n", "and, &&, <Space> ex: btm and cpu > 1 and mem > 1\n",
"or/|| ex: btm or firefox\n", "or, || ex: btm or firefox\n",
"\n", "\n",
"Supported units:\n", "Supported units:\n",
"B ex: read > 1 b\n", "B ex: read > 1 b\n",

View File

@ -1,12 +1,13 @@
//! This mainly concerns converting collected data into things that the canvas //! This mainly concerns converting collected data into things that the canvas
//! can actually handle. //! can actually handle.
use crate::Pid;
use std::collections::HashMap;
use crate::{ use crate::{
app::{data_farmer, data_harvester, App, Filter}, app::{data_farmer, data_harvester, App, Filter},
utils::gen_util::*, utils::{self, gen_util::*},
}; };
use data_harvester::processes::ProcessSorting;
use indexmap::IndexSet;
use std::collections::{HashMap, VecDeque};
/// Point is of time, data /// Point is of time, data
type Point = (f64, f64); type Point = (f64, f64);
@ -38,16 +39,19 @@ pub struct ConvertedNetworkData {
// mean_tx: f64, // mean_tx: f64,
} }
// TODO: [REFACTOR] Process data... stuff really needs a rewrite. Again.
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct ConvertedProcessData { pub struct ConvertedProcessData {
pub pid: u32, pub pid: Pid,
pub ppid: Option<Pid>,
pub name: String, pub name: String,
pub command: String, pub command: String,
pub is_thread: Option<bool>,
pub cpu_percent_usage: f64, pub cpu_percent_usage: f64,
pub mem_percent_usage: f64, pub mem_percent_usage: f64,
pub mem_usage_bytes: u64, pub mem_usage_bytes: u64,
pub mem_usage_str: (f64, String), pub mem_usage_str: (f64, String),
pub group_pids: Vec<u32>, pub group_pids: Vec<Pid>,
pub read_per_sec: String, pub read_per_sec: String,
pub write_per_sec: String, pub write_per_sec: String,
pub total_read: String, pub total_read: String,
@ -57,20 +61,11 @@ pub struct ConvertedProcessData {
pub tr_f64: f64, pub tr_f64: f64,
pub tw_f64: f64, pub tw_f64: f64,
pub process_state: String, pub process_state: String,
} pub process_char: char,
/// Prefix printed before the process when displayed.
#[derive(Clone, Default, Debug)] pub process_description_prefix: Option<String>,
pub struct SingleProcessData { /// Whether to mark this process entry as disabled (mostly for tree mode).
pub pid: u32, pub is_disabled_entry: bool,
pub cpu_percent_usage: f64,
pub mem_percent_usage: f64,
pub mem_usage_bytes: u64,
pub group_pids: Vec<u32>,
pub read_per_sec: u64,
pub write_per_sec: u64,
pub total_read: u64,
pub total_write: u64,
pub process_state: String,
} }
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
@ -418,6 +413,9 @@ pub enum ProcessNamingType {
pub fn convert_process_data( pub fn convert_process_data(
current_data: &data_farmer::DataCollection, current_data: &data_farmer::DataCollection,
) -> Vec<ConvertedProcessData> { ) -> Vec<ConvertedProcessData> {
// FIXME: Thread highlighting and hiding support
// For macOS see https://github.com/hishamhm/htop/pull/848/files
current_data current_data
.process_harvest .process_harvest
.iter() .iter()
@ -437,6 +435,8 @@ pub fn convert_process_data(
ConvertedProcessData { ConvertedProcessData {
pid: process.pid, pid: process.pid,
ppid: process.parent_pid,
is_thread: None,
name: process.name.to_string(), name: process.name.to_string(),
command: process.command.to_string(), command: process.command.to_string(),
cpu_percent_usage: process.cpu_usage_percent, cpu_percent_usage: process.cpu_usage_percent,
@ -453,21 +453,379 @@ pub fn convert_process_data(
tr_f64: process.total_read_bytes as f64, tr_f64: process.total_read_bytes as f64,
tw_f64: process.total_write_bytes as f64, tw_f64: process.total_write_bytes as f64,
process_state: process.process_state.to_owned(), process_state: process.process_state.to_owned(),
process_char: process.process_state_char,
process_description_prefix: None,
is_disabled_entry: false,
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub fn group_process_data( const BRANCH_ENDING: char = '└';
single_process_data: &[ConvertedProcessData], is_using_command: ProcessNamingType, const BRANCH_VERTICAL: char = '│';
const BRANCH_SPLIT: char = '├';
const BRANCH_HORIZONTAL: char = '─';
pub fn tree_process_data(
single_process_data: &[ConvertedProcessData], is_using_command: bool,
sort_type: &ProcessSorting, is_sort_descending: bool,
) -> Vec<ConvertedProcessData> { ) -> Vec<ConvertedProcessData> {
// TODO: [TREE] Allow for collapsing entries.
// Let's first build up a (really terrible) parent -> child mapping...
// At the same time, let's make a mapping of PID -> process data!
let mut parent_child_mapping: HashMap<Pid, IndexSet<Pid>> = HashMap::default();
let mut pid_process_mapping: HashMap<Pid, &ConvertedProcessData> = HashMap::default();
let mut orphan_set: IndexSet<Pid> = IndexSet::new();
single_process_data.iter().for_each(|process| {
if let Some(ppid) = process.ppid {
orphan_set.insert(ppid);
}
orphan_set.insert(process.pid);
});
single_process_data.iter().for_each(|process| {
// Create a mapping for the process if it DNE.
parent_child_mapping
.entry(process.pid)
.or_insert_with(IndexSet::new);
pid_process_mapping.insert(process.pid, process);
// Insert its mapping to the process' parent if needed (create if it DNE).
if let Some(ppid) = process.ppid {
orphan_set.remove(&process.pid);
parent_child_mapping
.entry(ppid)
.or_insert_with(IndexSet::new)
.insert(process.pid);
}
});
// Keep only orphans, or promote children of orphans to a top-level orphan
// if their parents DNE in our pid to process mapping...
#[allow(clippy::redundant_clone)]
orphan_set.clone().iter().for_each(|pid| {
if pid_process_mapping.get(pid).is_none() {
// DNE! Promote the mapped children and remove the current parent...
orphan_set.remove(pid);
if let Some(children) = parent_child_mapping.get(pid) {
orphan_set.extend(children);
}
}
});
// Turn the parent-child mapping into a "list" via DFS...
let mut pids_to_explore: VecDeque<Pid> = orphan_set.into_iter().collect();
let mut explored_pids: Vec<Pid> = vec![];
let mut lines: Vec<String> = vec![];
/// A post-order traversal to correctly prune entire branches that only contain children
/// that are disabled and themselves are also disabled ~~wait that sounds wrong~~.
///
/// Basically, go through the hashmap, and prune out all branches that are no longer relevant.
fn prune_disabled_pids(
current_pid: Pid, parent_child_mapping: &mut HashMap<Pid, IndexSet<Pid>>,
pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>,
) -> bool {
// Let's explore all the children first, and make sure they (and their children)
// aren't all disabled...
let mut are_all_children_disabled = true;
if let Some(children) = parent_child_mapping.get(&current_pid) {
for child_pid in children.clone() {
let is_child_disabled =
prune_disabled_pids(child_pid, parent_child_mapping, pid_process_mapping);
if is_child_disabled {
if let Some(current_mapping) = parent_child_mapping.get_mut(&current_pid) {
current_mapping.remove(&child_pid);
}
} else if are_all_children_disabled {
are_all_children_disabled = false;
}
}
}
// Now consider the current pid and whether to prune...
// If the node itself is not disabled, then never prune. If it is, then check if all
// of its are disabled.
if let Some(process) = pid_process_mapping.get(&current_pid) {
if process.is_disabled_entry && are_all_children_disabled {
parent_child_mapping.remove(&current_pid);
return true;
}
}
false
}
fn sort_remaining_pids(
current_pid: Pid, sort_type: &ProcessSorting, is_sort_descending: bool,
parent_child_mapping: &mut HashMap<Pid, IndexSet<Pid>>,
pid_process_mapping: &HashMap<Pid, &ConvertedProcessData>,
) {
// Sorting is special for tree data. So, by default, things are "sorted"
// via the DFS, except for (at least Unix) PID 1 and 2, which are in that order.
// Otherwise, since this is DFS of the scanned PIDs (which are in order), you actually
// get a REVERSE order --- so, you get higher PIDs earlier than lower ones.
// But this is a tree. So, you'll get a bit of a combination, but the general idea
// is that in a tree level, it's descending order, except, again, for the first layer.
// This is how htop does it by default.
//
// So how do we "sort"? The current idea is that:
// - We sort *per-level*. Say, I want to sort by CPU. The "first level" is sorted
// by CPU in terms of its usage. All its direct children are sorted by CPU
// with *their* siblings. Etc.
// - The default is thus PIDs in reverse order (descending). We set it to this when
// we first enable the mode.
// So first, let's look at the children... (post-order again)
if let Some(children) = parent_child_mapping.get(&current_pid) {
let mut to_sort_vec: Vec<(Pid, &ConvertedProcessData)> = vec![];
for child_pid in children.clone() {
if let Some(child_process) = pid_process_mapping.get(&child_pid) {
to_sort_vec.push((child_pid, child_process));
}
sort_remaining_pids(
child_pid,
sort_type,
is_sort_descending,
parent_child_mapping,
pid_process_mapping,
);
}
// Now let's sort the immediate children!
sort_vec(&mut to_sort_vec, sort_type, is_sort_descending);
// Need to reverse what we got, apparently...
if let Some(current_mapping) = parent_child_mapping.get_mut(&current_pid) {
*current_mapping = to_sort_vec
.iter()
.rev()
.map(|(pid, _proc)| *pid)
.collect::<IndexSet<Pid>>();
}
}
}
fn sort_vec(
to_sort_vec: &mut Vec<(Pid, &ConvertedProcessData)>, sort_type: &ProcessSorting,
is_sort_descending: bool,
) {
// Sort by PID first (descending)
to_sort_vec.sort_by(|a, b| utils::gen_util::get_ordering(a.1.pid, b.1.pid, false));
match sort_type {
ProcessSorting::CpuPercent => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.1.cpu_percent_usage,
b.1.cpu_percent_usage,
is_sort_descending,
)
});
}
ProcessSorting::Mem => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.1.mem_usage_bytes,
b.1.mem_usage_bytes,
is_sort_descending,
)
});
}
ProcessSorting::MemPercent => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.1.mem_percent_usage,
b.1.mem_percent_usage,
is_sort_descending,
)
});
}
ProcessSorting::ProcessName => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
&a.1.name.to_lowercase(),
&b.1.name.to_lowercase(),
is_sort_descending,
)
});
}
ProcessSorting::Command => to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
&a.1.command.to_lowercase(),
&b.1.command.to_lowercase(),
is_sort_descending,
)
}),
ProcessSorting::Pid => {
if is_sort_descending {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(a.0, b.0, is_sort_descending)
});
}
}
ProcessSorting::ReadPerSecond => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(a.1.rps_f64, b.1.rps_f64, is_sort_descending)
});
}
ProcessSorting::WritePerSecond => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(a.1.wps_f64, b.1.wps_f64, is_sort_descending)
});
}
ProcessSorting::TotalRead => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(a.1.tr_f64, b.1.tr_f64, is_sort_descending)
});
}
ProcessSorting::TotalWrite => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(a.1.tw_f64, b.1.tw_f64, is_sort_descending)
});
}
ProcessSorting::State => to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
&a.1.process_state.to_lowercase(),
&b.1.process_state.to_lowercase(),
is_sort_descending,
)
}),
ProcessSorting::Count => {
// Should never occur in this case.
}
}
}
/// A DFS traversal to correctly build the prefix lines (the pretty '├' and '─' lines) and
/// the correct order to the PID tree as a vector (DFS is the default order htop seems to use
/// so we're shamelessly copying that).
fn build_explored_pids(
current_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid>>,
prev_drawn_lines: &str,
) -> (Vec<Pid>, Vec<String>) {
let mut explored_pids: Vec<Pid> = vec![current_pid];
let mut lines: Vec<String> = vec![];
if let Some(children) = parent_child_mapping.get(&current_pid) {
for (itx, child) in children.iter().rev().enumerate() {
let new_drawn_lines = if itx == children.len() - 1 {
format!("{} ", prev_drawn_lines)
} else {
format!("{}{} ", prev_drawn_lines, BRANCH_VERTICAL)
};
let (pid_res, branch_res) =
build_explored_pids(*child, parent_child_mapping, new_drawn_lines.as_str());
if itx == children.len() - 1 {
lines.push(format!(
"{}{}",
prev_drawn_lines,
if !new_drawn_lines.is_empty() {
format!("{}{} ", BRANCH_ENDING, BRANCH_HORIZONTAL)
} else {
String::default()
}
));
} else {
lines.push(format!(
"{}{}",
prev_drawn_lines,
if !new_drawn_lines.is_empty() {
format!("{}{} ", BRANCH_SPLIT, BRANCH_HORIZONTAL)
} else {
String::default()
}
));
}
explored_pids.extend(pid_res);
lines.extend(branch_res);
}
}
(explored_pids, lines)
}
let mut to_sort_vec = Vec::new();
for pid in pids_to_explore {
if let Some(process) = pid_process_mapping.get(&pid) {
to_sort_vec.push((pid, *process));
}
}
sort_vec(&mut to_sort_vec, sort_type, is_sort_descending);
pids_to_explore = to_sort_vec.iter().map(|(pid, _proc)| *pid).collect();
while let Some(current_pid) = pids_to_explore.pop_front() {
if !prune_disabled_pids(current_pid, &mut parent_child_mapping, &pid_process_mapping) {
sort_remaining_pids(
current_pid,
sort_type,
is_sort_descending,
&mut parent_child_mapping,
&pid_process_mapping,
);
let (pid_res, branch_res) = build_explored_pids(current_pid, &parent_child_mapping, "");
lines.push(String::default());
lines.extend(branch_res);
explored_pids.extend(pid_res);
}
}
// Now let's "rearrange" our current list of converted process data into the correct
// order required... and we're done!
explored_pids
.iter()
.zip(lines)
.filter_map(|(pid, prefix)| match pid_process_mapping.remove(pid) {
Some(process) => {
let mut p = process.clone();
p.process_description_prefix = Some(format!(
"{}{}",
prefix,
if is_using_command {
&p.command
} else {
&p.name
}
));
Some(p)
}
None => None,
})
.collect::<Vec<_>>()
}
pub fn group_process_data(
single_process_data: &[ConvertedProcessData], is_using_command: bool,
) -> Vec<ConvertedProcessData> {
#[derive(Clone, Default, Debug)]
struct SingleProcessData {
pub pid: Pid,
pub cpu_percent_usage: f64,
pub mem_percent_usage: f64,
pub mem_usage_bytes: u64,
pub group_pids: Vec<Pid>,
pub read_per_sec: f64,
pub write_per_sec: f64,
pub total_read: f64,
pub total_write: f64,
pub process_state: String,
}
let mut grouped_hashmap: HashMap<String, SingleProcessData> = std::collections::HashMap::new(); let mut grouped_hashmap: HashMap<String, SingleProcessData> = std::collections::HashMap::new();
single_process_data.iter().for_each(|process| { single_process_data.iter().for_each(|process| {
let entry = grouped_hashmap let entry = grouped_hashmap
.entry(match is_using_command { .entry(if is_using_command {
ProcessNamingType::Name => process.name.to_string(), process.command.to_string()
ProcessNamingType::Path => process.command.to_string(), } else {
process.name.to_string()
}) })
.or_insert(SingleProcessData { .or_insert(SingleProcessData {
pid: process.pid, pid: process.pid,
@ -478,20 +836,20 @@ pub fn group_process_data(
(*entry).mem_percent_usage += process.mem_percent_usage; (*entry).mem_percent_usage += process.mem_percent_usage;
(*entry).mem_usage_bytes += process.mem_usage_bytes; (*entry).mem_usage_bytes += process.mem_usage_bytes;
(*entry).group_pids.push(process.pid); (*entry).group_pids.push(process.pid);
(*entry).read_per_sec += process.rps_f64 as u64; (*entry).read_per_sec += process.rps_f64;
(*entry).write_per_sec += process.wps_f64 as u64; (*entry).write_per_sec += process.wps_f64;
(*entry).total_read += process.tr_f64 as u64; (*entry).total_read += process.tr_f64;
(*entry).total_write += process.tw_f64 as u64; (*entry).total_write += process.tw_f64;
}); });
grouped_hashmap grouped_hashmap
.iter() .iter()
.map(|(identifier, process_details)| { .map(|(identifier, process_details)| {
let p = process_details.clone(); let p = process_details.clone();
let converted_rps = get_exact_byte_values(p.read_per_sec, false); let converted_rps = get_exact_byte_values(p.read_per_sec as u64, false);
let converted_wps = get_exact_byte_values(p.write_per_sec, false); let converted_wps = get_exact_byte_values(p.write_per_sec as u64, false);
let converted_total_read = get_exact_byte_values(p.total_read, false); let converted_total_read = get_exact_byte_values(p.total_read as u64, false);
let converted_total_write = get_exact_byte_values(p.total_write, false); let converted_total_write = get_exact_byte_values(p.total_write as u64, false);
let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); 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 write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1);
@ -503,6 +861,8 @@ pub fn group_process_data(
ConvertedProcessData { ConvertedProcessData {
pid: p.pid, pid: p.pid,
ppid: None,
is_thread: None,
name: identifier.to_string(), name: identifier.to_string(),
command: identifier.to_string(), command: identifier.to_string(),
cpu_percent_usage: p.cpu_percent_usage, cpu_percent_usage: p.cpu_percent_usage,
@ -514,11 +874,14 @@ pub fn group_process_data(
write_per_sec, write_per_sec,
total_read, total_read,
total_write, total_write,
rps_f64: p.read_per_sec as f64, rps_f64: p.read_per_sec,
wps_f64: p.write_per_sec as f64, wps_f64: p.write_per_sec,
tr_f64: p.total_read as f64, tr_f64: p.total_read,
tw_f64: p.total_write as f64, tw_f64: p.total_write,
process_state: p.process_state, process_state: p.process_state, // TODO: What the heck
process_description_prefix: None,
process_char: char::default(), // TODO: What the heck
is_disabled_entry: false,
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

@ -47,6 +47,12 @@ pub mod options;
pub mod clap; pub mod clap;
#[cfg(target_family = "windows")]
pub type Pid = usize;
#[cfg(target_family = "unix")]
pub type Pid = libc::pid_t;
pub enum BottomEvent<I, J> { pub enum BottomEvent<I, J> {
KeyInput(I), KeyInput(I),
MouseInput(J), MouseInput(J),
@ -111,6 +117,7 @@ pub fn handle_key_event_or_break(
KeyCode::F(1) => app.toggle_ignore_case(), KeyCode::F(1) => app.toggle_ignore_case(),
KeyCode::F(2) => app.toggle_search_whole_word(), KeyCode::F(2) => app.toggle_search_whole_word(),
KeyCode::F(3) => app.toggle_search_regex(), KeyCode::F(3) => app.toggle_search_regex(),
KeyCode::F(5) => app.toggle_tree_mode(),
KeyCode::F(6) => app.toggle_sort(), KeyCode::F(6) => app.toggle_sort(),
_ => {} _ => {}
} }
@ -458,88 +465,109 @@ pub fn update_all_process_lists(app: &mut App) {
} }
} }
pub fn update_final_process_list(app: &mut App, widget_id: u64) { fn update_final_process_list(app: &mut App, widget_id: u64) {
let (is_invalid_or_blank, is_using_command) = match app.proc_state.widget_states.get(&widget_id) let process_states = match app.proc_state.widget_states.get(&widget_id) {
{ Some(process_state) => Some((
Some(process_state) => (
process_state process_state
.process_search_state .process_search_state
.search_state .search_state
.is_invalid_or_blank_search(), .is_invalid_or_blank_search(),
process_state.is_using_command, process_state.is_using_command,
), process_state.is_grouped,
None => (false, false), process_state.is_tree_mode,
)),
None => None,
}; };
let is_grouped = app.is_grouped(widget_id);
if !app.is_frozen { if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
app.canvas_data.single_process_data = convert_process_data(&app.data_collection); if !app.is_frozen {
} app.canvas_data.single_process_data = convert_process_data(&app.data_collection);
if is_grouped {
app.canvas_data.process_data = group_process_data(
&app.canvas_data.single_process_data,
if is_using_command {
ProcessNamingType::Path
} else {
ProcessNamingType::Name
},
);
} else {
app.canvas_data.process_data = app.canvas_data.single_process_data.clone();
}
let process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = app
.canvas_data
.process_data
.iter()
.filter(|process| {
if !is_invalid_or_blank {
if let Some(process_filter) = process_filter {
process_filter.check(&process, is_using_command)
} else {
true
}
} else {
true
}
})
.cloned()
.collect::<Vec<_>>();
// Quick fix for tab updating the table headers
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
let mut resulting_processes = filtered_process_data;
sort_process_data(&mut resulting_processes, proc_widget_state);
if proc_widget_state.scroll_state.current_scroll_position >= resulting_processes.len() {
proc_widget_state.scroll_state.current_scroll_position =
resulting_processes.len().saturating_sub(1);
proc_widget_state.scroll_state.previous_scroll_position = 0;
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
} }
app.canvas_data let process_filter = app.get_process_filter(widget_id);
.finalized_process_data_map let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
.insert(widget_id, resulting_processes); app.canvas_data
.single_process_data
.iter()
.map(|process| {
let mut process_clone = process.clone();
if !is_invalid_or_blank {
if let Some(process_filter) = process_filter {
process_clone.is_disabled_entry =
!process_filter.check(&process_clone, is_using_command);
}
}
process_clone
})
.collect::<Vec<_>>()
} else {
app.canvas_data
.single_process_data
.iter()
.filter(|process| {
if !is_invalid_or_blank {
if let Some(process_filter) = process_filter {
process_filter.check(&process, is_using_command)
} else {
true
}
} else {
true
}
})
.cloned()
.collect::<Vec<_>>()
};
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
let mut finalized_process_data = if is_tree {
tree_process_data(
&filtered_process_data,
is_using_command,
&proc_widget_state.process_sorting_type,
proc_widget_state.is_process_sort_descending,
)
} else if is_grouped {
group_process_data(&filtered_process_data, is_using_command)
} else {
filtered_process_data
};
// Note tree mode is sorted well before this, as it's special.
if !is_tree {
sort_process_data(&mut finalized_process_data, proc_widget_state);
}
if proc_widget_state.scroll_state.current_scroll_position
>= finalized_process_data.len()
{
proc_widget_state.scroll_state.current_scroll_position =
finalized_process_data.len().saturating_sub(1);
proc_widget_state.scroll_state.previous_scroll_position = 0;
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
}
app.canvas_data
.finalized_process_data_map
.insert(widget_id, finalized_process_data);
}
} }
} }
pub fn sort_process_data( fn sort_process_data(
to_sort_vec: &mut Vec<ConvertedProcessData>, proc_widget_state: &app::ProcWidgetState, to_sort_vec: &mut Vec<ConvertedProcessData>, proc_widget_state: &app::ProcWidgetState,
) { ) {
to_sort_vec.sort_by(|a, b| { to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(&a.name.to_lowercase(), &b.name.to_lowercase(), false) utils::gen_util::get_ordering(&a.name.to_lowercase(), &b.name.to_lowercase(), false)
}); });
match proc_widget_state.process_sorting_type { match &proc_widget_state.process_sorting_type {
ProcessSorting::CpuPercent => { ProcessSorting::CpuPercent => {
to_sort_vec.sort_by(|a, b| { to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.cpu_percent_usage, a.cpu_percent_usage,
b.cpu_percent_usage, b.cpu_percent_usage,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -548,7 +576,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.mem_usage_bytes, a.mem_usage_bytes,
b.mem_usage_bytes, b.mem_usage_bytes,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -557,18 +585,18 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.mem_percent_usage, a.mem_percent_usage,
b.mem_percent_usage, b.mem_percent_usage,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
ProcessSorting::ProcessName => { ProcessSorting::ProcessName => {
// Don't repeat if false... it sorts by name by default anyways. // Don't repeat if false... it sorts by name by default anyways.
if proc_widget_state.process_sorting_reverse { if proc_widget_state.is_process_sort_descending {
to_sort_vec.sort_by(|a, b| { to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
&a.name.to_lowercase(), &a.name.to_lowercase(),
&b.name.to_lowercase(), &b.name.to_lowercase(),
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}) })
} }
@ -577,7 +605,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
&a.command.to_lowercase(), &a.command.to_lowercase(),
&b.command.to_lowercase(), &b.command.to_lowercase(),
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}), }),
ProcessSorting::Pid => { ProcessSorting::Pid => {
@ -586,7 +614,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.pid, a.pid,
b.pid, b.pid,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -596,7 +624,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.rps_f64, a.rps_f64,
b.rps_f64, b.rps_f64,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -605,7 +633,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.wps_f64, a.wps_f64,
b.wps_f64, b.wps_f64,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -614,7 +642,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.tr_f64, a.tr_f64,
b.tr_f64, b.tr_f64,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -623,7 +651,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.tw_f64, a.tw_f64,
b.tw_f64, b.tw_f64,
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }
@ -631,7 +659,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
&a.process_state.to_lowercase(), &a.process_state.to_lowercase(),
&b.process_state.to_lowercase(), &b.process_state.to_lowercase(),
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}), }),
ProcessSorting::Count => { ProcessSorting::Count => {
@ -640,7 +668,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering( utils::gen_util::get_ordering(
a.group_pids.len(), a.group_pids.len(),
b.group_pids.len(), b.group_pids.len(),
proc_widget_state.process_sorting_reverse, proc_widget_state.is_process_sort_descending,
) )
}); });
} }

View File

@ -91,19 +91,19 @@ pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) {
/// Gotta get partial ordering? No problem, here's something to deal with it~ /// Gotta get partial ordering? No problem, here's something to deal with it~
pub fn get_ordering<T: std::cmp::PartialOrd>( pub fn get_ordering<T: std::cmp::PartialOrd>(
a_val: T, b_val: T, reverse_order: bool, a_val: T, b_val: T, descending_order: bool,
) -> std::cmp::Ordering { ) -> std::cmp::Ordering {
match a_val.partial_cmp(&b_val) { match a_val.partial_cmp(&b_val) {
Some(x) => match x { Some(x) => match x {
Ordering::Greater => { Ordering::Greater => {
if reverse_order { if descending_order {
std::cmp::Ordering::Less std::cmp::Ordering::Less
} else { } else {
std::cmp::Ordering::Greater std::cmp::Ordering::Greater
} }
} }
Ordering::Less => { Ordering::Less => {
if reverse_order { if descending_order {
std::cmp::Ordering::Greater std::cmp::Ordering::Greater
} else { } else {
std::cmp::Ordering::Less std::cmp::Ordering::Less