feature: add configurable default column sort order (#2174)

Adds an optional configuration field for the temp/process/disk table widgets to control the default sort order.
This commit is contained in:
Clement Tsang
2026-07-26 20:37:20 -04:00
committed by GitHub
parent 6934fe1cd3
commit 95b60c8016
18 changed files with 210 additions and 58 deletions
+4
View File
@@ -22,6 +22,10 @@ That said, these are more guidelines rather than hard rules, though the project
## 0.14.7/0.15.0 - Unreleased
### Features
- [#2174](https://github.com/ClementTsang/bottom/pull/2174): Support setting default sort order in config.
### Other
- [#2167](https://github.com/ClementTsang/bottom/pull/2167): Remove filedescriptor hack used to suppress false-positive stderr messages for older versions of FreeBSD.
@@ -10,9 +10,9 @@ 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
## Default Sort Column
You can customize the default sort order (by default, it sorts by disk name). For example, to sort by the read rate:
You can customize the default sort column (by default, it sorts by disk name in ascending order). For example, to sort by the read rate:
```toml
[disk]
@@ -22,6 +22,14 @@ 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.
You can also set the sort order by changing `disk.sort_order` with `"Ascending"` or `"Descending"`. Defaults to
`"Ascending"`:
```toml
[disk]
sort_order = "Ascending"
```
## Show Unmounted Devices (Linux only)
By default, only mounted devices are shown. To also show unmounted devices on Linux, enable `include_unmounted`:
@@ -30,9 +30,9 @@ You can configure which columns are shown by the process widget by setting the `
columns = ["cpu%", "mem%", "pid", "name", "read", "write", "tread", "twrite", "state", "user", "time", "gmem%", "gpu%"]
```
## Default Sort Order
## Default Sort Column
By default, the process widget starts sorted by CPU usage. You can change the column it sorts by at startup:
By default, the process widget starts sorted by CPU usage in descending order. You can change the column it sorts by at startup:
```toml
[processes]
@@ -47,3 +47,10 @@ The same setting is also exposed as a CLI flag, which takes precedence over the
```
btm --process_default_sort mem
```
You can also set the default order by changing `processes.sort_order` with `"Ascending"` or `"Descending"`. If not set, then by default, it will use a sort order that is reasonable (e.g. descending for CPU/memory usage, ascending for process names).
```toml
[processes]
sort_order = "Ascending"
```
@@ -2,15 +2,23 @@
The temperature table widget is configured under `[temperature]`.
## Default Sort Order
## Default Sort Column
You can customize the default sort order (by default, it sorts by temperature sensor name). For example, to sort by temperature:
You can customize the default sort column (by default, it sorts by temperature sensor name in ascending order). For example, to sort by temperature:
```toml
[temperature]
default_sort = "Temp"
```
You can also set the sort order by changing `temperature.sort_order` with `"Ascending"` or `"Descending"`. Defaults to
`"Ascending"`:
```toml
[temperature]
sort_order = "Ascending"
```
## 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.
+12
View File
@@ -166,6 +166,10 @@
# If unset, defaults to CPU%.
#default_sort = "CPU%"
# The default sort order. Can be either "Ascending" or "Descending".
# Defaults to sensible defaults for the data type (e.g. descending for CPU/memory usage, ascending for process names).
#sort_order = "Ascending"
# Gather process child thread information
#get_threads = false
@@ -228,6 +232,10 @@
# Defaults to "Disk".
#default_sort = "Disk"
# The default sort order. Can be either "Ascending" or "Descending".
# Defaults to ascending.
#sort_order = "Ascending"
# Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
#include_unmounted = false
@@ -315,6 +323,10 @@
# Defaults to "Sensor".
#default_sort = "Sensor"
# The default sort order. Can be either "Ascending" or "Descending".
# Defaults to ascending.
#sort_order = "Ascending"
# 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.
+27
View File
@@ -295,6 +295,10 @@
"type": "null"
}
]
},
"sort_order": {
"description": "The default sort order. Defaults to ascending.",
"$ref": "#/$defs/SortOrder"
}
}
},
@@ -1163,6 +1167,17 @@
"null"
]
},
"sort_order": {
"description": "The default sort order. If not set, defaults to column-specific defaults\n(e.g. CPU usage is descending, process names are ascending).",
"anyOf": [
{
"$ref": "#/$defs/SortOrder"
},
{
"type": "null"
}
]
},
"tree_collapse": {
"description": "Collapse the process tree by default when tree mode is set.",
"type": [
@@ -1217,6 +1232,14 @@
}
]
},
"SortOrder": {
"description": "Denotes the sort order.",
"type": "string",
"enum": [
"Ascending",
"Descending"
]
},
"StringOrNum": {
"anyOf": [
{
@@ -1391,6 +1414,10 @@
"type": "null"
}
]
},
"sort_order": {
"description": "The default sort order. Defaults to ascending.",
"$ref": "#/$defs/SortOrder"
}
}
},
+4 -1
View File
@@ -13,7 +13,8 @@ pub use states::*;
use crate::{
canvas::{
components::time_series::LegendPosition, dialogs::process_kill_dialog::ProcessKillDialog,
components::{data_table::SortOrder, time_series::LegendPosition},
dialogs::process_kill_dialog::ProcessKillDialog,
},
components::time_series::TimeseriesState,
constants,
@@ -79,7 +80,9 @@ pub struct AppConfigFields {
pub dedicated_average_row: bool,
pub default_tree_collapse: bool,
pub default_temp_sort_column: Option<TempWidgetColumn>,
pub default_temp_sort_order: SortOrder,
pub default_disk_sort_column: Option<DiskWidgetColumn>,
pub default_disk_sort_order: SortOrder,
pub temperature_legend_position: Option<LegendPosition>,
pub disk_io_legend_position: Option<LegendPosition>,
pub disk_show_unmounted: bool,
@@ -3,6 +3,7 @@ use std::{borrow::Cow, marker::PhantomData, num::NonZeroU16};
use concat_string::concat_string;
use itertools::Itertools;
use ratatui::widgets::Row;
use serde::Deserialize;
use super::{
ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, DataTableState,
@@ -12,11 +13,31 @@ use crate::{canvas::components::data_table::Column, utils::strings::truncate_to_
/// Denotes the sort order.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "generate_schema",
derive(schemars::JsonSchema, strum::VariantArray)
)]
pub enum SortOrder {
Ascending,
Descending,
}
impl<'de> Deserialize<'de> for SortOrder {
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() {
"ascending" => Ok(SortOrder::Ascending),
"descending" => Ok(SortOrder::Descending),
_ => Err(serde::de::Error::custom(
"doesn't match any sort order variant",
)),
}
}
}
impl SortOrder {
/// Returns the reverse [`SortOrder`].
pub fn rev(&self) -> SortOrder {
+12
View File
@@ -415,6 +415,10 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# If unset, defaults to CPU%.
#default_sort = "CPU%"
# The default sort order. Can be either "Ascending" or "Descending".
# Defaults to sensible defaults for the data type (e.g. descending for CPU/memory usage, ascending for process names).
#sort_order = "Ascending"
# Gather process child thread information
#get_threads = false
@@ -477,6 +481,10 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# Defaults to "Disk".
#default_sort = "Disk"
# The default sort order. Can be either "Ascending" or "Descending".
# Defaults to ascending.
#sort_order = "Ascending"
# Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
#include_unmounted = false
@@ -564,6 +572,10 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# Defaults to "Sensor".
#default_sort = "Sensor"
# The default sort order. Can be either "Ascending" or "Descending".
# Defaults to ascending.
#sort_order = "Ascending"
# 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.
+13
View File
@@ -512,10 +512,20 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
.temperature
.as_ref()
.and_then(|cfg| cfg.default_sort.to_owned()),
default_temp_sort_order: config
.temperature
.as_ref()
.map(|cfg| cfg.sort_order)
.unwrap_or_default(),
default_disk_sort_column: config
.disk
.as_ref()
.and_then(|cfg| cfg.default_sort.to_owned()),
default_disk_sort_order: config
.disk
.as_ref()
.map(|cfg| cfg.sort_order)
.unwrap_or_default(),
temperature_legend_position,
disk_io_legend_position,
disk_show_unmounted,
@@ -538,6 +548,8 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
default_time_value: app_config_fields.default_time_value,
};
let process_default_sort_order = config.processes.as_ref().and_then(|cfg| cfg.sort_order);
let table_config = ProcTableConfig {
is_case_sensitive,
is_match_whole_word,
@@ -545,6 +557,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
show_memory_as_values: process_memory_as_value,
is_command: is_default_command,
default_sort: process_default_sort,
sort_order: process_default_sort_order,
};
for row in &widget_layout.rows {
+5 -1
View File
@@ -1,7 +1,7 @@
use serde::Deserialize;
use super::IgnoreList;
use crate::options::DiskWidgetColumn;
use crate::{canvas::components::data_table::SortOrder, options::DiskWidgetColumn};
/// Disk configuration.
#[derive(Clone, Debug, Default, Deserialize)]
@@ -26,6 +26,10 @@ pub(crate) struct DiskConfig {
/// The default sort column.
#[serde(default)]
pub(crate) default_sort: Option<DiskWidgetColumn>,
/// The default sort order. Defaults to ascending.
#[serde(default)]
pub(crate) sort_order: SortOrder,
}
#[cfg(test)]
+6 -1
View File
@@ -1,6 +1,6 @@
use serde::Deserialize;
use crate::widgets::ProcColumn;
use crate::{canvas::components::data_table::SortOrder, widgets::ProcColumn};
/// Process configuration fields.
#[derive(Clone, Debug, Default, Deserialize)]
@@ -15,6 +15,11 @@ pub(crate) struct ProcessesConfig {
#[serde(default)]
pub default_sort: Option<ProcColumn>,
/// The default sort order. If not set, defaults to column-specific defaults
/// (e.g. CPU usage is descending, process names are ascending).
#[serde(default)]
pub(crate) sort_order: Option<SortOrder>,
/// Whether to get process child threads.
pub get_threads: Option<bool>,
+5 -1
View File
@@ -1,7 +1,7 @@
use serde::Deserialize;
use super::IgnoreList;
use crate::widgets::TempWidgetColumn;
use crate::{canvas::components::data_table::SortOrder, widgets::TempWidgetColumn};
/// Temperature configuration.
#[derive(Clone, Debug, Default, Deserialize)]
@@ -14,6 +14,10 @@ pub(crate) struct TempConfig {
/// The default sort column.
#[serde(default)]
pub(crate) default_sort: Option<TempWidgetColumn>,
/// The default sort order. Defaults to ascending.
#[serde(default)]
pub(crate) sort_order: SortOrder,
}
#[cfg(test)]
+2 -2
View File
@@ -6,7 +6,7 @@ use crate::{
app::{AppConfigFields, data::StoredData},
canvas::components::data_table::{
ColumnHeader, DataTableColumn, DataTableProps, DataTableStyling, DataToCell, SortColumn,
SortDataTable, SortDataTableProps, SortOrder, SortsRow,
SortDataTable, SortDataTableProps, SortsRow,
},
options::config::style::Styles,
utils::{
@@ -347,7 +347,7 @@ impl DiskTableWidget {
}
None => 0,
},
order: SortOrder::Ascending,
order: config.default_disk_sort_order,
};
let styling = DataTableStyling::from_palette(palette);
+17 -10
View File
@@ -175,6 +175,7 @@ pub struct ProcTableConfig {
pub show_memory_as_values: bool,
pub is_command: bool,
pub default_sort: Option<ProcColumn>,
pub sort_order: Option<SortOrder>,
}
/// A hacky workaround for now.
@@ -417,19 +418,25 @@ impl ProcWidgetState {
.map(|index| (index, columns[index].default_order))
});
let (default_sort_index, default_sort_order) = if let Some(pair) = configured_default_sort {
pair
} else if matches!(mode, ProcWidgetMode::Tree { .. }) {
if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::PidOrCount) {
let (default_sort_index, mut default_sort_order) =
if let Some(pair) = configured_default_sort {
pair
} else if matches!(mode, ProcWidgetMode::Tree { .. }) {
if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::PidOrCount) {
(index, columns[index].default_order)
} else {
(0, columns[0].default_order)
}
} else if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::Cpu) {
(index, columns[index].default_order)
} else {
(0, columns[0].default_order)
}
} else if let Some(index) = column_mapping.get_index_of(&ProcWidgetColumn::Cpu) {
(index, columns[index].default_order)
} else {
(0, columns[0].default_order)
};
};
// If configured, override it with this.
if let Some(order) = table_config.sort_order {
default_sort_order = order;
}
let sort_table = Self::new_sort_table(config, colours);
let table = Self::new_process_table(
+2 -2
View File
@@ -6,7 +6,7 @@ use crate::{
app::{AppConfigFields, data::TypedTemperature},
canvas::components::data_table::{
ColumnHeader, DataTableColumn, DataTableProps, DataTableStyling, DataToCell, SortColumn,
SortDataTable, SortDataTableProps, SortOrder, SortsRow,
SortDataTable, SortDataTableProps, SortsRow,
},
options::config::style::Styles,
utils::general::sort_partial_fn,
@@ -143,7 +143,7 @@ impl TempWidgetState {
Some(TempWidgetColumn::Temperature) => 1,
Some(TempWidgetColumn::Sensor) | None => 0,
},
order: SortOrder::Ascending,
order: config.default_temp_sort_order,
};
let styling = DataTableStyling::from_palette(palette);
@@ -1,5 +1,11 @@
[temperature]
default_sort = "Temperature"
sort_order = "ASCENDING"
[disk]
default_sort = "R/s"
sort_order = "descending"
[processes]
default_sort = "rps"
sort_order = "ascENding"
+45 -34
View File
@@ -186,9 +186,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.6.1"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
dependencies = [
"clap_builder",
"clap_derive",
@@ -196,9 +196,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.0"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
@@ -228,9 +228,9 @@ dependencies = [
[[package]]
name = "clap_complete_nushell"
version = "4.6.0"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897"
checksum = "933b05d5d83ff65fd7eaf5d106c792f2264908790a2642aca57429767b762ce2"
dependencies = [
"clap",
"clap_complete",
@@ -238,14 +238,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.1"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 3.0.3",
]
[[package]]
@@ -390,7 +390,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -403,7 +403,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -414,7 +414,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core 0.20.11",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -425,7 +425,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -453,7 +453,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -519,7 +519,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -636,7 +636,7 @@ dependencies = [
"indoc",
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -688,9 +688,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.186"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libloading"
@@ -950,7 +950,7 @@ dependencies = [
"by_address",
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1132,7 +1132,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1254,7 +1254,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1296,7 +1296,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1307,7 +1307,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1427,7 +1427,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1439,7 +1439,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1453,6 +1453,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sysctl"
version = "0.7.1"
@@ -1469,9 +1480,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.39.5"
version = "0.39.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c8bd2130a9b60bee2581bf82cfe89ee836424d1f37dcfa4ce21509611684673"
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
dependencies = [
"libc",
"memchr",
@@ -1518,7 +1529,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1529,7 +1540,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1566,9 +1577,9 @@ dependencies = [
[[package]]
name = "timeless"
version = "0.0.15-alpha"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2be119240e5a387a47d456fb20aed535bbfe6db82ed698212a79b2cd30382c8"
checksum = "4cc3e5eaa88cd8a3e3cbfc3d3f2183252f8b65c40bc5dfe7bb7345c1de42e99e"
[[package]]
name = "toml_datetime"
@@ -1770,7 +1781,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1781,7 +1792,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]
@@ -1854,7 +1865,7 @@ dependencies = [
"darling 0.20.11",
"proc-macro2",
"quote",
"syn",
"syn 2.0.118",
]
[[package]]