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",
"czvf",
"denylist",
"eselect",
"fedoracentos",
"fpath",
"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/),
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

View File

@ -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 <MS> Sets a refresh rate in ms.
-R, --regex Enables regex by default.
-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 |
#### 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
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## Thanks

View File

@ -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<usize> {
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();
}
}
}
}
}
}

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
.search_state
.grapheme_cursor

View File

@ -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<Point>,
pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>,
pub single_process_data: Vec<ConvertedProcessData>, // Contains single process data
pub finalized_process_data_map: HashMap<u64, Vec<ConvertedProcessData>>, // What's actually displayed
pub stringified_process_data_map: HashMap<u64, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled
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, 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, key is the widget ID
pub mem_label_percent: String,
pub swap_label_percent: String,
pub mem_label_frac: String,

View File

@ -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(

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",
);
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)

View File

@ -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] = [

View File

@ -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<String>,
/// 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<ConvertedProcessData> {
existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
) {
// 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<Pid> =
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 &current_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::<Vec<_>>()
} 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<ConvertedProcessData> {
// 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<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 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 {
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<Pid, IndexSet<Pid>>,
prev_drawn_lines: &str,
prev_drawn_lines: &str, collapsed_set: &IndexSet<Pid>,
) -> (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) {
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() {
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::<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 !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<ConvertedProcessData> = 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()

View File

@ -115,6 +115,7 @@ pub struct ConfigFlags {
#[builder(default, setter(strip_option))]
pub no_write: Option<bool>,
// For built-in colour palettes.
#[builder(default, setter(strip_option))]
pub color: Option<String>,
@ -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;