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.
This commit is contained in:
Clement Tsang
2026-03-21 17:23:16 -04:00
committed by GitHub
parent b931dd9846
commit 7496bbdd54
18 changed files with 432 additions and 162 deletions
+6 -10
View File
@@ -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`.
@@ -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,
@@ -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.
+15 -1
View File
@@ -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.
+112 -20
View File
@@ -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": [
+9 -2
View File
@@ -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<TempWidgetColumn>,
pub default_disk_sort_column: Option<DiskWidgetColumn>,
}
/// For filtering out information
+39 -42
View File
@@ -16,52 +16,49 @@ struct SchemaOptions {
version: Option<String>,
}
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());
+16
View File
@@ -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");
+8
View File
@@ -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 {
+6 -2
View File
@@ -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<Vec<DiskColumn>>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets
pub(crate) columns: Option<Vec<DiskWidgetColumn>>, // 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<DiskWidgetColumn>,
}
#[cfg(test)]
+6
View File
@@ -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<IgnoreList>,
/// The default sort column.
#[serde(default)]
pub(crate) default_sort: Option<TempWidgetColumn>,
}
+116 -79
View File
@@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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<DiskColumn> for DiskWidgetData {
impl DataToCell<DiskWidgetColumn> 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<Cow<'static, str>> {
fn percent_string(value: Option<f64>) -> Cow<'static, str> {
match value {
@@ -185,21 +184,23 @@ impl DataToCell<DiskColumn> 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<C: DataTableColumn<DiskColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
fn column_widths<C: DataTableColumn<DiskWidgetColumn>>(
data: &[Self], _columns: &[C],
) -> Vec<u16>
where
Self: Sized,
{
@@ -215,46 +216,46 @@ impl DataToCell<DiskColumn> for DiskWidgetData {
}
pub struct DiskTableWidget {
pub table: SortDataTable<DiskWidgetData, DiskColumn>,
pub table: SortDataTable<DiskWidgetData, DiskWidgetColumn>,
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<DiskColumn> {
const fn create_column(column_type: &DiskWidgetColumn) -> SortColumn<DiskWidgetColumn> {
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<DiskColumn>; 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<DiskWidgetColumn>; 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,
};
+45 -6
View File
@@ -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<TypedTemperature>,
}
#[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<D>(deserializer: D) -> Result<Self, D::Error>
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<TempWidgetColumn> for TempWidgetData {
) -> Option<Cow<'static, str>> {
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,
};
+19
View File
@@ -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"));
}
+5
View File
@@ -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"]);
}
@@ -0,0 +1,2 @@
[disk]
default_sort = "soup"
@@ -0,0 +1,2 @@
[temperature]
default_sort = "soup"
@@ -0,0 +1,5 @@
[temperature]
default_sort = "Temperature"
[disk]
default_sort = "R/s"