From 605314d44c68791ba8225102b941d5547290dce9 Mon Sep 17 00:00:00 2001
From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com>
Date: Sat, 29 Apr 2023 19:21:48 -0400
Subject: [PATCH] feature: add customizable process columns (#1115)
* feature: add customizable process columns
* Add some tests and actual logic
* more tests
* update changelog
* update config field
* even more tests
* update documentation
* more testing
---
CHANGELOG.md | 1 +
docs/content/usage/widgets/process.md | 28 +-
src/app.rs | 12 +-
src/app/query.rs | 4 +-
src/constants.rs | 11 +-
src/options.rs | 40 +-
src/options/process_columns.rs | 83 +++
src/widgets/process_table.rs | 573 +++++++++++++-----
.../process_table/proc_widget_column.rs | 38 +-
9 files changed, 620 insertions(+), 170 deletions(-)
create mode 100644 src/options/process_columns.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 463e32c1..10555cc3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#1024](https://github.com/ClementTsang/bottom/pull/1024): Support FreeBSD temperature sensors based on `hw.temperature`.
- [#1063](https://github.com/ClementTsang/bottom/pull/1063): Add buffer and cache memory tracking.
- [#1106](https://github.com/ClementTsang/bottom/pull/1106): Add current battery charging state.
+- [#1115](https://github.com/ClementTsang/bottom/pull/1115): Add customizable process columns to config file.
## Changes
diff --git a/docs/content/usage/widgets/process.md b/docs/content/usage/widgets/process.md
index 52603098..0c5063f4 100644
--- a/docs/content/usage/widgets/process.md
+++ b/docs/content/usage/widgets/process.md
@@ -126,20 +126,20 @@ You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++).
Note all keywords are case-insensitive. To search for a process/command that collides with a keyword, surround the term with quotes (e.x. `"cpu"`).
-| Keywords | Example | Description |
-| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------------- |
-| | `btm` | Matches by process or command name; supports regex |
-| `pid` | `pid=1044` | Matches by PID; supports regex |
-| `cpu`
`cpu%` | `cpu > 0.5` | Matches the CPU column; supports comparison operators |
-| `memb` | `memb > 1000 b` | Matches the memory column in terms of bytes; supports comparison operators |
-| `mem`
`mem%` | `mem < 0.5` | Matches the memory column in terms of percent; supports comparison operators |
-| `read`
`r/s` | `read = 1 mb` | Matches the read/s column in terms of bytes; supports comparison operators |
-| `write`
`w/s` | `write >= 1 kb` | Matches the write/s column in terms of bytes; supports comparison operators |
-| `tread`
`t.read` | `tread <= 1024 gb` | Matches he total read column in terms of bytes; supports comparison operators |
-| `twrite`
`t.write` | `twrite > 1024 tb` | Matches the total write column in terms of bytes; supports comparison operators |
-| `user` | `user=root` | Matches by user; supports regex |
-| `state` | `state=running` | Matches by state; supports regex |
-| `()` | `( AND ) OR ` | Group together a condition |
+| Keywords | Example | Description |
+| ------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------- |
+| | `btm` | Matches by process or command name; supports regex |
+| `pid` | `pid=1044` | Matches by PID; supports regex |
+| `cpu`
`cpu%` | `cpu > 0.5` | Matches the CPU column; supports comparison operators |
+| `memb` | `memb > 1000 b` | Matches the memory column in terms of bytes; supports comparison operators |
+| `mem`
`mem%` | `mem < 0.5` | Matches the memory column in terms of percent; supports comparison operators |
+| `read`
`r/s`
`rps` | `read = 1 mb` | Matches the read/s column in terms of bytes; supports comparison operators |
+| `write`
`w/s`
`wps` | `write >= 1 kb` | Matches the write/s column in terms of bytes; supports comparison operators |
+| `tread`
`t.read` | `tread <= 1024 gb` | Matches he total read column in terms of bytes; supports comparison operators |
+| `twrite`
`t.write` | `twrite > 1024 tb` | Matches the total write column in terms of bytes; supports comparison operators |
+| `user` | `user=root` | Matches by user; supports regex |
+| `state` | `state=running` | Matches by state; supports regex |
+| `()` | `( AND ) OR ` | Group together a condition |
#### Comparison operators
diff --git a/src/app.rs b/src/app.rs
index 8af3c7ec..4496dd79 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -13,7 +13,7 @@ pub use states::*;
use typed_builder::*;
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
-use crate::widgets::{ProcWidgetMode, ProcWidgetState};
+use crate::widgets::{ProcWidgetColumn, ProcWidgetMode};
use crate::{
constants,
data_conversion::ConvertedData,
@@ -300,7 +300,7 @@ impl App {
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
- proc_widget_state.on_tab();
+ proc_widget_state.toggle_tab();
}
}
}
@@ -1193,7 +1193,7 @@ impl App {
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
- proc_widget_state.select_column(ProcWidgetState::CPU);
+ proc_widget_state.select_column(ProcWidgetColumn::Cpu);
}
}
}
@@ -1203,7 +1203,7 @@ impl App {
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
- proc_widget_state.select_column(ProcWidgetState::MEM);
+ proc_widget_state.select_column(ProcWidgetColumn::Mem);
}
} else if let Some(disk) = self
.disk_state
@@ -1218,7 +1218,7 @@ impl App {
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
- proc_widget_state.select_column(ProcWidgetState::PID_OR_COUNT);
+ proc_widget_state.select_column(ProcWidgetColumn::PidOrCount);
}
} else if let Some(disk) = self
.disk_state
@@ -1243,7 +1243,7 @@ impl App {
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
- proc_widget_state.select_column(ProcWidgetState::PROC_NAME_OR_CMD);
+ proc_widget_state.select_column(ProcWidgetColumn::ProcNameOrCmd);
}
} else if let Some(disk) = self
.disk_state
diff --git a/src/app/query.rs b/src/app/query.rs
index 20798716..cd9d4f6c 100644
--- a/src/app/query.rs
+++ b/src/app/query.rs
@@ -584,8 +584,8 @@ impl std::str::FromStr for PrefixType {
"cpu" | "cpu%" => Ok(PCpu),
"mem" | "mem%" => Ok(PMem),
"memb" => Ok(MemBytes),
- "read" | "r/s" => Ok(Rps),
- "write" | "w/s" => Ok(Wps),
+ "read" | "r/s" | "rps" => Ok(Rps),
+ "write" | "w/s" | "wps" => Ok(Wps),
"tread" | "t.read" => Ok(TRead),
"twrite" | "t.write" => Ok(TWrite),
"pid" => Ok(Pid),
diff --git a/src/constants.rs b/src/constants.rs
index 2e6475f2..c678d058 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -367,11 +367,11 @@ pub const SEARCH_HELP_TEXT: [&str; 48] = [
"cpu, cpu% ex: cpu > 4.2",
"mem, mem% ex: mem < 4.2",
"memb ex: memb < 100 kb",
- "read, r/s ex: read >= 1 b",
- "write, w/s ex: write <= 1 tb",
+ "read, r/s, rps ex: read >= 1 b",
+ "write, w/s, wps ex: write <= 1 tb",
"tread, t.read ex: tread = 1",
"twrite, t.write ex: twrite = 1",
- "user ex: user = root",
+ "user ex: user = root",
"state ex: state = running",
"",
"Comparison operators:",
@@ -588,6 +588,11 @@ pub const CONFIG_TEXT: &str = r##"# This is a default config file for bottom. A
# How much data is stored at once in terms of time.
#retention = "10m"
+# These are flags around the process widget.
+
+#[processes]
+#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State"]
+
# These are all the components that support custom theming. Note that colour support
# will depend on terminal support.
diff --git a/src/options.rs b/src/options.rs
index 4de8f60c..5d2fd8ce 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -7,6 +7,7 @@ use std::{
use clap::ArgMatches;
use hashbrown::{HashMap, HashSet};
+use indexmap::IndexSet;
use layout_options::*;
use regex::Regex;
use serde::{Deserialize, Serialize};
@@ -23,12 +24,15 @@ use crate::{
utils::error::{self, BottomError},
widgets::{
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
- ProcWidgetMode, ProcWidgetState, TempWidgetState,
+ ProcColumn, ProcTableConfig, ProcWidgetMode, ProcWidgetState, TempWidgetState,
},
};
pub mod layout_options;
+pub mod process_columns;
+use self::process_columns::ProcessConfig;
+
use anyhow::{Context, Result};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
@@ -40,6 +44,7 @@ pub struct Config {
pub mount_filter: Option,
pub temp_filter: Option,
pub net_filter: Option,
+ pub processes: Option,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TypedBuilder)]
@@ -218,6 +223,24 @@ pub fn build_app(
let network_scale_type = get_network_scale_type(matches, config);
let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, matches, config);
+ let proc_columns: Option> = {
+ let columns = config
+ .processes
+ .as_ref()
+ .and_then(|cfg| cfg.columns.clone());
+
+ match columns {
+ Some(columns) => {
+ if columns.is_empty() {
+ None
+ } else {
+ Some(IndexSet::from_iter(columns.into_iter()))
+ }
+ }
+ None => None,
+ }
+ };
+
let app_config_fields = AppConfigFields {
update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config)
.context("Update 'rate' in your config file.")?,
@@ -247,6 +270,14 @@ pub fn build_app(
retention_ms,
};
+ let table_config = ProcTableConfig {
+ is_case_sensitive,
+ is_match_whole_word,
+ is_use_regex,
+ show_memory_as_values,
+ is_command: is_default_command,
+ };
+
for row in &widget_layout.rows {
for col in &row.children {
for col_row in &col.children {
@@ -325,12 +356,9 @@ pub fn build_app(
ProcWidgetState::new(
&app_config_fields,
mode,
- is_case_sensitive,
- is_match_whole_word,
- is_use_regex,
- show_memory_as_values,
- is_default_command,
+ table_config,
colours,
+ &proc_columns,
),
);
}
diff --git a/src/options/process_columns.rs b/src/options/process_columns.rs
new file mode 100644
index 00000000..74676d35
--- /dev/null
+++ b/src/options/process_columns.rs
@@ -0,0 +1,83 @@
+use serde::{Deserialize, Serialize};
+
+use crate::widgets::ProcColumn;
+
+/// Process column settings.
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+pub struct ProcessConfig {
+ pub columns: Option>,
+}
+
+#[cfg(test)]
+mod test {
+ use crate::widgets::ProcColumn;
+
+ use super::ProcessConfig;
+
+ #[test]
+ fn empty_column_setting() {
+ let config = "";
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert!(generated.columns.is_none());
+ }
+
+ #[test]
+ fn process_column_settings() {
+ let config = r#"
+ columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s"]
+ "#;
+
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert_eq!(
+ generated.columns,
+ Some(vec![
+ ProcColumn::CpuPercent,
+ ProcColumn::Pid,
+ ProcColumn::User,
+ ProcColumn::MemoryVal,
+ ProcColumn::TotalRead,
+ ProcColumn::TotalWrite,
+ ProcColumn::ReadPerSecond,
+ ProcColumn::WritePerSecond,
+ ]),
+ );
+ }
+
+ #[test]
+ fn process_column_settings_2() {
+ let config = r#"
+ columns = ["MEM%"]
+ "#;
+
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert_eq!(generated.columns, Some(vec![ProcColumn::MemoryPercent]));
+ }
+
+ #[test]
+ fn process_column_settings_3() {
+ let config = r#"
+ columns = ["MEM%", "TWrite", "Cpuz", "read", "wps"]
+ "#;
+
+ toml_edit::de::from_str::(config).expect_err("Should error out!");
+ }
+
+ #[test]
+ fn process_column_settings_4() {
+ let config = r#"columns = ["Twrite", "T.Write"]"#;
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert_eq!(generated.columns, Some(vec![ProcColumn::TotalWrite; 2]));
+
+ let config = r#"columns = ["Tread", "T.read"]"#;
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert_eq!(generated.columns, Some(vec![ProcColumn::TotalRead; 2]));
+
+ let config = r#"columns = ["read", "rps", "r/s"]"#;
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert_eq!(generated.columns, Some(vec![ProcColumn::ReadPerSecond; 3]));
+
+ let config = r#"columns = ["write", "wps", "w/s"]"#;
+ let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
+ assert_eq!(generated.columns, Some(vec![ProcColumn::WritePerSecond; 3]));
+ }
+}
diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs
index d378d091..0e8c6a9f 100644
--- a/src/widgets/process_table.rs
+++ b/src/widgets/process_table.rs
@@ -1,6 +1,7 @@
use std::{borrow::Cow, collections::BTreeMap};
use hashbrown::{HashMap, HashSet};
+use indexmap::IndexSet;
use itertools::Itertools;
use crate::{
@@ -71,6 +72,50 @@ type ProcessTable = SortDataTable;
type SortTable = DataTable, SortTableColumn>;
type StringPidMap = HashMap>;
+fn make_column(column: ProcColumn) -> SortColumn {
+ use ProcColumn::*;
+
+ match column {
+ CpuPercent => SortColumn::new(CpuPercent).default_descending(),
+ MemoryVal => SortColumn::new(MemoryVal).default_descending(),
+ MemoryPercent => SortColumn::new(MemoryPercent).default_descending(),
+ Pid => SortColumn::new(Pid),
+ Count => SortColumn::new(Count),
+ Name => SortColumn::soft(Name, Some(0.3)),
+ Command => SortColumn::soft(Command, Some(0.3)),
+ ReadPerSecond => SortColumn::hard(ReadPerSecond, 8).default_descending(),
+ WritePerSecond => SortColumn::hard(WritePerSecond, 8).default_descending(),
+ TotalRead => SortColumn::hard(TotalRead, 8).default_descending(),
+ TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(),
+ User => SortColumn::soft(User, Some(0.05)),
+ State => SortColumn::hard(State, 7),
+ }
+}
+
+#[derive(Clone, Copy, Default)]
+pub struct ProcTableConfig {
+ pub is_case_sensitive: bool,
+ pub is_match_whole_word: bool,
+ pub is_use_regex: bool,
+ pub show_memory_as_values: bool,
+ pub is_command: bool,
+}
+
+/// A hacky workaround for now.
+#[derive(PartialEq, Eq, Hash)]
+pub enum ProcWidgetColumn {
+ PidOrCount,
+ ProcNameOrCmd,
+ Cpu,
+ Mem,
+ Rps,
+ Wps,
+ TotalRead,
+ TotalWrite,
+ User,
+ State,
+}
+
pub struct ProcWidgetState {
pub mode: ProcWidgetMode,
@@ -83,26 +128,24 @@ pub struct ProcWidgetState {
/// The state of the togglable table that controls sorting.
pub sort_table: SortTable,
+ /// The internal column mapping as an [`IndexSet`], to allow us to do quick mappings of column type -> index.
+ pub column_mapping: IndexSet,
+
/// A name-to-pid mapping.
pub id_pid_map: StringPidMap,
+ /// The default sort index.
+ default_sort_index: usize,
+
+ /// The default sort order.
+ default_sort_order: SortOrder,
+
pub is_sort_open: bool,
pub force_rerender: bool,
pub force_update_data: bool,
}
impl ProcWidgetState {
- pub const PID_OR_COUNT: usize = 0;
- pub const PROC_NAME_OR_CMD: usize = 1;
- pub const CPU: usize = 2;
- pub const MEM: usize = 3;
- pub const RPS: usize = 4;
- pub const WPS: usize = 5;
- pub const T_READ: usize = 6;
- pub const T_WRITE: usize = 7;
- pub const USER: usize = 8;
- pub const STATE: usize = 9;
-
fn new_sort_table(config: &AppConfigFields, colours: &CanvasColours) -> SortTable {
const COLUMNS: [Column; 1] = [Column::hard(SortTableColumn, 7)];
@@ -114,54 +157,15 @@ impl ProcWidgetState {
show_table_scroll_position: false,
show_current_entry_when_unfocused: false,
};
-
let styling = DataTableStyling::from_colours(colours);
DataTable::new(COLUMNS, props, styling)
}
fn new_process_table(
- config: &AppConfigFields, colours: &CanvasColours, mode: &ProcWidgetMode, is_count: bool,
- is_command: bool, show_memory_as_values: bool,
+ config: &AppConfigFields, colours: &CanvasColours, columns: Vec>,
+ default_index: usize, default_order: SortOrder,
) -> ProcessTable {
- let (default_index, default_order) = if matches!(mode, ProcWidgetMode::Tree { .. }) {
- (Self::PID_OR_COUNT, SortOrder::Ascending)
- } else {
- (Self::CPU, SortOrder::Descending)
- };
-
- let columns = {
- use ProcColumn::*;
-
- let pid_or_count = SortColumn::new(if is_count { Count } else { Pid });
- let name_or_cmd = SortColumn::soft(if is_command { Command } else { Name }, Some(0.3));
- let cpu = SortColumn::new(CpuPercent).default_descending();
- let mem = SortColumn::new(if show_memory_as_values {
- MemoryVal
- } else {
- MemoryPercent
- })
- .default_descending();
- let rps = SortColumn::hard(ReadPerSecond, 8).default_descending();
- let wps = SortColumn::hard(WritePerSecond, 8).default_descending();
- let tr = SortColumn::hard(TotalRead, 8).default_descending();
- let tw = SortColumn::hard(TotalWrite, 8).default_descending();
- let state = SortColumn::hard(State, 7);
-
- vec![
- pid_or_count,
- name_or_cmd,
- cpu,
- mem,
- rps,
- wps,
- tr,
- tw,
- SortColumn::soft(User, Some(0.05)),
- state,
- ]
- };
-
let inner_props = DataTableProps {
title: Some(" Processes ".into()),
table_gap: config.table_gap,
@@ -175,43 +179,102 @@ impl ProcWidgetState {
sort_index: default_index,
order: default_order,
};
-
let styling = DataTableStyling::from_colours(colours);
DataTable::new_sortable(columns, props, styling)
}
pub fn new(
- config: &AppConfigFields, mode: ProcWidgetMode, is_case_sensitive: bool,
- is_match_whole_word: bool, is_use_regex: bool, show_memory_as_values: bool,
- is_command: bool, colours: &CanvasColours,
+ config: &AppConfigFields, mode: ProcWidgetMode, table_config: ProcTableConfig,
+ colours: &CanvasColours, config_columns: &Option>,
) -> Self {
let process_search_state = {
let mut pss = ProcessSearchState::default();
- if is_case_sensitive {
- // By default it's off
+ if table_config.is_case_sensitive {
+ // By default it's off.
pss.search_toggle_ignore_case();
}
- if is_match_whole_word {
+ if table_config.is_match_whole_word {
pss.search_toggle_whole_word();
}
- if is_use_regex {
+ if table_config.is_use_regex {
pss.search_toggle_regex();
}
pss
};
- let is_count = matches!(mode, ProcWidgetMode::Grouped);
+ let columns: Vec> = {
+ use ProcColumn::*;
+
+ match config_columns {
+ Some(columns) if !columns.is_empty() => {
+ columns.iter().cloned().map(make_column).collect()
+ }
+ _ => {
+ let is_count = matches!(mode, ProcWidgetMode::Grouped);
+ let is_command = table_config.is_command;
+ let mem_vals = table_config.show_memory_as_values;
+
+ let default_columns = [
+ if is_count { Count } else { Pid },
+ if is_command { Command } else { Name },
+ CpuPercent,
+ if mem_vals { MemoryVal } else { MemoryPercent },
+ ReadPerSecond,
+ WritePerSecond,
+ TotalRead,
+ TotalWrite,
+ User,
+ State,
+ ];
+
+ default_columns.into_iter().map(make_column).collect()
+ }
+ }
+ };
+
+ let column_mapping = columns
+ .iter()
+ .map(|col| {
+ use ProcColumn::*;
+
+ match col.inner() {
+ CpuPercent => ProcWidgetColumn::Cpu,
+ MemoryVal | MemoryPercent => ProcWidgetColumn::Mem,
+ Pid | Count => ProcWidgetColumn::PidOrCount,
+ Name | Command => ProcWidgetColumn::ProcNameOrCmd,
+ ReadPerSecond => ProcWidgetColumn::Rps,
+ WritePerSecond => ProcWidgetColumn::Wps,
+ TotalRead => ProcWidgetColumn::TotalRead,
+ TotalWrite => ProcWidgetColumn::TotalWrite,
+ State => ProcWidgetColumn::State,
+ User => ProcWidgetColumn::User,
+ }
+ })
+ .collect::>();
+
+ let (default_sort_index, default_sort_order) =
+ if matches!(mode, ProcWidgetMode::Tree { .. }) {
+ if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::PidOrCount) {
+ (index, columns[index].default_order)
+ } else {
+ (0, columns[0].default_order)
+ }
+ } else if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::Cpu) {
+ (index, columns[index].default_order)
+ } else {
+ (0, columns[0].default_order)
+ };
+
let sort_table = Self::new_sort_table(config, colours);
let table = Self::new_process_table(
config,
colours,
- &mode,
- is_count,
- is_command,
- show_memory_as_values,
+ columns,
+ default_sort_index,
+ default_sort_order,
);
let id_pid_map = HashMap::default();
@@ -221,10 +284,13 @@ impl ProcWidgetState {
table,
sort_table,
id_pid_map,
+ column_mapping,
is_sort_open: false,
mode,
force_rerender: true,
force_update_data: false,
+ default_sort_index,
+ default_sort_order,
};
table.sort_table.set_data(table.column_text());
@@ -232,17 +298,21 @@ impl ProcWidgetState {
}
pub fn is_using_command(&self) -> bool {
- self.table
- .columns
- .get(ProcWidgetState::PROC_NAME_OR_CMD)
- .map(|col| matches!(col.inner(), ProcColumn::Command))
+ self.column_mapping
+ .get_index_of(&ProcWidgetColumn::ProcNameOrCmd)
+ .and_then(|index| {
+ self.table
+ .columns
+ .get(index)
+ .map(|col| matches!(col.inner(), ProcColumn::Command))
+ })
.unwrap_or(false)
}
pub fn is_mem_percent(&self) -> bool {
- self.table
- .columns
- .get(ProcWidgetState::MEM)
+ self.column_mapping
+ .get_index_of(&ProcWidgetColumn::Mem)
+ .and_then(|index| self.table.columns.get(index))
.map(|col| matches!(col.inner(), ProcColumn::MemoryPercent))
.unwrap_or(false)
}
@@ -594,19 +664,21 @@ impl ProcWidgetState {
}
pub fn toggle_mem_percentage(&mut self) {
- if let Some(mem) = self.get_mut_proc_col(Self::MEM) {
- match mem {
- ProcColumn::MemoryVal => {
- *mem = ProcColumn::MemoryPercent;
+ if let Some(index) = self.column_mapping.get_index_of(&ProcWidgetColumn::Mem) {
+ if let Some(mem) = self.get_mut_proc_col(index) {
+ match mem {
+ ProcColumn::MemoryVal => {
+ *mem = ProcColumn::MemoryPercent;
+ }
+ ProcColumn::MemoryPercent => {
+ *mem = ProcColumn::MemoryVal;
+ }
+ _ => unreachable!(),
}
- ProcColumn::MemoryPercent => {
- *mem = ProcColumn::MemoryVal;
- }
- _ => unreachable!(),
- }
- self.sort_table.set_data(self.column_text());
- self.force_data_update();
+ self.sort_table.set_data(self.column_text());
+ self.force_data_update();
+ }
}
}
@@ -623,30 +695,36 @@ impl ProcWidgetState {
self.force_update_data = true;
}
- /// Marks the selected column as hidden, and automatically resets the selected column to CPU
- /// and descending if that column was selected.
- fn hide_column(&mut self, index: usize) {
- if let Some(col) = self.table.columns.get_mut(index) {
- col.is_hidden = true;
+ /// Marks the selected column as hidden, and automatically resets the selected column to the default
+ /// sort index and order.
+ fn hide_column(&mut self, column: ProcWidgetColumn) {
+ if let Some(index) = self.column_mapping.get_index_of(&column) {
+ if let Some(col) = self.table.columns.get_mut(index) {
+ col.is_hidden = true;
- if self.table.sort_index() == index {
- self.table.set_sort_index(Self::CPU);
- self.table.set_order(SortOrder::Descending);
+ if self.table.sort_index() == index {
+ self.table.set_sort_index(self.default_sort_index);
+ self.table.set_order(self.default_sort_order);
+ }
}
}
}
/// Marks the selected column as shown.
- fn show_column(&mut self, index: usize) {
- if let Some(col) = self.table.columns.get_mut(index) {
- col.is_hidden = false;
+ fn show_column(&mut self, column: ProcWidgetColumn) {
+ if let Some(index) = self.column_mapping.get_index_of(&column) {
+ if let Some(col) = self.table.columns.get_mut(index) {
+ col.is_hidden = false;
+ }
}
}
/// Select a column. If the column is already selected, then just toggle the sort order.
- pub fn select_column(&mut self, new_sort_index: usize) {
- self.table.set_sort_index(new_sort_index);
- self.force_data_update();
+ pub fn select_column(&mut self, column: ProcWidgetColumn) {
+ if let Some(index) = self.column_mapping.get_index_of(&column) {
+ self.table.set_sort_index(index);
+ self.force_data_update();
+ }
}
pub fn toggle_current_tree_branch_entry(&mut self) {
@@ -663,28 +741,33 @@ impl ProcWidgetState {
}
pub fn toggle_command(&mut self) {
- if let Some(col) = self.table.columns.get_mut(Self::PROC_NAME_OR_CMD) {
- let inner = col.inner_mut();
- match inner {
- ProcColumn::Name => {
- *inner = ProcColumn::Command;
- if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {
- *max_percentage = Some(0.5);
+ if let Some(index) = self
+ .column_mapping
+ .get_index_of(&ProcWidgetColumn::ProcNameOrCmd)
+ {
+ if let Some(col) = self.table.columns.get_mut(index) {
+ let inner = col.inner_mut();
+ match inner {
+ ProcColumn::Name => {
+ *inner = ProcColumn::Command;
+ if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {
+ *max_percentage = Some(0.5);
+ }
}
- }
- ProcColumn::Command => {
- *inner = ProcColumn::Name;
- if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {
- *max_percentage = match self.mode {
- ProcWidgetMode::Tree { .. } => Some(0.5),
- ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3),
- };
+ ProcColumn::Command => {
+ *inner = ProcColumn::Name;
+ if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() {
+ *max_percentage = match self.mode {
+ ProcWidgetMode::Tree { .. } => Some(0.5),
+ ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3),
+ };
+ }
}
+ _ => unreachable!(),
}
- _ => unreachable!(),
+ self.sort_table.set_data(self.column_text());
+ self.force_rerender_and_update();
}
- self.sort_table.set_data(self.column_text());
- self.force_rerender_and_update();
}
}
@@ -694,34 +777,39 @@ impl ProcWidgetState {
/// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden
/// (handled by internal method calls), and go back to the "defaults".
///
- /// Otherwise, if count is disabled, then the User and State columns should be re-enabled, and the mode switched
- /// to [`ProcWidgetMode::Normal`].
- pub fn on_tab(&mut self) {
+ /// Otherwise, if count is disabled, then if the columns exist, the User and State columns should be re-enabled,
+ /// and the mode switched to [`ProcWidgetMode::Normal`].
+ pub fn toggle_tab(&mut self) {
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
- if let Some(sort_col) = self.table.columns.get_mut(Self::PID_OR_COUNT) {
- let col = sort_col.inner_mut();
- match col {
- ProcColumn::Pid => {
- *col = ProcColumn::Count;
- sort_col.default_order = SortOrder::Descending;
+ if let Some(index) = self
+ .column_mapping
+ .get_index_of(&ProcWidgetColumn::PidOrCount)
+ {
+ if let Some(sort_col) = self.table.columns.get_mut(index) {
+ let col = sort_col.inner_mut();
+ match col {
+ ProcColumn::Pid => {
+ *col = ProcColumn::Count;
+ sort_col.default_order = SortOrder::Descending;
- self.hide_column(Self::USER);
- self.hide_column(Self::STATE);
- self.mode = ProcWidgetMode::Grouped;
- }
- ProcColumn::Count => {
- *col = ProcColumn::Pid;
- sort_col.default_order = SortOrder::Ascending;
+ self.hide_column(ProcWidgetColumn::User);
+ self.hide_column(ProcWidgetColumn::State);
+ self.mode = ProcWidgetMode::Grouped;
+ }
+ ProcColumn::Count => {
+ *col = ProcColumn::Pid;
+ sort_col.default_order = SortOrder::Ascending;
- self.show_column(Self::USER);
- self.show_column(Self::STATE);
- self.mode = ProcWidgetMode::Normal;
+ self.show_column(ProcWidgetColumn::User);
+ self.show_column(ProcWidgetColumn::State);
+ self.mode = ProcWidgetMode::Normal;
+ }
+ _ => unreachable!(),
}
- _ => unreachable!(),
+
+ self.sort_table.set_data(self.column_text());
+ self.force_rerender_and_update();
}
-
- self.sort_table.set_data(self.column_text());
- self.force_rerender_and_update();
}
}
}
@@ -944,4 +1032,213 @@ mod test {
data.iter().map(|d| (d.pid)).collect::>(),
);
}
+
+ fn get_columns(table: &ProcessTable) -> Vec {
+ table
+ .columns
+ .iter()
+ .filter_map(|c| {
+ if c.is_hidden() {
+ None
+ } else {
+ Some(*c.inner())
+ }
+ })
+ .collect::>()
+ }
+
+ fn init_default_state(columns: &[ProcColumn]) -> ProcWidgetState {
+ let config = AppConfigFields::default();
+ let colours = CanvasColours::default();
+ let table_config = ProcTableConfig::default();
+ let columns = Some(columns.iter().cloned().collect());
+
+ ProcWidgetState::new(
+ &config,
+ ProcWidgetMode::Normal,
+ table_config,
+ &colours,
+ &columns,
+ )
+ }
+
+ #[test]
+ fn test_custom_columns() {
+ let columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::Command,
+ ProcColumn::MemoryPercent,
+ ProcColumn::State,
+ ];
+ let state = init_default_state(&columns);
+ assert_eq!(get_columns(&state.table), columns);
+ }
+
+ #[test]
+ fn toggle_count_pid() {
+ let original_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::Command,
+ ProcColumn::MemoryPercent,
+ ProcColumn::State,
+ ];
+ let new_columns = vec![
+ ProcColumn::Count,
+ ProcColumn::Command,
+ ProcColumn::MemoryPercent,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+
+ // This should hide the state.
+ state.toggle_tab();
+ assert_eq!(get_columns(&state.table), new_columns);
+
+ // This should re-reveal the state.
+ state.toggle_tab();
+ assert_eq!(get_columns(&state.table), original_columns);
+ }
+
+ #[test]
+ fn toggle_count_pid_2() {
+ let original_columns = vec![
+ ProcColumn::Command,
+ ProcColumn::MemoryPercent,
+ ProcColumn::User,
+ ProcColumn::State,
+ ProcColumn::Pid,
+ ];
+ let new_columns = vec![
+ ProcColumn::Command,
+ ProcColumn::MemoryPercent,
+ ProcColumn::Count,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+
+ // This should hide the state.
+ state.toggle_tab();
+ assert_eq!(get_columns(&state.table), new_columns);
+
+ // This should re-reveal the state.
+ state.toggle_tab();
+ assert_eq!(get_columns(&state.table), original_columns);
+ }
+
+ #[test]
+ fn toggle_command() {
+ let original_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryPercent,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+ let new_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryPercent,
+ ProcColumn::State,
+ ProcColumn::Name,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+
+ state.toggle_command();
+ assert_eq!(get_columns(&state.table), new_columns);
+
+ state.toggle_command();
+ assert_eq!(get_columns(&state.table), original_columns);
+ }
+
+ #[test]
+ fn toggle_mem_percentage() {
+ let original_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryPercent,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+ let new_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryVal,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+
+ state.toggle_mem_percentage();
+ assert_eq!(get_columns(&state.table), new_columns);
+
+ state.toggle_mem_percentage();
+ assert_eq!(get_columns(&state.table), original_columns);
+ }
+
+ #[test]
+ fn toggle_mem_percentage_2() {
+ let new_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryPercent,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+ let original_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryVal,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+
+ state.toggle_mem_percentage();
+ assert_eq!(get_columns(&state.table), new_columns);
+
+ state.toggle_mem_percentage();
+ assert_eq!(get_columns(&state.table), original_columns);
+ }
+
+ #[test]
+ fn test_is_using_command() {
+ let original_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryVal,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+ assert!(state.is_using_command());
+
+ state.toggle_command();
+ assert!(!state.is_using_command());
+
+ state.toggle_command();
+ assert!(state.is_using_command());
+ }
+
+ #[test]
+ fn test_is_memory() {
+ let original_columns = vec![
+ ProcColumn::Pid,
+ ProcColumn::MemoryVal,
+ ProcColumn::State,
+ ProcColumn::Command,
+ ];
+
+ let mut state = init_default_state(&original_columns);
+ assert_eq!(get_columns(&state.table), original_columns);
+ assert!(!state.is_mem_percent());
+
+ state.toggle_mem_percentage();
+ assert!(state.is_mem_percent());
+
+ state.toggle_mem_percentage();
+ assert!(!state.is_mem_percent());
+ }
}
diff --git a/src/widgets/process_table/proc_widget_column.rs b/src/widgets/process_table/proc_widget_column.rs
index c00eaa80..262936e1 100644
--- a/src/widgets/process_table/proc_widget_column.rs
+++ b/src/widgets/process_table/proc_widget_column.rs
@@ -1,12 +1,14 @@
use std::{borrow::Cow, cmp::Reverse};
+use serde::{de::Error, Deserialize, Serialize};
+
use super::ProcWidgetData;
use crate::{
components::data_table::{ColumnHeader, SortsRow},
utils::gen_util::sort_partial_fn,
};
-#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
pub enum ProcColumn {
CpuPercent,
MemoryVal,
@@ -23,6 +25,40 @@ pub enum ProcColumn {
User,
}
+impl<'de> Deserialize<'de> for ProcColumn {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let value = String::deserialize(deserializer)?.to_lowercase();
+ match value.as_str() {
+ "cpu%" => Ok(ProcColumn::CpuPercent),
+ "mem" => Ok(ProcColumn::MemoryVal),
+ "mem%" => Ok(ProcColumn::MemoryPercent),
+ "pid" => Ok(ProcColumn::Pid),
+ "count" => Ok(ProcColumn::Count),
+ "name" => Ok(ProcColumn::Name),
+ "command" => Ok(ProcColumn::Command),
+ "read" | "r/s" | "rps" => Ok(ProcColumn::ReadPerSecond),
+ "write" | "w/s" | "wps" => Ok(ProcColumn::WritePerSecond),
+ "tread" | "t.read" => Ok(ProcColumn::TotalRead),
+ "twrite" | "t.write" => Ok(ProcColumn::TotalWrite),
+ "state" => Ok(ProcColumn::State),
+ "user" => Ok(ProcColumn::User),
+ _ => Err(D::Error::custom("doesn't match any column type")),
+ }
+ }
+}
+
+impl Serialize for ProcColumn {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&self.text())
+ }
+}
+
impl ColumnHeader for ProcColumn {
fn text(&self) -> Cow<'static, str> {
match self {