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:
parent
e43456207b
commit
669b245367
|
@ -48,6 +48,7 @@
|
|||
"cvars",
|
||||
"czvf",
|
||||
"denylist",
|
||||
"eselect",
|
||||
"fedoracentos",
|
||||
"fpath",
|
||||
"fract",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
129
src/app.rs
129
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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
16
src/clap.rs
16
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)
|
||||
|
|
|
@ -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] = [
|
||||
|
|
|
@ -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,9 +196,8 @@ 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)
|
||||
{
|
||||
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 {
|
||||
|
@ -204,9 +205,8 @@ pub fn convert_cpu_data_points(
|
|||
}
|
||||
} else {
|
||||
String::default()
|
||||
};
|
||||
new_cpu_data.short_cpu_name =
|
||||
if let Some(cpu_harvest) = current_data.cpu_harvest.get(itx) {
|
||||
},
|
||||
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,16 +429,19 @@ 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 mut complete_pid_set: HashSet<Pid> =
|
||||
existing_converted_process_data.keys().copied().collect();
|
||||
|
||||
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);
|
||||
|
@ -449,6 +455,61 @@ pub fn convert_process_data(
|
|||
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,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
existing_converted_process_data.insert(
|
||||
process.pid,
|
||||
ConvertedProcessData {
|
||||
pid: process.pid,
|
||||
ppid: process.parent_pid,
|
||||
|
@ -472,9 +533,16 @@ pub fn convert_process_data(
|
|||
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);
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
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(¤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::<Vec<_>>()
|
||||
|
|
19
src/lib.rs
19
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<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 {
|
||||
true
|
||||
None
|
||||
}
|
||||
} else {
|
||||
true
|
||||
Some(process)
|
||||
}
|
||||
} else {
|
||||
Some(process)
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue