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.
This commit is contained in:
Clement Tsang 2020-11-18 01:28:53 -05:00 committed by GitHub
parent e43456207b
commit 669b245367
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 309 additions and 123 deletions

View File

@ -48,6 +48,7 @@
"cvars", "cvars",
"czvf", "czvf",
"denylist", "denylist",
"eselect",
"fedoracentos", "fedoracentos",
"fpath", "fpath",
"fract", "fract",

View File

@ -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/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.1] - Unreleased
### Features
### Changes
### Bug Fixes
## [0.5.0] - Unreleased ## [0.5.0] - Unreleased
### Features ### Features

View File

@ -137,6 +137,7 @@ sudo dnf install bottom
### Gentoo ### Gentoo
Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq) Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)
```bash ```bash
sudo eselect repository enable dm9pZCAq sudo eselect repository enable dm9pZCAq
sudo emerge --sync dm9pZCAq sudo emerge --sync dm9pZCAq
@ -232,7 +233,6 @@ Run using `btm`.
--hide_time Completely hides the time scaling. --hide_time Completely hides the time scaling.
-k, --kelvin Sets the temperature type to Kelvin. -k, --kelvin Sets the temperature type to Kelvin.
-l, --left_legend Puts the CPU chart legend to the left side. -l, --left_legend Puts the CPU chart legend to the left side.
--no_write Disables writing to the config file.
-r, --rate <MS> Sets a refresh rate in ms. -r, --rate <MS> Sets a refresh rate in ms.
-R, --regex Enables regex by default. -R, --regex Enables regex by default.
-d, --time_delta <MS> The amount in ms changed upon zooming. -d, --time_delta <MS> 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 | | 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 ## Features
As yet _another_ process/system visualization and management application, bottom supports the typical 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
<!-- markdownlint-enable --> <!-- markdownlint-enable -->
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
## Thanks ## Thanks

View File

