mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-05-03 21:40:32 +00:00
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:
+6
-10
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user