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
set -e
echo "Running pre-push hook:"
echo "Executing: cargo +nightly clippy -- -D clippy::all"

View file

@ -46,10 +46,13 @@
"fract",
"gnueabihf",
"gotop",
"gotop's",
"gtop",
"haase",
"heim",
"hjkl",
"htop",
"indexmap",
"libc",
"markdownlint",
"memb",
@ -62,6 +65,7 @@
"nvme",
"paren",
"pmem",
"ppid",
"prepush",
"processthreadsapi",
"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.
- [#223](https://github.com/ClementTsang/bottom/pull/223): Add tree mode for processes.
### Changes
- [#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",
"futures",
"heim",
"indexmap",
"itertools",
"lazy_static",
"libc",
@ -534,6 +535,12 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
[[package]]
name = "hashbrown"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7"
[[package]]
name = "heim"
version = "0.0.10"
@ -723,6 +730,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "iovec"
version = "0.1.4"

View file

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

View file

@ -41,6 +41,7 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Processes](#processes)
- [Process searching](#process-searching)
- [Process sorting](#process-sorting)
- [Tree mode](#tree-mode)
- [Zoom](#zoom)
- [Expanding](#expanding)
- [Basic mode](#basic-mode)
@ -246,23 +247,24 @@ Run using `btm`.
| `s, F6` | Open process sort widget |
| `I` | Invert current sort |
| `%` | Toggle between values and percentages for memory usage |
| `t`, `F5` | Toggle tree mode |
#### Process search bindings
| | |
| ------------ | -------------------------------------------- |
| `Tab` | Toggle between searching by PID or name |
| `Esc` | Close the search widget (retains the filter) |
| `Ctrl-a` | Skip to the start of the search query |
| `Ctrl-e` | Skip to the end of the search query |
| `Ctrl-u` | Clear the current search query |
| `Backspace` | Delete the character behind the cursor |
| `Delete` | Delete the character at the cursor |
| `Alt-c`/`F1` | Toggle matching case |
| `Alt-w`/`F2` | Toggle matching the entire word |
| `Alt-r`/`F3` | Toggle using regex |
| `Left` | Move cursor left |
| `Right` | Move cursor right |
| | |
| ------------- | -------------------------------------------- |
| `Tab` | Toggle between searching by PID or name |
| `Esc` | Close the search widget (retains the filter) |
| `Ctrl-a` | Skip to the start of the search query |
| `Ctrl-e` | Skip to the end of the search query |
| `Ctrl-u` | Clear the current search query |
| `Backspace` | Delete the character behind the cursor |
| `Delete` | Delete the character at the cursor |
| `Alt-c`, `F1` | Toggle matching case |
| `Alt-w`, `F2` | Toggle matching the entire word |
| `Alt-r`, `F3` | Toggle using regex |
| `Left` | Move cursor left |
| `Right` | Move cursor right |
### 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)
#### 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
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::{
canvas, constants,
utils::error::{BottomError, Result},
Pid,
};
pub mod data_farmer;
@ -67,7 +68,7 @@ pub struct App {
pub dd_err: Option<String>,
#[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))]
pub is_frozen: bool,
@ -265,37 +266,40 @@ impl App {
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
// Toggles process widget grouping state
proc_widget_state.is_grouped = !(proc_widget_state.is_grouped);
// Do NOT allow when in tree mode!
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...
if (proc_widget_state.is_grouped
&& proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Pid)
|| (!proc_widget_state.is_grouped
// Forcefully switch off column if we were on it...
if (proc_widget_state.is_grouped
&& proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Count)
{
proc_widget_state.process_sorting_type =
data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group
proc_widget_state.process_sorting_reverse = true;
== data_harvester::processes::ProcessSorting::Pid)
|| (!proc_widget_state.is_grouped
&& proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Count)
{
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) {
proc_widget_state.process_sorting_reverse =
!proc_widget_state.process_sorting_reverse;
proc_widget_state.is_process_sort_descending =
!proc_widget_state.is_process_sort_descending;
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...
pub fn on_enter(&mut self) {
if self.delete_dialog_state.is_showing_dd {
@ -889,7 +911,7 @@ impl App {
if proc_widget_state.scroll_state.current_scroll_position
< 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 let Some(process) = &corresponding_filtered_process_list
.get(proc_widget_state.scroll_state.current_scroll_position)
@ -1069,13 +1091,13 @@ impl App {
{
match proc_widget_state.process_sorting_type {
processes::ProcessSorting::CpuPercent => {
proc_widget_state.process_sorting_reverse =
!proc_widget_state.process_sorting_reverse
proc_widget_state.is_process_sort_descending =
!proc_widget_state.is_process_sort_descending
}
_ => {
proc_widget_state.process_sorting_type =
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);
@ -1092,13 +1114,13 @@ impl App {
{
match proc_widget_state.process_sorting_type {
processes::ProcessSorting::MemPercent => {
proc_widget_state.process_sorting_reverse =
!proc_widget_state.process_sorting_reverse
proc_widget_state.is_process_sort_descending =
!proc_widget_state.is_process_sort_descending
}
_ => {
proc_widget_state.process_sorting_type =
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);
@ -1116,13 +1138,13 @@ impl App {
if !proc_widget_state.is_grouped {
match proc_widget_state.process_sorting_type {
processes::ProcessSorting::Pid => {
proc_widget_state.process_sorting_reverse =
!proc_widget_state.process_sorting_reverse
proc_widget_state.is_process_sort_descending =
!proc_widget_state.is_process_sort_descending
}
_ => {
proc_widget_state.process_sorting_type =
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);
@ -1168,8 +1190,8 @@ impl App {
match proc_widget_state.process_sorting_type {
processes::ProcessSorting::ProcessName
| processes::ProcessSorting::Command => {
proc_widget_state.process_sorting_reverse =
!proc_widget_state.process_sorting_reverse
proc_widget_state.is_process_sort_descending =
!proc_widget_state.is_process_sort_descending
}
_ => {
proc_widget_state.process_sorting_type =
@ -1178,7 +1200,7 @@ impl App {
} else {
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);
@ -1194,6 +1216,7 @@ impl App {
'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right),
'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up),
'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down),
't' => self.toggle_tree_mode(),
'+' => self.zoom_in(),
'-' => self.zoom_out(),
'=' => 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()
}

View file

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

View file

@ -1,3 +1,4 @@
use crate::Pid;
use std::path::PathBuf;
use sysinfo::ProcessStatus;
@ -59,7 +60,8 @@ impl Default for ProcessSorting {
#[derive(Debug, Clone, Default)]
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 mem_usage_percent: f64,
pub mem_usage_bytes: u64,
@ -89,7 +91,7 @@ pub struct PrevProcDetails {
}
impl PrevProcDetails {
pub fn new(pid: u32) -> Self {
pub fn new(pid: Pid) -> Self {
PrevProcDetails {
proc_io_path: PathBuf::from(format!("/proc/{}/io", 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")]
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() {
(
*first_char,
@ -241,8 +243,8 @@ fn get_linux_cpu_usage(
#[allow(clippy::too_many_arguments)]
#[cfg(target_os = "linux")]
fn read_proc<S: core::hash::BuildHasher>(
pid: u32, cpu_usage: f64, cpu_fraction: f64,
pid_mapping: &mut HashMap<u32, PrevProcDetails, S>, use_current_cpu_total: bool,
pid: Pid, cpu_usage: f64, cpu_fraction: f64,
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,
) -> error::Result<ProcessHarvest> {
let pid_stat = pid_mapping
@ -282,6 +284,7 @@ fn read_proc<S: core::hash::BuildHasher>(
&mut pid_stat.cpu_time,
use_current_cpu_total,
)?;
let parent_pid = stat[1].parse::<Pid>().ok();
let (_vsize, rss) = get_linux_process_vsize_rss(&stat);
let mem_usage_kb = rss * page_file_kb;
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 {
pid,
parent_pid,
name,
command,
mem_usage_percent,
@ -337,14 +341,16 @@ fn read_proc<S: core::hash::BuildHasher>(
#[cfg(target_os = "linux")]
pub fn linux_get_processes_list(
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,
) -> 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) {
let process_vector: Vec<ProcessHarvest> = std::fs::read_dir("/proc")?
.filter_map(|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 {
// I skip checking if the path is also a directory, it's not needed I think?
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();
process_vector.push(ProcessHarvest {
pid: process_val.pid() as u32,
pid: process_val.pid(),
parent_pid: process_val.parent(),
name,
command,
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.
use crate::utils::error::BottomError;
use crate::Pid;
#[cfg(target_os = "windows")]
struct Process(HANDLE);
@ -31,9 +32,9 @@ impl Process {
}
/// Kills a process, given a PID.
pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> {
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn kill_process_given_pid(pid: Pid) -> crate::utils::error::Result<()> {
if cfg!(target_family = "unix") {
#[cfg(any(target_family = "unix"))]
{
let output = unsafe { libc::kill(pid as i32, libc::SIGTERM) };
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") {
#[cfg(target_os = "windows")]
} else if cfg!(target_family = "windows") {
#[cfg(target_family = "windows")]
{
let process = Process::open(pid as DWORD)?;
process.kill()?;

View file

@ -9,6 +9,7 @@ use crate::{
constants,
data_harvester::processes::{self, ProcessSorting},
};
use ProcessSorting::*;
#[derive(Debug)]
pub enum ScrollDirection {
@ -159,7 +160,6 @@ pub struct ProcColumn {
impl Default for ProcColumn {
fn default() -> Self {
use ProcessSorting::*;
let ordered_columns = vec![
Count,
Pid,
@ -352,11 +352,12 @@ pub struct ProcWidgetState {
pub is_grouped: bool,
pub scroll_state: AppScrollWidgetState,
pub process_sorting_type: processes::ProcessSorting,
pub process_sorting_reverse: bool,
pub is_process_sort_descending: bool,
pub is_using_command: bool,
pub current_column_index: usize,
pub is_sort_open: bool,
pub columns: ProcColumn,
pub is_tree_mode: bool,
}
impl ProcWidgetState {
@ -390,11 +391,12 @@ impl ProcWidgetState {
is_grouped,
scroll_state: AppScrollWidgetState::default(),
process_sorting_type,
process_sorting_reverse: true,
is_process_sort_descending: true,
is_using_command: false,
current_column_index: 0,
is_sort_open: false,
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 *new_sort_type == self.process_sorting_type {
// 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 {
self.process_sorting_type = new_sort_type.clone();
match self.process_sorting_type {
@ -431,7 +433,7 @@ impl ProcWidgetState {
| ProcessSorting::ProcessName
| ProcessSorting::Command => {
// 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 temp_sensor_data: Vec<Vec<String>>,
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 mem_label_percent: String,
pub swap_label_percent: String,

View file

@ -28,6 +28,7 @@ pub struct CanvasColours {
// Full, Medium, Low
pub battery_bar_styles: Vec<Style>,
pub invalid_query_style: Style,
pub disabled_text_style: Style,
}
impl Default for CanvasColours {
@ -63,7 +64,8 @@ impl Default for CanvasColours {
Style::default().fg(Color::Green),
Style::default().fg(Color::Green),
],
invalid_query_style: tui::style::Style::default().fg(tui::style::Color::Red),
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);
let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1];
let (border_and_title_style, highlight_style) = if is_on_widget {
(
self.colours.highlighted_border_style,
self.colours.currently_selected_text_style,
)
// 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
} else {
(self.colours.border_style, self.colours.text_style)
self.colours.border_style
};
// Draw
@ -367,7 +365,7 @@ impl CpuGraphWidget for Painter {
.border_style(border_and_title_style),
)
.header_style(self.colours.table_header_style)
.highlight_style(highlight_style)
.highlight_style(self.colours.currently_selected_text_style)
.widths(
&(intrinsic_widths
.iter()

View file

@ -36,7 +36,7 @@ pub trait ProcessTableWidget {
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
/// state that is stored.
///
@ -173,15 +173,6 @@ impl ProcessTableWidget for Painter {
.finalized_process_data_map
.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 {
0
} else {
@ -217,39 +208,52 @@ impl ProcessTableWidget for Painter {
// Draw!
let is_proc_widget_grouped = proc_widget_state.is_grouped;
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);
// FIXME: [PROC OPTIMIZE] This can definitely be optimized; string references work fine here!
let process_rows = sliced_vec.iter().map(|process| {
Row::Data(
vec![
if is_proc_widget_grouped {
process.group_pids.len().to_string()
let data = vec![
if is_proc_widget_grouped {
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 {
process.pid.to_string()
},
if is_using_command {
process.command.clone()
} else {
process.name.clone()
},
format!("{:.1}%", process.cpu_percent_usage),
if mem_enabled {
format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
} else {
format!("{:.1}%", process.mem_percent_usage)
},
process.read_per_sec.to_string(),
process.write_per_sec.to_string(),
process.total_read.to_string(),
process.total_write.to_string(),
process.process_state.to_string(),
]
.into_iter(),
)
String::default()
}
} else if is_using_command {
process.command.clone()
} else {
process.name.clone()
},
format!("{:.1}%", process.cpu_percent_usage),
if mem_enabled {
format!("{:.0}{}", process.mem_usage_str.0, process.mem_usage_str.1)
} else {
format!("{:.1}%", process.mem_percent_usage)
},
process.read_per_sec.clone(),
process.write_per_sec.clone(),
process.total_read.clone(),
process.total_write.clone(),
process.process_state.clone(),
]
.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(
&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
@ -269,6 +273,8 @@ impl ProcessTableWidget for Painter {
}
} 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]
} 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 {
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 =
&(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(
Table::new(process_headers.iter(), process_rows)
.block(process_block)
@ -592,6 +599,7 @@ impl ProcessTableWidget for Painter {
.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;
column_state.select(Some(
proc_widget_state

View file

@ -92,7 +92,7 @@ pub const CPU_HELP_TEXT: [&str; 2] = [
// TODO [Help]: Search in help?
// 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",
"dd Kill the selected process\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",
"s, F6 Open process sort widget\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] = [
@ -116,9 +117,9 @@ pub const SEARCH_HELP_TEXT: [&str; 46] = [
"Ctrl-u Clear the current search query\n",
"Backspace Delete the character behind the cursor\n",
"Delete Delete the character at the cursor\n",
"Alt-c/F1 Toggle matching case\n",
"Alt-w/F2 Toggle matching the entire word\n",
"Alt-r/F3 Toggle using regex\n",
"Alt-c, F1 Toggle matching case\n",
"Alt-w, F2 Toggle matching the entire word\n",
"Alt-r, F3 Toggle using regex\n",
"Left, Alt-h Move cursor left\n",
"Right, Alt-l Move cursor right\n",
"\n",
@ -142,8 +143,8 @@ pub const SEARCH_HELP_TEXT: [&str; 46] = [
"<= ex: cpu <= 1\n",
"\n",
"Logical operators:\n",
"and/&&/<Space> ex: btm and cpu > 1 and mem > 1\n",
"or/|| ex: btm or firefox\n",
"and, &&, <Space> ex: btm and cpu > 1 and mem > 1\n",
"or, || ex: btm or firefox\n",
"\n",
"Supported units:\n",
"B ex: read > 1 b\n",

View file

@ -1,12 +1,13 @@
//! This mainly concerns converting collected data into things that the canvas
//! can actually handle.
use std::collections::HashMap;
use crate::Pid;
use crate::{
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
type Point = (f64, f64);
@ -38,16 +39,19 @@ pub struct ConvertedNetworkData {
// mean_tx: f64,
}
// TODO: [REFACTOR] Process data... stuff really needs a rewrite. Again.
#[derive(Clone, Default, Debug)]
pub struct ConvertedProcessData {
pub pid: u32,
pub pid: Pid,
pub ppid: Option<Pid>,
pub name: String,
pub command: String,
pub is_thread: Option<bool>,
pub cpu_percent_usage: f64,
pub mem_percent_usage: f64,
pub mem_usage_bytes: u64,
pub mem_usage_str: (f64, String),
pub group_pids: Vec<u32>,
pub group_pids: Vec<Pid>,
pub read_per_sec: String,
pub write_per_sec: String,
pub total_read: String,
@ -57,20 +61,11 @@ pub struct ConvertedProcessData {
pub tr_f64: f64,
pub tw_f64: f64,
pub process_state: String,
}
#[derive(Clone, Default, Debug)]
pub struct SingleProcessData {
pub pid: u32,
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,
pub process_char: char,
/// Prefix printed before the process when displayed.
pub process_description_prefix: Option<String>,
/// Whether to mark this process entry as disabled (mostly for tree mode).
pub is_disabled_entry: bool,
}
#[derive(Clone, Default, Debug)]
@ -418,6 +413,9 @@ pub enum ProcessNamingType {
pub fn convert_process_data(
current_data: &data_farmer::DataCollection,
) -> Vec<ConvertedProcessData> {
// FIXME: Thread highlighting and hiding support
// For macOS see https://github.com/hishamhm/htop/pull/848/files
current_data
.process_harvest
.iter()
@ -437,6 +435,8 @@ pub fn convert_process_data(
ConvertedProcessData {
pid: process.pid,
ppid: process.parent_pid,
is_thread: None,
name: process.name.to_string(),
command: process.command.to_string(),
cpu_percent_usage: process.cpu_usage_percent,
@ -453,21 +453,379 @@ pub fn convert_process_data(
tr_f64: process.total_read_bytes as f64,
tw_f64: process.total_write_bytes as f64,
process_state: process.process_state.to_owned(),
process_char: process.process_state_char,
process_description_prefix: None,
is_disabled_entry: false,
}
})
.collect::<Vec<_>>()
}
pub fn group_process_data(
single_process_data: &[ConvertedProcessData], is_using_command: ProcessNamingType,
const BRANCH_ENDING: char = '└';
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> {
// 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();
single_process_data.iter().for_each(|process| {
let entry = grouped_hashmap
.entry(match is_using_command {
ProcessNamingType::Name => process.name.to_string(),
ProcessNamingType::Path => process.command.to_string(),
.entry(if is_using_command {
process.command.to_string()
} else {
process.name.to_string()
})
.or_insert(SingleProcessData {
pid: process.pid,
@ -478,20 +836,20 @@ pub fn group_process_data(
(*entry).mem_percent_usage += process.mem_percent_usage;
(*entry).mem_usage_bytes += process.mem_usage_bytes;
(*entry).group_pids.push(process.pid);
(*entry).read_per_sec += process.rps_f64 as u64;
(*entry).write_per_sec += process.wps_f64 as u64;
(*entry).total_read += process.tr_f64 as u64;
(*entry).total_write += process.tw_f64 as u64;
(*entry).read_per_sec += process.rps_f64;
(*entry).write_per_sec += process.wps_f64;
(*entry).total_read += process.tr_f64;
(*entry).total_write += process.tw_f64;
});
grouped_hashmap
.iter()
.map(|(identifier, process_details)| {
let p = process_details.clone();
let converted_rps = get_exact_byte_values(p.read_per_sec, false);
let converted_wps = get_exact_byte_values(p.write_per_sec, false);
let converted_total_read = get_exact_byte_values(p.total_read, false);
let converted_total_write = get_exact_byte_values(p.total_write, 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 as u64, 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 as u64, false);
let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1);
let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1);
@ -503,6 +861,8 @@ pub fn group_process_data(
ConvertedProcessData {
pid: p.pid,
ppid: None,
is_thread: None,
name: identifier.to_string(),
command: identifier.to_string(),
cpu_percent_usage: p.cpu_percent_usage,
@ -514,11 +874,14 @@ pub fn group_process_data(
write_per_sec,
total_read,
total_write,
rps_f64: p.read_per_sec as f64,
wps_f64: p.write_per_sec as f64,
tr_f64: p.total_read as f64,
tw_f64: p.total_write as f64,
process_state: p.process_state,
rps_f64: p.read_per_sec,
wps_f64: p.write_per_sec,
tr_f64: p.total_read,
tw_f64: p.total_write,
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<_>>()

View file

@ -47,6 +47,12 @@ pub mod options;
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> {
KeyInput(I),
MouseInput(J),
@ -111,6 +117,7 @@ pub fn handle_key_event_or_break(
KeyCode::F(1) => app.toggle_ignore_case(),
KeyCode::F(2) => app.toggle_search_whole_word(),
KeyCode::F(3) => app.toggle_search_regex(),
KeyCode::F(5) => app.toggle_tree_mode(),
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) {
let (is_invalid_or_blank, is_using_command) = match app.proc_state.widget_states.get(&widget_id)
{
Some(process_state) => (
fn update_final_process_list(app: &mut App, widget_id: u64) {
let process_states = match app.proc_state.widget_states.get(&widget_id) {
Some(process_state) => Some((
process_state
.process_search_state
.search_state
.is_invalid_or_blank_search(),
process_state.is_using_command,
),
None => (false, false),
process_state.is_grouped,
process_state.is_tree_mode,
)),
None => None,
};
let is_grouped = app.is_grouped(widget_id);
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;
if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
if !app.is_frozen {
app.canvas_data.single_process_data = convert_process_data(&app.data_collection);
}
app.canvas_data
.finalized_process_data_map
.insert(widget_id, resulting_processes);
let process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
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.sort_by(|a, b| {
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 => {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.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(
a.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(
a.mem_percent_usage,
b.mem_percent_usage,
proc_widget_state.process_sorting_reverse,
proc_widget_state.is_process_sort_descending,
)
});
}
ProcessSorting::ProcessName => {
// 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| {
utils::gen_util::get_ordering(
&a.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(
&a.command.to_lowercase(),
&b.command.to_lowercase(),
proc_widget_state.process_sorting_reverse,
proc_widget_state.is_process_sort_descending,
)
}),
ProcessSorting::Pid => {
@ -586,7 +614,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering(
a.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(
a.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(
a.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(
a.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(
a.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(
&a.process_state.to_lowercase(),
&b.process_state.to_lowercase(),
proc_widget_state.process_sorting_reverse,
proc_widget_state.is_process_sort_descending,
)
}),
ProcessSorting::Count => {
@ -640,7 +668,7 @@ pub fn sort_process_data(
utils::gen_util::get_ordering(
a.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~
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 {
match a_val.partial_cmp(&b_val) {
Some(x) => match x {
Ordering::Greater => {
if reverse_order {
if descending_order {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
}
Ordering::Less => {
if reverse_order {
if descending_order {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Less