mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 15:44:17 +02:00
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
This commit is contained in:
parent
7162e9c483
commit
605314d44c
@ -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`.
|
- [#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.
|
- [#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.
|
- [#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
|
## Changes
|
||||||
|
|
||||||
|
@ -127,14 +127,14 @@ 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"`).
|
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 |
|
| Keywords | Example | Description |
|
||||||
| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------------- |
|
| ------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
| | `btm` | Matches by process or command name; supports regex |
|
| | `btm` | Matches by process or command name; supports regex |
|
||||||
| `pid` | `pid=1044` | Matches by PID; supports regex |
|
| `pid` | `pid=1044` | Matches by PID; supports regex |
|
||||||
| `cpu` <br/> `cpu%` | `cpu > 0.5` | Matches the CPU column; supports comparison operators |
|
| `cpu` <br/> `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 |
|
| `memb` | `memb > 1000 b` | Matches the memory column in terms of bytes; supports comparison operators |
|
||||||
| `mem` <br/> `mem%` | `mem < 0.5` | Matches the memory column in terms of percent; supports comparison operators |
|
| `mem` <br/> `mem%` | `mem < 0.5` | Matches the memory column in terms of percent; supports comparison operators |
|
||||||
| `read` <br/> `r/s` | `read = 1 mb` | Matches the read/s column in terms of bytes; supports comparison operators |
|
| `read` <br/> `r/s` <br/> `rps` | `read = 1 mb` | Matches the read/s column in terms of bytes; supports comparison operators |
|
||||||
| `write` <br/> `w/s` | `write >= 1 kb` | Matches the write/s column in terms of bytes; supports comparison operators |
|
| `write` <br/> `w/s` <br/> `wps` | `write >= 1 kb` | Matches the write/s column in terms of bytes; supports comparison operators |
|
||||||
| `tread` <br/> `t.read` | `tread <= 1024 gb` | Matches he total read column in terms of bytes; supports comparison operators |
|
| `tread` <br/> `t.read` | `tread <= 1024 gb` | Matches he total read column in terms of bytes; supports comparison operators |
|
||||||
| `twrite` <br/> `t.write` | `twrite > 1024 tb` | Matches the total write column in terms of bytes; supports comparison operators |
|
| `twrite` <br/> `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 |
|
| `user` | `user=root` | Matches by user; supports regex |
|
||||||
|
12
src/app.rs
12
src/app.rs
@ -13,7 +13,7 @@ pub use states::*;
|
|||||||
use typed_builder::*;
|
use typed_builder::*;
|
||||||
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
||||||
|
|
||||||
use crate::widgets::{ProcWidgetMode, ProcWidgetState};
|
use crate::widgets::{ProcWidgetColumn, ProcWidgetMode};
|
||||||
use crate::{
|
use crate::{
|
||||||
constants,
|
constants,
|
||||||
data_conversion::ConvertedData,
|
data_conversion::ConvertedData,
|
||||||
@ -300,7 +300,7 @@ impl App {
|
|||||||
.proc_state
|
.proc_state
|
||||||
.get_mut_widget_state(self.current_widget.widget_id)
|
.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
|
.proc_state
|
||||||
.get_mut_widget_state(self.current_widget.widget_id)
|
.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
|
.proc_state
|
||||||
.get_mut_widget_state(self.current_widget.widget_id)
|
.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
|
} else if let Some(disk) = self
|
||||||
.disk_state
|
.disk_state
|
||||||
@ -1218,7 +1218,7 @@ impl App {
|
|||||||
.proc_state
|
.proc_state
|
||||||
.get_mut_widget_state(self.current_widget.widget_id)
|
.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
|
} else if let Some(disk) = self
|
||||||
.disk_state
|
.disk_state
|
||||||
@ -1243,7 +1243,7 @@ impl App {
|
|||||||
.proc_state
|
.proc_state
|
||||||
.get_mut_widget_state(self.current_widget.widget_id)
|
.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
|
} else if let Some(disk) = self
|
||||||
.disk_state
|
.disk_state
|
||||||
|
@ -584,8 +584,8 @@ impl std::str::FromStr for PrefixType {
|
|||||||
"cpu" | "cpu%" => Ok(PCpu),
|
"cpu" | "cpu%" => Ok(PCpu),
|
||||||
"mem" | "mem%" => Ok(PMem),
|
"mem" | "mem%" => Ok(PMem),
|
||||||
"memb" => Ok(MemBytes),
|
"memb" => Ok(MemBytes),
|
||||||
"read" | "r/s" => Ok(Rps),
|
"read" | "r/s" | "rps" => Ok(Rps),
|
||||||
"write" | "w/s" => Ok(Wps),
|
"write" | "w/s" | "wps" => Ok(Wps),
|
||||||
"tread" | "t.read" => Ok(TRead),
|
"tread" | "t.read" => Ok(TRead),
|
||||||
"twrite" | "t.write" => Ok(TWrite),
|
"twrite" | "t.write" => Ok(TWrite),
|
||||||
"pid" => Ok(Pid),
|
"pid" => Ok(Pid),
|
||||||
|
@ -367,8 +367,8 @@ pub const SEARCH_HELP_TEXT: [&str; 48] = [
|
|||||||
"cpu, cpu% ex: cpu > 4.2",
|
"cpu, cpu% ex: cpu > 4.2",
|
||||||
"mem, mem% ex: mem < 4.2",
|
"mem, mem% ex: mem < 4.2",
|
||||||
"memb ex: memb < 100 kb",
|
"memb ex: memb < 100 kb",
|
||||||
"read, r/s ex: read >= 1 b",
|
"read, r/s, rps ex: read >= 1 b",
|
||||||
"write, w/s ex: write <= 1 tb",
|
"write, w/s, wps ex: write <= 1 tb",
|
||||||
"tread, t.read ex: tread = 1",
|
"tread, t.read ex: tread = 1",
|
||||||
"twrite, t.write ex: twrite = 1",
|
"twrite, t.write ex: twrite = 1",
|
||||||
"user ex: user = root",
|
"user ex: user = root",
|
||||||
@ -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.
|
# How much data is stored at once in terms of time.
|
||||||
#retention = "10m"
|
#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
|
# These are all the components that support custom theming. Note that colour support
|
||||||
# will depend on terminal support.
|
# will depend on terminal support.
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ use std::{
|
|||||||
|
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
|
use indexmap::IndexSet;
|
||||||
use layout_options::*;
|
use layout_options::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -23,12 +24,15 @@ use crate::{
|
|||||||
utils::error::{self, BottomError},
|
utils::error::{self, BottomError},
|
||||||
widgets::{
|
widgets::{
|
||||||
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
||||||
ProcWidgetMode, ProcWidgetState, TempWidgetState,
|
ProcColumn, ProcTableConfig, ProcWidgetMode, ProcWidgetState, TempWidgetState,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod layout_options;
|
pub mod layout_options;
|
||||||
|
|
||||||
|
pub mod process_columns;
|
||||||
|
use self::process_columns::ProcessConfig;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
@ -40,6 +44,7 @@ pub struct Config {
|
|||||||
pub mount_filter: Option<IgnoreList>,
|
pub mount_filter: Option<IgnoreList>,
|
||||||
pub temp_filter: Option<IgnoreList>,
|
pub temp_filter: Option<IgnoreList>,
|
||||||
pub net_filter: Option<IgnoreList>,
|
pub net_filter: Option<IgnoreList>,
|
||||||
|
pub processes: Option<ProcessConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, TypedBuilder)]
|
#[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_scale_type = get_network_scale_type(matches, config);
|
||||||
let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, matches, config);
|
let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, matches, config);
|
||||||
|
|
||||||
|
let proc_columns: Option<IndexSet<ProcColumn>> = {
|
||||||
|
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 {
|
let app_config_fields = AppConfigFields {
|
||||||
update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config)
|
update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config)
|
||||||
.context("Update 'rate' in your config file.")?,
|
.context("Update 'rate' in your config file.")?,
|
||||||
@ -247,6 +270,14 @@ pub fn build_app(
|
|||||||
retention_ms,
|
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 row in &widget_layout.rows {
|
||||||
for col in &row.children {
|
for col in &row.children {
|
||||||
for col_row in &col.children {
|
for col_row in &col.children {
|
||||||
@ -325,12 +356,9 @@ pub fn build_app(
|
|||||||
ProcWidgetState::new(
|
ProcWidgetState::new(
|
||||||
&app_config_fields,
|
&app_config_fields,
|
||||||
mode,
|
mode,
|
||||||
is_case_sensitive,
|
table_config,
|
||||||
is_match_whole_word,
|
|
||||||
is_use_regex,
|
|
||||||
show_memory_as_values,
|
|
||||||
is_default_command,
|
|
||||||
colours,
|
colours,
|
||||||
|
&proc_columns,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
83
src/options/process_columns.rs
Normal file
83
src/options/process_columns.rs
Normal file
@ -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<Vec<ProcColumn>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<ProcessConfig>(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]));
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
use std::{borrow::Cow, collections::BTreeMap};
|
use std::{borrow::Cow, collections::BTreeMap};
|
||||||
|
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
|
use indexmap::IndexSet;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -71,6 +72,50 @@ type ProcessTable = SortDataTable<ProcWidgetData, ProcColumn>;
|
|||||||
type SortTable = DataTable<Cow<'static, str>, SortTableColumn>;
|
type SortTable = DataTable<Cow<'static, str>, SortTableColumn>;
|
||||||
type StringPidMap = HashMap<String, Vec<Pid>>;
|
type StringPidMap = HashMap<String, Vec<Pid>>;
|
||||||
|
|
||||||
|
fn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {
|
||||||
|
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 struct ProcWidgetState {
|
||||||
pub mode: ProcWidgetMode,
|
pub mode: ProcWidgetMode,
|
||||||
|
|
||||||
@ -83,26 +128,24 @@ pub struct ProcWidgetState {
|
|||||||
/// The state of the togglable table that controls sorting.
|
/// The state of the togglable table that controls sorting.
|
||||||
pub sort_table: SortTable,
|
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<ProcWidgetColumn>,
|
||||||
|
|
||||||
/// A name-to-pid mapping.
|
/// A name-to-pid mapping.
|
||||||
pub id_pid_map: StringPidMap,
|
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 is_sort_open: bool,
|
||||||
pub force_rerender: bool,
|
pub force_rerender: bool,
|
||||||
pub force_update_data: bool,
|
pub force_update_data: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcWidgetState {
|
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 {
|
fn new_sort_table(config: &AppConfigFields, colours: &CanvasColours) -> SortTable {
|
||||||
const COLUMNS: [Column<SortTableColumn>; 1] = [Column::hard(SortTableColumn, 7)];
|
const COLUMNS: [Column<SortTableColumn>; 1] = [Column::hard(SortTableColumn, 7)];
|
||||||
|
|
||||||
@ -114,54 +157,15 @@ impl ProcWidgetState {
|
|||||||
show_table_scroll_position: false,
|
show_table_scroll_position: false,
|
||||||
show_current_entry_when_unfocused: false,
|
show_current_entry_when_unfocused: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let styling = DataTableStyling::from_colours(colours);
|
let styling = DataTableStyling::from_colours(colours);
|
||||||
|
|
||||||
DataTable::new(COLUMNS, props, styling)
|
DataTable::new(COLUMNS, props, styling)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_process_table(
|
fn new_process_table(
|
||||||
config: &AppConfigFields, colours: &CanvasColours, mode: &ProcWidgetMode, is_count: bool,
|
config: &AppConfigFields, colours: &CanvasColours, columns: Vec<SortColumn<ProcColumn>>,
|
||||||
is_command: bool, show_memory_as_values: bool,
|
default_index: usize, default_order: SortOrder,
|
||||||
) -> ProcessTable {
|
) -> 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 {
|
let inner_props = DataTableProps {
|
||||||
title: Some(" Processes ".into()),
|
title: Some(" Processes ".into()),
|
||||||
table_gap: config.table_gap,
|
table_gap: config.table_gap,
|
||||||
@ -175,43 +179,102 @@ impl ProcWidgetState {
|
|||||||
sort_index: default_index,
|
sort_index: default_index,
|
||||||
order: default_order,
|
order: default_order,
|
||||||
};
|
};
|
||||||
|
|
||||||
let styling = DataTableStyling::from_colours(colours);
|
let styling = DataTableStyling::from_colours(colours);
|
||||||
|
|
||||||
DataTable::new_sortable(columns, props, styling)
|
DataTable::new_sortable(columns, props, styling)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
config: &AppConfigFields, mode: ProcWidgetMode, is_case_sensitive: bool,
|
config: &AppConfigFields, mode: ProcWidgetMode, table_config: ProcTableConfig,
|
||||||
is_match_whole_word: bool, is_use_regex: bool, show_memory_as_values: bool,
|
colours: &CanvasColours, config_columns: &Option<IndexSet<ProcColumn>>,
|
||||||
is_command: bool, colours: &CanvasColours,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let process_search_state = {
|
let process_search_state = {
|
||||||
let mut pss = ProcessSearchState::default();
|
let mut pss = ProcessSearchState::default();
|
||||||
|
|
||||||
if is_case_sensitive {
|
if table_config.is_case_sensitive {
|
||||||
// By default it's off
|
// By default it's off.
|
||||||
pss.search_toggle_ignore_case();
|
pss.search_toggle_ignore_case();
|
||||||
}
|
}
|
||||||
if is_match_whole_word {
|
if table_config.is_match_whole_word {
|
||||||
pss.search_toggle_whole_word();
|
pss.search_toggle_whole_word();
|
||||||
}
|
}
|
||||||
if is_use_regex {
|
if table_config.is_use_regex {
|
||||||
pss.search_toggle_regex();
|
pss.search_toggle_regex();
|
||||||
}
|
}
|
||||||
|
|
||||||
pss
|
pss
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let columns: Vec<SortColumn<ProcColumn>> = {
|
||||||
|
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_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::<IndexSet<_>>();
|
||||||
|
|
||||||
|
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 sort_table = Self::new_sort_table(config, colours);
|
||||||
let table = Self::new_process_table(
|
let table = Self::new_process_table(
|
||||||
config,
|
config,
|
||||||
colours,
|
colours,
|
||||||
&mode,
|
columns,
|
||||||
is_count,
|
default_sort_index,
|
||||||
is_command,
|
default_sort_order,
|
||||||
show_memory_as_values,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let id_pid_map = HashMap::default();
|
let id_pid_map = HashMap::default();
|
||||||
@ -221,10 +284,13 @@ impl ProcWidgetState {
|
|||||||
table,
|
table,
|
||||||
sort_table,
|
sort_table,
|
||||||
id_pid_map,
|
id_pid_map,
|
||||||
|
column_mapping,
|
||||||
is_sort_open: false,
|
is_sort_open: false,
|
||||||
mode,
|
mode,
|
||||||
force_rerender: true,
|
force_rerender: true,
|
||||||
force_update_data: false,
|
force_update_data: false,
|
||||||
|
default_sort_index,
|
||||||
|
default_sort_order,
|
||||||
};
|
};
|
||||||
table.sort_table.set_data(table.column_text());
|
table.sort_table.set_data(table.column_text());
|
||||||
|
|
||||||
@ -232,17 +298,21 @@ impl ProcWidgetState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_using_command(&self) -> bool {
|
pub fn is_using_command(&self) -> bool {
|
||||||
|
self.column_mapping
|
||||||
|
.get_index_of(&ProcWidgetColumn::ProcNameOrCmd)
|
||||||
|
.and_then(|index| {
|
||||||
self.table
|
self.table
|
||||||
.columns
|
.columns
|
||||||
.get(ProcWidgetState::PROC_NAME_OR_CMD)
|
.get(index)
|
||||||
.map(|col| matches!(col.inner(), ProcColumn::Command))
|
.map(|col| matches!(col.inner(), ProcColumn::Command))
|
||||||
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_mem_percent(&self) -> bool {
|
pub fn is_mem_percent(&self) -> bool {
|
||||||
self.table
|
self.column_mapping
|
||||||
.columns
|
.get_index_of(&ProcWidgetColumn::Mem)
|
||||||
.get(ProcWidgetState::MEM)
|
.and_then(|index| self.table.columns.get(index))
|
||||||
.map(|col| matches!(col.inner(), ProcColumn::MemoryPercent))
|
.map(|col| matches!(col.inner(), ProcColumn::MemoryPercent))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
@ -594,7 +664,8 @@ impl ProcWidgetState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_mem_percentage(&mut self) {
|
pub fn toggle_mem_percentage(&mut self) {
|
||||||
if let Some(mem) = self.get_mut_proc_col(Self::MEM) {
|
if let Some(index) = self.column_mapping.get_index_of(&ProcWidgetColumn::Mem) {
|
||||||
|
if let Some(mem) = self.get_mut_proc_col(index) {
|
||||||
match mem {
|
match mem {
|
||||||
ProcColumn::MemoryVal => {
|
ProcColumn::MemoryVal => {
|
||||||
*mem = ProcColumn::MemoryPercent;
|
*mem = ProcColumn::MemoryPercent;
|
||||||
@ -609,6 +680,7 @@ impl ProcWidgetState {
|
|||||||
self.force_data_update();
|
self.force_data_update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Forces an update of the data stored.
|
/// Forces an update of the data stored.
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -623,31 +695,37 @@ impl ProcWidgetState {
|
|||||||
self.force_update_data = true;
|
self.force_update_data = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the selected column as hidden, and automatically resets the selected column to CPU
|
/// Marks the selected column as hidden, and automatically resets the selected column to the default
|
||||||
/// and descending if that column was selected.
|
/// sort index and order.
|
||||||
fn hide_column(&mut self, index: usize) {
|
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) {
|
if let Some(col) = self.table.columns.get_mut(index) {
|
||||||
col.is_hidden = true;
|
col.is_hidden = true;
|
||||||
|
|
||||||
if self.table.sort_index() == index {
|
if self.table.sort_index() == index {
|
||||||
self.table.set_sort_index(Self::CPU);
|
self.table.set_sort_index(self.default_sort_index);
|
||||||
self.table.set_order(SortOrder::Descending);
|
self.table.set_order(self.default_sort_order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the selected column as shown.
|
/// Marks the selected column as shown.
|
||||||
fn show_column(&mut self, index: usize) {
|
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) {
|
if let Some(col) = self.table.columns.get_mut(index) {
|
||||||
col.is_hidden = false;
|
col.is_hidden = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Select a column. If the column is already selected, then just toggle the sort order.
|
/// 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) {
|
pub fn select_column(&mut self, column: ProcWidgetColumn) {
|
||||||
self.table.set_sort_index(new_sort_index);
|
if let Some(index) = self.column_mapping.get_index_of(&column) {
|
||||||
|
self.table.set_sort_index(index);
|
||||||
self.force_data_update();
|
self.force_data_update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_current_tree_branch_entry(&mut self) {
|
pub fn toggle_current_tree_branch_entry(&mut self) {
|
||||||
if let ProcWidgetMode::Tree { collapsed_pids } = &mut self.mode {
|
if let ProcWidgetMode::Tree { collapsed_pids } = &mut self.mode {
|
||||||
@ -663,7 +741,11 @@ impl ProcWidgetState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_command(&mut self) {
|
pub fn toggle_command(&mut self) {
|
||||||
if let Some(col) = self.table.columns.get_mut(Self::PROC_NAME_OR_CMD) {
|
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();
|
let inner = col.inner_mut();
|
||||||
match inner {
|
match inner {
|
||||||
ProcColumn::Name => {
|
ProcColumn::Name => {
|
||||||
@ -687,6 +769,7 @@ impl ProcWidgetState {
|
|||||||
self.force_rerender_and_update();
|
self.force_rerender_and_update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggles the appropriate columns/settings when tab is pressed.
|
/// Toggles the appropriate columns/settings when tab is pressed.
|
||||||
///
|
///
|
||||||
@ -694,27 +777,31 @@ impl ProcWidgetState {
|
|||||||
/// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden
|
/// 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".
|
/// (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
|
/// Otherwise, if count is disabled, then if the columns exist, the User and State columns should be re-enabled,
|
||||||
/// to [`ProcWidgetMode::Normal`].
|
/// and the mode switched to [`ProcWidgetMode::Normal`].
|
||||||
pub fn on_tab(&mut self) {
|
pub fn toggle_tab(&mut self) {
|
||||||
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
|
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
|
||||||
if let Some(sort_col) = self.table.columns.get_mut(Self::PID_OR_COUNT) {
|
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();
|
let col = sort_col.inner_mut();
|
||||||
match col {
|
match col {
|
||||||
ProcColumn::Pid => {
|
ProcColumn::Pid => {
|
||||||
*col = ProcColumn::Count;
|
*col = ProcColumn::Count;
|
||||||
sort_col.default_order = SortOrder::Descending;
|
sort_col.default_order = SortOrder::Descending;
|
||||||
|
|
||||||
self.hide_column(Self::USER);
|
self.hide_column(ProcWidgetColumn::User);
|
||||||
self.hide_column(Self::STATE);
|
self.hide_column(ProcWidgetColumn::State);
|
||||||
self.mode = ProcWidgetMode::Grouped;
|
self.mode = ProcWidgetMode::Grouped;
|
||||||
}
|
}
|
||||||
ProcColumn::Count => {
|
ProcColumn::Count => {
|
||||||
*col = ProcColumn::Pid;
|
*col = ProcColumn::Pid;
|
||||||
sort_col.default_order = SortOrder::Ascending;
|
sort_col.default_order = SortOrder::Ascending;
|
||||||
|
|
||||||
self.show_column(Self::USER);
|
self.show_column(ProcWidgetColumn::User);
|
||||||
self.show_column(Self::STATE);
|
self.show_column(ProcWidgetColumn::State);
|
||||||
self.mode = ProcWidgetMode::Normal;
|
self.mode = ProcWidgetMode::Normal;
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@ -725,6 +812,7 @@ impl ProcWidgetState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn column_text(&self) -> Vec<Cow<'static, str>> {
|
pub fn column_text(&self) -> Vec<Cow<'static, str>> {
|
||||||
self.table
|
self.table
|
||||||
@ -944,4 +1032,213 @@ mod test {
|
|||||||
data.iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
data.iter().map(|d| (d.pid)).collect::<Vec<_>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_columns(table: &ProcessTable) -> Vec<ProcColumn> {
|
||||||
|
table
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| {
|
||||||
|
if c.is_hidden() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(*c.inner())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
use std::{borrow::Cow, cmp::Reverse};
|
use std::{borrow::Cow, cmp::Reverse};
|
||||||
|
|
||||||
|
use serde::{de::Error, Deserialize, Serialize};
|
||||||
|
|
||||||
use super::ProcWidgetData;
|
use super::ProcWidgetData;
|
||||||
use crate::{
|
use crate::{
|
||||||
components::data_table::{ColumnHeader, SortsRow},
|
components::data_table::{ColumnHeader, SortsRow},
|
||||||
utils::gen_util::sort_partial_fn,
|
utils::gen_util::sort_partial_fn,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
|
||||||
pub enum ProcColumn {
|
pub enum ProcColumn {
|
||||||
CpuPercent,
|
CpuPercent,
|
||||||
MemoryVal,
|
MemoryVal,
|
||||||
@ -23,6 +25,40 @@ pub enum ProcColumn {
|
|||||||
User,
|
User,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ProcColumn {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ColumnHeader for ProcColumn {
|
impl ColumnHeader for ProcColumn {
|
||||||
fn text(&self) -> Cow<'static, str> {
|
fn text(&self) -> Cow<'static, str> {
|
||||||
match self {
|
match self {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user