mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-09-23 11:05:34 +00:00
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:
@@ -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`:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user