@ -697,7 +697,7 @@ impl App {
.process_search_state .process_search_state
.search_state .search_state
.is_enabled .is_enabled
&& proc_widget_state.get_cursor_position() && proc_widget_state.get_search_cursor_position()
< proc_widget_state < proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
@ -708,13 +708,13 @@ impl App {
.process_search_state .process_search_state
.search_state .search_state
.current_search_query .current_search_query
.remove(proc_widget_state.get_cursor_position()); .remove(proc_widget_state.get_search_cursor_position());
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.grapheme_cursor = GraphemeCursor::new( .grapheme_cursor = GraphemeCursor::new(
proc_widget_state.get_cursor_position(), proc_widget_state.get_search_cursor_position(),
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
@ -746,21 +746,22 @@ impl App {
.process_search_state .process_search_state
.search_state .search_state
.is_enabled .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 let removed_char = proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.current_search_query .current_search_query
.remove(proc_widget_state.get_cursor_position()); .remove(proc_widget_state.get_search_cursor_position());
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.grapheme_cursor = GraphemeCursor::new( .grapheme_cursor = GraphemeCursor::new(
proc_widget_state.get_cursor_position(), proc_widget_state.get_search_cursor_position(),
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
@ -838,15 +839,15 @@ impl App {
.get_mut_widget_state(self.current_widget.widget_id - 1) .get_mut_widget_state(self.current_widget.widget_id - 1)
{ {
if is_in_search_widget { 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 proc_widget_state
.search_walk_back(proc_widget_state.get_cursor_position()); .search_walk_back(proc_widget_state.get_search_cursor_position());
if proc_widget_state.get_cursor_position() < prev_cursor { if proc_widget_state.get_search_cursor_position() < prev_cursor {
let str_slice = &proc_widget_state let str_slice = &proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.current_search_query .current_search_query
[proc_widget_state.get_cursor_position()..prev_cursor]; [proc_widget_state.get_search_cursor_position()..prev_cursor];
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
@ -905,15 +906,16 @@ impl App {
.get_mut_widget_state(self.current_widget.widget_id - 1) .get_mut_widget_state(self.current_widget.widget_id - 1)
{ {
if is_in_search_widget { 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 proc_widget_state.search_walk_forward(
.search_walk_forward(proc_widget_state.get_cursor_position()); proc_widget_state.get_search_cursor_position(),
if proc_widget_state.get_cursor_position() > prev_cursor { );
if proc_widget_state.get_search_cursor_position() > prev_cursor {
let str_slice = &proc_widget_state let str_slice = &proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.current_search_query .current_search_query
[prev_cursor..proc_widget_state.get_cursor_position()]; [prev_cursor..proc_widget_state.get_search_cursor_position()];
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
@ -1124,13 +1126,13 @@ impl App {
.process_search_state .process_search_state
.search_state .search_state
.current_search_query .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 proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
.grapheme_cursor = GraphemeCursor::new( .grapheme_cursor = GraphemeCursor::new(
proc_widget_state.get_cursor_position(), proc_widget_state.get_search_cursor_position(),
proc_widget_state proc_widget_state
.process_search_state .process_search_state
.search_state .search_state
@ -1139,7 +1141,7 @@ impl App {
true, true,
); );
proc_widget_state 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 proc_widget_state
.process_search_state .process_search_state
@ -1371,8 +1373,8 @@ impl App {
'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(), 't' => self.toggle_tree_mode(),
'+' => self.zoom_in(), '+' => self.on_plus(),
'-' => self.zoom_out(), '-' => self.on_minus(),
'=' => self.reset_zoom(), '=' => self.reset_zoom(),
'e' => self.toggle_expand_widget(), 'e' => self.toggle_expand_widget(),
's' => self.toggle_sort(), 's' => self.toggle_sort(),
@ -2058,7 +2060,9 @@ impl App {
pub fn decrement_position_count(&mut self) { pub fn decrement_position_count(&mut self) {
if !self.ignore_normal_keybinds() { if !self.ignore_normal_keybinds() {
match self.current_widget.widget_type { 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::ProcSort => self.increment_process_sort_position(-1),
BottomWidgetType::Temp => self.increment_temp_position(-1), BottomWidgetType::Temp => self.increment_temp_position(-1),
BottomWidgetType::Disk => self.increment_disk_position(-1), BottomWidgetType::Disk => self.increment_disk_position(-1),
@ -2071,7 +2075,9 @@ impl App {
pub fn increment_position_count(&mut self) { pub fn increment_position_count(&mut self) {
if !self.ignore_normal_keybinds() { if !self.ignore_normal_keybinds() {
match self.current_widget.widget_type { 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::ProcSort => self.increment_process_sort_position(1),
BottomWidgetType::Temp => self.increment_temp_position(1), BottomWidgetType::Temp => self.increment_temp_position(1),
BottomWidgetType::Disk => self.increment_disk_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<usize> {
if let Some(proc_widget_state) = self if let Some(proc_widget_state) = self
.proc_state .proc_state
.get_mut_widget_state(self.current_widget.widget_id) .get_mut_widget_state(self.current_widget.widget_id)
@ -2144,6 +2151,8 @@ impl App {
{ {
proc_widget_state.scroll_state.current_scroll_position = proc_widget_state.scroll_state.current_scroll_position =
(current_posn as i64 + num_to_change_by) as usize; (current_posn as i64 + num_to_change_by) as usize;
} else {
return None;
} }
} }
@ -2152,7 +2161,11 @@ impl App {
} else { } else {
proc_widget_state.scroll_state.scroll_direction = ScrollDirection::Down; 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) { 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) { fn zoom_out(&mut self) {
match self.current_widget.widget_type { match self.current_widget.widget_type {
BottomWidgetType::Cpu => { BottomWidgetType::Cpu => {
@ -2464,11 +2524,12 @@ impl App {
// Pretty dead simple - iterate through the widget map and go to the widget where the click // Pretty dead simple - iterate through the widget map and go to the widget where the click
// is within. // is within.
// TODO: [MOUSE] double click functionality...?
// TODO: [REFACTOR] might want to refactor this, it's ugly as sin. // TODO: [REFACTOR] might want to refactor this, it's ugly as sin.
// TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently everything // 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. // 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 // Short circuit if we're in basic table... we might have to handle the basic table arrow
// case here... // case here...
if let Some(bt) = &mut self.basic_table_widget_state { if let Some(bt) = &mut self.basic_table_widget_state {
@ -2620,9 +2681,25 @@ impl App {
if let Some(visual_index) = if let Some(visual_index) =
proc_widget_state.scroll_state.table_state.selected() 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, 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();
}
}
}
} }
} }
} }

View File

@ -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 self.process_search_state
.search_state .search_state
.grapheme_cursor .grapheme_cursor

View File

@ -25,6 +25,7 @@ use crate::{
options::Config, options::Config,
utils::error, utils::error,
utils::error::BottomError, utils::error::BottomError,
Pid,
}; };
mod canvas_colours; mod canvas_colours;
@ -46,9 +47,9 @@ pub struct DisplayableData {
pub network_data_tx: Vec<Point>, pub network_data_tx: Vec<Point>,
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: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
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, key is the widget ID.
pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
pub mem_label_percent: String, pub mem_label_percent: String,
pub swap_label_percent: String, pub swap_label_percent: String,
pub mem_label_frac: String, pub mem_label_frac: String,

View File

@ -506,7 +506,7 @@ impl ProcessTableWidget for Painter {
let search_title = "> "; let search_title = "> ";
let num_chars_for_text = search_title.len(); 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 current_cursor_position = proc_widget_state.get_char_cursor_position();
let start_position: usize = get_search_start_position( let start_position: usize = get_search_start_position(

View File

@ -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", Puts the CPU chart legend to the left side rather than the right side.\n\n",
); );
let no_write = Arg::with_name("no_write") // let no_write = Arg::with_name("no_write")
.long("no_write") // .long("no_write")
.help("Disables writing to the config file.") // .help("Disables writing to the config file.")
.long_help( // .long_help(
"\ // "\
Disables config changes in-app from writing to the config file.", // Disables config changes in-app from writing to the config file.",
); // );
let regex = Arg::with_name("regex") let regex = Arg::with_name("regex")
.short("R") .short("R")
.long("regex") .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_table_gap)
.arg(hide_time) .arg(hide_time)
.arg(left_legend) .arg(left_legend)
.arg(no_write) // .arg(no_write)
.arg(rate) .arg(rate)
.arg(regex) .arg(regex)
.arg(time_delta) .arg(time_delta)

View File

@ -158,7 +158,6 @@ lazy_static! {
// }; // };
} }
// FIXME: [HELP] I wanna update this before release... it's missing mouse too.
// Help text // Help text
pub const HELP_CONTENTS_TEXT: [&str; 8] = [ pub const HELP_CONTENTS_TEXT: [&str; 8] = [
"Press the corresponding numbers to jump to the section, or scroll:", "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", "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", "1 - General",
"q, Ctrl-c Quit", "q, Ctrl-c Quit",
"Esc Close dialog windows, search, widgets, or exit expanded mode", "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)", "- Zoom out on chart (increase time range)",
"= Reset zoom", "= Reset zoom",
"Mouse scroll Scroll through the tables or zoom in/out of charts by scrolling up/down", "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] = [ 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", "Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart",
]; ];
// TODO [Help]: Search in help? pub const PROCESS_HELP_TEXT: [&str; 14] = [
// TODO [Help]: Move to using tables for easier formatting?
pub const PROCESS_HELP_TEXT: [&str; 13] = [
"3 - Process widget", "3 - Process widget",
"dd Kill the selected process", "dd Kill the selected process",
"c Sort by CPU usage, press again to reverse sorting order", "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", "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", "t, F5 Toggle tree mode",
"+, -, click Collapse/expand a branch while in tree mode",
]; ];
pub const SEARCH_HELP_TEXT: [&str; 46] = [ pub const SEARCH_HELP_TEXT: [&str; 46] = [

View File

@ -7,7 +7,7 @@ use crate::{
}; };
use data_harvester::processes::ProcessSorting; use data_harvester::processes::ProcessSorting;
use indexmap::IndexSet; use indexmap::IndexSet;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
/// Point is of time, data /// Point is of time, data
type Point = (f64, f64); type Point = (f64, f64);
@ -66,6 +66,8 @@ pub struct ConvertedProcessData {
pub process_description_prefix: Option<String>, pub process_description_prefix: Option<String>,
/// Whether to mark this process entry as disabled (mostly for tree mode). /// Whether to mark this process entry as disabled (mostly for tree mode).
pub is_disabled_entry: bool, 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)] #[derive(Clone, Default, Debug)]
@ -194,19 +196,17 @@ pub fn convert_cpu_data_points(
for (itx, cpu) in data.cpu_data.iter().enumerate() { for (itx, cpu) in data.cpu_data.iter().enumerate() {
// Check if the vector exists yet // Check if the vector exists yet
if cpu_data_vector.len() <= itx { if cpu_data_vector.len() <= itx {
let mut new_cpu_data = ConvertedCpuData::default(); let new_cpu_data = ConvertedCpuData {
new_cpu_data.cpu_name = if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) cpu_name: if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) {
{ if let Some(cpu_count) = cpu_harvest.cpu_count {
if let Some(cpu_count) = cpu_harvest.cpu_count { format!("{}{}", cpu_harvest.cpu_prefix, cpu_count)
format!("{}{}", cpu_harvest.cpu_prefix, cpu_count) } else {
cpu_harvest.cpu_prefix.to_string()
}
} else { } else {
cpu_harvest.cpu_prefix.to_string() String::default()
} },
} else { short_cpu_name: if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) {
String::default()
};
new_cpu_data.short_cpu_name =
if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) {
if let Some(cpu_count) = cpu_harvest.cpu_count { if let Some(cpu_count) = cpu_harvest.cpu_count {
cpu_count.to_string() cpu_count.to_string()
} else { } else {
@ -214,7 +214,10 @@ pub fn convert_cpu_data_points(
} }
} else { } else {
String::default() String::default()
}; },
..ConvertedCpuData::default()
};
cpu_data_vector.push(new_cpu_data); cpu_data_vector.push(new_cpu_data);
} }
@ -426,55 +429,120 @@ pub enum ProcessNamingType {
Path, Path,
} }
/// Because we needed to UPDATE data entries rather than REPLACING entries, we instead update
/// the existing vector.
pub fn convert_process_data( pub fn convert_process_data(
current_data: &data_farmer::DataCollection, current_data: &data_farmer::DataCollection,
) -> Vec<ConvertedProcessData> { existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
) {
// TODO [THREAD]: Thread highlighting and hiding support // TODO [THREAD]: Thread highlighting and hiding support
// For macOS see https://github.com/hishamhm/htop/pull/848/files // For macOS see https://github.com/hishamhm/htop/pull/848/files
current_data let mut complete_pid_set: HashSet<Pid> =
.process_harvest existing_converted_process_data.keys().copied().collect();
.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 read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); for process in &current_data.process_harvest {
let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false);
let total_read = format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1); let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false);
let total_write = format!( 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);
0, converted_total_write.0, converted_total_write.1
);
ConvertedProcessData { let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1);
pid: process.pid, let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1);
ppid: process.parent_pid, let total_read = format!("{:.*}{}", 0, converted_total_read.0, converted_total_read.1);
is_thread: None, let total_write = format!(
name: process.name.to_string(), "{:.*}{}",
command: process.command.to_string(), 0, converted_total_write.0, converted_total_write.1
cpu_percent_usage: process.cpu_usage_percent, );
mem_percent_usage: process.mem_usage_percent,
mem_usage_bytes: process.mem_usage_bytes, if let Some(process_entry) = existing_converted_process_data.get_mut(&process.pid) {
mem_usage_str: get_exact_byte_values(process.mem_usage_bytes, false), complete_pid_set.remove(&process.pid);
group_pids: vec![process.pid],
read_per_sec, // Very dumb way to see if there's PID reuse...
write_per_sec, if process_entry.ppid == process.parent_pid {
total_read, process_entry.name = process.name.to_string();
total_write, process_entry.command = process.command.to_string();
rps_f64: process.read_bytes_per_sec as f64, process_entry.cpu_percent_usage = process.cpu_usage_percent;
wps_f64: process.write_bytes_per_sec as f64, process_entry.mem_percent_usage = process.mem_usage_percent;
tr_f64: process.total_read_bytes as f64, process_entry.mem_usage_bytes = process.mem_usage_bytes;
tw_f64: process.total_write_bytes as f64, process_entry.mem_usage_str = get_exact_byte_values(process.mem_usage_bytes, false);
process_state: process.process_state.to_owned(), process_entry.group_pids = vec![process.pid];
process_char: process.process_state_char, process_entry.read_per_sec = read_per_sec;
process_description_prefix: None, process_entry.write_per_sec = write_per_sec;
is_disabled_entry: false, 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,
};
} }
}) } else {
.collect::<Vec<_>>() 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 = '└'; const BRANCH_ENDING: char = '└';
@ -483,32 +551,36 @@ const BRANCH_SPLIT: char = '├';
const BRANCH_HORIZONTAL: char = '─'; const BRANCH_HORIZONTAL: char = '─';
pub fn tree_process_data( pub fn tree_process_data(
single_process_data: &[ConvertedProcessData], is_using_command: bool, filtered_process_data: &[ConvertedProcessData], is_using_command: bool,
sort_type: &ProcessSorting, is_sort_descending: bool, sorting_type: &ProcessSorting, is_sort_descending: bool,
) -> Vec<ConvertedProcessData> { ) -> Vec<ConvertedProcessData> {
// FIXME: [TREE] Allow for collapsing entries.
// TODO: [TREE] Option to sort usage by total branch usage or individual value usage? // 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... // Let's first build up a (really terrible) parent -> child mapping...
// At the same time, let's make a mapping of PID -> process data! // 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 parent_child_mapping: HashMap<Pid, IndexSet<Pid>> = HashMap::default();
let mut pid_process_mapping: HashMap<Pid, &ConvertedProcessData> = HashMap::default(); let mut pid_process_mapping: HashMap<Pid, &ConvertedProcessData> = HashMap::default(); // We actually already have this stored, but it's unfiltered... oh well.
let mut orphan_set: IndexSet<Pid> = IndexSet::new(); let mut orphan_set: IndexSet<Pid> = IndexSet::new();
let mut collapsed_set: IndexSet<Pid> = IndexSet::new();
single_process_data.iter().for_each(|process| { filtered_process_data.iter().for_each(|process| {
if let Some(ppid) = process.ppid { if let Some(ppid) = process.ppid {
orphan_set.insert(ppid); orphan_set.insert(ppid);
} }
orphan_set.insert(process.pid); 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. // Create a mapping for the process if it DNE.
parent_child_mapping parent_child_mapping
.entry(process.pid) .entry(process.pid)
.or_insert_with(IndexSet::new); .or_insert_with(IndexSet::new);
pid_process_mapping.insert(process.pid, process); 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). // Insert its mapping to the process' parent if needed (create if it DNE).
if let Some(ppid) = process.ppid { if let Some(ppid) = process.ppid {
orphan_set.remove(&process.pid); 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 // Keep only orphans, or promote children of orphans to a top-level orphan
// if their parents DNE in our pid to process mapping... // if their parents DNE in our pid to process mapping...
#[allow(clippy::redundant_clone)] let old_orphan_set = orphan_set.clone();
orphan_set.clone().iter().for_each(|pid| { old_orphan_set.iter().for_each(|pid| {
if pid_process_mapping.get(pid).is_none() { if pid_process_mapping.get(pid).is_none() {
// DNE! Promote the mapped children and remove the current parent... // DNE! Promote the mapped children and remove the current parent...
orphan_set.remove(pid); orphan_set.remove(pid);
@ -717,12 +789,14 @@ pub fn tree_process_data(
/// the correct order to the PID tree as a vector. /// the correct order to the PID tree as a vector.
fn build_explored_pids( fn build_explored_pids(
current_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid>>, current_pid: Pid, parent_child_mapping: &HashMap<Pid, IndexSet<Pid>>,
prev_drawn_lines: &str, prev_drawn_lines: &str, collapsed_set: &IndexSet<Pid>,
) -> (Vec<Pid>, Vec<String>) { ) -> (Vec<Pid>, Vec<String>) {
let mut explored_pids: Vec<Pid> = vec![current_pid]; let mut explored_pids: Vec<Pid> = vec![current_pid];
let mut lines: Vec<String> = vec![]; let mut lines: Vec<String> = vec![];
if let Some(children) = parent_child_mapping.get(&current_pid) { if collapsed_set.contains(&current_pid) {
return (explored_pids, lines);
} else if let Some(children) = parent_child_mapping.get(&current_pid) {
for (itx, child) in children.iter().rev().enumerate() { for (itx, child) in children.iter().rev().enumerate() {
let new_drawn_lines = if itx == children.len() - 1 { let new_drawn_lines = if itx == children.len() - 1 {
format!("{} ", prev_drawn_lines) format!("{} ", prev_drawn_lines)
@ -730,8 +804,12 @@ pub fn tree_process_data(
format!("{}{} ", prev_drawn_lines, BRANCH_VERTICAL) format!("{}{} ", prev_drawn_lines, BRANCH_VERTICAL)
}; };
let (pid_res, branch_res) = let (pid_res, branch_res) = build_explored_pids(
build_explored_pids(*child, parent_child_mapping, new_drawn_lines.as_str()); *child,
parent_child_mapping,
new_drawn_lines.as_str(),
collapsed_set,
);
if itx == children.len() - 1 { if itx == children.len() - 1 {
lines.push(format!( lines.push(format!(
@ -769,20 +847,21 @@ pub fn tree_process_data(
to_sort_vec.push((pid, *process)); 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(); pids_to_explore = to_sort_vec.iter().map(|(pid, _proc)| *pid).collect();
while let Some(current_pid) = pids_to_explore.pop_front() { while let Some(current_pid) = pids_to_explore.pop_front() {
if !prune_disabled_pids(current_pid, &mut parent_child_mapping, &pid_process_mapping) { if !prune_disabled_pids(current_pid, &mut parent_child_mapping, &pid_process_mapping) {
sort_remaining_pids( sort_remaining_pids(
current_pid, current_pid,
sort_type, sorting_type,
is_sort_descending, is_sort_descending,
&mut parent_child_mapping, &mut parent_child_mapping,
&pid_process_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.push(String::default());
lines.extend(branch_res); lines.extend(branch_res);
explored_pids.extend(pid_res); explored_pids.extend(pid_res);
@ -798,8 +877,9 @@ pub fn tree_process_data(
Some(process) => { Some(process) => {
let mut p = process.clone(); let mut p = process.clone();
p.process_description_prefix = Some(format!( p.process_description_prefix = Some(format!(
"{}{}", "{}{}{}",
prefix, 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 { if is_using_command {
&p.command &p.command
} else { } else {
@ -953,6 +1033,7 @@ pub fn group_process_data(
process_description_prefix: None, process_description_prefix: None,
process_char: char::default(), process_char: char::default(),
is_disabled_entry: false, is_disabled_entry: false,
is_collapsed_entry: false,
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

@ -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 let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
if !app.is_frozen { 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 process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree { let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
app.canvas_data app.canvas_data
.single_process_data .single_process_data
.iter() .iter()
.map(|process| { .map(|(_pid, process)| {
let mut process_clone = process.clone(); let mut process_clone = process.clone();
if !is_invalid_or_blank { if !is_invalid_or_blank {
if let Some(process_filter) = process_filter { 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 app.canvas_data
.single_process_data .single_process_data
.iter() .iter()
.filter(|process| { .filter_map(|(_pid, process)| {
if !is_invalid_or_blank { if !is_invalid_or_blank {
if let Some(process_filter) = process_filter { 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 { } else {
true Some(process)
} }
} else { } else {
true Some(process)
} }
}) })
.cloned() .cloned()

View File

@ -115,6 +115,7 @@ pub struct ConfigFlags {
#[builder(default, setter(strip_option))] #[builder(default, setter(strip_option))]
pub no_write: Option<bool>, pub no_write: Option<bool>,
// For built-in colour palettes.
#[builder(default, setter(strip_option))] #[builder(default, setter(strip_option))]
pub color: Option<String>, pub color: Option<String>,
@ -362,7 +363,8 @@ pub fn build_app(
1 1
}, },
disable_click: get_disable_click(matches, config), 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 { let used_widgets = UsedWidgets {
@ -845,6 +847,7 @@ fn get_use_battery(matches: &clap::ArgMatches<'static>, config: &Config) -> bool
false false
} }
#[allow(dead_code)]
fn get_no_write(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { fn get_no_write(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("no_write") { if matches.is_present("no_write") {
return true; return true;