feat: add configurable binary disk capacity units (#2236)

Enable binary disk capacity units with:

```toml
[disk]
use_binary_prefix = true
```

Or pass `--disk_use_binary_prefix` (alias `--disk-use-binary-prefix`). The CLI flag takes precedence over a false config setting. Decimal units remain the default.

Used, Free, and Total use the existing binary conversion helper. For example, 536,870,912,000 bytes changes from `537GB` to `500GiB`. Percentages, sorting by raw byte values, I/O rates, and network units retain their existing behavior.

The change passes the option from argument/config parsing through application settings and collected disk data to cell formatting. Four regression tests cover option precedence and both CLI spellings, collection-to-cell wiring, unit boundaries, missing values, percentages, I/O behavior, and sorting.
This commit is contained in:
Peder Bergan
2026-09-15 20:47:06 -04:00
committed by GitHub
parent 25b6b36299
commit c10a2adbd4
11 changed files with 169 additions and 6 deletions
@@ -25,6 +25,12 @@ see information on these options by running `btm -h`, or run `btm --help` to dis
| | for table widgets. |
| `-d`, `--time_delta <TIME>` | The amount of time changed upon zooming. |
## Disk Options
| Option | Behaviour |
| -------------------------- | --------------------------------------------------------------- |
| `--disk_use_binary_prefix` | Displays used, free, and total disk space with binary prefixes. |
## Process Options
| Option | Behaviour |
@@ -30,6 +30,16 @@ You can also set the sort order by changing `disk.sort_order` with `"Ascending"`
sort_order = "Ascending"
```
## Disk Space Units
Disk space uses decimal prefixes (KB, MB, GB, TB) by default. To display the Used, Free, and Total columns
with binary prefixes (KiB, MiB, GiB, TiB), enable `use_binary_prefix`:
```toml
[disk]
use_binary_prefix = true
```
## Show Unmounted Devices (Linux only)
By default, only mounted devices are shown. To also show unmounted devices on Linux, enable `include_unmounted`:
+3
View File
@@ -222,6 +222,9 @@
# Disk widget configuration
#[disk]
# Whether to display used, free, and total disk space with binary prefixes (e.g. GiB instead of GB).
#use_binary_prefix = false
# 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"]
+7
View File
@@ -299,6 +299,13 @@
"sort_order": {
"description": "The default sort order. Defaults to ascending.",
"$ref": "#/$defs/SortOrder"
},
"use_binary_prefix": {
"description": "Displays used, free, and total disk space with binary prefixes (e.g. GiB).\nDefaults to decimal prefixes (e.g. GB).",
"type": [
"boolean",
"null"
]
}
}
},
+1
View File
@@ -87,6 +87,7 @@ pub struct AppConfigFields {
pub temperature_legend_position: Option<LegendPosition>,
pub disk_io_legend_position: Option<LegendPosition>,
pub disk_show_unmounted: bool,
pub disk_use_binary_prefix: bool,
pub disk_io_graph_show_unmounted: bool,
}
+6 -2
View File
@@ -214,7 +214,7 @@ impl InnerData {
if let Some(disks) = data.disks
&& let Some(io) = data.io
{
self.eat_disks(disks, io, harvested_time);
self.eat_disks(disks, io, harvested_time, settings.disk_use_binary_prefix);
if used_widgets.use_disk_io_graph {
self.time_series_data.update_disk_io(
@@ -240,7 +240,10 @@ impl InnerData {
self.last_update_time = harvested_time;
}
fn eat_disks(&mut self, disks: Vec<DiskHarvest>, io: IoHarvest, harvested_time: Instant) {
fn eat_disks(
&mut self, disks: Vec<DiskHarvest>, io: IoHarvest, harvested_time: Instant,
use_binary_prefix: bool,
) {
let time_since_last_harvest = harvested_time
.duration_since(self.last_update_time)
.as_secs_f64();
@@ -350,6 +353,7 @@ impl InnerData {
};
self.disk_harvest.push(DiskWidgetData {
use_binary_prefix,
name: disk.name,
mount_point: disk.mount_point,
free_bytes: disk.free_space,
+3
View File
@@ -471,6 +471,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# Disk widget configuration
#[disk]
# Whether to display used, free, and total disk space with binary prefixes (e.g. GiB instead of GB).
#use_binary_prefix = false
# 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"]
+6
View File
@@ -542,6 +542,12 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
temperature_legend_position,
disk_io_legend_position,
disk_show_unmounted,
disk_use_binary_prefix: args.disk.disk_use_binary_prefix
|| config
.disk
.as_ref()
.and_then(|cfg| cfg.use_binary_prefix)
.unwrap_or(false),
disk_io_graph_show_unmounted,
};
+18
View File
@@ -73,6 +73,9 @@ pub struct BottomArgs {
#[command(flatten)]
pub network: NetworkArgs,
#[command(flatten)]
pub disk: DiskArgs,
#[cfg(feature = "battery")]
#[command(flatten)]
pub battery: BatteryArgs,
@@ -552,6 +555,21 @@ pub struct MemoryArgs {
pub short_gpu_names: bool,
}
/// Disk arguments/config options.
#[derive(Args, Clone, Debug, Default)]
#[command(next_help_heading = "Disk Options", rename_all = "snake_case")]
pub struct DiskArgs {
#[arg(
long,
action = ArgAction::SetTrue,
help = "Displays disk space with binary prefixes.",
long_help = "Displays used, free, and total disk space with binary prefixes (e.g. KiB, MiB, GiB) \
rather than decimal prefixes (e.g. KB, MB, GB). Defaults to decimal prefixes.",
alias = "disk-use-binary-prefix"
)]
pub disk_use_binary_prefix: bool,
}
/// Network arguments/config options.
#[derive(Args, Clone, Debug, Default)]
#[command(next_help_heading = "Network Options", rename_all = "snake_case")]
+4
View File
@@ -8,6 +8,10 @@ use crate::{canvas::components::data_table::SortOrder, options::DiskWidgetColumn
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub(crate) struct DiskConfig {
/// Displays used, free, and total disk space with binary prefixes (e.g. GiB).
/// Defaults to decimal prefixes (e.g. GB).
pub(crate) use_binary_prefix: Option<bool>,
/// A filter over the disk names.
pub(crate) name_filter: Option<IgnoreList>,
+105 -4
View File
@@ -10,13 +10,14 @@ use crate::{
},
options::config::style::Styles,
utils::{
conversion::dec_bytes_per_second_string, data_units::get_decimal_bytes,
conversion::dec_bytes_per_second_string, data_units::convert_bytes,
general::sort_partial_fn,
},
};
#[derive(Clone, Debug)]
pub struct DiskWidgetData {
pub use_binary_prefix: bool,
pub name: String,
pub mount_point: String,
pub free_bytes: Option<u64>,
@@ -30,7 +31,7 @@ pub struct DiskWidgetData {
impl DiskWidgetData {
fn total_space(&self) -> Cow<'static, str> {
if let Some(total_bytes) = self.total_bytes {
let converted_total_space = get_decimal_bytes(total_bytes);
let converted_total_space = convert_bytes(total_bytes, self.use_binary_prefix);
format!("{:.0}{}", converted_total_space.0, converted_total_space.1).into()
} else {
"N/A".into()
@@ -39,7 +40,7 @@ impl DiskWidgetData {
fn free_space(&self) -> Cow<'static, str> {
if let Some(free_bytes) = self.free_bytes {
let converted_free_space = get_decimal_bytes(free_bytes);
let converted_free_space = convert_bytes(free_bytes, self.use_binary_prefix);
format!("{:.0}{}", converted_free_space.0, converted_free_space.1).into()
} else {
"N/A".into()
@@ -48,7 +49,7 @@ impl DiskWidgetData {
fn used_space(&self) -> Cow<'static, str> {
if let Some(used_bytes) = self.used_bytes {
let converted_free_space = get_decimal_bytes(used_bytes);
let converted_free_space = convert_bytes(used_bytes, self.use_binary_prefix);
format!("{:.0}{}", converted_free_space.0, converted_free_space.1).into()
} else {
"N/A".into()
@@ -410,3 +411,103 @@ impl DiskTableWidget {
self.force_data_update();
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::utils::data_units::{GIBI_LIMIT, MEBI_LIMIT, TEBI_LIMIT};
fn disk(bytes: Option<u64>, use_binary_prefix: bool) -> DiskWidgetData {
DiskWidgetData {
use_binary_prefix,
name: "disk".into(),
mount_point: "/".into(),
free_bytes: bytes,
used_bytes: bytes,
total_bytes: bytes,
summed_total_bytes: bytes.map(|bytes| bytes * 2),
io_read_rate_bytes: bytes,
io_write_rate_bytes: bytes,
}
}
fn cell(data: &DiskWidgetData, column: DiskWidgetColumn) -> Cow<'static, str> {
data.to_cell_text(&column, NonZeroU16::new(10).unwrap())
.unwrap()
}
#[test]
fn disk_space_units() {
for (bytes, decimal, binary) in [
(0, "0B", "0B"),
(999, "999B", "999B"),
(1000, "1KB", "1000B"),
(1023, "1KB", "1023B"),
(1024, "1KB", "1KiB"),
(MEBI_LIMIT, "1MB", "1MiB"),
(500 * GIBI_LIMIT, "537GB", "500GiB"),
(TEBI_LIMIT, "1TB", "1TiB"),
] {
for (use_binary_prefix, expected) in [(false, decimal), (true, binary)] {
let data = disk(Some(bytes), use_binary_prefix);
for column in [
DiskWidgetColumn::Used,
DiskWidgetColumn::Free,
DiskWidgetColumn::Total,
] {
assert_eq!(cell(&data, column), expected);
}
}
}
}
#[test]
fn disk_missing_space_and_percentages() {
for use_binary_prefix in [false, true] {
let missing = disk(None, use_binary_prefix);
for column in [
DiskWidgetColumn::Used,
DiskWidgetColumn::Free,
DiskWidgetColumn::Total,
DiskWidgetColumn::UsedPercent,
DiskWidgetColumn::FreePercent,
DiskWidgetColumn::IoRead,
DiskWidgetColumn::IoWrite,
] {
assert_eq!(cell(&missing, column), "N/A");
}
let data = disk(Some(500 * GIBI_LIMIT), use_binary_prefix);
assert_eq!(cell(&data, DiskWidgetColumn::UsedPercent), "50.0%");
assert_eq!(cell(&data, DiskWidgetColumn::FreePercent), "50.0%");
assert_eq!(cell(&data, DiskWidgetColumn::IoRead), "536.9GB/s");
assert_eq!(cell(&data, DiskWidgetColumn::IoWrite), "536.9GB/s");
assert_eq!(
cell(
&disk(Some(0), use_binary_prefix),
DiskWidgetColumn::UsedPercent
),
"N/A"
);
}
}
#[test]
fn disk_space_sorting_uses_bytes() {
for use_binary_prefix in [false, true] {
for column in [
DiskWidgetColumn::Used,
DiskWidgetColumn::Free,
DiskWidgetColumn::Total,
] {
let mut data = [
disk(Some(GIBI_LIMIT), use_binary_prefix),
disk(Some(2 * MEBI_LIMIT), use_binary_prefix),
];
column.sort_data(&mut data, false);
assert_eq!(data[0].total_bytes, Some(2 * MEBI_LIMIT));
column.sort_data(&mut data, true);
assert_eq!(data[0].total_bytes, Some(GIBI_LIMIT));
}
}
}
}