mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-07-11 06:57:08 +00:00
feature: add disk I/O time series chart (#2073)
This PR adds a disk I/O (read and write rate) graph.
This commit is contained in:
@@ -41,6 +41,7 @@ That said, these are more guidelines rather than hard rules, though the project
|
||||
- [#2053](https://github.com/ClementTsang/bottom/pull/2053): Add a configurable default sort column for the process widget (`processes.default_sort` or `--process-default-sort`).
|
||||
- [#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.
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -53,6 +54,7 @@ That said, these are more guidelines rather than hard rules, though the project
|
||||
- [#2064](https://github.com/ClementTsang/bottom/pull/2064): Move CPU config file options from `[flags]` to be under `[cpu]`.
|
||||
- [#2064](https://github.com/ClementTsang/bottom/pull/2064): Move memory config file options from `[flags]` to be under `[memory_graph]`.
|
||||
- [#2066](https://github.com/ClementTsang/bottom/pull/2066): Take cgroup into account for CPU usage% calculations.
|
||||
- [#2074](https://github.com/ClementTsang/bottom/pull/2074): Treat uppercase 'Q' as a quit shortcut.
|
||||
|
||||
### Other
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -1,4 +1,4 @@
|
||||
# CPU
|
||||
# CPU Configuration
|
||||
|
||||
If you want to change some of the default behaviour of the CPU graph widget, you can configure things under the `[cpu]` section.
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Disk I/O Graph Configuration
|
||||
|
||||
The disk I/O graph widget is configured under `[disk_io_graph]`.
|
||||
|
||||
## Showing Read/Write Lines
|
||||
|
||||
By default, both the read and write rate lines are shown. They can be toggled off independently.
|
||||
|
||||
```toml
|
||||
[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
|
||||
```
|
||||
|
||||
## Legend Labels
|
||||
|
||||
By default, legend entries are labelled by disk name (e.g. `sda`, `nvme0n1`). To use mount points
|
||||
instead (e.g. `/`, `/home`):
|
||||
|
||||
```toml
|
||||
[disk_io_graph]
|
||||
legend = "mount"
|
||||
```
|
||||
|
||||
Valid values are `"disk"` (default) and `"mount"`.
|
||||
|
||||
## Logarithmic Scale
|
||||
|
||||
The y-axis can be switched to a logarithmic scale:
|
||||
|
||||
```toml
|
||||
[disk_io_graph]
|
||||
use_log = true
|
||||
```
|
||||
|
||||
## Legend Position
|
||||
|
||||
The location of the legend can be set with `legend_position`. Valid values are `none`, `top-left`, `top`, `top-right`,
|
||||
`left`, `right`, `bottom-left`, `bottom`, and `bottom-right`. Defaults to `top-right`.
|
||||
|
||||
```toml
|
||||
[disk_io_graph]
|
||||
legend_position = "top-right"
|
||||
```
|
||||
|
||||
## 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
|
||||
name and mount point respectively. 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.
|
||||
|
||||
If we wanted to ignoring any entry with a name that matches `/dev/sda`:
|
||||
|
||||
```toml
|
||||
[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"]
|
||||
|
||||
# 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
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# Disk Table
|
||||
# Disk Table Configuration
|
||||
|
||||
## Columns
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Memory Graph
|
||||
# Memory Graph Configuration
|
||||
|
||||
If you want to change some of the default behaviour of the memory graph widget, you can configure things under the `[memory_graph]` (or `[memory]`) section.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Network Graph
|
||||
# Network Graph Configuration
|
||||
|
||||
## Settings
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Processes
|
||||
# Processes Configuration
|
||||
|
||||
## Settings
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Temperature Graph
|
||||
# Temperature Graph Configuration
|
||||
|
||||
The temperature graph widget is configured under `[temperature_graph]`.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Temperature Table
|
||||
# Temperature Table Configuration
|
||||
|
||||
The temperature table widget is configured under `[temperature]`.
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Disk I/O Graph Widget
|
||||
|
||||
The disk I/O graph widget shows read and write rates over time.
|
||||
|
||||
<figure>
|
||||
<img src="../../../assets/screenshots/disk_graph.webp" alt="A picture of an expanded disk I/O graph widget."/>
|
||||
</figure>
|
||||
|
||||
## Features
|
||||
|
||||
The legend displays the current reads and writes per second in bytes for each mount/disk item. The y-axis
|
||||
automatically scales based on shown read/write values. By default this is in a linear scale, but it can be
|
||||
switched to a logarithmic scale through the [config file](../../configuration/config-file/network-graph.md).
|
||||
|
||||
The displayed time range can be adjusted through either the keyboard or mouse.
|
||||
|
||||
## Key bindings
|
||||
|
||||
Note that key bindings are generally case-sensitive.
|
||||
|
||||
| Binding | Action |
|
||||
| --------- | --------------------------------------- |
|
||||
| ++plus++ | Zoom in on chart (decrease time range) |
|
||||
| ++minus++ | Zoom out on chart (increase time range) |
|
||||
| ++equal++ | Reset zoom |
|
||||
|
||||
## Mouse bindings
|
||||
|
||||
| Binding | Action |
|
||||
| ------------ | -------------------------------------------------------------- |
|
||||
| ++"Scroll"++ | Scrolling up or down zooms in or out of the graph respectively |
|
||||
@@ -12,7 +12,7 @@ received and transmitted.
|
||||
The legend displays the current reads and writes per second in bits, as well as the total amount read/written.
|
||||
|
||||
The y-axis automatically scales based on shown read/write values, and by default, is a linear scale based on base-10 units (e.x. kilobit, gigabit, etc.).
|
||||
Through [configuration](../../configuration/command-line-options.md), the read/write per second unit can be changed to bytes, while the y-axis can be changed to a
|
||||
Through [configuration](../../configuration/config-file/network-graph.md), the read/write per second unit can be changed to bytes, while the y-axis can be changed to a
|
||||
log scale and/or use base-2 units (e.x. kibibit, gibibit, etc.).
|
||||
|
||||
One can also adjust the displayed time range through either the keyboard or mouse, with a range of 30s to 600s.
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
The temperature graph widget provides temperature readings over time.
|
||||
|
||||
<figure>
|
||||
<img src="../../../assets/screenshots/temperature_graph.webp" alt="A picture of an expanded temperature graph widget."/>
|
||||
</figure>
|
||||
|
||||
## Features
|
||||
|
||||
Each detected sensor is drawn as its own line. The y-axis is in the configured temperature unit (Celsius by default;
|
||||
see the `--celsius`/`--fahrenheit`/`--kelvin` arguments or `flags.temperature_type` config option).
|
||||
|
||||
By default the y-axis is bounded at 100°C (or the equivalent in the configured unit) and grows automatically. An upper bound can also be set explicitly via the [config file](../../configuration/config-file/temperature-graph.md).
|
||||
By default the y-axis is bounded at 100°C (or the equivalent in the configured unit) and grows automatically.
|
||||
An upper bound can also be set explicitly via the [config file](../../configuration/config-file/temperature-graph.md).
|
||||
|
||||
The displayed time range can be adjusted through either the keyboard or mouse.
|
||||
|
||||
@@ -26,5 +31,3 @@ Note that key bindings are generally case-sensitive.
|
||||
| Binding | Action |
|
||||
| ------------ | -------------------------------------------------------------- |
|
||||
| ++"Scroll"++ | Scrolling up or down zooms in or out of the graph respectively |
|
||||
|
||||
<!--TODO: Add screenshots of widgets -->
|
||||
|
||||
@@ -168,6 +168,7 @@ nav:
|
||||
- "Disk Widget": usage/widgets/disk.md
|
||||
- "Temperature Widget": usage/widgets/temperature-table.md
|
||||
- "Temperature Graph Widget": usage/widgets/temperature-graph.md
|
||||
- "Disk I/O Graph Widget": usage/widgets/disk-io-graph.md
|
||||
- "Battery Widget": usage/widgets/battery.md
|
||||
- "Auto-Complete": usage/autocomplete.md
|
||||
- "Configuration":
|
||||
@@ -181,6 +182,7 @@ nav:
|
||||
- "Processes Widget": configuration/config-file/processes.md
|
||||
- "Temperature Table Widget": configuration/config-file/temperature-table.md
|
||||
- "Temperature Graph Widget": configuration/config-file/temperature-graph.md
|
||||
- "Disk I/O Graph Widget": configuration/config-file/disk-io-graph.md
|
||||
- "Flags": configuration/config-file/flags.md
|
||||
- "Layout": configuration/config-file/layout.md
|
||||
- "Styling": configuration/config-file/styling.md
|
||||
|
||||
+10
@@ -81,6 +81,7 @@ pub struct AppConfigFields {
|
||||
pub default_temp_sort_column: Option<TempWidgetColumn>,
|
||||
pub default_disk_sort_column: Option<DiskWidgetColumn>,
|
||||
pub temperature_legend_position: Option<LegendPosition>,
|
||||
pub disk_io_legend_position: Option<LegendPosition>,
|
||||
}
|
||||
|
||||
/// For filtering out information
|
||||
@@ -90,6 +91,7 @@ pub struct DataFilters {
|
||||
pub mount_filter: Option<Filter>,
|
||||
pub temp_filter: Option<Filter>,
|
||||
pub temp_graph_filter: Option<Filter>,
|
||||
pub disk_io_graph_filter: Option<Filter>,
|
||||
pub net_filter: Option<Filter>,
|
||||
}
|
||||
|
||||
@@ -2073,6 +2075,14 @@ impl App {
|
||||
{
|
||||
Some(widget_state.graph.state_mut())
|
||||
}
|
||||
BottomWidgetType::DiskIoGraph
|
||||
if let Some(widget_state) = self
|
||||
.states
|
||||
.disk_io_graph_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id) =>
|
||||
{
|
||||
Some(widget_state.graph.state_mut())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+10
-4
@@ -11,7 +11,7 @@ use crate::{
|
||||
collection::{
|
||||
Data,
|
||||
cpu::{CpuHarvest, LoadAvgHarvest},
|
||||
disks,
|
||||
disks::{DiskHarvest, IoHarvest},
|
||||
memory::MemData,
|
||||
network::NetworkHarvest,
|
||||
},
|
||||
@@ -153,6 +153,11 @@ impl StoredData {
|
||||
if let Some(disks) = data.disks {
|
||||
if let Some(io) = data.io {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,9 +176,8 @@ impl StoredData {
|
||||
self.last_update_time = harvested_time;
|
||||
}
|
||||
|
||||
fn eat_disks(
|
||||
&mut self, disks: Vec<disks::DiskHarvest>, io: disks::IoHarvest, harvested_time: Instant,
|
||||
) {
|
||||
// TODO: There's a spike on the first hit. We should probably fix this and the index issue.
|
||||
fn eat_disks(&mut self, disks: Vec<DiskHarvest>, io: IoHarvest, harvested_time: Instant) {
|
||||
let time_since_last_harvest = harvested_time
|
||||
.duration_since(self.last_update_time)
|
||||
.as_secs_f64();
|
||||
@@ -184,6 +188,8 @@ impl StoredData {
|
||||
self.prev_io.reserve(prev_io_diff);
|
||||
self.prev_io.extend((0..prev_io_diff).map(|_| (0, 0)));
|
||||
|
||||
// FIXME: prev_io is indexed by position (itx), not by device name, which might cause problems
|
||||
// if the order changes or something.
|
||||
for (itx, device) in disks.into_iter().enumerate() {
|
||||
let Some(checked_name) = ({
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -12,6 +12,7 @@ use timeless::data::ChunkedData;
|
||||
use crate::{
|
||||
app::{AppConfigFields, DataFilters, filter::Filter, layout_manager::UsedWidgets},
|
||||
collection::Data,
|
||||
widgets::DiskWidgetData,
|
||||
};
|
||||
|
||||
/// Values corresponding to a time slice.
|
||||
@@ -65,6 +66,12 @@ pub struct TimeSeriesData {
|
||||
///
|
||||
/// TODO: Maybe make this use TypedTemperature?
|
||||
pub temperature: HashMap<String, ChunkedData<f32>>,
|
||||
|
||||
/// Disk I/O read rate data, keyed by device name.
|
||||
pub disk_io_read: HashMap<String, ChunkedData<f64>>,
|
||||
|
||||
/// Disk I/O write rate data, keyed by device name.
|
||||
pub disk_io_write: HashMap<String, ChunkedData<f64>>,
|
||||
}
|
||||
|
||||
impl TimeSeriesData {
|
||||
@@ -226,6 +233,42 @@ 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>) {
|
||||
let mut not_visited: HashSet<String> = self.disk_io_read.keys().cloned().collect();
|
||||
|
||||
for disk in disks {
|
||||
if !Filter::optional_should_keep(filter, &disk.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
not_visited.remove(&disk.name);
|
||||
|
||||
let read_entry = self.disk_io_read.entry(disk.name.clone()).or_default();
|
||||
match disk.io_read_rate_bytes {
|
||||
Some(v) => read_entry.push(v as f64),
|
||||
None => read_entry.insert_break(),
|
||||
}
|
||||
|
||||
let write_entry = self.disk_io_write.entry(disk.name.clone()).or_default();
|
||||
match disk.io_write_rate_bytes {
|
||||
Some(v) => write_entry.push(v as f64),
|
||||
None => write_entry.insert_break(),
|
||||
}
|
||||
}
|
||||
|
||||
for name in not_visited {
|
||||
if let Some(entry) = self.disk_io_read.get_mut(&name) {
|
||||
entry.insert_break();
|
||||
}
|
||||
if let Some(entry) = self.disk_io_write.get_mut(&name) {
|
||||
entry.insert_break();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prune any data older than the given duration.
|
||||
pub fn prune(&mut self, max_age: Duration) {
|
||||
if self.time.is_empty() {
|
||||
@@ -297,5 +340,27 @@ impl TimeSeriesData {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
self.disk_io_read.retain(|_, data| {
|
||||
let _ = data.prune(end);
|
||||
|
||||
if data.no_elements() {
|
||||
false
|
||||
} else {
|
||||
data.shrink_to_fit();
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
self.disk_io_write.retain(|_, data| {
|
||||
let _ = data.prune(end);
|
||||
|
||||
if data.no_elements() {
|
||||
false
|
||||
} else {
|
||||
data.shrink_to_fit();
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,6 +943,7 @@ pub enum BottomWidgetType {
|
||||
Temp,
|
||||
TempGraph,
|
||||
Disk,
|
||||
DiskIoGraph,
|
||||
BasicCpu,
|
||||
BasicMem,
|
||||
BasicNet,
|
||||
@@ -958,7 +959,7 @@ impl BottomWidgetType {
|
||||
|
||||
pub fn is_widget_graph(&self) -> bool {
|
||||
use BottomWidgetType::*;
|
||||
matches!(self, Cpu | Net | Mem | TempGraph)
|
||||
matches!(self, Cpu | Net | Mem | TempGraph | DiskIoGraph)
|
||||
}
|
||||
|
||||
pub fn get_pretty_name(&self) -> &str {
|
||||
@@ -990,6 +991,7 @@ impl std::str::FromStr for BottomWidgetType {
|
||||
"temp" | "temperature" => Ok(BottomWidgetType::Temp),
|
||||
"temp_graph" | "temperature_graph" => Ok(BottomWidgetType::TempGraph),
|
||||
"disk" => Ok(BottomWidgetType::Disk),
|
||||
"disk_io_graph" => Ok(BottomWidgetType::DiskIoGraph),
|
||||
"empty" => Ok(BottomWidgetType::Empty),
|
||||
#[cfg(feature = "battery")]
|
||||
"battery" | "batt" => Ok(BottomWidgetType::Battery),
|
||||
@@ -1015,6 +1017,8 @@ Supported widget names:
|
||||
+--------------------------------+
|
||||
| disk |
|
||||
+--------------------------------+
|
||||
| disk_io_graph |
|
||||
+--------------------------------+
|
||||
| batt, battery |
|
||||
+--------------------------------+
|
||||
| empty |
|
||||
@@ -1043,6 +1047,8 @@ Supported widget names:
|
||||
+--------------------------------+
|
||||
| disk |
|
||||
+--------------------------------+
|
||||
| disk_io_graph |
|
||||
+--------------------------------+
|
||||
| empty |
|
||||
+--------------------------------+
|
||||
",
|
||||
@@ -1064,5 +1070,6 @@ pub struct UsedWidgets {
|
||||
pub use_disk: bool,
|
||||
pub use_temp: bool,
|
||||
pub use_temp_graph: bool,
|
||||
pub use_disk_io_graph: bool,
|
||||
pub use_battery: bool,
|
||||
}
|
||||
|
||||
+19
-2
@@ -5,8 +5,9 @@ use crate::{
|
||||
constants,
|
||||
utils::input::InputFieldState,
|
||||
widgets::{
|
||||
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
||||
ProcWidgetState, TempGraphWidgetState, TempWidgetState, query::ProcessQuery,
|
||||
BatteryWidgetState, CpuWidgetState, DiskIoGraphWidgetState, DiskTableWidget,
|
||||
MemWidgetState, NetWidgetState, ProcWidgetState, TempGraphWidgetState, TempWidgetState,
|
||||
query::ProcessQuery,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,6 +19,7 @@ pub struct AppWidgetStates {
|
||||
pub temp_state: TempState,
|
||||
pub temp_graph_state: TempGraphStates,
|
||||
pub disk_state: DiskState,
|
||||
pub disk_io_graph_state: DiskIoGraphStates,
|
||||
pub battery_state: AppBatteryState,
|
||||
pub basic_table_widget_state: Option<BasicTableWidgetState>,
|
||||
}
|
||||
@@ -192,6 +194,21 @@ impl TempGraphStates {
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds per-widget state for all disk I/O graph instances in the layout.
|
||||
pub struct DiskIoGraphStates {
|
||||
pub widget_states: HashMap<u64, DiskIoGraphWidgetState>,
|
||||
}
|
||||
|
||||
impl DiskIoGraphStates {
|
||||
pub fn init(widget_states: HashMap<u64, DiskIoGraphWidgetState>) -> Self {
|
||||
DiskIoGraphStates { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskIoGraphWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskState {
|
||||
pub widget_states: HashMap<u64, DiskTableWidget>,
|
||||
}
|
||||
|
||||
@@ -281,6 +281,12 @@ impl Painter {
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
DiskIoGraph => self.draw_disk_io_graph(
|
||||
f,
|
||||
app_state,
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
} else if app_state.app_config_fields.use_basic_mode {
|
||||
@@ -401,6 +407,9 @@ impl Painter {
|
||||
vertical_chunks[3],
|
||||
widget_id,
|
||||
),
|
||||
DiskIoGraph => {
|
||||
self.draw_disk_io_graph(f, app_state, vertical_chunks[3], widget_id)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +491,9 @@ impl Painter {
|
||||
TempGraph => {
|
||||
self.draw_temperature_graph(f, app_state, *draw_loc, widget.widget_id)
|
||||
}
|
||||
DiskIoGraph => {
|
||||
self.draw_disk_io_graph(f, app_state, *draw_loc, widget.widget_id)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use timeless::data::ChunkedData;
|
||||
use tui::{
|
||||
Frame,
|
||||
layout::{Constraint, Rect},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::App,
|
||||
canvas::{
|
||||
Painter,
|
||||
components::time_series::{AxisBound, ChartScaling, GraphData, LegendConstraints},
|
||||
drawing_utils::should_hide_x_label,
|
||||
},
|
||||
components::time_series::GraphDrawCtx,
|
||||
options::config::disk_io_graph::DiskGraphLegend,
|
||||
utils::data_units::*,
|
||||
};
|
||||
|
||||
impl Painter {
|
||||
pub fn draw_disk_io_graph(
|
||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
if let Some(widget_state) = app_state
|
||||
.states
|
||||
.disk_io_graph_state
|
||||
.get_mut_widget_state(widget_id)
|
||||
{
|
||||
let shared_data = app_state.data_store.get_data();
|
||||
let read_data = &shared_data.time_series_data.disk_io_read;
|
||||
let write_data = &shared_data.time_series_data.disk_io_write;
|
||||
let times = &shared_data.time_series_data.time;
|
||||
|
||||
let mount_map: FxHashMap<&str, &str> = shared_data
|
||||
.disk_harvest
|
||||
.iter()
|
||||
.map(|d| (d.name.as_str(), d.mount_point.as_str()))
|
||||
.collect();
|
||||
|
||||
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
|
||||
let graph_state = widget_state.graph.state_mut();
|
||||
let hide_x_labels = should_hide_x_label(
|
||||
app_state.app_config_fields.hide_time,
|
||||
app_state.app_config_fields.autohide_time,
|
||||
graph_state.autohide_timer_mut(),
|
||||
draw_loc,
|
||||
);
|
||||
let current_display_time = graph_state.current_display_time();
|
||||
|
||||
let show_read = widget_state.show_read;
|
||||
let show_write = widget_state.show_write;
|
||||
let use_log = widget_state.use_log;
|
||||
let legend_type = &widget_state.legend;
|
||||
|
||||
let mut device_names: Vec<&String> = read_data
|
||||
.keys()
|
||||
.filter(|name| {
|
||||
mount_map.contains_key(name.as_str())
|
||||
|| read_data
|
||||
.get(*name)
|
||||
.is_some_and(|d| has_data_in_window(d, times, current_display_time))
|
||||
})
|
||||
.collect();
|
||||
device_names.sort_unstable();
|
||||
|
||||
// Compute y_max across all visible series.
|
||||
let mut visible_sources: Vec<&_> = Vec::new();
|
||||
for name in &device_names {
|
||||
if show_read {
|
||||
if let Some(data) = read_data.get(*name) {
|
||||
visible_sources.push(data);
|
||||
}
|
||||
}
|
||||
if show_write {
|
||||
if let Some(data) = write_data.get(*name) {
|
||||
visible_sources.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
let y_max = widget_state.graph.y_max(visible_sources.into_iter(), times);
|
||||
|
||||
let (adjusted_y_max, y_labels) = if use_log {
|
||||
adjust_disk_io_log(y_max)
|
||||
} else {
|
||||
adjust_disk_io_linear(y_max)
|
||||
};
|
||||
|
||||
let legend_constraints = LegendConstraints {
|
||||
width: Constraint::Ratio(9, 10),
|
||||
height: Constraint::Ratio(1, 2),
|
||||
};
|
||||
|
||||
// Removed devices still visible in the window show "N/A".
|
||||
// TODO: Maybe should make it so the colour is based on entry name? As then it may shift.
|
||||
let read_colours = &self.styles.disk_io_read_colour_styles;
|
||||
let write_colours = &self.styles.disk_io_write_colour_styles;
|
||||
|
||||
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()),
|
||||
};
|
||||
|
||||
let is_active = mount_map.contains_key(name.as_str());
|
||||
|
||||
let read_values = show_read.then(|| read_data.get(*name)).flatten();
|
||||
let write_values = show_write.then(|| write_data.get(*name)).flatten();
|
||||
|
||||
let read_style = if read_colours.is_empty() {
|
||||
tui::style::Style::default()
|
||||
} else {
|
||||
read_colours[idx % read_colours.len()]
|
||||
};
|
||||
let write_style = if write_colours.is_empty() {
|
||||
tui::style::Style::default()
|
||||
} else {
|
||||
write_colours[idx % write_colours.len()]
|
||||
};
|
||||
|
||||
// TODO: Combine into one line; probably need to add some kind of multi-styled GraphData.
|
||||
if let Some(values) = read_values {
|
||||
let rate = if is_active {
|
||||
format_rate_fixed(values.last().copied().unwrap_or(0.0))
|
||||
} else {
|
||||
" N/A".to_string()
|
||||
};
|
||||
graph_data.push(
|
||||
GraphData::default()
|
||||
.name(format!("{display_name} R:{rate}").into())
|
||||
.style(read_style)
|
||||
.time(times)
|
||||
.values(values),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(values) = write_values {
|
||||
let rate = if is_active {
|
||||
format_rate_fixed(values.last().copied().unwrap_or(0.0))
|
||||
} else {
|
||||
" N/A".to_string()
|
||||
};
|
||||
graph_data.push(
|
||||
GraphData::default()
|
||||
.name(format!("{display_name} W:{rate}").into())
|
||||
.style(write_style)
|
||||
.time(times)
|
||||
.values(values),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let marker = self.get_marker(app_state.app_config_fields.use_dot);
|
||||
let y_bounds = AxisBound::Max(adjusted_y_max);
|
||||
let scaling = if use_log {
|
||||
ChartScaling::Log2
|
||||
} else {
|
||||
ChartScaling::Linear
|
||||
};
|
||||
let y_labels: Vec<Cow<'_, str>> = y_labels.into_iter().map(Into::into).collect();
|
||||
|
||||
widget_state.graph.draw(
|
||||
f,
|
||||
draw_loc,
|
||||
GraphDrawCtx {
|
||||
title: " Disk I/O ".into(),
|
||||
border_style,
|
||||
title_style: self.styles.widget_title_style,
|
||||
graph_style: self.styles.graph_style,
|
||||
general_widget_style: self.styles.general_widget_style,
|
||||
border_type: self.styles.border_type,
|
||||
marker,
|
||||
hide_x_labels,
|
||||
is_selected: app_state.current_widget.widget_id == widget_id,
|
||||
is_expanded: app_state.is_expanded,
|
||||
legend_position: app_state.app_config_fields.disk_io_legend_position,
|
||||
legend_constraints: Some(legend_constraints),
|
||||
},
|
||||
y_bounds,
|
||||
&y_labels,
|
||||
scaling,
|
||||
graph_data,
|
||||
);
|
||||
}
|
||||
|
||||
if app_state.should_get_widget_bounds() {
|
||||
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
||||
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||
widget.bottom_right_corner =
|
||||
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `data` has at least one real (non-gap) data point within the
|
||||
/// visible time window defined by `current_display_time` milliseconds from the end
|
||||
/// of `times`.
|
||||
fn has_data_in_window<F: Copy + Default + Into<f64>>(
|
||||
data: &ChunkedData<F>, times: &[Instant], current_display_time: u64,
|
||||
) -> bool {
|
||||
let Some(&last_time) = times.last() else {
|
||||
return false;
|
||||
};
|
||||
let display_duration = Duration::from_millis(current_display_time);
|
||||
let oldest = last_time.checked_sub(display_duration).unwrap_or(last_time);
|
||||
data.iter_along_base(times)
|
||||
.next_back()
|
||||
.is_some_and(|(t, _)| *t >= oldest)
|
||||
}
|
||||
|
||||
/// Format a byte/s rate as a fixed-width string (always 11 chars, right-aligned)
|
||||
/// to keep legend labels a stable width and prevent legend box shifting between frames.
|
||||
fn format_rate_fixed(bytes_per_sec: f64) -> String {
|
||||
let (val, unit) = if bytes_per_sec < KIBI_LIMIT_F64 {
|
||||
(bytes_per_sec, "B/s")
|
||||
} else if bytes_per_sec < MEBI_LIMIT_F64 {
|
||||
(bytes_per_sec / KIBI_LIMIT_F64, "KiB/s")
|
||||
} else if bytes_per_sec < GIBI_LIMIT_F64 {
|
||||
(bytes_per_sec / MEBI_LIMIT_F64, "MiB/s")
|
||||
} else if bytes_per_sec < TEBI_LIMIT_F64 {
|
||||
(bytes_per_sec / GIBI_LIMIT_F64, "GiB/s")
|
||||
} else {
|
||||
(bytes_per_sec / TEBI_LIMIT_F64, "TiB/s")
|
||||
};
|
||||
format!("{:>11}", format!("{val:.1}{unit}"))
|
||||
}
|
||||
|
||||
fn adjust_disk_io_linear(max_entry: f64) -> (f64, Vec<String>) {
|
||||
let max_entry_upper = if max_entry == 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
max_entry * 1.5
|
||||
};
|
||||
|
||||
let (max_value_scaled, unit) = if max_entry_upper < KIBI_LIMIT_F64 {
|
||||
(max_entry, "B/s")
|
||||
} else if max_entry_upper < MEBI_LIMIT_F64 {
|
||||
(max_entry / KIBI_LIMIT_F64, "KiB/s")
|
||||
} else if max_entry_upper < GIBI_LIMIT_F64 {
|
||||
(max_entry / MEBI_LIMIT_F64, "MiB/s")
|
||||
} else if max_entry_upper < TEBI_LIMIT_F64 {
|
||||
(max_entry / GIBI_LIMIT_F64, "GiB/s")
|
||||
} else {
|
||||
(max_entry / TEBI_LIMIT_F64, "TiB/s")
|
||||
};
|
||||
|
||||
let labels = vec![
|
||||
format!("0{unit}"),
|
||||
format!("{:.1}", max_value_scaled * 0.5),
|
||||
format!("{:.1}", max_value_scaled),
|
||||
format!("{:.1}", max_value_scaled * 1.5),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| format!("{s:>8}"))
|
||||
.collect();
|
||||
|
||||
(max_entry_upper, labels)
|
||||
}
|
||||
|
||||
fn adjust_disk_io_log(max_entry: f64) -> (f64, Vec<String>) {
|
||||
use crate::utils::general::saturating_log2;
|
||||
|
||||
let log_max = saturating_log2(max_entry);
|
||||
|
||||
if log_max < LOG_MEBI_LIMIT {
|
||||
(
|
||||
LOG_MEBI_LIMIT,
|
||||
vec![" 0B/s".into(), "1KiB/s".into(), "1MiB/s".into()],
|
||||
)
|
||||
} else if log_max < LOG_GIBI_LIMIT {
|
||||
(
|
||||
LOG_GIBI_LIMIT,
|
||||
vec![
|
||||
" 0B/s".into(),
|
||||
"1KiB/s".into(),
|
||||
"1MiB/s".into(),
|
||||
"1GiB/s".into(),
|
||||
],
|
||||
)
|
||||
} else if log_max < LOG_TEBI_LIMIT {
|
||||
(
|
||||
LOG_TEBI_LIMIT,
|
||||
vec![
|
||||
" 0B/s".into(),
|
||||
"1KiB/s".into(),
|
||||
"1MiB/s".into(),
|
||||
"1GiB/s".into(),
|
||||
"1TiB/s".into(),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
LOG_PEBI_LIMIT,
|
||||
vec![
|
||||
" 0B/s".into(),
|
||||
"1KiB/s".into(),
|
||||
"1MiB/s".into(),
|
||||
"1GiB/s".into(),
|
||||
"1TiB/s".into(),
|
||||
"1PiB/s".into(),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use crate::{collection::network::NetworkHarvest, utils::data_units::convert_byte
|
||||
|
||||
pub mod cpu_basic;
|
||||
pub mod cpu_graph;
|
||||
pub mod disk_io_graph;
|
||||
pub mod disk_table;
|
||||
pub mod mem_basic;
|
||||
pub mod mem_graph;
|
||||
|
||||
+2
-1
@@ -627,7 +627,7 @@ impl DataCollector {
|
||||
|
||||
#[inline]
|
||||
fn update_disks(&mut self) {
|
||||
if self.widgets_to_harvest.use_disk {
|
||||
if self.widgets_to_harvest.use_disk || self.widgets_to_harvest.use_disk_io_graph {
|
||||
self.data.disks = disks::get_disk_usage(self).ok();
|
||||
self.data.io = disks::get_io_usage().ok();
|
||||
}
|
||||
@@ -669,6 +669,7 @@ mod tests {
|
||||
mount_filter: None,
|
||||
temp_filter: None,
|
||||
temp_graph_filter: None,
|
||||
disk_io_graph_filter: None,
|
||||
net_filter: None,
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ impl TimeseriesState {
|
||||
}
|
||||
|
||||
/// Get the current display time.
|
||||
fn current_display_time(&self) -> u64 {
|
||||
pub(crate) fn current_display_time(&self) -> u64 {
|
||||
self.current_display_time
|
||||
}
|
||||
|
||||
|
||||
@@ -360,6 +360,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
let mut temp_state_map: FxHashMap<u64, TempWidgetState> = FxHashMap::default();
|
||||
let mut temp_graph_state_map: FxHashMap<u64, TempGraphWidgetState> = FxHashMap::default();
|
||||
let mut disk_state_map: FxHashMap<u64, DiskTableWidget> = FxHashMap::default();
|
||||
let mut disk_io_graph_state_map: FxHashMap<u64, DiskIoGraphWidgetState> = FxHashMap::default();
|
||||
let mut battery_state_map: FxHashMap<u64, BatteryWidgetState> = FxHashMap::default();
|
||||
|
||||
let autohide_timer = if autohide_time {
|
||||
@@ -402,6 +403,12 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
let network_legend_position = get_network_legend_position(args, config)?;
|
||||
let memory_legend_position = get_memory_legend_position(args, config)?;
|
||||
let temperature_legend_position = get_temperature_legend_position(config)?;
|
||||
let disk_io_legend_position = get_disk_io_legend_position(config)?;
|
||||
let disk_io_name_filter = match &config.disk_io_graph {
|
||||
Some(cfg) => get_ignore_list(&cfg.name_filter)
|
||||
.context("Update 'disk_io_graph.name_filter' in your config file")?,
|
||||
None => None,
|
||||
};
|
||||
|
||||
// TODO: Can probably just reuse the options struct.
|
||||
let app_config_fields = AppConfigFields {
|
||||
@@ -478,6 +485,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.default_sort.to_owned()),
|
||||
temperature_legend_position,
|
||||
disk_io_legend_position,
|
||||
};
|
||||
|
||||
let process_default_sort = match &args.process.process_default_sort {
|
||||
@@ -614,6 +622,43 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
TempGraphWidgetState::new(ts_config, autohide_timer, upper_limit),
|
||||
);
|
||||
}
|
||||
DiskIoGraph => {
|
||||
let show_read = config
|
||||
.disk_io_graph
|
||||
.as_ref()
|
||||
.and_then(|c| c.show_read)
|
||||
.unwrap_or(true);
|
||||
|
||||
let show_write = config
|
||||
.disk_io_graph
|
||||
.as_ref()
|
||||
.and_then(|c| c.show_write)
|
||||
.unwrap_or(true);
|
||||
|
||||
let legend = config
|
||||
.disk_io_graph
|
||||
.as_ref()
|
||||
.and_then(|c| c.legend.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let use_log = config
|
||||
.disk_io_graph
|
||||
.as_ref()
|
||||
.and_then(|c| c.use_log)
|
||||
.unwrap_or(false);
|
||||
|
||||
disk_io_graph_state_map.insert(
|
||||
widget.widget_id,
|
||||
DiskIoGraphWidgetState::new(
|
||||
ts_config,
|
||||
autohide_timer,
|
||||
show_read,
|
||||
show_write,
|
||||
legend,
|
||||
use_log,
|
||||
),
|
||||
);
|
||||
}
|
||||
Battery => {
|
||||
battery_state_map
|
||||
.insert(widget.widget_id, BatteryWidgetState::default());
|
||||
@@ -661,6 +706,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
use_disk: used_widget_set.contains(&Disk),
|
||||
use_temp: used_widget_set.contains(&Temp),
|
||||
use_temp_graph: used_widget_set.contains(&TempGraph),
|
||||
use_disk_io_graph: used_widget_set.contains(&DiskIoGraph),
|
||||
use_battery: used_widget_set.contains(&Battery),
|
||||
};
|
||||
|
||||
@@ -701,6 +747,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
temp_state: TempState::init(temp_state_map),
|
||||
temp_graph_state: TempGraphStates::init(temp_graph_state_map),
|
||||
disk_state: DiskState::init(disk_state_map),
|
||||
disk_io_graph_state: DiskIoGraphStates::init(disk_io_graph_state_map),
|
||||
battery_state: AppBatteryState::init(battery_state_map),
|
||||
basic_table_widget_state,
|
||||
};
|
||||
@@ -714,6 +761,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
mount_filter: disk_mount_filter,
|
||||
temp_filter: temp_sensor_filter,
|
||||
temp_graph_filter: temp_graph_sensor_filter,
|
||||
disk_io_graph_filter: disk_io_name_filter,
|
||||
net_filter: net_interface_filter,
|
||||
};
|
||||
let is_expanded = expanded && !use_basic_mode;
|
||||
@@ -1284,6 +1332,18 @@ fn get_temperature_legend_position(config: &Config) -> OptionResult<Option<Legen
|
||||
)
|
||||
}
|
||||
|
||||
fn get_disk_io_legend_position(config: &Config) -> OptionResult<Option<LegendPosition>> {
|
||||
parse_legend_position(
|
||||
None,
|
||||
config
|
||||
.disk_io_graph
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.legend_position.as_ref()),
|
||||
None,
|
||||
"disk_io_graph.legend_position",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use clap::Parser;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod cpu;
|
||||
pub mod disk;
|
||||
pub mod disk_io_graph;
|
||||
pub mod flags;
|
||||
mod ignore_list;
|
||||
pub mod layout;
|
||||
@@ -11,6 +12,7 @@ pub mod temperature;
|
||||
pub mod temperature_graph;
|
||||
|
||||
use disk::DiskConfig;
|
||||
use disk_io_graph::DiskIoGraphConfig;
|
||||
use flags::GeneralConfig;
|
||||
use network_graph::NetworkGraphConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -32,6 +34,7 @@ pub struct Config {
|
||||
pub(crate) row: Option<Vec<Row>>,
|
||||
pub(crate) processes: Option<ProcessesConfig>,
|
||||
pub(crate) disk: Option<DiskConfig>,
|
||||
pub(crate) disk_io_graph: Option<DiskIoGraphConfig>,
|
||||
pub(crate) temperature: Option<TempConfig>,
|
||||
pub(crate) temperature_graph: Option<TempGraphConfig>,
|
||||
#[serde(alias = "network")]
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::IgnoreList;
|
||||
|
||||
/// Whether the disk graph legend labels use device names or mount points.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DiskGraphLegend {
|
||||
/// Label entries by kernel device name (e.g. `sda`, `nvme0n1`).
|
||||
#[default]
|
||||
Disk,
|
||||
/// Label entries by mount point (e.g. `/`, `/home`).
|
||||
Mount,
|
||||
}
|
||||
|
||||
/// Disk I/O graph configuration.
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
||||
pub(crate) struct DiskIoGraphConfig {
|
||||
/// Whether to show the read rate line. Defaults to true.
|
||||
pub(crate) show_read: Option<bool>,
|
||||
|
||||
/// Whether to show the write rate line. Defaults to true.
|
||||
pub(crate) show_write: Option<bool>,
|
||||
|
||||
/// Whether to label legend entries by device name or mount point. Defaults to disk name.
|
||||
pub(crate) legend: Option<DiskGraphLegend>,
|
||||
|
||||
/// Whether to use a logarithmic scale on the y-axis. Defaults to false.
|
||||
pub(crate) use_log: Option<bool>,
|
||||
|
||||
/// Where to position the legend within the widget.
|
||||
pub(crate) legend_position: Option<String>,
|
||||
|
||||
/// An optional list of device names to include or exclude.
|
||||
pub(crate) name_filter: Option<IgnoreList>,
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
mod battery;
|
||||
mod borders;
|
||||
mod cpu;
|
||||
mod disk_io_graph;
|
||||
mod graphs;
|
||||
mod memory;
|
||||
mod network;
|
||||
@@ -16,6 +17,7 @@ use std::borrow::Cow;
|
||||
|
||||
use battery::BatteryStyle;
|
||||
use cpu::CpuStyle;
|
||||
use disk_io_graph::DiskIoGraphStyle;
|
||||
use graphs::GraphStyle;
|
||||
use memory::MemoryStyle;
|
||||
use network::NetworkStyle;
|
||||
@@ -84,6 +86,9 @@ pub(crate) struct StyleConfig {
|
||||
/// Styling for the network widget.
|
||||
pub(crate) network: Option<NetworkStyle>,
|
||||
|
||||
/// Styling for the disk I/O graph widget.
|
||||
pub(crate) disk_io_graph: Option<DiskIoGraphStyle>,
|
||||
|
||||
/// Styling for the temperature graph widget.
|
||||
pub(crate) temp_graph: Option<TempGraphStyle>,
|
||||
|
||||
@@ -119,6 +124,8 @@ pub struct Styles {
|
||||
pub(crate) avg_cpu_colour: Style,
|
||||
pub(crate) cpu_colour_styles: Vec<Style>,
|
||||
pub(crate) temp_graph_colour_styles: Vec<Style>,
|
||||
pub(crate) disk_io_read_colour_styles: Vec<Style>,
|
||||
pub(crate) disk_io_write_colour_styles: Vec<Style>,
|
||||
pub(crate) border_style: Style,
|
||||
pub(crate) highlighted_border_style: Style,
|
||||
pub(crate) text_style: Style,
|
||||
@@ -191,6 +198,18 @@ impl Styles {
|
||||
temp_graph_colour_styles
|
||||
);
|
||||
|
||||
// Disk I/O graph
|
||||
set_colour_list!(
|
||||
self.disk_io_read_colour_styles,
|
||||
config.disk_io_graph,
|
||||
read_colours
|
||||
);
|
||||
set_colour_list!(
|
||||
self.disk_io_write_colour_styles,
|
||||
config.disk_io_graph,
|
||||
write_colours
|
||||
);
|
||||
|
||||
// Memory
|
||||
set_colour!(self.ram_style, config.memory, ram_colour);
|
||||
set_colour!(self.swap_style, config.memory, swap_colour);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::ColourStr;
|
||||
|
||||
/// Styling specific to the disk I/O graph widget.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
|
||||
pub(crate) struct DiskIoGraphStyle {
|
||||
/// Colour of each disk's read rate graph line. Read in order.
|
||||
#[serde(alias = "read_colours")]
|
||||
pub(crate) read_colours: Option<Vec<ColourStr>>,
|
||||
|
||||
/// Colour of each disk's write rate graph line. Read in order.
|
||||
#[serde(alias = "write_colours")]
|
||||
pub(crate) write_colours: Option<Vec<ColourStr>>,
|
||||
}
|
||||
@@ -31,6 +31,16 @@ impl Styles {
|
||||
colour!(Color::Blue),
|
||||
];
|
||||
|
||||
let second_colours_list = vec![
|
||||
colour!(FOURTH_COLOUR),
|
||||
colour!(Color::LightBlue),
|
||||
colour!(Color::LightRed),
|
||||
colour!(Color::Cyan),
|
||||
colour!(Color::Green),
|
||||
colour!(Color::Blue),
|
||||
colour!(Color::Red),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: colour!(FIRST_COLOUR),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -39,15 +49,7 @@ impl Styles {
|
||||
#[cfg(feature = "zfs")]
|
||||
arc_style: colour!(THIRD_COLOUR),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_colours: vec![
|
||||
colour!(FOURTH_COLOUR),
|
||||
colour!(Color::LightBlue),
|
||||
colour!(Color::LightRed),
|
||||
colour!(Color::Cyan),
|
||||
colour!(Color::Green),
|
||||
colour!(Color::Blue),
|
||||
colour!(Color::Red),
|
||||
],
|
||||
gpu_colours: second_colours_list.clone(),
|
||||
rx_style: colour!(FIRST_COLOUR),
|
||||
tx_style: colour!(SECOND_COLOUR),
|
||||
total_rx_style: colour!(THIRD_COLOUR),
|
||||
@@ -55,7 +57,9 @@ impl Styles {
|
||||
all_cpu_colour: colour!(ALL_COLOUR),
|
||||
avg_cpu_colour: colour!(AVG_COLOUR),
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
temp_graph_colour_styles: list_colours.clone(),
|
||||
disk_io_read_colour_styles: list_colours,
|
||||
disk_io_write_colour_styles: second_colours_list,
|
||||
border_style: colour!(TEXT_COLOUR),
|
||||
highlighted_border_style: colour!(HIGHLIGHT_COLOUR),
|
||||
text_style: colour!(TEXT_COLOUR),
|
||||
@@ -87,6 +91,16 @@ impl Styles {
|
||||
colour!(Color::Red),
|
||||
];
|
||||
|
||||
let second_colours_list = vec![
|
||||
colour!(Color::LightGreen),
|
||||
colour!(Color::LightCyan),
|
||||
colour!(Color::LightRed),
|
||||
colour!(Color::Cyan),
|
||||
colour!(Color::Green),
|
||||
colour!(Color::Blue),
|
||||
colour!(Color::Red),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: colour!(Color::Blue),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -95,21 +109,15 @@ impl Styles {
|
||||
#[cfg(feature = "zfs")]
|
||||
arc_style: colour!(Color::LightBlue),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_colours: vec![
|
||||
colour!(Color::LightGreen),
|
||||
colour!(Color::LightCyan),
|
||||
colour!(Color::LightRed),
|
||||
colour!(Color::Cyan),
|
||||
colour!(Color::Green),
|
||||
colour!(Color::Blue),
|
||||
colour!(Color::Red),
|
||||
],
|
||||
gpu_colours: second_colours_list.clone(),
|
||||
rx_style: colour!(Color::Blue),
|
||||
tx_style: colour!(Color::Red),
|
||||
total_rx_style: colour!(Color::LightBlue),
|
||||
total_tx_style: colour!(Color::LightRed),
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
temp_graph_colour_styles: list_colours.clone(),
|
||||
disk_io_read_colour_styles: list_colours,
|
||||
disk_io_write_colour_styles: second_colours_list,
|
||||
border_style: colour!(Color::Black),
|
||||
text_style: colour!(Color::Black),
|
||||
selected_text_style: colour!(Color::White).bg(Color::LightBlue),
|
||||
|
||||
@@ -31,6 +31,16 @@ impl Styles {
|
||||
hex!("#af3a03"),
|
||||
];
|
||||
|
||||
let second_colours_list = vec![
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
hex!("#cc241d"),
|
||||
hex!("#98971a"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#8ec07c"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -39,15 +49,7 @@ impl Styles {
|
||||
#[cfg(feature = "zfs")]
|
||||
arc_style: hex!("#689d6a"),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_colours: vec![
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
hex!("#cc241d"),
|
||||
hex!("#98971a"),
|
||||
],
|
||||
gpu_colours: second_colours_list.clone(),
|
||||
rx_style: hex!("#8ec07c"),
|
||||
tx_style: hex!("#fabd2f"),
|
||||
total_rx_style: hex!("#689d6a"),
|
||||
@@ -55,7 +57,9 @@ impl Styles {
|
||||
all_cpu_colour: hex!("#8ec07c"),
|
||||
avg_cpu_colour: hex!("#fb4934"),
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
temp_graph_colour_styles: list_colours.clone(),
|
||||
disk_io_read_colour_styles: list_colours,
|
||||
disk_io_write_colour_styles: second_colours_list,
|
||||
border_style: hex!("#ebdbb2"),
|
||||
highlighted_border_style: hex!("#fe8019"),
|
||||
text_style: hex!("#ebdbb2"),
|
||||
@@ -124,7 +128,17 @@ impl Styles {
|
||||
all_cpu_colour: hex!("#8ec07c"),
|
||||
avg_cpu_colour: hex!("#fb4934"),
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
temp_graph_colour_styles: list_colours.clone(),
|
||||
disk_io_read_colour_styles: list_colours,
|
||||
disk_io_write_colour_styles: vec![
|
||||
hex!("#9d0006"),
|
||||
hex!("#98971a"),
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
],
|
||||
border_style: hex!("#3c3836"),
|
||||
highlighted_border_style: hex!("#af3a03"),
|
||||
text_style: hex!("#3c3836"),
|
||||
|
||||
@@ -19,6 +19,16 @@ impl Styles {
|
||||
hex!("#bf616a"),
|
||||
];
|
||||
|
||||
let second_colours_list = vec![
|
||||
hex!("#8fbcbb"),
|
||||
hex!("#81a1c1"),
|
||||
hex!("#d8dee9"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#bf616a"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#88c0d0"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -27,15 +37,7 @@ impl Styles {
|
||||
#[cfg(feature = "zfs")]
|
||||
arc_style: hex!("#5e81ac"),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_colours: vec![
|
||||
hex!("#8fbcbb"),
|
||||
hex!("#81a1c1"),
|
||||
hex!("#d8dee9"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#bf616a"),
|
||||
],
|
||||
gpu_colours: second_colours_list.clone(),
|
||||
rx_style: hex!("#88c0d0"),
|
||||
tx_style: hex!("#d08770"),
|
||||
total_rx_style: hex!("#5e81ac"),
|
||||
@@ -43,7 +45,9 @@ impl Styles {
|
||||
all_cpu_colour: hex!("#88c0d0"),
|
||||
avg_cpu_colour: hex!("#8fbcbb"),
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
temp_graph_colour_styles: list_colours.clone(),
|
||||
disk_io_read_colour_styles: list_colours,
|
||||
disk_io_write_colour_styles: second_colours_list,
|
||||
border_style: hex!("#88c0d0"),
|
||||
highlighted_border_style: hex!("#5e81ac"),
|
||||
text_style: hex!("#e5e9f0"),
|
||||
@@ -76,6 +80,16 @@ impl Styles {
|
||||
hex!("#bf616a"),
|
||||
];
|
||||
|
||||
let second_colours_list = vec![
|
||||
hex!("#8fbcbb"),
|
||||
hex!("#88c0d0"),
|
||||
hex!("#4c566a"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#bf616a"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#81a1c1"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -84,15 +98,7 @@ impl Styles {
|
||||
#[cfg(feature = "zfs")]
|
||||
arc_style: hex!("#5e81ac"),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu_colours: vec![
|
||||
hex!("#8fbcbb"),
|
||||
hex!("#88c0d0"),
|
||||
hex!("#4c566a"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#bf616a"),
|
||||
],
|
||||
gpu_colours: second_colours_list.clone(),
|
||||
rx_style: hex!("#81a1c1"),
|
||||
tx_style: hex!("#d08770"),
|
||||
total_rx_style: hex!("#5e81ac"),
|
||||
@@ -100,7 +106,9 @@ impl Styles {
|
||||
all_cpu_colour: hex!("#81a1c1"),
|
||||
avg_cpu_colour: hex!("#8fbcbb"),
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
temp_graph_colour_styles: list_colours.clone(),
|
||||
disk_io_read_colour_styles: list_colours,
|
||||
disk_io_write_colour_styles: second_colours_list,
|
||||
border_style: hex!("#2e3440"),
|
||||
highlighted_border_style: hex!("#5e81ac"),
|
||||
text_style: hex!("#2e3440"),
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
components::time_series::{AutoYAxisTimeGraph, TimeseriesConfig},
|
||||
options::config::disk_io_graph::DiskGraphLegend,
|
||||
};
|
||||
|
||||
/// Runtime state for a disk I/O graph widget.
|
||||
pub struct DiskIoGraphWidgetState {
|
||||
/// The underlying time-series graph with automatic y-axis scaling.
|
||||
pub graph: AutoYAxisTimeGraph,
|
||||
/// Whether the read rate line is currently shown.
|
||||
pub show_read: bool,
|
||||
/// Whether the write rate line is currently shown.
|
||||
pub show_write: bool,
|
||||
/// Whether legend entries use device names or mount points.
|
||||
pub legend: DiskGraphLegend,
|
||||
/// Whether the y-axis uses a logarithmic scale.
|
||||
pub use_log: bool,
|
||||
}
|
||||
|
||||
impl DiskIoGraphWidgetState {
|
||||
pub fn new(
|
||||
config: TimeseriesConfig, autohide_timer: Option<Instant>, show_read: bool,
|
||||
show_write: bool, legend: DiskGraphLegend, use_log: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
graph: AutoYAxisTimeGraph::new(config, autohide_timer),
|
||||
show_read,
|
||||
show_write,
|
||||
legend,
|
||||
use_log,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod battery_info;
|
||||
pub mod cpu_graph;
|
||||
pub mod disk_io_graph;
|
||||
pub mod disk_table;
|
||||
pub mod mem_graph;
|
||||
pub mod network_graph;
|
||||
@@ -9,6 +10,7 @@ pub mod temperature_table;
|
||||
|
||||
pub use battery_info::*;
|
||||
pub use cpu_graph::*;
|
||||
pub use disk_io_graph::*;
|
||||
pub use disk_table::*;
|
||||
pub use mem_graph::*;
|
||||
pub use network_graph::*;
|
||||
|
||||
@@ -271,6 +271,11 @@ fn test_newer_temperature() {
|
||||
run_and_kill_cfg("./tests/valid_configs/widget/temperature.toml");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_io_graph() {
|
||||
run_and_kill_cfg("./tests/valid_configs/widget/disk_io_graph.toml");
|
||||
}
|
||||
|
||||
/// This uses deprecated temperature settings - once they are removed, this test file should be moved to invalid configs.
|
||||
#[test]
|
||||
fn test_deprecated_temperature() {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
[disk_io_graph]
|
||||
show_read = true
|
||||
show_write = false
|
||||
legend = "mount"
|
||||
use_log = true
|
||||
legend_position = "bottom-left"
|
||||
|
||||
[disk_io_graph.name_filter]
|
||||
is_list_ignored = true
|
||||
list = ["sda"]
|
||||
regex = false
|
||||
case_sensitive = false
|
||||
whole_word = false
|
||||
Reference in New Issue
Block a user