diff --git a/CHANGELOG.md b/CHANGELOG.md index 463e32c12..10555cc38 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 52603098b..0c5063f46 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 8af3c7ec4..4496dd79b 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 20798716b..cd9d4f6c9 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 2e6475f29..c678d058a 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 4de8f60c9..5d2fd8ced 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 000000000..74676d356 --- /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 d378d091a..0e8c6a9fb 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; - } - ProcColumn::MemoryPercent => { - *mem = ProcColumn::MemoryVal; + 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!(), } - _ => 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; - - 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; + 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(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 c00eaa807..262936e1c 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 {