From 7496bbdd547ee81ce851899e6a8afde1d2390eeb Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sat, 21 Mar 2026 17:23:16 -0400 Subject: [PATCH] feature: configurable default sort column for temperature and disk table widgets. (#2003) Adds support for configuring the default sort column for temperature and disk widgets via config file. --- CHANGELOG.md | 16 +- .../configuration/config-file/disk-table.md | 12 ++ .../config-file/temperature-table.md | 9 + sample_configs/default_config.toml | 16 +- schema/nightly/bottom.json | 132 ++++++++++-- src/app.rs | 11 +- src/bin/schema.rs | 81 ++++---- src/constants.rs | 16 ++ src/options.rs | 8 + src/options/config/disk.rs | 8 +- src/options/config/temperature.rs | 6 + src/widgets/disk_table.rs | 195 +++++++++++------- src/widgets/temperature_table.rs | 51 ++++- tests/integration/invalid_config_tests.rs | 19 ++ tests/integration/valid_config_tests.rs | 5 + .../invalid_disk_default_sort.toml | 2 + .../invalid_temp_default_sort.toml | 2 + .../valid_configs/temp_disk_sort_columns.toml | 5 + 18 files changed, 432 insertions(+), 162 deletions(-) create mode 100644 tests/invalid_configs/invalid_disk_default_sort.toml create mode 100644 tests/invalid_configs/invalid_temp_default_sort.toml create mode 100644 tests/valid_configs/temp_disk_sort_columns.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 713ab38c..2e071ba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ That said, these are more guidelines rather than hard rules, though the project ### Features - [#1938](https://github.com/ClementTsang/bottom/pull/1938), [#1980](https://github.com/ClementTsang/bottom/pull/1980): Report average packet size and packet rate. +- [#2003](https://github.com/ClementTsang/bottom/pull/2003): Configurable default sort column for temperature and disk table widgets. ### Other @@ -565,8 +566,7 @@ That said, these are more guidelines rather than hard rules, though the project ### Bug Fixes -- [#575](https://github.com/ClementTsang/bottom/pull/575): Updates the procfs library to not crash on kernel version > - 255. +- [#575](https://github.com/ClementTsang/bottom/pull/575): Updates the procfs library to not crash on kernel version > 255. ### Internal Changes @@ -777,7 +777,6 @@ That said, these are more guidelines rather than hard rules, though the project config. - [#223](https://github.com/ClementTsang/bottom/pull/223): Add tree mode for processes. - - [#312](https://github.com/ClementTsang/bottom/pull/312): Add a `tree` flag to default to the tree mode. - [#269](https://github.com/ClementTsang/bottom/pull/269): Add simple indicator for when data updating is frozen. @@ -900,13 +899,12 @@ That said, these are more guidelines rather than hard rules, though the project clunky to use, was not really useful, and hard to work with large core counts. Furthermore: - - `show_disabled_data` option and flag is removed. - Average CPU is now on by _default_. You can disable it via `-a, --hide_avg_cpu` or `hide_avg_cpu = true`. - Make highlighted CPU persist even if widget is not selected - this should help make it easier to know what CPU you - are looking at even if you aren't currently on the CPU widget. + are looking at even if you aren't currently on the CPU widget. ### Bug Fixes @@ -982,7 +980,6 @@ is equivalent to: Powershell colour conflicts. - Updated the widget type keyword list to accept the following keywords as existing types: - - `"memory"` - `"network"` - `"process"` @@ -990,15 +987,14 @@ is equivalent to: - `"temperature"` - [#117](https://github.com/ClementTsang/bottom/issues/117): Update tui to 0.9: - - Removed an (undocumented) feature in allowing modifying total RX/TX colours. This is mainly due to the legend - change. + change. - Use custom legend-hiding to stop hiding legends for memory and network widgets. - In addition, changed to using only legends within the graph for network, as well as redesigned the legend. - The old legend style can still be used via the `--use_old_network_legend` flag or `use_old_network_legend = true` - config option. + The old legend style can still be used via the `--use_old_network_legend` flag or `use_old_network_legend = true` + config option. - Allow for option to hide the header gap on tables via `--hide_table_gap` or `hide_table_gap = true`. diff --git a/docs/content/configuration/config-file/disk-table.md b/docs/content/configuration/config-file/disk-table.md index 99fb7676..297ad164 100644 --- a/docs/content/configuration/config-file/disk-table.md +++ b/docs/content/configuration/config-file/disk-table.md @@ -10,6 +10,18 @@ You can configure which columns are shown by the disk table widget by setting th columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"] ``` +## Default Sort Order + +You can customize the default sort order (by default, it sorts by disk name). For example, to sort by the read rate: + +```toml +[disk] +default_sort = "R/s" +``` + +You can use any valid [column](#columns) name here (e.g. "Disk", "Mount", etc.). Note that if you put a column name that +is not actually used, the default sort will just be the first column shown. + ## Filtering Entries You can filter out what entries to show by configuring `[disk.name_filter]` and `[disk.mount_filter]` to filter by name and mount point respectively. In particular, diff --git a/docs/content/configuration/config-file/temperature-table.md b/docs/content/configuration/config-file/temperature-table.md index 2becd174..694fc227 100644 --- a/docs/content/configuration/config-file/temperature-table.md +++ b/docs/content/configuration/config-file/temperature-table.md @@ -1,5 +1,14 @@ # Temperature Table +## Default Sort Order + +You can customize the default sort order (by default, it sorts by temperature sensor name). For example, to sort by temperature: + +```toml +[temperature] +default_sort = "Temp" +``` + ## Filtering Entries You can filter out what entries to show by configuring `[temperature.sensor_filter]`. In particular you can set a list of things to filter with by setting `list`, and configure how that list is processed with the other options. diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index 29da831d..44286645 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -135,7 +135,7 @@ #[processes] # The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built): # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority -#columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%", "Priority", "Nice"] +#columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%", "Priority"] # Gather process child thread information #get_threads = false @@ -149,10 +149,17 @@ # Disk widget configuration #[disk] + # The columns shown by the process widget. The following columns are supported: # Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s #columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"] +# The default sort type. Can be one of the following: +# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s +# +# Defaults to "Disk". +#default_sort = "Disk" + # By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you # don't want to see them. An example use case is provided below. #[disk.name_filter] @@ -191,6 +198,13 @@ # Temperature widget configuration #[temperature] + +# The default sort type. Can be one of the following: +# Temp, Temperature, Sensor +# +# Defaults to "Sensor". +#default_sort = "Sensor" + # By default, there are no temperature sensor filters enabled. An example use case is provided below. #[temperature.sensor_filter] # Whether to ignore any matches. Defaults to true. diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index 1c5618da..8ba11c7c 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -184,24 +184,6 @@ } } }, - "DiskColumn": { - "type": "string", - "enum": [ - "Disk", - "Free", - "Free%", - "Mount", - "R/s", - "Read", - "Rps", - "Total", - "Used", - "Used%", - "W/s", - "Wps", - "Write" - ] - }, "DiskConfig": { "description": "Disk configuration.", "type": "object", @@ -213,9 +195,20 @@ "null" ], "items": { - "$ref": "#/$defs/DiskColumn" + "$ref": "#/$defs/DiskWidgetColumn" } }, + "default_sort": { + "description": "The default sort column.", + "anyOf": [ + { + "$ref": "#/$defs/DiskWidgetColumn" + }, + { + "type": "null" + } + ] + }, "mount_filter": { "description": "A filter over the mount names.", "anyOf": [ @@ -240,6 +233,37 @@ } } }, + "DiskWidgetColumn": { + "type": "string", + "enum": [ + "Disk", + "Free", + "Free%", + "Mount", + "R/s", + "Read", + "Rps", + "Total", + "Used", + "Used%", + "W/s", + "Wps", + "Write", + "disk", + "free", + "free%", + "mount", + "r/s", + "read", + "rps", + "total", + "used", + "used%", + "w/s", + "wps", + "write" + ] + }, "FinalWidget": { "description": "Represents a widget.", "type": "object", @@ -494,6 +518,12 @@ } ] }, + "show_packets": { + "type": [ + "boolean", + "null" + ] + }, "show_table_scroll_position": { "type": [ "boolean", @@ -680,6 +710,13 @@ "type": "null" } ] + }, + "show_packets": { + "description": "Whether to show packets information (packet rate and average packet size).", + "type": [ + "boolean", + "null" + ] } } }, @@ -769,7 +806,40 @@ "Virtual Memory", "W/s", "Wps", - "Write" + "Write", + "command", + "count", + "cpu%", + "gmem", + "gmem%", + "gpu%", + "mem", + "mem%", + "memory", + "memory%", + "name", + "nice", + "pid", + "priority", + "r/s", + "read", + "rps", + "state", + "t.read", + "t.write", + "time", + "total read", + "total write", + "tread", + "twrite", + "user", + "virt", + "virtmem", + "virtual", + "virtual memory", + "w/s", + "wps", + "write" ] }, "ProcessesConfig": { @@ -946,6 +1016,17 @@ "description": "Temperature configuration.", "type": "object", "properties": { + "default_sort": { + "description": "The default sort column.", + "anyOf": [ + { + "$ref": "#/$defs/TempWidgetColumn" + }, + { + "type": "null" + } + ] + }, "sensor_filter": { "description": "A filter over the sensor names.", "anyOf": [ @@ -959,6 +1040,17 @@ } } }, + "TempWidgetColumn": { + "type": "string", + "enum": [ + "Sensor", + "Temp", + "Temperature", + "sensor", + "temp", + "temperature" + ] + }, "TextStyleConfig": { "description": "A style for text.", "anyOf": [ diff --git a/src/app.rs b/src/app.rs index 86103a6b..e5f8e554 100644 --- a/src/app.rs +++ b/src/app.rs @@ -19,7 +19,9 @@ use crate::{ }, constants, utils::data_units::DataUnit, - widgets::{ProcWidgetColumn, ProcWidgetMode, TreeCollapsed}, + widgets::{ + DiskWidgetColumn, ProcWidgetColumn, ProcWidgetMode, TempWidgetColumn, TreeCollapsed, + }, }; const STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds @@ -33,7 +35,10 @@ pub enum AxisScaling { /// AppConfigFields is meant to cover basic fields that would normally be set /// by config files or launch options. -#[derive(Debug, Default, Eq, PartialEq)] +/// +/// TODO: Clean this up, we probably don't need to have this duplicated. +#[derive(Debug, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct AppConfigFields { pub update_rate: u64, pub temperature_type: TemperatureType, @@ -72,6 +77,8 @@ pub struct AppConfigFields { pub retention_ms: u64, pub dedicated_average_row: bool, pub default_tree_collapse: bool, + pub default_temp_sort_column: Option, + pub default_disk_sort_column: Option, } /// For filtering out information diff --git a/src/bin/schema.rs b/src/bin/schema.rs index ca41ca30..a2becbb6 100644 --- a/src/bin/schema.rs +++ b/src/bin/schema.rs @@ -16,52 +16,49 @@ struct SchemaOptions { version: Option, } +macro_rules! generate_column_schemas { + ($struct_name:literal, $variants:expr, $schema:expr) => { + match $schema + .as_object_mut() + .unwrap() + .get_mut("$defs") + .unwrap() + .get_mut($struct_name) + .unwrap() + { + Value::Object(original) => { + let enums = original.get_mut("enum").unwrap(); + *enums = $variants + .iter() + .flat_map(|variant| variant.get_schema_names()) + .flat_map(|variant| [variant.to_string(), variant.to_lowercase()]) + .sorted() // Remember that dedup only works if it's sorted... + .dedup() + .map(|variant| serde_json::Value::String(variant)) // Have to do it after as it doesn't implement partialeq/eq + .collect(); + + Ok(()) + } + _ => Err(anyhow::anyhow!("missing proc columns definition")), + } + }; +} + fn generate_schema(schema_options: SchemaOptions) -> anyhow::Result<()> { let mut schema = schemars::schema_for!(config::Config); { // TODO: Maybe make this case insensitive? See https://stackoverflow.com/a/68639341 - - match schema - .as_object_mut() - .unwrap() - .get_mut("$defs") - .unwrap() - .get_mut("ProcColumn") - .unwrap() - { - Value::Object(proc_columns) => { - let enums = proc_columns.get_mut("enum").unwrap(); - *enums = widgets::ProcColumn::VARIANTS - .iter() - .flat_map(|var| var.get_schema_names()) - .sorted() - .map(|v| serde_json::Value::String(v.to_string())) - .dedup() - .collect(); - } - _ => anyhow::bail!("missing proc columns definition"), - } - - match schema - .as_object_mut() - .unwrap() - .get_mut("$defs") - .unwrap() - .get_mut("DiskColumn") - .unwrap() - { - Value::Object(disk_columns) => { - let enums = disk_columns.get_mut("enum").unwrap(); - *enums = widgets::DiskColumn::VARIANTS - .iter() - .flat_map(|var| var.get_schema_names()) - .sorted() - .map(|v| serde_json::Value::String(v.to_string())) - .dedup() - .collect(); - } - _ => anyhow::bail!("missing disk columns definition"), - } + generate_column_schemas!("ProcColumn", widgets::ProcColumn::VARIANTS, schema)?; + generate_column_schemas!( + "DiskWidgetColumn", + widgets::DiskWidgetColumn::VARIANTS, + schema + )?; + generate_column_schemas!( + "TempWidgetColumn", + widgets::TempWidgetColumn::VARIANTS, + schema + )?; } let version = schema_options.version.unwrap_or("nightly".to_string()); diff --git a/src/constants.rs b/src/constants.rs index a413dfc3..ba69a0a3 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -395,10 +395,17 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott # Disk widget configuration #[disk] + # The columns shown by the process widget. The following columns are supported: # Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s #columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"] +# The default sort type. Can be one of the following: +# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s +# +# Defaults to "Disk". +#default_sort = "Disk" + # By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you # don't want to see them. An example use case is provided below. #[disk.name_filter] @@ -437,6 +444,13 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott # Temperature widget configuration #[temperature] + +# The default sort type. Can be one of the following: +# Temp, Temperature, Sensor +# +# Defaults to "Sensor". +#default_sort = "Sensor" + # By default, there are no temperature sensor filters enabled. An example use case is provided below. #[temperature.sensor_filter] # Whether to ignore any matches. Defaults to true. @@ -595,10 +609,12 @@ mod test { use crate::options::Config; + // Trim off the starting comment if it's a "#" directly following an alphabetical character or '['. let default_config = Regex::new(r"(?m)^#([a-zA-Z\[])") .unwrap() .replace_all(CONFIG_TEXT, "$1"); + // Then, trim off anything that has more than 2 spaces + alphabetical character or '[' following a "#". let default_config = Regex::new(r"(?m)^#(\s\s+)([a-zA-Z\[])") .unwrap() .replace_all(&default_config, "$2"); diff --git a/src/options.rs b/src/options.rs index b85073f7..a6558c26 100644 --- a/src/options.rs +++ b/src/options.rs @@ -337,6 +337,14 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL default_tree_collapse: is_default_tree_collapsed, #[cfg(feature = "zfs")] free_arc, + default_temp_sort_column: config + .temperature + .as_ref() + .and_then(|cfg| cfg.default_sort.to_owned()), + default_disk_sort_column: config + .disk + .as_ref() + .and_then(|cfg| cfg.default_sort.to_owned()), }; let table_config = ProcTableConfig { diff --git a/src/options/config/disk.rs b/src/options/config/disk.rs index 2e1f7a4e..8f07ac2a 100644 --- a/src/options/config/disk.rs +++ b/src/options/config/disk.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use super::IgnoreList; -use crate::options::DiskColumn; +use crate::options::DiskWidgetColumn; /// Disk configuration. #[derive(Clone, Debug, Default, Deserialize)] @@ -16,7 +16,11 @@ pub(crate) struct DiskConfig { /// A list of disk widget columns. #[serde(default)] - pub(crate) columns: Option>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets + pub(crate) columns: Option>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets + + /// The default sort column. + #[serde(default)] + pub(crate) default_sort: Option, } #[cfg(test)] diff --git a/src/options/config/temperature.rs b/src/options/config/temperature.rs index 8ae57a96..bf457fac 100644 --- a/src/options/config/temperature.rs +++ b/src/options/config/temperature.rs @@ -1,5 +1,7 @@ use serde::Deserialize; +use crate::widgets::TempWidgetColumn; + use super::IgnoreList; /// Temperature configuration. @@ -9,4 +11,8 @@ use super::IgnoreList; pub(crate) struct TempConfig { /// A filter over the sensor names. pub(crate) sensor_filter: Option, + + /// The default sort column. + #[serde(default)] + pub(crate) default_sort: Option, } diff --git a/src/widgets/disk_table.rs b/src/widgets/disk_table.rs index 9e2e64f2..88cc31fc 100644 --- a/src/widgets/disk_table.rs +++ b/src/widgets/disk_table.rs @@ -96,13 +96,12 @@ impl DiskWidgetData { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "generate_schema", derive(schemars::JsonSchema, strum::VariantArray) )] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub enum DiskColumn { +pub enum DiskWidgetColumn { Disk, Mount, Used, @@ -114,22 +113,22 @@ pub enum DiskColumn { IoWrite, } -impl<'de> Deserialize<'de> for DiskColumn { +impl<'de> Deserialize<'de> for DiskWidgetColumn { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let value = String::deserialize(deserializer)?.to_lowercase(); match value.as_str() { - "disk" => Ok(DiskColumn::Disk), - "mount" => Ok(DiskColumn::Mount), - "used" => Ok(DiskColumn::Used), - "free" => Ok(DiskColumn::Free), - "total" => Ok(DiskColumn::Total), - "usedpercent" | "used%" => Ok(DiskColumn::UsedPercent), - "freepercent" | "free%" => Ok(DiskColumn::FreePercent), - "r/s" => Ok(DiskColumn::IoRead), - "w/s" => Ok(DiskColumn::IoWrite), + "disk" => Ok(DiskWidgetColumn::Disk), + "mount" => Ok(DiskWidgetColumn::Mount), + "used" => Ok(DiskWidgetColumn::Used), + "free" => Ok(DiskWidgetColumn::Free), + "total" => Ok(DiskWidgetColumn::Total), + "usedpercent" | "used%" => Ok(DiskWidgetColumn::UsedPercent), + "freepercent" | "free%" => Ok(DiskWidgetColumn::FreePercent), + "r/s" => Ok(DiskWidgetColumn::IoRead), + "w/s" => Ok(DiskWidgetColumn::IoWrite), _ => Err(serde::de::Error::custom( "doesn't match any disk column name", )), @@ -137,45 +136,45 @@ impl<'de> Deserialize<'de> for DiskColumn { } } -impl DiskColumn { +impl DiskWidgetColumn { /// An ugly hack to generate the JSON schema. #[cfg(feature = "generate_schema")] pub fn get_schema_names(&self) -> &[&'static str] { match self { - DiskColumn::Disk => &["Disk"], - DiskColumn::Mount => &["Mount"], - DiskColumn::Used => &["Used"], - DiskColumn::Free => &["Free"], - DiskColumn::Total => &["Total"], - DiskColumn::UsedPercent => &["Used%"], - DiskColumn::FreePercent => &["Free%"], - DiskColumn::IoRead => &["R/s", "Read", "Rps"], - DiskColumn::IoWrite => &["W/s", "Write", "Wps"], + DiskWidgetColumn::Disk => &["Disk"], + DiskWidgetColumn::Mount => &["Mount"], + DiskWidgetColumn::Used => &["Used"], + DiskWidgetColumn::Free => &["Free"], + DiskWidgetColumn::Total => &["Total"], + DiskWidgetColumn::UsedPercent => &["Used%"], + DiskWidgetColumn::FreePercent => &["Free%"], + DiskWidgetColumn::IoRead => &["R/s", "Read", "Rps"], + DiskWidgetColumn::IoWrite => &["W/s", "Write", "Wps"], } } } -impl ColumnHeader for DiskColumn { +impl ColumnHeader for DiskWidgetColumn { fn text(&self) -> Cow<'static, str> { match self { - DiskColumn::Disk => "Disk(d)", - DiskColumn::Mount => "Mount(m)", - DiskColumn::Used => "Used(u)", - DiskColumn::Free => "Free(n)", - DiskColumn::Total => "Total(t)", - DiskColumn::UsedPercent => "Used%(p)", - DiskColumn::FreePercent => "Free%", - DiskColumn::IoRead => "R/s(r)", - DiskColumn::IoWrite => "W/s(w)", + DiskWidgetColumn::Disk => "Disk(d)", + DiskWidgetColumn::Mount => "Mount(m)", + DiskWidgetColumn::Used => "Used(u)", + DiskWidgetColumn::Free => "Free(n)", + DiskWidgetColumn::Total => "Total(t)", + DiskWidgetColumn::UsedPercent => "Used%(p)", + DiskWidgetColumn::FreePercent => "Free%", + DiskWidgetColumn::IoRead => "R/s(r)", + DiskWidgetColumn::IoWrite => "W/s(w)", } .into() } } -impl DataToCell for DiskWidgetData { +impl DataToCell for DiskWidgetData { // FIXME: (points_rework_v1) Can we change the return type to 'a instead of 'static? fn to_cell_text( - &self, column: &DiskColumn, _calculated_width: NonZeroU16, + &self, column: &DiskWidgetColumn, _calculated_width: NonZeroU16, ) -> Option> { fn percent_string(value: Option) -> Cow<'static, str> { match value { @@ -185,21 +184,23 @@ impl DataToCell for DiskWidgetData { } let text = match column { - DiskColumn::Disk => self.name.clone().into(), - DiskColumn::Mount => self.mount_point.clone().into(), - DiskColumn::Used => self.used_space(), - DiskColumn::Free => self.free_space(), - DiskColumn::UsedPercent => percent_string(self.used_percent()), - DiskColumn::FreePercent => percent_string(self.free_percent()), - DiskColumn::Total => self.total_space(), - DiskColumn::IoRead => self.io_read(), - DiskColumn::IoWrite => self.io_write(), + DiskWidgetColumn::Disk => self.name.clone().into(), + DiskWidgetColumn::Mount => self.mount_point.clone().into(), + DiskWidgetColumn::Used => self.used_space(), + DiskWidgetColumn::Free => self.free_space(), + DiskWidgetColumn::UsedPercent => percent_string(self.used_percent()), + DiskWidgetColumn::FreePercent => percent_string(self.free_percent()), + DiskWidgetColumn::Total => self.total_space(), + DiskWidgetColumn::IoRead => self.io_read(), + DiskWidgetColumn::IoWrite => self.io_write(), }; Some(text) } - fn column_widths>(data: &[Self], _columns: &[C]) -> Vec + fn column_widths>( + data: &[Self], _columns: &[C], + ) -> Vec where Self: Sized, { @@ -215,46 +216,46 @@ impl DataToCell for DiskWidgetData { } pub struct DiskTableWidget { - pub table: SortDataTable, + pub table: SortDataTable, pub force_update_data: bool, } -impl SortsRow for DiskColumn { +impl SortsRow for DiskWidgetColumn { type DataType = DiskWidgetData; fn sort_data(&self, data: &mut [Self::DataType], descending: bool) { match self { - DiskColumn::Disk => { + DiskWidgetColumn::Disk => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.name, &b.name)); } - DiskColumn::Mount => { + DiskWidgetColumn::Mount => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.mount_point, &b.mount_point)); } - DiskColumn::Used => { + DiskWidgetColumn::Used => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.used_bytes, &b.used_bytes)); } - DiskColumn::UsedPercent => { + DiskWidgetColumn::UsedPercent => { data.sort_by(|a, b| { sort_partial_fn(descending)(&a.used_percent(), &b.used_percent()) }); } - DiskColumn::Free => { + DiskWidgetColumn::Free => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.free_bytes, &b.free_bytes)); } - DiskColumn::FreePercent => { + DiskWidgetColumn::FreePercent => { data.sort_by(|a, b| { sort_partial_fn(descending)(&a.free_percent(), &b.free_percent()) }); } - DiskColumn::Total => { + DiskWidgetColumn::Total => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.total_bytes, &b.total_bytes)); } - DiskColumn::IoRead => { + DiskWidgetColumn::IoRead => { data.sort_by(|a, b| { sort_partial_fn(descending)(&a.io_read_rate_bytes, &b.io_read_rate_bytes) }); } - DiskColumn::IoWrite => { + DiskWidgetColumn::IoWrite => { data.sort_by(|a, b| { sort_partial_fn(descending)(&a.io_write_rate_bytes, &b.io_write_rate_bytes) }); @@ -263,39 +264,60 @@ impl SortsRow for DiskColumn { } } -const fn create_column(column_type: &DiskColumn) -> SortColumn { +const fn create_column(column_type: &DiskWidgetColumn) -> SortColumn { match column_type { - DiskColumn::Disk => SortColumn::soft(DiskColumn::Disk, Some(0.2)), - DiskColumn::Mount => SortColumn::soft(DiskColumn::Mount, Some(0.2)), - DiskColumn::Used => SortColumn::hard(DiskColumn::Used, 8).default_descending(), - DiskColumn::Free => SortColumn::hard(DiskColumn::Free, 8).default_descending(), - DiskColumn::Total => SortColumn::hard(DiskColumn::Total, 9).default_descending(), - DiskColumn::UsedPercent => { - SortColumn::hard(DiskColumn::UsedPercent, 9).default_descending() + DiskWidgetColumn::Disk => SortColumn::soft(DiskWidgetColumn::Disk, Some(0.2)), + DiskWidgetColumn::Mount => SortColumn::soft(DiskWidgetColumn::Mount, Some(0.2)), + DiskWidgetColumn::Used => SortColumn::hard(DiskWidgetColumn::Used, 8).default_descending(), + DiskWidgetColumn::Free => SortColumn::hard(DiskWidgetColumn::Free, 8).default_descending(), + DiskWidgetColumn::Total => { + SortColumn::hard(DiskWidgetColumn::Total, 9).default_descending() } - DiskColumn::FreePercent => { - SortColumn::hard(DiskColumn::FreePercent, 9).default_descending() + DiskWidgetColumn::UsedPercent => { + SortColumn::hard(DiskWidgetColumn::UsedPercent, 9).default_descending() + } + DiskWidgetColumn::FreePercent => { + SortColumn::hard(DiskWidgetColumn::FreePercent, 9).default_descending() + } + DiskWidgetColumn::IoRead => { + SortColumn::hard(DiskWidgetColumn::IoRead, 10).default_descending() + } + DiskWidgetColumn::IoWrite => { + SortColumn::hard(DiskWidgetColumn::IoWrite, 11).default_descending() } - DiskColumn::IoRead => SortColumn::hard(DiskColumn::IoRead, 10).default_descending(), - DiskColumn::IoWrite => SortColumn::hard(DiskColumn::IoWrite, 11).default_descending(), } } -const fn default_disk_columns() -> [SortColumn; 8] { +const fn default_disk_column_list() -> [DiskWidgetColumn; 8] { [ - create_column(&DiskColumn::Disk), - create_column(&DiskColumn::Mount), - create_column(&DiskColumn::Used), - create_column(&DiskColumn::Free), - create_column(&DiskColumn::Total), - create_column(&DiskColumn::UsedPercent), - create_column(&DiskColumn::IoRead), - create_column(&DiskColumn::IoWrite), + DiskWidgetColumn::Disk, + DiskWidgetColumn::Mount, + DiskWidgetColumn::Used, + DiskWidgetColumn::Free, + DiskWidgetColumn::Total, + DiskWidgetColumn::UsedPercent, + DiskWidgetColumn::IoRead, + DiskWidgetColumn::IoWrite, + ] +} + +const fn default_disk_columns() -> [SortColumn; 8] { + [ + create_column(&DiskWidgetColumn::Disk), + create_column(&DiskWidgetColumn::Mount), + create_column(&DiskWidgetColumn::Used), + create_column(&DiskWidgetColumn::Free), + create_column(&DiskWidgetColumn::Total), + create_column(&DiskWidgetColumn::UsedPercent), + create_column(&DiskWidgetColumn::IoRead), + create_column(&DiskWidgetColumn::IoWrite), ] } impl DiskTableWidget { - pub fn new(config: &AppConfigFields, palette: &Styles, columns: Option<&[DiskColumn]>) -> Self { + pub fn new( + config: &AppConfigFields, palette: &Styles, columns: Option<&[DiskWidgetColumn]>, + ) -> Self { let props = SortDataTableProps { inner: DataTableProps { title: Some(" Disks ".into()), @@ -305,7 +327,22 @@ impl DiskTableWidget { show_table_scroll_position: config.show_table_scroll_position, show_current_entry_when_unfocused: false, }, - sort_index: 0, + sort_index: match &config.default_disk_sort_column { + Some(column) => { + // Must check that the column used exists. If not, fall back to 0. + + let existing_columns = match columns { + Some(c) => c, + None => &default_disk_column_list(), + }; + + existing_columns + .iter() + .position(|c| c == column) + .unwrap_or_default() + } + None => 0, + }, order: SortOrder::Ascending, }; diff --git a/src/widgets/temperature_table.rs b/src/widgets/temperature_table.rs index c9496916..e96f86a7 100644 --- a/src/widgets/temperature_table.rs +++ b/src/widgets/temperature_table.rs @@ -1,5 +1,7 @@ use std::{borrow::Cow, cmp::max, num::NonZeroU16}; +use serde::Deserialize; + use crate::{ app::{AppConfigFields, data::TypedTemperature}, canvas::components::data_table::{ @@ -16,16 +18,49 @@ pub struct TempWidgetData { pub temperature: Option, } +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "generate_schema", + derive(schemars::JsonSchema, strum::VariantArray) +)] +#[cfg_attr(test, derive(PartialEq, Eq))] pub enum TempWidgetColumn { Sensor, - Temp, + Temperature, +} + +impl<'de> Deserialize<'de> for TempWidgetColumn { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?.to_lowercase(); + match value.as_str() { + "sensor" => Ok(TempWidgetColumn::Sensor), + "temp" | "temperature" => Ok(TempWidgetColumn::Temperature), + _ => Err(serde::de::Error::custom( + "doesn't match any temperature column name", + )), + } + } +} + +impl TempWidgetColumn { + /// An ugly hack to generate the JSON schema. + #[cfg(feature = "generate_schema")] + pub fn get_schema_names(&self) -> &[&'static str] { + match self { + TempWidgetColumn::Sensor => &["Sensor"], + TempWidgetColumn::Temperature => &["Temp", "Temperature"], + } + } } impl ColumnHeader for TempWidgetColumn { fn text(&self) -> Cow<'static, str> { match self { TempWidgetColumn::Sensor => "Sensor(s)".into(), - TempWidgetColumn::Temp => "Temp(t)".into(), + TempWidgetColumn::Temperature => "Temp(t)".into(), } } } @@ -45,7 +80,7 @@ impl DataToCell for TempWidgetData { ) -> Option> { Some(match column { TempWidgetColumn::Sensor => self.sensor.clone().into(), - TempWidgetColumn::Temp => self.temperature(), + TempWidgetColumn::Temperature => self.temperature(), }) } @@ -74,7 +109,7 @@ impl SortsRow for TempWidgetColumn { TempWidgetColumn::Sensor => { data.sort_by(move |a, b| sort_partial_fn(descending)(&a.sensor, &b.sensor)); } - TempWidgetColumn::Temp => { + TempWidgetColumn::Temperature => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.temperature, &b.temperature)); } } @@ -90,7 +125,7 @@ impl TempWidgetState { pub(crate) fn new(config: &AppConfigFields, palette: &Styles) -> Self { let columns = [ SortColumn::soft(TempWidgetColumn::Sensor, Some(0.8)), - SortColumn::soft(TempWidgetColumn::Temp, None).default_descending(), + SortColumn::soft(TempWidgetColumn::Temperature, None).default_descending(), ]; let props = SortDataTableProps { @@ -102,7 +137,11 @@ impl TempWidgetState { show_table_scroll_position: config.show_table_scroll_position, show_current_entry_when_unfocused: false, }, - sort_index: 0, + // This is hard-coded, but there's only two columns so it's fine. + sort_index: match config.default_temp_sort_column { + Some(TempWidgetColumn::Temperature) => 1, + Some(TempWidgetColumn::Sensor) | None => 0, + }, order: SortOrder::Ascending, }; diff --git a/tests/integration/invalid_config_tests.rs b/tests/integration/invalid_config_tests.rs index a92ec06a..4f92f690 100644 --- a/tests/integration/invalid_config_tests.rs +++ b/tests/integration/invalid_config_tests.rs @@ -139,3 +139,22 @@ fn test_invalid_disk_column() { .failure() .stderr(predicate::str::contains("doesn't match")); } + +#[test] +fn test_invalid_temp_disk_default_sorts() { + btm_command(&[ + "-C", + "./tests/invalid_configs/invalid_temp_default_sort.toml", + ]) + .assert() + .failure() + .stderr(predicate::str::contains("doesn't match")); + + btm_command(&[ + "-C", + "./tests/invalid_configs/invalid_disk_default_sort.toml", + ]) + .assert() + .failure() + .stderr(predicate::str::contains("doesn't match")); +} diff --git a/tests/integration/valid_config_tests.rs b/tests/integration/valid_config_tests.rs index c7ff1a93..9021617f 100644 --- a/tests/integration/valid_config_tests.rs +++ b/tests/integration/valid_config_tests.rs @@ -195,3 +195,8 @@ fn test_proc_columns() { fn test_linux_only() { run_and_kill(&["-C", "./tests/valid_configs/os_specific/linux.toml"]); } + +#[test] +fn test_temp_disk_sort_columns() { + run_and_kill(&["-C", "./tests/valid_configs/temp_disk_sort_columns.toml"]); +} diff --git a/tests/invalid_configs/invalid_disk_default_sort.toml b/tests/invalid_configs/invalid_disk_default_sort.toml new file mode 100644 index 00000000..b38a4719 --- /dev/null +++ b/tests/invalid_configs/invalid_disk_default_sort.toml @@ -0,0 +1,2 @@ +[disk] +default_sort = "soup" diff --git a/tests/invalid_configs/invalid_temp_default_sort.toml b/tests/invalid_configs/invalid_temp_default_sort.toml new file mode 100644 index 00000000..43678a6b --- /dev/null +++ b/tests/invalid_configs/invalid_temp_default_sort.toml @@ -0,0 +1,2 @@ +[temperature] +default_sort = "soup" diff --git a/tests/valid_configs/temp_disk_sort_columns.toml b/tests/valid_configs/temp_disk_sort_columns.toml new file mode 100644 index 00000000..9df24a03 --- /dev/null +++ b/tests/valid_configs/temp_disk_sort_columns.toml @@ -0,0 +1,5 @@ +[temperature] +default_sort = "Temperature" + +[disk] +default_sort = "R/s"