From 669b245367d194b7f4f7a12fe29573fcd9ca4e4e Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 18 Nov 2020 01:28:53 -0500 Subject: [PATCH] feature: Add collapsible tree entries (#304) Adds collapsible trees to the tree mode for processes. These can be toggled via the + or - keys and the mouse by clicking on a selected entry. --- .vscode/settings.json | 1 + CHANGELOG.md | 8 + README.md | 9 +- src/app.rs | 129 ++++++++++++---- src/app/states.rs | 2 +- src/canvas.rs | 7 +- src/canvas/widgets/process_table.rs | 2 +- src/clap.rs | 16 +- src/constants.rs | 11 +- src/data_conversion.rs | 223 +++++++++++++++++++--------- src/lib.rs | 19 ++- src/options.rs | 5 +- 12 files changed, 309 insertions(+), 123 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f5f60e4f..0ebd8ca7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,6 +48,7 @@ "cvars", "czvf", "denylist", + "eselect", "fedoracentos", "fpath", "fract", diff --git a/CHANGELOG.md b/CHANGELOG.md index eedb7ed2..8c25a651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - Unreleased + +### Features + +### Changes + +### Bug Fixes + ## [0.5.0] - Unreleased ### Features diff --git a/README.md b/README.md index afe7df74..e3346e4c 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ sudo dnf install bottom ### Gentoo Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq) + ```bash sudo eselect repository enable dm9pZCAq sudo emerge --sync dm9pZCAq @@ -232,7 +233,6 @@ Run using `btm`. --hide_time Completely hides the time scaling. -k, --kelvin Sets the temperature type to Kelvin. -l, --left_legend Puts the CPU chart legend to the left side. - --no_write Disables writing to the config file. -r, --rate Sets a refresh rate in ms. -R, --regex Enables regex by default. -d, --time_delta The amount in ms changed upon zooming. @@ -401,6 +401,12 @@ Note that the `and` operator takes precedence over the `or` operator. | ------ | --------------------------------------------------------------------- | | Scroll | Scrolling over an CPU core/average shows only that entry on the chart | +#### Process bindings + +| | | +| ----- | --------------------------------------------------------------------------------------------------- | +| Click | If in tree mode and you click on a selected entry, it toggles whether the branch is expanded or not | + ## Features As yet _another_ process/system visualization and management application, bottom supports the typical features: @@ -742,6 +748,7 @@ Thanks to all contributors ([emoji key](https://allcontributors.org/docs/en/emoj + ## Thanks diff --git a/src/app.rs b/src/app.rs index d80a206c..9f8fb577 100644 --- a/src/app.rs +++ b/src/app.rs @@ -697,7 +697,7 @@ impl App { .process_search_state .search_state .is_enabled - && proc_widget_state.get_cursor_position() + && proc_widget_state.get_search_cursor_position() < proc_widget_state .process_search_state .search_state @@ -708,13 +708,13 @@ impl App { .process_search_state .search_state .current_search_query - .remove(proc_widget_state.get_cursor_position()); + .remove(proc_widget_state.get_search_cursor_position()); proc_widget_state .process_search_state .search_state .grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_cursor_position(), + proc_widget_state.get_search_cursor_position(), proc_widget_state .process_search_state .search_state @@ -746,21 +746,22 @@ impl App { .process_search_state .search_state .is_enabled - && proc_widget_state.get_cursor_position() > 0 + && proc_widget_state.get_search_cursor_position() > 0 { - proc_widget_state.search_walk_back(proc_widget_state.get_cursor_position()); + proc_widget_state + .search_walk_back(proc_widget_state.get_search_cursor_position()); let removed_char = proc_widget_state .process_search_state .search_state .current_search_query - .remove(proc_widget_state.get_cursor_position()); + .remove(proc_widget_state.get_search_cursor_position()); proc_widget_state .process_search_state .search_state .grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_cursor_position(), + proc_widget_state.get_search_cursor_position(), proc_widget_state .process_search_state .search_state @@ -838,15 +839,15 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id - 1) { if is_in_search_widget { - let prev_cursor = proc_widget_state.get_cursor_position(); + let prev_cursor = proc_widget_state.get_search_cursor_position(); proc_widget_state - .search_walk_back(proc_widget_state.get_cursor_position()); - if proc_widget_state.get_cursor_position() < prev_cursor { + .search_walk_back(proc_widget_state.get_search_cursor_position()); + if proc_widget_state.get_search_cursor_position() < prev_cursor { let str_slice = &proc_widget_state .process_search_state .search_state .current_search_query - [proc_widget_state.get_cursor_position()..prev_cursor]; + [proc_widget_state.get_search_cursor_position()..prev_cursor]; proc_widget_state .process_search_state .search_state @@ -905,15 +906,16 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id - 1) { if is_in_search_widget { - let prev_cursor = proc_widget_state.get_cursor_position(); - proc_widget_state - .search_walk_forward(proc_widget_state.get_cursor_position()); - if proc_widget_state.get_cursor_position() > prev_cursor { + let prev_cursor = proc_widget_state.get_search_cursor_position(); + proc_widget_state.search_walk_forward( + proc_widget_state.get_search_cursor_position(), + ); + if proc_widget_state.get_search_cursor_position() > prev_cursor { let str_slice = &proc_widget_state .process_search_state .search_state .current_search_query - [prev_cursor..proc_widget_state.get_cursor_position()]; + [prev_cursor..proc_widget_state.get_search_cursor_position()]; proc_widget_state .process_search_state .search_state @@ -1124,13 +1126,13 @@ impl App { .process_search_state .search_state .current_search_query - .insert(proc_widget_state.get_cursor_position(), caught_char); + .insert(proc_widget_state.get_search_cursor_position(), caught_char); proc_widget_state .process_search_state .search_state .grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_cursor_position(), + proc_widget_state.get_search_cursor_position(), proc_widget_state .process_search_state .search_state @@ -1139,7 +1141,7 @@ impl App { true, ); proc_widget_state - .search_walk_forward(proc_widget_state.get_cursor_position()); + .search_walk_forward(proc_widget_state.get_search_cursor_position()); proc_widget_state .process_search_state @@ -1371,8 +1373,8 @@ impl App { '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.on_plus(), + '-' => self.on_minus(), '=' => self.reset_zoom(), 'e' => self.toggle_expand_widget(), 's' => self.toggle_sort(), @@ -2058,7 +2060,9 @@ impl App { pub fn decrement_position_count(&mut self) { if !self.ignore_normal_keybinds() { match self.current_widget.widget_type { - BottomWidgetType::Proc => self.increment_process_position(-1), + BottomWidgetType::Proc => { + self.increment_process_position(-1); + } BottomWidgetType::ProcSort => self.increment_process_sort_position(-1), BottomWidgetType::Temp => self.increment_temp_position(-1), BottomWidgetType::Disk => self.increment_disk_position(-1), @@ -2071,7 +2075,9 @@ impl App { pub fn increment_position_count(&mut self) { if !self.ignore_normal_keybinds() { match self.current_widget.widget_type { - BottomWidgetType::Proc => self.increment_process_position(1), + BottomWidgetType::Proc => { + self.increment_process_position(1); + } BottomWidgetType::ProcSort => self.increment_process_sort_position(1), BottomWidgetType::Temp => self.increment_temp_position(1), BottomWidgetType::Disk => self.increment_disk_position(1), @@ -2128,7 +2134,8 @@ impl App { } } - fn increment_process_position(&mut self, num_to_change_by: i64) { + /// Returns the new position. + fn increment_process_position(&mut self, num_to_change_by: i64) -> Option { if let Some(proc_widget_state) = self .proc_state .get_mut_widget_state(self.current_widget.widget_id) @@ -2144,6 +2151,8 @@ impl App { { proc_widget_state.scroll_state.current_scroll_position = (current_posn as i64 + num_to_change_by) as usize; + } else { + return None; } } @@ -2152,7 +2161,11 @@ impl App { } else { proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; } + + return Some(proc_widget_state.scroll_state.current_scroll_position); } + + None } fn increment_temp_position(&mut self, num_to_change_by: i64) { @@ -2245,6 +2258,53 @@ impl App { } } + fn on_plus(&mut self) { + if let BottomWidgetType::Proc = self.current_widget.widget_type { + // Toggle collapsing if tree + self.toggle_collapsing_process_branch(); + } else { + self.zoom_in(); + } + } + + fn on_minus(&mut self) { + if let BottomWidgetType::Proc = self.current_widget.widget_type { + // Toggle collapsing if tree + self.toggle_collapsing_process_branch(); + } else { + self.zoom_out(); + } + } + + fn toggle_collapsing_process_branch(&mut self) { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get_mut(&self.current_widget.widget_id) + { + let current_posn = proc_widget_state.scroll_state.current_scroll_position; + + if let Some(displayed_process_list) = self + .canvas_data + .finalized_process_data_map + .get(&self.current_widget.widget_id) + { + if let Some(corresponding_process) = displayed_process_list.get(current_posn) { + let corresponding_pid = corresponding_process.pid; + + if let Some(process_data) = self + .canvas_data + .single_process_data + .get_mut(&corresponding_pid) + { + process_data.is_collapsed_entry = !process_data.is_collapsed_entry; + self.proc_state.force_update = Some(self.current_widget.widget_id); + } + } + } + } + } + fn zoom_out(&mut self) { match self.current_widget.widget_type { BottomWidgetType::Cpu => { @@ -2464,11 +2524,12 @@ impl App { // Pretty dead simple - iterate through the widget map and go to the widget where the click // is within. - // TODO: [MOUSE] double click functionality...? // TODO: [REFACTOR] might want to refactor this, it's ugly as sin. // TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently everything // is grouped up as an app state. We should separate stuff like event state and gui state and etc. + // TODO: [MOUSE] double click functionality...? We would do this above all other actions and SC if needed. + // Short circuit if we're in basic table... we might have to handle the basic table arrow // case here... if let Some(bt) = &mut self.basic_table_widget_state { @@ -2620,9 +2681,25 @@ impl App { if let Some(visual_index) = proc_widget_state.scroll_state.table_state.selected() { - self.increment_process_position( + // If in tree mode, also check to see if this click is on + // the same entry as the already selected one - if it is, + // then we minimize. + + let previous_scroll_position = + proc_widget_state.scroll_state.current_scroll_position; + let is_tree_mode = proc_widget_state.is_tree_mode; + + let new_position = self.increment_process_position( offset_clicked_entry as i64 - visual_index as i64, ); + + if is_tree_mode { + if let Some(new_position) = new_position { + if previous_scroll_position == new_position { + self.toggle_collapsing_process_branch(); + } + } + } } } } diff --git a/src/app/states.rs b/src/app/states.rs index dea49b18..12037907 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -488,7 +488,7 @@ impl ProcWidgetState { } } - pub fn get_cursor_position(&self) -> usize { + pub fn get_search_cursor_position(&self) -> usize { self.process_search_state .search_state .grapheme_cursor diff --git a/src/canvas.rs b/src/canvas.rs index 626f4f7f..7af29f33 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -25,6 +25,7 @@ use crate::{ options::Config, utils::error, utils::error::BottomError, + Pid, }; mod canvas_colours; @@ -46,9 +47,9 @@ pub struct DisplayableData { pub network_data_tx: Vec, pub disk_data: Vec>, pub temp_sensor_data: Vec>, - pub single_process_data: Vec, // Contains single process data - pub finalized_process_data_map: HashMap>, // What's actually displayed - pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled + pub single_process_data: HashMap, // Contains single process data, key is PID + pub finalized_process_data_map: HashMap>, // What's actually displayed, key is the widget ID. + pub stringified_process_data_map: HashMap)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID pub mem_label_percent: String, pub swap_label_percent: String, pub mem_label_frac: String, diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index 0ee25b1f..22e54c16 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -506,7 +506,7 @@ impl ProcessTableWidget for Painter { let search_title = "> "; let num_chars_for_text = search_title.len(); - let cursor_position = proc_widget_state.get_cursor_position(); + let cursor_position = proc_widget_state.get_search_cursor_position(); let current_cursor_position = proc_widget_state.get_char_cursor_position(); let start_position: usize = get_search_start_position( diff --git a/src/clap.rs b/src/clap.rs index 5f6d7667..70972d62 100644 --- a/src/clap.rs +++ b/src/clap.rs @@ -143,13 +143,13 @@ Completely hides the time scaling from being shown.\n\n", "\ Puts the CPU chart legend to the left side rather than the right side.\n\n", ); - let no_write = Arg::with_name("no_write") - .long("no_write") - .help("Disables writing to the config file.") - .long_help( - "\ -Disables config changes in-app from writing to the config file.", - ); + // let no_write = Arg::with_name("no_write") + // .long("no_write") + // .help("Disables writing to the config file.") + // .long_help( + // "\ + // Disables config changes in-app from writing to the config file.", + // ); let regex = Arg::with_name("regex") .short("R") .long("regex") @@ -355,7 +355,7 @@ The minimum is 1s (1000), and defaults to 15s (15000).\n\n\n", .arg(hide_table_gap) .arg(hide_time) .arg(left_legend) - .arg(no_write) + // .arg(no_write) .arg(rate) .arg(regex) .arg(time_delta) diff --git a/src/constants.rs b/src/constants.rs index 91e9419c..0bccec77 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -158,7 +158,6 @@ lazy_static! { // }; } -// FIXME: [HELP] I wanna update this before release... it's missing mouse too. // Help text pub const HELP_CONTENTS_TEXT: [&str; 8] = [ "Press the corresponding numbers to jump to the section, or scroll:", @@ -171,7 +170,9 @@ pub const HELP_CONTENTS_TEXT: [&str; 8] = [ "7 - Basic memory widget", ]; -pub const GENERAL_HELP_TEXT: [&str; 29] = [ +// TODO [Help]: Search in help? +// TODO [Help]: Move to using tables for easier formatting? +pub const GENERAL_HELP_TEXT: [&str; 30] = [ "1 - General", "q, Ctrl-c Quit", "Esc Close dialog windows, search, widgets, or exit expanded mode", @@ -201,6 +202,7 @@ pub const GENERAL_HELP_TEXT: [&str; 29] = [ "- Zoom out on chart (increase time range)", "= Reset zoom", "Mouse scroll Scroll through the tables or zoom in/out of charts by scrolling up/down", + "Mouse click Selects the clicked widget, table entry, dialog option, or tab", ]; pub const CPU_HELP_TEXT: [&str; 2] = [ @@ -208,9 +210,7 @@ pub const CPU_HELP_TEXT: [&str; 2] = [ "Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart", ]; -// TODO [Help]: Search in help? -// TODO [Help]: Move to using tables for easier formatting? -pub const PROCESS_HELP_TEXT: [&str; 13] = [ +pub const PROCESS_HELP_TEXT: [&str; 14] = [ "3 - Process widget", "dd Kill the selected process", "c Sort by CPU usage, press again to reverse sorting order", @@ -224,6 +224,7 @@ pub const PROCESS_HELP_TEXT: [&str; 13] = [ "I Invert current sort", "% Toggle between values and percentages for memory usage", "t, F5 Toggle tree mode", + "+, -, click Collapse/expand a branch while in tree mode", ]; pub const SEARCH_HELP_TEXT: [&str; 46] = [ diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 3b83d935..5bf31edf 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -7,7 +7,7 @@ use crate::{ }; use data_harvester::processes::ProcessSorting; use indexmap::IndexSet; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; /// Point is of time, data type Point = (f64, f64); @@ -66,6 +66,8 @@ pub struct ConvertedProcessData { pub process_description_prefix: Option, /// Whether to mark this process entry as disabled (mostly for tree mode). pub is_disabled_entry: bool, + /// Whether this entry is collapsed, hiding all its children (for tree mode). + pub is_collapsed_entry: bool, } #[derive(Clone, Default, Debug)] @@ -194,19 +196,17 @@ pub fn convert_cpu_data_points( for (itx, cpu) in data.cpu_data.iter().enumerate() { // Check if the vector exists yet if cpu_data_vector.len() <= itx { - let mut new_cpu_data = ConvertedCpuData::default(); - new_cpu_data.cpu_name = if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) - { - if let Some(cpu_count) = cpu_harvest.cpu_count { - format!("{}{}", cpu_harvest.cpu_prefix, cpu_count) + let new_cpu_data = ConvertedCpuData { + cpu_name: if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) { + if let Some(cpu_count) = cpu_harvest.cpu_count { + format!("{}{}", cpu_harvest.cpu_prefix, cpu_count) + } else { + cpu_harvest.cpu_prefix.to_string() + } } else { - cpu_harvest.cpu_prefix.to_string() - } - } else { - String::default() - }; - new_cpu_data.short_cpu_name = - if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) { + String::default() + }, + short_cpu_name: if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) { if let Some(cpu_count) = cpu_harvest.cpu_count { cpu_count.to_string() } else { @@ -214,7 +214,10 @@ pub fn convert_cpu_data_points( } } else { String::default() - }; + }, + ..ConvertedCpuData::default() + }; + cpu_data_vector.push(new_cpu_data); } @@ -426,55 +429,120 @@ pub enum ProcessNamingType { Path, } +/// Because we needed to UPDATE data entries rather than REPLACING entries, we instead update +/// the existing vector. pub fn convert_process_data( current_data: &data_farmer::DataCollection, -) -> Vec { + existing_converted_process_data: &mut HashMap, +) { // TODO [THREAD]: Thread highlighting and hiding support // For macOS see https://github.com/hishamhm/htop/pull/848/files - current_data - .process_harvest - .iter() - .map(|process| { - let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false); - let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false); - let converted_total_read = get_exact_byte_values(process.total_read_bytes, false); - let converted_total_write = get_exact_byte_values(process.total_write_bytes, false); + let mut complete_pid_set: HashSet = + existing_converted_process_data.keys().copied().collect(); - let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); - let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); - let total_read = format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1); - let total_write = format!( - "{:.*}{}", - 0, converted_total_write.0, converted_total_write.1 - ); + for process in ¤t_data.process_harvest { + let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false); + let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false); + let converted_total_read = get_exact_byte_values(process.total_read_bytes, false); + let converted_total_write = get_exact_byte_values(process.total_write_bytes, false); - 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, - mem_percent_usage: process.mem_usage_percent, - mem_usage_bytes: process.mem_usage_bytes, - mem_usage_str: get_exact_byte_values(process.mem_usage_bytes, false), - group_pids: vec![process.pid], - read_per_sec, - write_per_sec, - total_read, - total_write, - rps_f64: process.read_bytes_per_sec as f64, - wps_f64: process.write_bytes_per_sec as f64, - tr_f64: process.total_read_bytes as f64, - tw_f64: process.total_write_bytes as f64, - process_state: process.process_state.to_owned(), - process_char: process.process_state_char, - process_description_prefix: None, - is_disabled_entry: false, + let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); + let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); + let total_read = format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1); + let total_write = format!( + "{:.*}{}", + 0, converted_total_write.0, converted_total_write.1 + ); + + if let Some(process_entry) = existing_converted_process_data.get_mut(&process.pid) { + complete_pid_set.remove(&process.pid); + + // Very dumb way to see if there's PID reuse... + if process_entry.ppid == process.parent_pid { + process_entry.name = process.name.to_string(); + process_entry.command = process.command.to_string(); + process_entry.cpu_percent_usage = process.cpu_usage_percent; + process_entry.mem_percent_usage = process.mem_usage_percent; + process_entry.mem_usage_bytes = process.mem_usage_bytes; + process_entry.mem_usage_str = get_exact_byte_values(process.mem_usage_bytes, false); + process_entry.group_pids = vec![process.pid]; + process_entry.read_per_sec = read_per_sec; + process_entry.write_per_sec = write_per_sec; + process_entry.total_read = total_read; + process_entry.total_write = total_write; + process_entry.rps_f64 = process.read_bytes_per_sec as f64; + process_entry.wps_f64 = process.write_bytes_per_sec as f64; + process_entry.tr_f64 = process.total_read_bytes as f64; + process_entry.tw_f64 = process.total_write_bytes as f64; + process_entry.process_state = process.process_state.to_owned(); + process_entry.process_char = process.process_state_char; + process_entry.process_description_prefix = None; + process_entry.is_disabled_entry = false; + } else { + // ...I hate that I can't combine if let and an if statement in one line... + *process_entry = 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, + mem_percent_usage: process.mem_usage_percent, + mem_usage_bytes: process.mem_usage_bytes, + mem_usage_str: get_exact_byte_values(process.mem_usage_bytes, false), + group_pids: vec![process.pid], + read_per_sec, + write_per_sec, + total_read, + total_write, + rps_f64: process.read_bytes_per_sec as f64, + wps_f64: process.write_bytes_per_sec as f64, + tr_f64: process.total_read_bytes as f64, + tw_f64: process.total_write_bytes as f64, + process_state: process.process_state.to_owned(), + process_char: process.process_state_char, + process_description_prefix: None, + is_disabled_entry: false, + is_collapsed_entry: false, + }; } - }) - .collect::>() + } else { + existing_converted_process_data.insert( + process.pid, + 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, + mem_percent_usage: process.mem_usage_percent, + mem_usage_bytes: process.mem_usage_bytes, + mem_usage_str: get_exact_byte_values(process.mem_usage_bytes, false), + group_pids: vec![process.pid], + read_per_sec, + write_per_sec, + total_read, + total_write, + rps_f64: process.read_bytes_per_sec as f64, + wps_f64: process.write_bytes_per_sec as f64, + tr_f64: process.total_read_bytes as f64, + tw_f64: process.total_write_bytes as f64, + process_state: process.process_state.to_owned(), + process_char: process.process_state_char, + process_description_prefix: None, + is_disabled_entry: false, + is_collapsed_entry: false, + }, + ); + } + } + + // Now clean up any spare entries that weren't visited, to avoid clutter: + complete_pid_set.iter().for_each(|pid| { + existing_converted_process_data.remove(pid); + }) } const BRANCH_ENDING: char = '└'; @@ -483,32 +551,36 @@ 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, + filtered_process_data: &[ConvertedProcessData], is_using_command: bool, + sorting_type: &ProcessSorting, is_sort_descending: bool, ) -> Vec { - // FIXME: [TREE] Allow for collapsing entries. // TODO: [TREE] Option to sort usage by total branch usage or individual value usage? // 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> = HashMap::default(); - let mut pid_process_mapping: HashMap = HashMap::default(); + let mut pid_process_mapping: HashMap = HashMap::default(); // We actually already have this stored, but it's unfiltered... oh well. let mut orphan_set: IndexSet = IndexSet::new(); + let mut collapsed_set: IndexSet = IndexSet::new(); - single_process_data.iter().for_each(|process| { + filtered_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| { + filtered_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); + if process.is_collapsed_entry { + collapsed_set.insert(process.pid); + } + // Insert its mapping to the process' parent if needed (create if it DNE). if let Some(ppid) = process.ppid { orphan_set.remove(&process.pid); @@ -521,8 +593,8 @@ pub fn tree_process_data( // 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| { + let old_orphan_set = orphan_set.clone(); + old_orphan_set.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); @@ -717,12 +789,14 @@ pub fn tree_process_data( /// the correct order to the PID tree as a vector. fn build_explored_pids( current_pid: Pid, parent_child_mapping: &HashMap>, - prev_drawn_lines: &str, + prev_drawn_lines: &str, collapsed_set: &IndexSet, ) -> (Vec, Vec) { let mut explored_pids: Vec = vec![current_pid]; let mut lines: Vec = vec![]; - if let Some(children) = parent_child_mapping.get(¤t_pid) { + if collapsed_set.contains(¤t_pid) { + return (explored_pids, lines); + } else if let Some(children) = parent_child_mapping.get(¤t_pid) { for (itx, child) in children.iter().rev().enumerate() { let new_drawn_lines = if itx == children.len() - 1 { format!("{} ", prev_drawn_lines) @@ -730,8 +804,12 @@ pub fn tree_process_data( format!("{}{} ", prev_drawn_lines, BRANCH_VERTICAL) }; - let (pid_res, branch_res) = - build_explored_pids(*child, parent_child_mapping, new_drawn_lines.as_str()); + let (pid_res, branch_res) = build_explored_pids( + *child, + parent_child_mapping, + new_drawn_lines.as_str(), + collapsed_set, + ); if itx == children.len() - 1 { lines.push(format!( @@ -769,20 +847,21 @@ pub fn tree_process_data( to_sort_vec.push((pid, *process)); } } - sort_vec(&mut to_sort_vec, sort_type, is_sort_descending); + sort_vec(&mut to_sort_vec, sorting_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, + sorting_type, is_sort_descending, &mut parent_child_mapping, &pid_process_mapping, ); - let (pid_res, branch_res) = build_explored_pids(current_pid, &parent_child_mapping, ""); + let (pid_res, branch_res) = + build_explored_pids(current_pid, &parent_child_mapping, "", &collapsed_set); lines.push(String::default()); lines.extend(branch_res); explored_pids.extend(pid_res); @@ -798,8 +877,9 @@ pub fn tree_process_data( Some(process) => { let mut p = process.clone(); p.process_description_prefix = Some(format!( - "{}{}", + "{}{}{}", prefix, + if p.is_collapsed_entry { "+ " } else { "" }, // I do the + sign thing here because I'm kinda too lazy to do it in the prefix, tbh. if is_using_command { &p.command } else { @@ -953,6 +1033,7 @@ pub fn group_process_data( process_description_prefix: None, process_char: char::default(), is_disabled_entry: false, + is_collapsed_entry: false, } }) .collect::>() diff --git a/src/lib.rs b/src/lib.rs index 443328de..c7a2a807 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -361,14 +361,17 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { 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); + convert_process_data( + &app.data_collection, + &mut app.canvas_data.single_process_data, + ); } let process_filter = app.get_process_filter(widget_id); let filtered_process_data: Vec = if is_tree { app.canvas_data .single_process_data .iter() - .map(|process| { + .map(|(_pid, process)| { let mut process_clone = process.clone(); if !is_invalid_or_blank { if let Some(process_filter) = process_filter { @@ -383,15 +386,19 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { app.canvas_data .single_process_data .iter() - .filter(|process| { + .filter_map(|(_pid, process)| { if !is_invalid_or_blank { if let Some(process_filter) = process_filter { - process_filter.check(&process, is_using_command) + if process_filter.check(&process, is_using_command) { + Some(process) + } else { + None + } } else { - true + Some(process) } } else { - true + Some(process) } }) .cloned() diff --git a/src/options.rs b/src/options.rs index 07be698f..9c1e6215 100644 --- a/src/options.rs +++ b/src/options.rs @@ -115,6 +115,7 @@ pub struct ConfigFlags { #[builder(default, setter(strip_option))] pub no_write: Option, + // For built-in colour palettes. #[builder(default, setter(strip_option))] pub color: Option, @@ -362,7 +363,8 @@ pub fn build_app( 1 }, disable_click: get_disable_click(matches, config), - no_write: get_no_write(matches, config), + // no_write: get_no_write(matches, config), + no_write: false, }; let used_widgets = UsedWidgets { @@ -845,6 +847,7 @@ fn get_use_battery(matches: &clap::ArgMatches<'static>, config: &Config) -> bool false } +#[allow(dead_code)] fn get_no_write(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { if matches.is_present("no_write") { return true;