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. |
|
| | for table widgets. |
|
||||||
| `-d`, `--time_delta <TIME>` | The amount of time changed upon zooming. |
|
| `-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
|
## Process Options
|
||||||
|
|
||||||
| Option | Behaviour |
|
| Option | Behaviour |
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ You can also set the sort order by changing `disk.sort_order` with `"Ascending"`
|
|||||||
sort_order = "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)
|
## Show Unmounted Devices (Linux only)
|
||||||
|
|
||||||
By default, only mounted devices are shown. To also show unmounted devices on Linux, enable `include_unmounted`:
|
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 widget configuration
|
||||||
#[disk]
|
#[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:
|
# The columns shown by the process widget. The following columns are supported:
|
||||||
# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s
|
# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s
|
||||||
#columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"]
|
#columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"]
|
||||||
|
|||||||
@@ -299,6 +299,13 @@
|
|||||||
"sort_order": {
|
"sort_order": {
|
||||||
"description": "The default sort order. Defaults to ascending.",
|
"description": "The default sort order. Defaults to ascending.",
|
||||||
"$ref": "#/$defs/SortOrder"
|
"$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 temperature_legend_position: Option<LegendPosition>,
|
||||||
pub disk_io_legend_position: Option<LegendPosition>,
|
pub disk_io_legend_position: Option<LegendPosition>,
|
||||||
pub disk_show_unmounted: bool,
|
pub disk_show_unmounted: bool,
|
||||||
|
pub disk_use_binary_prefix: bool,
|
||||||
pub disk_io_graph_show_unmounted: bool,
|
pub disk_io_graph_show_unmounted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ impl InnerData {
|
|||||||
if let Some(disks) = data.disks
|
if let Some(disks) = data.disks
|
||||||
&& let Some(io) = data.io
|
&& 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 {
|
if used_widgets.use_disk_io_graph {
|
||||||
self.time_series_data.update_disk_io(
|
self.time_series_data.update_disk_io(
|
||||||
@@ -240,7 +240,10 @@ impl InnerData {
|
|||||||
self.last_update_time = harvested_time;
|
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
|
let time_since_last_harvest = harvested_time
|
||||||
.duration_since(self.last_update_time)
|
.duration_since(self.last_update_time)
|
||||||
.as_secs_f64();
|
.as_secs_f64();
|
||||||
@@ -350,6 +353,7 @@ impl InnerData {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.disk_harvest.push(DiskWidgetData {
|
self.disk_harvest.push(DiskWidgetData {
|
||||||
|
use_binary_prefix,
|
||||||
name: disk.name,
|
name: disk.name,
|
||||||
mount_point: disk.mount_point,
|
mount_point: disk.mount_point,
|
||||||
free_bytes: disk.free_space,
|
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 widget configuration
|
||||||
#[disk]
|
#[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:
|
# The columns shown by the process widget. The following columns are supported:
|
||||||
# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s
|
# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s
|
||||||
#columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "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,
|
temperature_legend_position,
|
||||||
disk_io_legend_position,
|
disk_io_legend_position,
|
||||||
disk_show_unmounted,
|
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,
|
disk_io_graph_show_unmounted,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ pub struct BottomArgs {
|
|||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub network: NetworkArgs,
|
pub network: NetworkArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub disk: DiskArgs,
|
||||||
|
|
||||||
#[cfg(feature = "battery")]
|
#[cfg(feature = "battery")]
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
pub battery: BatteryArgs,
|
pub battery: BatteryArgs,
|
||||||
@@ -552,6 +555,21 @@ pub struct MemoryArgs {
|
|||||||
pub short_gpu_names: bool,
|
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.
|
/// Network arguments/config options.
|
||||||
#[derive(Args, Clone, Debug, Default)]
|
#[derive(Args, Clone, Debug, Default)]
|
||||||
#[command(next_help_heading = "Network Options", rename_all = "snake_case")]
|
#[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(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||||
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
||||||
pub(crate) struct DiskConfig {
|
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.
|
/// A filter over the disk names.
|
||||||
pub(crate) name_filter: Option<IgnoreList>,
|
pub(crate) name_filter: Option<IgnoreList>,
|
||||||
|
|
||||||
|
|||||||
+105
-4
@@ -10,13 +10,14 @@ use crate::{
|
|||||||
},
|
},
|
||||||
options::config::style::Styles,
|
options::config::style::Styles,
|
||||||
utils::{
|
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,
|
general::sort_partial_fn,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DiskWidgetData {
|
pub struct DiskWidgetData {
|
||||||
|
pub use_binary_prefix: bool,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub mount_point: String,
|
pub mount_point: String,
|
||||||
pub free_bytes: Option<u64>,
|
pub free_bytes: Option<u64>,
|
||||||
@@ -30,7 +31,7 @@ pub struct DiskWidgetData {
|
|||||||
impl DiskWidgetData {
|
impl DiskWidgetData {
|
||||||
fn total_space(&self) -> Cow<'static, str> {
|
fn total_space(&self) -> Cow<'static, str> {
|
||||||
if let Some(total_bytes) = self.total_bytes {
|
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()
|
format!("{:.0}{}", converted_total_space.0, converted_total_space.1).into()
|
||||||
} else {
|
} else {
|
||||||
"N/A".into()
|
"N/A".into()
|
||||||
@@ -39,7 +40,7 @@ impl DiskWidgetData {
|
|||||||
|
|
||||||
fn free_space(&self) -> Cow<'static, str> {
|
fn free_space(&self) -> Cow<'static, str> {
|
||||||
if let Some(free_bytes) = self.free_bytes {
|
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()
|
format!("{:.0}{}", converted_free_space.0, converted_free_space.1).into()
|
||||||
} else {
|
} else {
|
||||||
"N/A".into()
|
"N/A".into()
|
||||||
@@ -48,7 +49,7 @@ impl DiskWidgetData {
|
|||||||
|
|
||||||
fn used_space(&self) -> Cow<'static, str> {
|
fn used_space(&self) -> Cow<'static, str> {
|
||||||
if let Some(used_bytes) = self.used_bytes {
|
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()
|
format!("{:.0}{}", converted_free_space.0, converted_free_space.1).into()
|
||||||
} else {
|
} else {
|
||||||
"N/A".into()
|
"N/A".into()
|
||||||
@@ -410,3 +411,103 @@ impl DiskTableWidget {
|
|||||||
self.force_data_update();
|
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