feature: add option to track unmounted disks on Linux (#2088)

This PR adds the option to additionally scan unmounted disk partitions. This currently only works on Linux.
This commit is contained in:
Clement Tsang
2026-06-20 00:19:32 -04:00
committed by GitHub
parent 7916c918fd
commit ddc169c6d0
20 changed files with 521 additions and 16 deletions
+1
View File
@@ -42,6 +42,7 @@ That said, these are more guidelines rather than hard rules, though the project
- [#2066](https://github.com/ClementTsang/bottom/pull/2066): Add search support in the help dialog.
- [#1791](https://github.com/ClementTsang/bottom/pull/1791), [#2072](https://github.com/ClementTsang/bottom/pull/2072): Add support for using a short name for the GPU in memory usage.
- [#2073](https://github.com/ClementTsang/bottom/pull/2073): Add disk I/O time series graph.
- [#2088](https://github.com/ClementTsang/bottom/pull/2088): Add option to show unmounted disks on Linux.
### Changes
@@ -46,6 +46,15 @@ The location of the legend can be set with `legend_position`. Valid values are `
legend_position = "top-right"
```
## Show Unmounted Devices (Linux only)
By default, only mounted devices are shown. To also show unmounted devices on Linux, enable `include_unmounted`:
```toml
[disk_io_graph]
include_unmounted = true
```
## Filtering Entries
You can filter out what entries to show by configuring `[disk_io_graph.name_filter]` and `[disk_io_graph.mount_filter]` to filter by
@@ -22,6 +22,19 @@ 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.
## Show Unmounted Devices (Linux only)
By default, only mounted devices are shown. To also show unmounted devices on Linux, enable `include_unmounted`:
```toml
[disk]
include_unmounted = true
```
Unmounted devices show I/O activity but have no used/free space (those require a live mount), so those
columns appear as `N/A`. Pseudo-devices (`loop*`, `ram*`, `zram*`) are skipped, and you can hide other
noisy devices with the [name filter](#filtering-entries).
## 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,
+42
View File
@@ -228,6 +228,9 @@
# Defaults to "Disk".
#default_sort = "Disk"
# Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
#include_unmounted = false
# 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]
@@ -264,6 +267,45 @@
#whole_word = false
# Disk I/O graph widget configuration
#[disk_io_graph]
# Whether to show the read rate line. Defaults to true.
#show_read = true
# Whether to show the write rate line. Defaults to true.
#show_write = true
# Whether to label legend entries by device name ("disk") or mount point ("mount"). Defaults to "disk".
#legend = "disk"
# Whether to use a logarithmic scale on the y-axis. Defaults to false.
#use_log = false
# Where to place the legend. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
#legend_position = "top-right"
# Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
#include_unmounted = false
# By default, there are no disk I/O name filters enabled. An example use case is provided below.
#[disk_io_graph.name_filter]
# Whether to ignore any matches. Defaults to true.
#is_list_ignored = true
# A list of filters to try and match.
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
# Whether to use regex. Defaults to false.
#regex = true
# Whether to be case-sensitive. Defaults to false.
#case_sensitive = false
# Whether to require matching the whole word. Defaults to false.
#whole_word = false
# Temperature widget configuration
#[temperature]
+132
View File
@@ -25,6 +25,16 @@
}
]
},
"disk_io_graph": {
"anyOf": [
{
"$ref": "#/$defs/DiskIoGraphConfig"
},
{
"type": "null"
}
]
},
"flags": {
"anyOf": [
{
@@ -257,6 +267,13 @@
}
]
},
"include_unmounted": {
"description": "Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.",
"type": [
"boolean",
"null"
]
},
"mount_filter": {
"description": "A filter over the mount names.",
"anyOf": [
@@ -281,6 +298,110 @@
}
}
},
"DiskGraphLegend": {
"description": "Whether the disk graph legend labels use device names or mount points.",
"oneOf": [
{
"description": "Label entries by kernel device name (e.g. `sda`, `nvme0n1`).",
"type": "string",
"const": "disk"
},
{
"description": "Label entries by mount point (e.g. `/`, `/home`).",
"type": "string",
"const": "mount"
}
]
},
"DiskIoGraphConfig": {
"description": "Disk I/O graph configuration.",
"type": "object",
"properties": {
"include_unmounted": {
"description": "Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.",
"type": [
"boolean",
"null"
]
},
"legend": {
"description": "Whether to label legend entries by device name or mount point. Defaults to disk name.",
"anyOf": [
{
"$ref": "#/$defs/DiskGraphLegend"
},
{
"type": "null"
}
]
},
"legend_position": {
"description": "Where to position the legend within the widget.",
"type": [
"string",
"null"
]
},
"name_filter": {
"description": "An optional list of device names to include or exclude.",
"anyOf": [
{
"$ref": "#/$defs/IgnoreList"
},
{
"type": "null"
}
]
},
"show_read": {
"description": "Whether to show the read rate line. Defaults to true.",
"type": [
"boolean",
"null"
]
},
"show_write": {
"description": "Whether to show the write rate line. Defaults to true.",
"type": [
"boolean",
"null"
]
},
"use_log": {
"description": "Whether to use a logarithmic scale on the y-axis. Defaults to false.",
"type": [
"boolean",
"null"
]
}
}
},
"DiskIoGraphStyle": {
"description": "Styling specific to the disk I/O graph widget.",
"type": "object",
"properties": {
"read_colours": {
"description": "Colour of each disk's read rate graph line. Read in order.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/ColourStr"
}
},
"write_colours": {
"description": "Colour of each disk's write rate graph line. Read in order.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/$defs/ColourStr"
}
}
}
},
"DiskWidgetColumn": {
"type": "string",
"enum": [
@@ -1134,6 +1255,17 @@
}
]
},
"disk_io_graph": {
"description": "Styling for the disk I/O graph widget.",
"anyOf": [
{
"$ref": "#/$defs/DiskIoGraphStyle"
},
{
"type": "null"
}
]
},
"graphs": {
"description": "Styling for graph widgets.",
"anyOf": [
+2
View File
@@ -82,6 +82,8 @@ pub struct AppConfigFields {
pub default_disk_sort_column: Option<DiskWidgetColumn>,
pub temperature_legend_position: Option<LegendPosition>,
pub disk_io_legend_position: Option<LegendPosition>,
pub disk_show_unmounted: bool,
pub disk_io_graph_show_unmounted: bool,
}
/// For filtering out information
+5 -2
View File
@@ -155,8 +155,11 @@ impl StoredData {
self.eat_disks(disks, io, harvested_time);
if used_widgets.use_disk_io_graph {
self.time_series_data
.update_disk_io(&self.disk_harvest, &filters.disk_io_graph_filter);
self.time_series_data.update_disk_io(
&self.disk_harvest,
&filters.disk_io_graph_filter,
settings.disk_io_graph_show_unmounted,
);
}
}
}
+11 -1
View File
@@ -236,10 +236,20 @@ impl TimeSeriesData {
/// Update disk I/O time series using already-calculated rates from
/// [`DiskWidgetData`]. Devices not present in `disks` get a break inserted
/// so the legend keeps them around for the window's remaining lifetime.
pub fn update_disk_io(&mut self, disks: &[DiskWidgetData], filter: &Option<Filter>) {
pub fn update_disk_io(
&mut self, disks: &[DiskWidgetData], filter: &Option<Filter>, show_unmounted: bool,
) {
#[cfg(not(target_os = "linux"))]
let _ = show_unmounted;
let mut not_visited: HashSet<String> = self.disk_io_read.keys().cloned().collect();
for disk in disks {
#[cfg(target_os = "linux")]
if !show_unmounted && disk.mount_point.is_empty() {
continue;
}
if !Filter::optional_should_keep(filter, &disk.name) {
continue;
}
+48 -10
View File
@@ -8,6 +8,7 @@ use timeless::data::ChunkedData;
use tui::{
Frame,
layout::{Constraint, Rect},
style::Style,
};
use crate::{
@@ -60,10 +61,31 @@ impl Painter {
let mut device_names: Vec<&String> = read_data
.keys()
.filter(|name| {
mount_map.contains_key(name.as_str())
|| read_data
let has_read_data = || {
read_data
.get(*name)
.is_some_and(|d| has_data_in_window(d, times, current_display_time))
};
// If there is a mount point and we're in mount legend mode, it must be non-empty
// (i.e. actually mounted), or we will short-circuit and ignore it.
if let Some(mount_point) = mount_map.get(name.as_str()) {
match legend_type {
DiskGraphLegend::Disk => true,
DiskGraphLegend::Mount => !mount_point.is_empty() && has_read_data(),
}
} else {
match legend_type {
DiskGraphLegend::Disk => {
// Otherwise, it may have _previously_ been a valid mount point, so keep showing it until it ages out.
has_read_data()
}
DiskGraphLegend::Mount => {
// Since it would be misleading in this case, just skip it in mount mode.
false
}
}
}
})
.collect();
device_names.sort_unstable();
@@ -100,15 +122,31 @@ impl Painter {
let read_colours = &self.styles.disk_io_read_colour_styles;
let write_colours = &self.styles.disk_io_write_colour_styles;
// Pad the device/mount labels to the widest visible one so the rate columns
// line up in the legend (the rate itself is already fixed-width).
let name_width = device_names
.iter()
.map(|name| match legend_type {
DiskGraphLegend::Disk => name.len(),
DiskGraphLegend::Mount => mount_map
.get(name.as_str())
.map(|mount| mount.len())
.unwrap_or(0),
})
.max()
.unwrap_or(0);
let mut graph_data: Vec<GraphData<'_, f64>> =
Vec::with_capacity(device_names.len() * 2);
for (idx, name) in device_names.iter().enumerate() {
let display_name = match legend_type {
DiskGraphLegend::Disk => name.as_str(),
DiskGraphLegend::Mount => mount_map
.get(name.as_str())
.copied()
.unwrap_or(name.as_str()),
DiskGraphLegend::Mount => match mount_map.get(name.as_str()).copied() {
Some(mount) => mount,
// This wouldn't trigger anyway, we filter out devices without mount points in mount legend mode.
None => continue,
},
};
let is_active = mount_map.contains_key(name.as_str());
@@ -117,12 +155,12 @@ impl Painter {
let write_values = show_write.then(|| write_data.get(*name)).flatten();
let read_style = if read_colours.is_empty() {
tui::style::Style::default()
Style::default()
} else {
read_colours[idx % read_colours.len()]
};
let write_style = if write_colours.is_empty() {
tui::style::Style::default()
Style::default()
} else {
write_colours[idx % write_colours.len()]
};
@@ -136,7 +174,7 @@ impl Painter {
};
graph_data.push(
GraphData::default()
.name(format!("{display_name} R:{rate}").into())
.name(format!("{display_name:<name_width$} R:{rate}").into())
.style(read_style)
.time(times)
.values(values),
@@ -151,7 +189,7 @@ impl Painter {
};
graph_data.push(
GraphData::default()
.name(format!("{display_name} W:{rate}").into())
.name(format!("{display_name:<name_width$} W:{rate}").into())
.style(write_style)
.time(times)
.values(values),
+6
View File
@@ -156,6 +156,7 @@ pub struct DataCollector {
last_collection_time: Instant,
widgets_to_harvest: UsedWidgets,
filters: DataFilters,
include_unmounted_disks: bool,
total_rx: u64,
total_tx: u64,
@@ -245,6 +246,7 @@ impl DataCollector {
cgroup_memory_data: CgroupMemCollector::default(),
#[cfg(target_os = "linux")]
cgroup_cpu_data: CgroupCpuCollector::default(),
include_unmounted_disks: false,
}
}
@@ -290,6 +292,10 @@ impl DataCollector {
self.get_process_threads = get_process_threads;
}
pub fn set_include_unmounted_disks(&mut self, include_unmounted_disks: bool) {
self.include_unmounted_disks = include_unmounted_disks;
}
#[cfg(feature = "zfs")]
pub fn set_free_arc_mem(&mut self, free_mem: bool) {
self.free_arc_mem = free_mem;
+2
View File
@@ -35,6 +35,8 @@ use crate::app::filter::Filter;
#[derive(Clone, Debug, Default)]
pub struct DiskHarvest {
pub name: String,
/// Current disk mount point. Empty string if not currently mounted.
pub mount_point: String,
/// Windows also contains an additional volume name field.
+23
View File
@@ -33,10 +33,20 @@ pub fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarve
let mount_filter = &collector.filters.mount_filter;
let mut vec_disks: Vec<DiskHarvest> = Vec::new();
#[cfg(target_os = "linux")]
let mut mounted_names = std::collections::HashSet::new();
for partition in physical_partitions()? {
let name = partition.get_device_name();
let mount_point = partition.mount_point().to_string_lossy().to_string();
#[cfg(target_os = "linux")]
if collector.include_unmounted_disks {
if let Some(base) = name.rsplit('/').next() {
mounted_names.insert(base.to_string());
}
}
// Precedence ordering in the case where name and mount filters disagree,
// "allow" takes precedence over "deny".
//
@@ -74,5 +84,18 @@ pub fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarve
}
}
#[cfg(target_os = "linux")]
{
// If we're including unmounted disks, then we'll add any disks that we previously
// did not mark as mounted.
if collector.include_unmounted_disks {
for disk in unmounted_disks(&mounted_names) {
if keep_disk_entry(&disk.name, &disk.mount_point, disk_filter, &None) {
vec_disks.push(disk);
}
}
}
}
Ok(vec_disks)
}
+2
View File
@@ -1,5 +1,7 @@
mod counters;
mod partition;
mod unmounted;
pub use counters::*;
pub(crate) use partition::*;
pub(crate) use unmounted::*;
@@ -0,0 +1,137 @@
//! Return block devices that aren't currently mounted on Linux.
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader},
};
use crate::collection::disks::DiskHarvest;
/// `/proc/partitions` reports sizes in 1024-byte blocks.
const PARTITION_BLOCK_SIZE: u64 = 1024;
/// Returns [`DiskHarvest`] entries for block devices that aren't in `mounted`.
///
/// These come from `/proc/partitions`, which lists every block device (so even
/// devices with no I/O activity are covered) along with its size in terms of blocks.
/// Note this also filters out some devices, like `loop*`, `ram*`, `zram*`, etc., as
/// these are not "disks".
pub(crate) fn unmounted_disks(mounted: &HashSet<String>) -> Vec<DiskHarvest> {
const PROC_PARTITIONS: &str = "/proc/partitions";
let Ok(file) = File::open(PROC_PARTITIONS) else {
return Vec::new();
};
parse_unmounted_disks(BufReader::new(file), mounted)
}
/// Parses `/proc/partitions` into [`DiskHarvest`] entries for block devices not present in `mounted`.
fn parse_unmounted_disks<R: BufRead>(mut reader: R, mounted: &HashSet<String>) -> Vec<DiskHarvest> {
let mut disks = Vec::new();
let mut line = String::new();
while let Ok(bytes) = reader.read_line(&mut line) {
if bytes == 0 {
break;
}
// Format: `major minor #blocks name`. The header line and the blank line
// after it simply fail to parse here and get skipped.
let mut parts = line.split_whitespace();
let blocks = parts.nth(2).and_then(|b| b.parse::<u64>().ok());
let name = parts.next();
if let (Some(blocks), Some(name)) = (blocks, name) {
if !(mounted.contains(name)
|| name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("zram"))
{
disks.push(DiskHarvest {
name: format!("/dev/{name}"),
mount_point: String::new(),
free_space: None,
used_space: None,
total_space: Some(blocks * PARTITION_BLOCK_SIZE),
});
}
}
line.clear();
}
disks
}
#[cfg(test)]
mod tests {
use super::*;
// Note the header + blank line at the top, exactly like the real file.
const SAMPLE: &str = "\
major minor #blocks name
8 0 500 sda
8 1 1000 sda1
8 2 4000000 sda2
259 0 90000000 nvme0n1
259 1 900000 nvme0n1p1
7 0 100000 loop0
1 0 6000 ram0
252 0 1000000 zram0
8 16 0 sdb
";
fn names(disks: &[DiskHarvest]) -> Vec<&str> {
disks.iter().map(|d| d.name.as_str()).collect()
}
#[test]
fn skip_non_disk_entries() {
let disks = parse_unmounted_disks(SAMPLE.as_bytes(), &HashSet::new());
assert_eq!(
names(&disks),
vec![
"/dev/sda",
"/dev/sda1",
"/dev/sda2",
"/dev/nvme0n1",
"/dev/nvme0n1p1",
"/dev/sdb",
]
);
}
#[test]
fn excludes_mounted_devices() {
let mounted = HashSet::from(["sda2".to_string(), "nvme0n1p1".to_string()]);
let disks = parse_unmounted_disks(SAMPLE.as_bytes(), &mounted);
assert!(!names(&disks).contains(&"/dev/sda2"));
assert!(!names(&disks).contains(&"/dev/nvme0n1p1"));
// Whole disk + other partitions still appear (kept on purpose).
assert!(names(&disks).contains(&"/dev/sda"));
assert!(names(&disks).contains(&"/dev/nvme0n1"));
}
#[test]
fn check_disk_harvest_entry() {
let disks = parse_unmounted_disks(SAMPLE.as_bytes(), &HashSet::new());
let sda = disks.iter().find(|d| d.name == "/dev/sda").unwrap();
assert_eq!(sda.total_space, Some(500 * PARTITION_BLOCK_SIZE));
assert_eq!(sda.mount_point, "");
assert_eq!(sda.free_space, None);
assert_eq!(sda.used_space, None);
}
#[test]
fn zero_block_entry() {
let disks = parse_unmounted_disks(SAMPLE.as_bytes(), &HashSet::new());
let sdb = disks.iter().find(|d| d.name == "/dev/sdb").unwrap();
assert_eq!(sdb.total_space, Some(0));
}
}
+42
View File
@@ -477,6 +477,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
# Defaults to "Disk".
#default_sort = "Disk"
# Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
#include_unmounted = false
# 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]
@@ -513,6 +516,45 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
#whole_word = false
# Disk I/O graph widget configuration
#[disk_io_graph]
# Whether to show the read rate line. Defaults to true.
#show_read = true
# Whether to show the write rate line. Defaults to true.
#show_write = true
# Whether to label legend entries by device name ("disk") or mount point ("mount"). Defaults to "disk".
#legend = "disk"
# Whether to use a logarithmic scale on the y-axis. Defaults to false.
#use_log = false
# Where to place the legend. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
#legend_position = "top-right"
# Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
#include_unmounted = false
# By default, there are no disk I/O name filters enabled. An example use case is provided below.
#[disk_io_graph.name_filter]
# Whether to ignore any matches. Defaults to true.
#is_list_ignored = true
# A list of filters to try and match.
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
# Whether to use regex. Defaults to false.
#regex = true
# Whether to be case-sensitive. Defaults to false.
#case_sensitive = false
# Whether to require matching the whole word. Defaults to false.
#whole_word = false
# Temperature widget configuration
#[temperature]
+3
View File
@@ -234,6 +234,8 @@ fn create_collection_thread(
let get_process_threads = app_config_fields.get_process_threads;
#[cfg(feature = "zfs")]
let get_arc_free = app_config_fields.free_arc;
let include_unmounted_disks =
app_config_fields.disk_show_unmounted || app_config_fields.disk_io_graph_show_unmounted;
thread::spawn(move || {
let mut data_collector = collection::DataCollector::new(filters);
@@ -245,6 +247,7 @@ fn create_collection_thread(
data_collector.set_get_process_threads(get_process_threads);
#[cfg(feature = "zfs")]
data_collector.set_free_arc_mem(get_arc_free);
data_collector.set_include_unmounted_disks(include_unmounted_disks);
data_collector.update_data();
data_collector.data = Data::default();
+13
View File
@@ -409,6 +409,16 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
.context("Update 'disk_io_graph.name_filter' in your config file")?,
None => None,
};
let disk_show_unmounted = config
.disk
.as_ref()
.and_then(|cfg| cfg.include_unmounted)
.unwrap_or(false);
let disk_io_graph_show_unmounted = config
.disk_io_graph
.as_ref()
.and_then(|cfg| cfg.include_unmounted)
.unwrap_or(false);
// TODO: Can probably just reuse the options struct.
let app_config_fields = AppConfigFields {
@@ -486,6 +496,8 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
.and_then(|cfg| cfg.default_sort.to_owned()),
temperature_legend_position,
disk_io_legend_position,
disk_show_unmounted,
disk_io_graph_show_unmounted,
};
let process_default_sort = match &args.process.process_default_sort {
@@ -602,6 +614,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
&app_config_fields,
&styling,
config.disk.as_ref().and_then(|cfg| cfg.columns.as_deref()),
disk_show_unmounted,
),
);
}
+3
View File
@@ -14,6 +14,9 @@ pub(crate) struct DiskConfig {
/// A filter over the mount names.
pub(crate) mount_filter: Option<IgnoreList>,
/// Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
pub(crate) include_unmounted: Option<bool>,
/// A list of disk widget columns.
// TODO: make this more composable(?) in the future, we might need to
// rethink how it's done for custom widgets.
+3
View File
@@ -36,4 +36,7 @@ pub(crate) struct DiskIoGraphConfig {
/// An optional list of device names to include or exclude.
pub(crate) name_filter: Option<IgnoreList>,
/// Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
pub(crate) include_unmounted: Option<bool>,
}
+24 -3
View File
@@ -217,8 +217,9 @@ impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
}
pub struct DiskTableWidget {
pub table: SortDataTable<DiskWidgetData, DiskWidgetColumn>,
pub force_update_data: bool,
pub(crate) table: SortDataTable<DiskWidgetData, DiskWidgetColumn>,
pub(crate) force_update_data: bool,
pub(crate) show_unmounted: bool,
}
impl SortsRow for DiskWidgetColumn {
@@ -318,6 +319,7 @@ const fn default_disk_columns() -> [SortColumn<DiskWidgetColumn>; 8] {
impl DiskTableWidget {
pub fn new(
config: &AppConfigFields, palette: &Styles, columns: Option<&[DiskWidgetColumn]>,
show_unmounted: bool,
) -> Self {
let props = SortDataTableProps {
inner: DataTableProps {
@@ -356,11 +358,13 @@ impl DiskTableWidget {
Self {
table: SortDataTable::new_sortable(columns, props, styling),
force_update_data: false,
show_unmounted,
}
}
None => Self {
table: SortDataTable::new_sortable(default_disk_columns(), props, styling),
force_update_data: false,
show_unmounted,
},
}
}
@@ -373,7 +377,24 @@ impl DiskTableWidget {
/// Update the current table data.
pub fn set_table_data(&mut self, data: &StoredData) {
let mut data = data.disk_harvest.clone();
// Note that the data may contain unmounted disks (e.g. we enable it for another disk widget),
// so we have to potentially filter it out here too.
let mut data: Vec<DiskWidgetData> = if self.show_unmounted {
data.disk_harvest.clone()
} else {
#[cfg(target_os = "linux")]
{
data.disk_harvest
.iter()
.filter(|disk| !disk.mount_point.is_empty())
.cloned()
.collect()
}
#[cfg(not(target_os = "linux"))]
{
data.disk_harvest.clone()
}
};
if let Some(column) = self.table.columns.get(self.table.sort_index()) {
column.sort_by(&mut data, self.table.order());