mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-05-03 21:40:32 +00:00
feature: add temperature graph (#2048)
Add support for a temperature graph widget. Quick-and-dirty implementation for now, will clean this up with other widgets later (maybe after adding a disk graph widget).
This commit is contained in:
@@ -36,6 +36,7 @@ That said, these are more guidelines rather than hard rules, though the project
|
||||
- [#1948](https://github.com/ClementTsang/bottom/pull/1948): Add support for both an `!=` operator and `!` negation prefixes in query searches.
|
||||
- [#2045](https://github.com/ClementTsang/bottom/pull/2045): Add support for showing a decimal place for CPU usage
|
||||
- [#2046](https://github.com/ClementTsang/bottom/pull/2046): Add a `show_table_scroll_bar` config option to show a scroll bar on table widgets.
|
||||
- [#2048](https://github.com/ClementTsang/bottom/pull/2048): Add support for a temperature graph to show sensor temperature change over time.
|
||||
|
||||
### Changes
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ regex = true
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
whole_word = false
|
||||
```
|
||||
|
||||
|
||||
@@ -39,16 +39,17 @@ represents a _widget_. A widget is represented by having a `type` field set to a
|
||||
|
||||
The following `type` values are supported:
|
||||
|
||||
| | |
|
||||
| -------------------------------- | ------------------------ |
|
||||
| `"cpu"` | CPU chart and legend |
|
||||
| `"mem", "memory"` | Memory chart |
|
||||
| `"net", "network"` | Network chart and legend |
|
||||
| `"proc", "process", "processes"` | Process table and search |
|
||||
| `"temp", "temperature"` | Temperature table |
|
||||
| `"disk"` | Disk table |
|
||||
| `"empty"` | An empty space |
|
||||
| `"batt", "battery"` | Battery statistics |
|
||||
| | |
|
||||
| ----------------------------------- | ------------------------ |
|
||||
| `"cpu"` | CPU chart and legend |
|
||||
| `"mem", "memory"` | Memory chart |
|
||||
| `"net", "network"` | Network chart and legend |
|
||||
| `"proc", "process", "processes"` | Process table and search |
|
||||
| `"temp", "temperature"` | Temperature table |
|
||||
| `"temp_graph", "temperature_graph"` | Temperature graph |
|
||||
| `"disk"` | Disk table |
|
||||
| `"empty"` | An empty space |
|
||||
| `"batt", "battery"` | Battery statistics |
|
||||
|
||||
Each component of the layout accepts a `ratio` value. If this is not set, it defaults to 1.
|
||||
|
||||
|
||||
@@ -29,6 +29,6 @@ regex = true
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
whole_word = false
|
||||
```
|
||||
|
||||
@@ -103,6 +103,14 @@ These can be set under `[styles.cpu]`:
|
||||
| `avg_entry_color` | The colour of the average CPU label and graph line | `avg_entry_color = "255, 0, 255"` |
|
||||
| `cpu_core_colors` | Colour of each CPU threads' label and graph line. Read in order. | `cpu_core_colors = ["Red", "Blue", "Green"]` |
|
||||
|
||||
#### Temperature Graph
|
||||
|
||||
These can be set under `[styles.temp_graph]`:
|
||||
|
||||
| Config field | Details | Examples |
|
||||
| ------------------------- | -------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| `temp_graph_color_styles` | Colour of each temperature sensor's graph line. Read in order. | `temp_graph_color_styles = ["Red", "Blue", "Green"]` |
|
||||
|
||||
#### Memory
|
||||
|
||||
These can be set under `[styles.memory]`:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Temperature Graph
|
||||
|
||||
The temperature graph widget is configured under `[temperature_graph]`.
|
||||
|
||||
## 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
|
||||
[temperature_graph]
|
||||
legend_position = "top-right"
|
||||
```
|
||||
|
||||
## Upper Limit
|
||||
|
||||
By default, the y-axis is bounded at 100°C (or the equivalent in the configured `temperature_type`) and grows
|
||||
automatically if a reading exceeds that. An explicit upper bound can be set with `max_temp` (uses the same unit as
|
||||
`temperature_type`). Sensor readings above this value will be drawn off the chart.
|
||||
|
||||
```toml
|
||||
[temperature_graph]
|
||||
max_temp = 90.0
|
||||
```
|
||||
|
||||
## Filtering Entries
|
||||
|
||||
You can filter which sensors to plot by configuring `[temperature_graph.sensor_filter]`. This works the same way as
|
||||
the temperature table's filter.
|
||||
|
||||
For example, here we are ignoring any sensor that has "cpu" or "wifi" in its name:
|
||||
|
||||
```toml
|
||||
[temperature_graph.sensor_filter]
|
||||
# Whether to ignore any matches. Defaults to true.
|
||||
is_list_ignored = true
|
||||
|
||||
# A list of filters to try and match.
|
||||
list = ["cpu", "wifi"]
|
||||
|
||||
# Whether to use regex. Defaults to false.
|
||||
regex = false
|
||||
|
||||
# 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,5 +1,7 @@
|
||||
# Temperature Table
|
||||
|
||||
The temperature table widget is configured under `[temperature]`.
|
||||
|
||||
## Default Sort Order
|
||||
|
||||
You can customize the default sort order (by default, it sorts by temperature sensor name). For example, to sort by temperature:
|
||||
@@ -29,6 +31,6 @@ regex = false
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
whole_word = false
|
||||
```
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Temperature Graph Widget
|
||||
|
||||
The temperature graph widget provides temperature readings over time.
|
||||
|
||||
## Features
|
||||
|
||||
Each detected sensor is drawn as its own line. The y-axis is in the configured temperature unit (Celsius by default;
|
||||
see the `temperature_type` flag).
|
||||
|
||||
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.
|
||||
|
||||
## 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 |
|
||||
+5
-2
@@ -3,7 +3,8 @@ site_name: bottom
|
||||
site_author: Clement Tsang
|
||||
site_url: https://bottom.pages.dev
|
||||
site_description: >-
|
||||
A customizable cross-platform graphical process/system monitor for the terminal. Supports Linux, macOS, and Windows.
|
||||
A customizable cross-platform graphical process/system monitor for the
|
||||
terminal. Supports Linux, macOS, and Windows.
|
||||
docs_dir: "content/"
|
||||
# Project information
|
||||
repo_name: ClementTsang/bottom
|
||||
@@ -165,7 +166,8 @@ nav:
|
||||
- "Network Widget": usage/widgets/network.md
|
||||
- "Process Widget": usage/widgets/process.md
|
||||
- "Disk Widget": usage/widgets/disk.md
|
||||
- "Temperature Widget": usage/widgets/temperature.md
|
||||
- "Temperature Widget": usage/widgets/temperature-table.md
|
||||
- "Temperature Graph Widget": usage/widgets/temperature-graph.md
|
||||
- "Battery Widget": usage/widgets/battery.md
|
||||
- "Auto-Complete": usage/autocomplete.md
|
||||
- "Configuration":
|
||||
@@ -177,6 +179,7 @@ nav:
|
||||
- "Network Widget": configuration/config-file/network.md
|
||||
- "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
|
||||
- "Flags": configuration/config-file/flags.md
|
||||
- "Layout": configuration/config-file/layout.md
|
||||
- "Styling": configuration/config-file/styling.md
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
# By default, there are no mount name filters enabled. An example use case is provided below.
|
||||
@@ -199,7 +199,7 @@
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
@@ -226,7 +226,35 @@
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
# Temperature graph widget configuration
|
||||
#[temperature_graph]
|
||||
|
||||
# Where to place the legend for the temperature graph widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
|
||||
#legend_position = "top-right"
|
||||
|
||||
# An upper temperature value for the graph; entries higher than this will be hidden. If not set,
|
||||
# there is no limit. Is in the unit of `temperature_type`.
|
||||
#max_temp = 100.0
|
||||
|
||||
# By default, there are no temperature sensor filters enabled. An example use case is provided below.
|
||||
#[temperature_graph.sensor_filter]
|
||||
# Whether to ignore any matches. Defaults to true.
|
||||
#is_list_ignored = true
|
||||
|
||||
# A list of filters to try and match.
|
||||
#list = ["cpu", "wifi"]
|
||||
|
||||
# Whether to use regex. Defaults to false.
|
||||
#regex = false
|
||||
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
@@ -246,7 +274,7 @@
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
@@ -270,6 +298,9 @@
|
||||
#avg_entry_color = "red"
|
||||
#cpu_core_colors = ["light magenta", "light yellow", "light cyan", "light green", "light blue", "cyan", "green", "blue"]
|
||||
|
||||
#[styles.temp_graph]
|
||||
#temp_graph_color_styles = ["light magenta", "light yellow", "light cyan", "light green", "light blue", "cyan", "green", "blue"]
|
||||
|
||||
#[styles.memory]
|
||||
#ram_color = "light magenta"
|
||||
#cache_color = "light red"
|
||||
@@ -312,7 +343,7 @@
|
||||
# [[row.child]] represents either a widget or a column.
|
||||
# [[row.child.child]] represents a widget.
|
||||
#
|
||||
# All widgets must have the type value set to one of ["cpu", "mem", "proc", "net", "temp", "disk", "empty"].
|
||||
# All widgets must have the type value set to one of ["cpu", "mem", "proc", "net", "temp", "temp_graph", "disk", "empty"].
|
||||
# All layout components have a ratio value - if this is not set, then it defaults to 1.
|
||||
# The default widget layout:
|
||||
#[[row]]
|
||||
|
||||
@@ -83,6 +83,16 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"temperature_graph": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/TempGraphConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -980,6 +990,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"temp_graph": {
|
||||
"description": "Styling for the temperature graph widget.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/TempGraphStyle"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"theme": {
|
||||
"description": "A built-in theme.\n\nIf this is and a custom colour are both set, in the config file,\nthe custom colour scheme will be prioritized first. If a theme\nis set in the command-line args, however, it will always be\nprioritized first.",
|
||||
"type": [
|
||||
@@ -1053,6 +1074,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TempGraphConfig": {
|
||||
"description": "Temperature graph configuration.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"legend_position": {
|
||||
"description": "The location of the graph's legend.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"max_temp": {
|
||||
"description": "An upper temperature value for the graph; entries higher than this will be hidden. If not set,\nthere is no limit.\n\nIs in the unit of `temperature_type`.",
|
||||
"type": [
|
||||
"number",
|
||||
"null"
|
||||
],
|
||||
"format": "double",
|
||||
"default": null
|
||||
},
|
||||
"sensor_filter": {
|
||||
"description": "A filter over the sensor names.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/IgnoreList"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TempGraphStyle": {
|
||||
"description": "Styling specific to the temperature graph widget.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"temp_graph_color_styles": {
|
||||
"description": "Colour of each temperature sensor's graph line. Read in order.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/$defs/ColorStr"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TempWidgetColumn": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
+69
-2
@@ -80,14 +80,16 @@ pub struct AppConfigFields {
|
||||
pub default_tree_collapse: bool,
|
||||
pub default_temp_sort_column: Option<TempWidgetColumn>,
|
||||
pub default_disk_sort_column: Option<DiskWidgetColumn>,
|
||||
pub temperature_legend_position: Option<LegendPosition>,
|
||||
}
|
||||
|
||||
/// For filtering out information
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DataFilters {
|
||||
pub disk_filter: Option<Filter>,
|
||||
pub mount_filter: Option<Filter>,
|
||||
pub temp_filter: Option<Filter>,
|
||||
pub temp_graph_filter: Option<Filter>,
|
||||
pub net_filter: Option<Filter>,
|
||||
}
|
||||
|
||||
@@ -117,10 +119,13 @@ impl App {
|
||||
widget_map: HashMap<u64, BottomWidget>, current_widget: BottomWidget,
|
||||
used_widgets: UsedWidgets, filters: DataFilters, is_expanded: bool,
|
||||
) -> Self {
|
||||
let mut data_store = DataStore::new(used_widgets);
|
||||
data_store.set_filters(filters.clone());
|
||||
|
||||
Self {
|
||||
awaiting_second_char: false,
|
||||
second_char: None,
|
||||
data_store: DataStore::default(),
|
||||
data_store,
|
||||
last_key_press: Instant::now(),
|
||||
process_kill_dialog: ProcessKillDialog::default(),
|
||||
help_dialog_state: AppHelpDialogState::default(),
|
||||
@@ -2038,6 +2043,31 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomWidgetType::TempGraph => {
|
||||
if let Some(widget_state) = self
|
||||
.states
|
||||
.temp_graph_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
let new_time = widget_state
|
||||
.current_display_time
|
||||
.saturating_add(self.app_config_fields.time_interval);
|
||||
|
||||
if new_time <= self.app_config_fields.retention_ms {
|
||||
widget_state.current_display_time = new_time;
|
||||
if self.app_config_fields.autohide_time {
|
||||
widget_state.autohide_timer = Some(Instant::now());
|
||||
}
|
||||
} else if widget_state.current_display_time
|
||||
!= self.app_config_fields.retention_ms
|
||||
{
|
||||
widget_state.current_display_time = self.app_config_fields.retention_ms;
|
||||
if self.app_config_fields.autohide_time {
|
||||
widget_state.autohide_timer = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -2116,6 +2146,29 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
BottomWidgetType::TempGraph => {
|
||||
if let Some(widget_state) = self
|
||||
.states
|
||||
.temp_graph_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
let new_time = widget_state
|
||||
.current_display_time
|
||||
.saturating_sub(self.app_config_fields.time_interval);
|
||||
|
||||
if new_time >= STALE_MIN_MILLISECONDS {
|
||||
widget_state.current_display_time = new_time;
|
||||
if self.app_config_fields.autohide_time {
|
||||
widget_state.autohide_timer = Some(Instant::now());
|
||||
}
|
||||
} else if widget_state.current_display_time != STALE_MIN_MILLISECONDS {
|
||||
widget_state.current_display_time = STALE_MIN_MILLISECONDS;
|
||||
if self.app_config_fields.autohide_time {
|
||||
widget_state.autohide_timer = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -2162,11 +2215,25 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_temp_graph_zoom(&mut self) {
|
||||
if let Some(widget_state) = self
|
||||
.states
|
||||
.temp_graph_state
|
||||
.get_mut_widget_state(self.current_widget.widget_id)
|
||||
{
|
||||
widget_state.current_display_time = self.app_config_fields.default_time_value;
|
||||
if self.app_config_fields.autohide_time {
|
||||
widget_state.autohide_timer = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_zoom(&mut self) {
|
||||
match self.current_widget.widget_type {
|
||||
BottomWidgetType::Cpu => self.reset_cpu_zoom(),
|
||||
BottomWidgetType::Mem => self.reset_mem_zoom(),
|
||||
BottomWidgetType::Net => self.reset_net_zoom(),
|
||||
BottomWidgetType::TempGraph => self.reset_temp_graph_zoom(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+39
-12
@@ -7,8 +7,14 @@ use super::{ProcessData, TimeSeriesData};
|
||||
#[cfg(feature = "battery")]
|
||||
use crate::collection::batteries;
|
||||
use crate::{
|
||||
app::AppConfigFields,
|
||||
collection::{Data, cpu, disks, memory::MemData, network},
|
||||
app::{AppConfigFields, DataFilters, filter::Filter, layout_manager::UsedWidgets},
|
||||
collection::{
|
||||
Data,
|
||||
cpu::{CpuHarvest, LoadAvgHarvest},
|
||||
disks,
|
||||
memory::MemData,
|
||||
network::NetworkHarvest,
|
||||
},
|
||||
utils::data_units::DataUnit,
|
||||
widgets::{DiskWidgetData, TempWidgetData},
|
||||
};
|
||||
@@ -22,7 +28,7 @@ pub struct StoredData {
|
||||
// FIXME: (points_rework_v1) we could be able to remove this with some more refactoring.
|
||||
pub last_update_time: Instant,
|
||||
pub timeseries_data: TimeSeriesData,
|
||||
pub network_harvest: network::NetworkHarvest,
|
||||
pub network_harvest: NetworkHarvest,
|
||||
pub ram_harvest: Option<MemData>,
|
||||
pub swap_harvest: Option<MemData>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -31,8 +37,8 @@ pub struct StoredData {
|
||||
pub arc_harvest: Option<MemData>,
|
||||
#[cfg(feature = "gpu")]
|
||||
pub gpu_harvest: Vec<(String, MemData)>,
|
||||
pub cpu_harvest: cpu::CpuHarvest,
|
||||
pub load_avg_harvest: cpu::LoadAvgHarvest,
|
||||
pub cpu_harvest: CpuHarvest,
|
||||
pub load_avg_harvest: LoadAvgHarvest,
|
||||
pub process_data: ProcessData,
|
||||
/// TODO: (points_rework_v1) Might be a better way to do this without having
|
||||
/// to store here?
|
||||
@@ -48,13 +54,13 @@ impl Default for StoredData {
|
||||
StoredData {
|
||||
last_update_time: Instant::now(),
|
||||
timeseries_data: TimeSeriesData::default(),
|
||||
network_harvest: network::NetworkHarvest::default(),
|
||||
network_harvest: NetworkHarvest::default(),
|
||||
ram_harvest: None,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
cache_harvest: None,
|
||||
swap_harvest: None,
|
||||
cpu_harvest: cpu::CpuHarvest::default(),
|
||||
load_avg_harvest: cpu::LoadAvgHarvest::default(),
|
||||
cpu_harvest: CpuHarvest::default(),
|
||||
load_avg_harvest: LoadAvgHarvest::default(),
|
||||
process_data: Default::default(),
|
||||
prev_io: Vec::default(),
|
||||
disk_harvest: Vec::default(),
|
||||
@@ -78,7 +84,10 @@ impl StoredData {
|
||||
clippy::boxed_local,
|
||||
reason = "This avoids warnings on certain platforms (e.g. 32-bit)."
|
||||
)]
|
||||
fn eat_data(&mut self, mut data: Box<Data>, settings: &AppConfigFields) {
|
||||
fn eat_data(
|
||||
&mut self, mut data: Box<Data>, settings: &AppConfigFields, used_widgets: &UsedWidgets,
|
||||
filters: &DataFilters,
|
||||
) {
|
||||
let harvested_time = data.collection_time;
|
||||
|
||||
// We must adjust all the network values to their selected type (defaults to
|
||||
@@ -91,7 +100,8 @@ impl StoredData {
|
||||
}
|
||||
|
||||
if !settings.use_basic_mode {
|
||||
self.timeseries_data.add(&data);
|
||||
self.timeseries_data
|
||||
.add(&data, used_widgets, settings, filters);
|
||||
}
|
||||
|
||||
if let Some(network) = data.network {
|
||||
@@ -129,6 +139,7 @@ impl StoredData {
|
||||
.map(|sensors| {
|
||||
sensors
|
||||
.into_iter()
|
||||
.filter(|temp| Filter::optional_should_keep(&filters.temp_filter, &temp.name))
|
||||
.map(|temp| TempWidgetData {
|
||||
sensor: temp.name,
|
||||
temperature: temp
|
||||
@@ -279,13 +290,24 @@ pub enum FrozenState {
|
||||
}
|
||||
|
||||
/// What data to share to other parts of the application.
|
||||
#[derive(Default)]
|
||||
pub struct DataStore {
|
||||
frozen_state: FrozenState,
|
||||
main: StoredData,
|
||||
used_widgets: UsedWidgets,
|
||||
filters: DataFilters,
|
||||
}
|
||||
|
||||
impl DataStore {
|
||||
/// Create a new [`DataStore`]
|
||||
pub fn new(used_widgets: UsedWidgets) -> Self {
|
||||
Self {
|
||||
frozen_state: FrozenState::default(),
|
||||
main: StoredData::default(),
|
||||
used_widgets,
|
||||
filters: DataFilters::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle whether the [`DataState`] is frozen or not.
|
||||
pub fn toggle_frozen(&mut self) {
|
||||
match &self.frozen_state {
|
||||
@@ -311,9 +333,14 @@ impl DataStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_filters(&mut self, filters: DataFilters) {
|
||||
self.filters = filters;
|
||||
}
|
||||
|
||||
/// Eat data.
|
||||
pub fn eat_data(&mut self, data: Box<Data>, settings: &AppConfigFields) {
|
||||
self.main.eat_data(data, settings);
|
||||
self.main
|
||||
.eat_data(data, settings, &self.used_widgets, &self.filters);
|
||||
}
|
||||
|
||||
/// Clean data.
|
||||
|
||||
@@ -26,6 +26,15 @@ impl FromStr for TemperatureType {
|
||||
}
|
||||
|
||||
impl TemperatureType {
|
||||
/// Return the unit string.
|
||||
pub fn unit(&self) -> &'static str {
|
||||
match self {
|
||||
TemperatureType::Celsius => "°C",
|
||||
TemperatureType::Kelvin => "K",
|
||||
TemperatureType::Fahrenheit => "°F",
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a temperature in Celsius, convert it if necessary for a different
|
||||
/// unit.
|
||||
pub fn convert_temp_unit(&self, celsius: f32) -> TypedTemperature {
|
||||
@@ -37,6 +46,16 @@ impl TemperatureType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a temperature in Celsius, convert it if necessary for a different
|
||||
/// unit as a bare float.
|
||||
pub fn convert_temp_unit_float(&self, celsius: f32) -> f32 {
|
||||
match self {
|
||||
TemperatureType::Celsius => celsius,
|
||||
TemperatureType::Kelvin => celsius + 273.15,
|
||||
TemperatureType::Fahrenheit => celsius * (9.0 / 5.0) + 32.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A temperature and its type.
|
||||
|
||||
@@ -6,11 +6,13 @@ use std::{
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
use timeless::data::ChunkedData;
|
||||
|
||||
use crate::collection::Data;
|
||||
use crate::{
|
||||
app::{AppConfigFields, DataFilters, filter::Filter, layout_manager::UsedWidgets},
|
||||
collection::Data,
|
||||
};
|
||||
|
||||
/// Values corresponding to a time slice.
|
||||
pub type Values = ChunkedData<f64>;
|
||||
@@ -58,11 +60,17 @@ pub struct TimeSeriesData {
|
||||
#[cfg(feature = "gpu")]
|
||||
/// GPU memory data.
|
||||
pub gpu_mem: HashMap<String, Values>,
|
||||
|
||||
/// Temperature data.
|
||||
pub temperature: HashMap<String, ChunkedData<f32>>,
|
||||
}
|
||||
|
||||
impl TimeSeriesData {
|
||||
/// Add a new data point.
|
||||
pub fn add(&mut self, data: &Data) {
|
||||
pub fn add(
|
||||
&mut self, data: &Data, used_widgets: &UsedWidgets, settings: &AppConfigFields,
|
||||
filters: &DataFilters,
|
||||
) {
|
||||
self.time.push(data.collection_time);
|
||||
|
||||
if let Some(network) = &data.network {
|
||||
@@ -168,6 +176,52 @@ impl TimeSeriesData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if used_widgets.use_temp_graph {
|
||||
if let Some(temperature_sensors) = &data.temperature_sensors {
|
||||
let mut not_visited = self
|
||||
.temperature
|
||||
.keys()
|
||||
.map(String::to_owned)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for sensor_data in temperature_sensors {
|
||||
if !Filter::optional_should_keep(&filters.temp_graph_filter, &sensor_data.name)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(temperature) = sensor_data.temperature {
|
||||
not_visited.remove(&sensor_data.name);
|
||||
|
||||
if !self.temperature.contains_key(&sensor_data.name) {
|
||||
self.temperature
|
||||
.insert(sensor_data.name.clone(), ChunkedData::default());
|
||||
}
|
||||
|
||||
let curr = self
|
||||
.temperature
|
||||
.get_mut(&sensor_data.name)
|
||||
.expect("entry must exist as it was created above");
|
||||
|
||||
let converted_temperature = settings
|
||||
.temperature_type
|
||||
.convert_temp_unit_float(temperature);
|
||||
curr.push(converted_temperature);
|
||||
}
|
||||
}
|
||||
|
||||
for nv in not_visited {
|
||||
if let Some(entry) = self.temperature.get_mut(&nv) {
|
||||
entry.insert_break();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for g in self.temperature.values_mut() {
|
||||
g.insert_break();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prune any data older than the given duration.
|
||||
@@ -229,5 +283,17 @@ impl TimeSeriesData {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.temperature.retain(|_, data| {
|
||||
let _ = data.prune(end);
|
||||
|
||||
// Remove the entry if it is empty. We can always add it again later.
|
||||
if data.no_elements() {
|
||||
false
|
||||
} else {
|
||||
data.shrink_to_fit();
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+41
-33
@@ -941,6 +941,7 @@ pub enum BottomWidgetType {
|
||||
ProcSearch,
|
||||
ProcSort,
|
||||
Temp,
|
||||
TempGraph,
|
||||
Disk,
|
||||
BasicCpu,
|
||||
BasicMem,
|
||||
@@ -957,7 +958,7 @@ impl BottomWidgetType {
|
||||
|
||||
pub fn is_widget_graph(&self) -> bool {
|
||||
use BottomWidgetType::*;
|
||||
matches!(self, Cpu | Net | Mem)
|
||||
matches!(self, Cpu | Net | Mem | TempGraph)
|
||||
}
|
||||
|
||||
pub fn get_pretty_name(&self) -> &str {
|
||||
@@ -970,6 +971,7 @@ impl BottomWidgetType {
|
||||
Temp => "Temperature",
|
||||
Disk => "Disks",
|
||||
Battery => "Battery",
|
||||
TempGraph => "Temperature",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
@@ -986,6 +988,7 @@ impl std::str::FromStr for BottomWidgetType {
|
||||
"net" | "network" => Ok(BottomWidgetType::Net),
|
||||
"proc" | "process" | "processes" => Ok(BottomWidgetType::Proc),
|
||||
"temp" | "temperature" => Ok(BottomWidgetType::Temp),
|
||||
"temp_graph" | "temperature_graph" => Ok(BottomWidgetType::TempGraph),
|
||||
"disk" => Ok(BottomWidgetType::Disk),
|
||||
"empty" => Ok(BottomWidgetType::Empty),
|
||||
#[cfg(feature = "battery")]
|
||||
@@ -997,23 +1000,25 @@ impl std::str::FromStr for BottomWidgetType {
|
||||
"'{s}' is an invalid widget name.
|
||||
|
||||
Supported widget names:
|
||||
+--------------------------+
|
||||
| cpu |
|
||||
+--------------------------+
|
||||
| mem, memory |
|
||||
+--------------------------+
|
||||
| net, network |
|
||||
+--------------------------+
|
||||
| proc, process, processes |
|
||||
+--------------------------+
|
||||
| temp, temperature |
|
||||
+--------------------------+
|
||||
| disk |
|
||||
+--------------------------+
|
||||
| batt, battery |
|
||||
+--------------------------+
|
||||
| empty |
|
||||
+--------------------------+
|
||||
+--------------------------------+
|
||||
| cpu |
|
||||
+--------------------------------+
|
||||
| mem, memory |
|
||||
+--------------------------------+
|
||||
| net, network |
|
||||
+--------------------------------+
|
||||
| proc, process, processes |
|
||||
+--------------------------------+
|
||||
| temp, temperature |
|
||||
+--------------------------------+
|
||||
| temp_graph, temperature_graph |
|
||||
+--------------------------------+
|
||||
| disk |
|
||||
+--------------------------------+
|
||||
| batt, battery |
|
||||
+--------------------------------+
|
||||
| empty |
|
||||
+--------------------------------+
|
||||
",
|
||||
)))
|
||||
}
|
||||
@@ -1023,21 +1028,23 @@ Supported widget names:
|
||||
"'{s}' is an invalid widget name.
|
||||
|
||||
Supported widget names:
|
||||
+--------------------------+
|
||||
| cpu |
|
||||
+--------------------------+
|
||||
| mem, memory |
|
||||
+--------------------------+
|
||||
| net, network |
|
||||
+--------------------------+
|
||||
| proc, process, processes |
|
||||
+--------------------------+
|
||||
| temp, temperature |
|
||||
+--------------------------+
|
||||
| disk |
|
||||
+--------------------------+
|
||||
| empty |
|
||||
+--------------------------+
|
||||
+--------------------------------+
|
||||
| cpu |
|
||||
+--------------------------------+
|
||||
| mem, memory |
|
||||
+--------------------------------+
|
||||
| net, network |
|
||||
+--------------------------------+
|
||||
| proc, process, processes |
|
||||
+--------------------------------+
|
||||
| temp, temperature |
|
||||
+--------------------------------+
|
||||
| temp_graph, temperature_graph |
|
||||
+--------------------------------+
|
||||
| disk |
|
||||
+--------------------------------+
|
||||
| empty |
|
||||
+--------------------------------+
|
||||
",
|
||||
)))
|
||||
}
|
||||
@@ -1056,5 +1063,6 @@ pub struct UsedWidgets {
|
||||
pub use_proc: bool,
|
||||
pub use_disk: bool,
|
||||
pub use_temp: bool,
|
||||
pub use_temp_graph: bool,
|
||||
pub use_battery: bool,
|
||||
}
|
||||
|
||||
+20
-1
@@ -6,7 +6,7 @@ use crate::{
|
||||
utils::input::InputFieldState,
|
||||
widgets::{
|
||||
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
||||
ProcWidgetState, TempWidgetState, query::ProcessQuery,
|
||||
ProcWidgetState, TempGraphWidgetState, TempWidgetState, query::ProcessQuery,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct AppWidgetStates {
|
||||
pub net_state: NetState,
|
||||
pub proc_state: ProcState,
|
||||
pub temp_state: TempState,
|
||||
pub temp_graph_state: TempGraphStates,
|
||||
pub disk_state: DiskState,
|
||||
pub battery_state: AppBatteryState,
|
||||
pub basic_table_widget_state: Option<BasicTableWidgetState>,
|
||||
@@ -148,6 +149,24 @@ impl TempState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TempGraphStates {
|
||||
pub widget_states: HashMap<u64, TempGraphWidgetState>,
|
||||
}
|
||||
|
||||
impl TempGraphStates {
|
||||
pub fn init(widget_states: HashMap<u64, TempGraphWidgetState>) -> Self {
|
||||
TempGraphStates { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut TempGraphWidgetState> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&TempGraphWidgetState> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskState {
|
||||
pub widget_states: HashMap<u64, DiskTableWidget>,
|
||||
}
|
||||
|
||||
@@ -266,6 +266,12 @@ impl Painter {
|
||||
#[cfg(feature = "battery")]
|
||||
self.draw_battery(f, app_state, rect[0], app_state.current_widget.widget_id)
|
||||
}
|
||||
TempGraph => self.draw_temperature_graph(
|
||||
f,
|
||||
app_state,
|
||||
rect[0],
|
||||
app_state.current_widget.widget_id,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
} else if app_state.app_config_fields.use_basic_mode {
|
||||
@@ -380,6 +386,12 @@ impl Painter {
|
||||
#[cfg(feature = "battery")]
|
||||
self.draw_battery(f, app_state, vertical_chunks[3], widget_id)
|
||||
}
|
||||
TempGraph => self.draw_temperature_graph(
|
||||
f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
widget_id,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -458,6 +470,9 @@ impl Painter {
|
||||
#[cfg(feature = "battery")]
|
||||
self.draw_battery(f, app_state, *draw_loc, widget.widget_id)
|
||||
}
|
||||
TempGraph => {
|
||||
self.draw_temperature_graph(f, app_state, *draw_loc, widget.widget_id)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{borrow::Cow, time::Instant};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use timeless::data::ChunkedData;
|
||||
use tui::{
|
||||
Frame,
|
||||
layout::{Constraint, Rect},
|
||||
@@ -10,29 +11,26 @@ use tui::{
|
||||
widgets::{BorderType, GraphType},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::data::Values,
|
||||
canvas::{components::time_graph::*, drawing_utils::widget_block},
|
||||
};
|
||||
use crate::canvas::{components::time_graph::*, drawing_utils::widget_block};
|
||||
|
||||
/// Represents the data required by the [`TimeGraph`].
|
||||
///
|
||||
/// TODO: We may be able to get rid of this intermediary data structure.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct GraphData<'a> {
|
||||
pub(crate) struct GraphData<'a, F = f64> {
|
||||
time: &'a [Instant],
|
||||
values: Option<&'a Values>,
|
||||
values: Option<&'a ChunkedData<F>>,
|
||||
style: Style,
|
||||
name: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> GraphData<'a> {
|
||||
impl<'a, F> GraphData<'a, F> {
|
||||
pub fn time(mut self, time: &'a [Instant]) -> Self {
|
||||
self.time = time;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn values(mut self, values: &'a Values) -> Self {
|
||||
pub fn values(mut self, values: &'a ChunkedData<F>) -> Self {
|
||||
self.values = Some(values);
|
||||
self
|
||||
}
|
||||
@@ -154,7 +152,9 @@ impl TimeGraph<'_> {
|
||||
/// graph.
|
||||
/// - Expects `graph_data`, which represents *what* data to draw, and
|
||||
/// various details like style and optional legends.
|
||||
pub fn draw(&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec<GraphData<'_>>) {
|
||||
pub fn draw<F: Copy + Default + Into<f64>>(
|
||||
&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec<GraphData<'_, F>>,
|
||||
) {
|
||||
// TODO: (points_rework_v1) can we reduce allocations in the underlying graph by
|
||||
// saving some sort of state?
|
||||
|
||||
@@ -202,7 +202,7 @@ impl TimeGraph<'_> {
|
||||
}
|
||||
|
||||
/// Creates a new [`Dataset`].
|
||||
fn create_dataset(data: GraphData<'_>) -> Dataset<'_> {
|
||||
fn create_dataset<F: Copy + Default + Into<f64>>(data: GraphData<'_, F>) -> Dataset<'_, F> {
|
||||
let GraphData {
|
||||
time,
|
||||
values,
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
};
|
||||
|
||||
use canvas::*;
|
||||
use timeless::data::ChunkedData;
|
||||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Flex, Layout, Rect},
|
||||
@@ -26,7 +27,6 @@ use tui::{
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
app::data::Values,
|
||||
canvas::components::time_graph::LegendConstraints,
|
||||
utils::general::{saturating_log2, saturating_log10},
|
||||
};
|
||||
@@ -268,10 +268,10 @@ impl FromStr for LegendPosition {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
enum Data<'a> {
|
||||
enum Data<'a, F> {
|
||||
Some {
|
||||
times: &'a [Instant],
|
||||
values: &'a Values,
|
||||
values: &'a ChunkedData<F>,
|
||||
},
|
||||
#[default]
|
||||
None,
|
||||
@@ -284,11 +284,11 @@ enum Data<'a> {
|
||||
/// A dataset can be [named](Dataset::name). Only named datasets will be
|
||||
/// rendered in the legend.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Dataset<'a> {
|
||||
pub struct Dataset<'a, F: Copy + Default + Into<f64> = f64> {
|
||||
/// Name of the dataset (used in the legend if shown)
|
||||
name: Option<Line<'a>>,
|
||||
/// A reference to data.
|
||||
data: Data<'a>,
|
||||
data: Data<'a, F>,
|
||||
/// Symbol used for each points of this dataset
|
||||
marker: symbols::Marker,
|
||||
/// Determines graph type used for drawing points
|
||||
@@ -297,10 +297,10 @@ pub struct Dataset<'a> {
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Dataset<'a> {
|
||||
impl<'a, F: Copy + Default + Into<f64>> Dataset<'a, F> {
|
||||
/// Sets the name of the dataset.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn name<S>(mut self, name: S) -> Dataset<'a>
|
||||
pub fn name<S>(mut self, name: S) -> Dataset<'a, F>
|
||||
where
|
||||
S: Into<Line<'a>>,
|
||||
{
|
||||
@@ -317,7 +317,7 @@ impl<'a> Dataset<'a> {
|
||||
/// element being X and the second Y. It's also worth noting that,
|
||||
/// unlike the [`Rect`], here the Y axis is bottom to top, as in math.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn data(mut self, times: &'a [Instant], values: &'a Values) -> Dataset<'a> {
|
||||
pub fn data(mut self, times: &'a [Instant], values: &'a ChunkedData<F>) -> Dataset<'a, F> {
|
||||
self.data = Data::Some { times, values };
|
||||
self
|
||||
}
|
||||
@@ -332,7 +332,7 @@ impl<'a> Dataset<'a> {
|
||||
/// Patterns.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
#[expect(dead_code)]
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a, F> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
@@ -344,7 +344,7 @@ impl<'a> Dataset<'a> {
|
||||
/// in the dataset while a line will also draw a line between them. See
|
||||
/// [`GraphType`] for more details
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a, F> {
|
||||
self.graph_type = graph_type;
|
||||
self
|
||||
}
|
||||
@@ -359,7 +359,7 @@ impl<'a> Dataset<'a> {
|
||||
/// [`Style`], [`Color`], or your own type that implements
|
||||
/// [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Dataset<'a> {
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Dataset<'a, F> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
@@ -419,7 +419,7 @@ impl ChartScaling {
|
||||
/// - Automatically trimming out redundant draws in the x-bounds.
|
||||
/// - Automatic interpolation to points that fall *just* outside of the screen.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(super) struct TimeChart<'a> {
|
||||
pub(super) struct TimeChart<'a, F: Copy + Default + Into<f64> = f64> {
|
||||
/// A block to display around the widget eventually
|
||||
block: Option<Block<'a>>,
|
||||
/// The horizontal axis
|
||||
@@ -427,12 +427,12 @@ pub(super) struct TimeChart<'a> {
|
||||
/// The vertical axis
|
||||
y_axis: Axis<'a>,
|
||||
/// A reference to the datasets
|
||||
datasets: Vec<Dataset<'a>>,
|
||||
datasets: Vec<Dataset<'a, F>>,
|
||||
/// The widget base style
|
||||
style: Style,
|
||||
/// The legend's style.
|
||||
legend_style: Style,
|
||||
/// Constraints used to determine whether the legend should be shown or not
|
||||
/// Constraints used to determine whether the legend should be shown or not.
|
||||
hidden_legend_constraints: (Constraint, Constraint),
|
||||
/// The position determining whether the length is shown or hidden,
|
||||
/// regardless of `hidden_legend_constraints`
|
||||
@@ -443,9 +443,9 @@ pub(super) struct TimeChart<'a> {
|
||||
scaling: ChartScaling,
|
||||
}
|
||||
|
||||
impl<'a> TimeChart<'a> {
|
||||
impl<'a, F: Copy + Default + Into<f64>> TimeChart<'a, F> {
|
||||
/// Creates a chart with the given [datasets](Dataset).
|
||||
pub fn new(datasets: Vec<Dataset<'a>>) -> TimeChart<'a> {
|
||||
pub fn new(datasets: Vec<Dataset<'a, F>>) -> TimeChart<'a, F> {
|
||||
TimeChart {
|
||||
block: None,
|
||||
x_axis: Axis::default(),
|
||||
@@ -462,7 +462,7 @@ impl<'a> TimeChart<'a> {
|
||||
|
||||
/// Wraps the chart with the given [`Block`]
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> TimeChart<'a> {
|
||||
pub fn block(mut self, block: Block<'a>) -> TimeChart<'a, F> {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
@@ -475,14 +475,14 @@ impl<'a> TimeChart<'a> {
|
||||
///
|
||||
/// Styles of [`Axis`] and [`Dataset`] will have priority over this style.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> TimeChart<'a> {
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> TimeChart<'a, F> {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the legend's style.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn legend_style(mut self, legend_style: Style) -> TimeChart<'a> {
|
||||
pub fn legend_style(mut self, legend_style: Style) -> TimeChart<'a, F> {
|
||||
self.legend_style = legend_style;
|
||||
self
|
||||
}
|
||||
@@ -491,7 +491,7 @@ impl<'a> TimeChart<'a> {
|
||||
///
|
||||
/// The default is an empty [`Axis`], i.e. only a line.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn x_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {
|
||||
pub fn x_axis(mut self, axis: Axis<'a>) -> TimeChart<'a, F> {
|
||||
self.x_axis = axis;
|
||||
self
|
||||
}
|
||||
@@ -500,14 +500,14 @@ impl<'a> TimeChart<'a> {
|
||||
///
|
||||
/// The default is an empty [`Axis`], i.e. only a line.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn y_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {
|
||||
pub fn y_axis(mut self, axis: Axis<'a>) -> TimeChart<'a, F> {
|
||||
self.y_axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the marker type.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn marker(mut self, marker: Marker) -> TimeChart<'a> {
|
||||
pub fn marker(mut self, marker: Marker) -> TimeChart<'a, F> {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
@@ -525,7 +525,7 @@ impl<'a> TimeChart<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn hidden_legend_constraints(
|
||||
mut self, constraints: (Constraint, Constraint),
|
||||
) -> TimeChart<'a> {
|
||||
) -> TimeChart<'a, F> {
|
||||
self.hidden_legend_constraints = constraints;
|
||||
self
|
||||
}
|
||||
@@ -543,14 +543,14 @@ impl<'a> TimeChart<'a> {
|
||||
///
|
||||
/// [`hidden_legend_constraints`]: Self::hidden_legend_constraints
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn legend_position(mut self, position: Option<LegendPosition>) -> TimeChart<'a> {
|
||||
pub fn legend_position(mut self, position: Option<LegendPosition>) -> TimeChart<'a, F> {
|
||||
self.legend_position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set chart scaling.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn scaling(mut self, scaling: ChartScaling) -> TimeChart<'a> {
|
||||
pub fn scaling(mut self, scaling: ChartScaling) -> TimeChart<'a, F> {
|
||||
self.scaling = scaling;
|
||||
self
|
||||
}
|
||||
@@ -793,7 +793,7 @@ impl<'a> TimeChart<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TimeChart<'_> {
|
||||
impl<F: Copy + Default + Into<f64>> Widget for TimeChart<'_, F> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
@@ -958,8 +958,8 @@ impl<'a> Styled for Axis<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Dataset<'a> {
|
||||
type Item = Dataset<'a>;
|
||||
impl<'a, F: Copy + Default + Into<f64>> Styled for Dataset<'a, F> {
|
||||
type Item = Dataset<'a, F>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
@@ -970,8 +970,8 @@ impl<'a> Styled for Dataset<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for TimeChart<'a> {
|
||||
type Item = TimeChart<'a>;
|
||||
impl<'a, F: Copy + Default + Into<f64>> Styled for TimeChart<'a, F> {
|
||||
type Item = TimeChart<'a, F>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
@@ -1040,6 +1040,7 @@ mod tests {
|
||||
use tui::style::{Modifier, Stylize};
|
||||
|
||||
use super::*;
|
||||
use crate::app::data::Values;
|
||||
|
||||
struct LegendTestCase {
|
||||
chart_area: Rect,
|
||||
@@ -1103,7 +1104,12 @@ mod tests {
|
||||
#[test]
|
||||
fn dataset_can_be_stylized() {
|
||||
assert_eq!(
|
||||
Dataset::default().black().on_white().bold().not_dim().style,
|
||||
Dataset::<f64>::default()
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_dim()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
@@ -1115,7 +1121,7 @@ mod tests {
|
||||
#[test]
|
||||
fn chart_can_be_stylized() {
|
||||
assert_eq!(
|
||||
TimeChart::new(vec![])
|
||||
TimeChart::<f64>::new(vec![])
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
@@ -1144,7 +1150,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_title_is_wider_than_buffer() {
|
||||
let widget = TimeChart::default()
|
||||
let widget = TimeChart::<f64>::default()
|
||||
.y_axis(Axis::default().title("xxxxxxxxxxxxxxxx"))
|
||||
.x_axis(Axis::default().title("xxxxxxxxxxxxxxxx"));
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 4));
|
||||
@@ -1158,7 +1164,7 @@ mod tests {
|
||||
let data_named_1 = Dataset::default().name("data1"); // must occupy a row in legend
|
||||
let data_named_2 = Dataset::default().name(""); // must occupy a row in legend, even if name is empty
|
||||
let data_unnamed = Dataset::default(); // must not occupy a row in legend
|
||||
let widget = TimeChart::new(vec![data_named_1, data_unnamed, data_named_2]);
|
||||
let widget = TimeChart::<f64>::new(vec![data_named_1, data_unnamed, data_named_2]);
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
|
||||
let layout = widget.layout(buffer.area);
|
||||
|
||||
@@ -1169,7 +1175,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn no_legend_if_no_named_datasets() {
|
||||
let dataset = Dataset::default();
|
||||
let dataset = Dataset::<f64>::default();
|
||||
let widget = TimeChart::new(vec![dataset; 3]);
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
|
||||
let layout = widget.layout(buffer.area);
|
||||
@@ -1179,7 +1185,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn dataset_legend_style_is_patched() {
|
||||
let long_dataset_name = Dataset::default().name("Very long name");
|
||||
let long_dataset_name = Dataset::<f64>::default().name("Very long name");
|
||||
let short_dataset =
|
||||
Dataset::default().name(Line::from("Short name").alignment(Alignment::Right));
|
||||
let widget = TimeChart::new(vec![long_dataset_name, short_dataset])
|
||||
@@ -1200,7 +1206,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_chart_have_a_topleft_legend() {
|
||||
let chart = TimeChart::new(vec![Dataset::default().name("Ds1")])
|
||||
let chart = TimeChart::<f64>::new(vec![Dataset::default().name("Ds1")])
|
||||
.legend_position(Some(LegendPosition::TopLeft));
|
||||
|
||||
let area = Rect::new(0, 0, 30, 20);
|
||||
@@ -1236,7 +1242,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_chart_have_a_long_y_axis_title_overlapping_legend() {
|
||||
let chart = TimeChart::new(vec![Dataset::default().name("Ds1")])
|
||||
let chart = TimeChart::<f64>::new(vec![Dataset::default().name("Ds1")])
|
||||
.y_axis(Axis::default().title("The title overlap a legend."));
|
||||
|
||||
let area = Rect::new(0, 0, 30, 20);
|
||||
@@ -1272,7 +1278,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_chart_have_overflowed_y_axis() {
|
||||
let chart = TimeChart::new(vec![Dataset::default().name("Ds1")])
|
||||
let chart = TimeChart::<f64>::new(vec![Dataset::default().name("Ds1")])
|
||||
.y_axis(Axis::default().title("The title overlap a legend."));
|
||||
|
||||
let area = Rect::new(0, 0, 10, 10);
|
||||
@@ -1299,7 +1305,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_legend_area_can_fit_same_chart_area() {
|
||||
let name = "Data";
|
||||
let chart = TimeChart::new(vec![Dataset::default().name(name)])
|
||||
let chart = TimeChart::<f64>::new(vec![Dataset::default().name(name)])
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
let area = Rect::new(0, 0, name.len() as u16 + 2, 3);
|
||||
@@ -1329,7 +1335,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_legend_of_chart_have_odd_margin_size() {
|
||||
let name = "Data";
|
||||
let base_chart = TimeChart::new(vec![Dataset::default().name(name)])
|
||||
let base_chart = TimeChart::<f64>::new(vec![Dataset::default().name(name)])
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
let area = Rect::new(0, 0, name.len() as u16 + 2 + 3, 3 + 3);
|
||||
@@ -1496,7 +1502,7 @@ mod tests {
|
||||
let datasets: Vec<_> = (0..5)
|
||||
.map(|i| Dataset::default().name(format!("D{i}")))
|
||||
.collect();
|
||||
let chart = TimeChart::new(datasets)
|
||||
let chart = TimeChart::<f64>::new(datasets)
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
// Height 5 means room for 3 entries (5 - 2 borders).
|
||||
@@ -1514,7 +1520,7 @@ mod tests {
|
||||
let datasets: Vec<_> = (0..5)
|
||||
.map(|i| Dataset::default().name(format!("D{i}")))
|
||||
.collect();
|
||||
let chart = TimeChart::new(datasets)
|
||||
let chart = TimeChart::<f64>::new(datasets)
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
let area = Rect::new(0, 0, 20, 4); // 4 - 2 = 2 entries
|
||||
@@ -1536,7 +1542,7 @@ mod tests {
|
||||
Dataset::default().name("Short"),
|
||||
Dataset::default().name("A very long dataset name"),
|
||||
];
|
||||
let chart = TimeChart::new(datasets)
|
||||
let chart = TimeChart::<f64>::new(datasets)
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
// Width 12 means legend_width capped to 12, inner width = 10.
|
||||
@@ -1556,7 +1562,7 @@ mod tests {
|
||||
Dataset::default().name("AB"),
|
||||
Dataset::default().name("Very long name here"),
|
||||
];
|
||||
let chart = TimeChart::new(datasets)
|
||||
let chart = TimeChart::<f64>::new(datasets)
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
// Width 8, legend box capped to 8, inner is 6, long name truncated.
|
||||
@@ -1584,7 +1590,7 @@ mod tests {
|
||||
Dataset::default().name("CD"),
|
||||
Dataset::default().name("A very long dataset name"),
|
||||
];
|
||||
let chart = TimeChart::new(datasets)
|
||||
let chart = TimeChart::<f64>::new(datasets)
|
||||
.hidden_legend_constraints((Constraint::Percentage(100), Constraint::Percentage(100)));
|
||||
|
||||
let area = Rect::new(0, 0, 30, 4); // height 4 → 2 visible entries
|
||||
|
||||
@@ -9,7 +9,7 @@ use tui::{
|
||||
|
||||
use super::{Context, Data, Point, TimeChart};
|
||||
|
||||
impl TimeChart<'_> {
|
||||
impl<F: Copy + Default + Into<f64>> TimeChart<'_, F> {
|
||||
pub(crate) fn draw_points(&self, ctx: &mut Context<'_>) {
|
||||
// Idea is to:
|
||||
// - Go over all datasets, determine *where* a point will be drawn.
|
||||
@@ -52,7 +52,7 @@ impl TimeChart<'_> {
|
||||
// XXX: Should this be generic over dataset.graph_type instead? That would allow
|
||||
// us to move transformations behind a type - however, that
|
||||
// also means that there's some complexity added.
|
||||
(from_start, self.scaling.scale(val))
|
||||
(from_start, self.scaling.scale(val.into()))
|
||||
})
|
||||
.tuple_windows()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod mem_graph;
|
||||
pub mod network_basic;
|
||||
pub mod network_graph;
|
||||
pub mod process_table;
|
||||
pub mod temperature_graph;
|
||||
pub mod temperature_table;
|
||||
|
||||
#[cfg(feature = "battery")]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use tui::{
|
||||
Frame,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
@@ -22,7 +20,6 @@ use crate::{
|
||||
data_units::*,
|
||||
general::{saturating_log2, saturating_log10},
|
||||
},
|
||||
widgets::{NetWidgetHeightCache, NetWidgetState},
|
||||
};
|
||||
|
||||
impl Painter {
|
||||
@@ -81,68 +78,13 @@ impl Painter {
|
||||
|
||||
let y_max = {
|
||||
if let Some(last_time) = times.last() {
|
||||
let cached_network_height =
|
||||
check_network_height_cache(network_widget_state, last_time);
|
||||
|
||||
let (mut biggest, mut biggest_time, oldest_to_check) = cached_network_height
|
||||
.unwrap_or_else(|| {
|
||||
let visible_duration =
|
||||
Duration::from_millis(network_widget_state.current_display_time);
|
||||
|
||||
let visible_left_bound = match last_time.checked_sub(visible_duration) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
// On some systems (like Windows) it can be possible that the
|
||||
// current display time
|
||||
// causes subtraction to fail if, for example, the uptime of the
|
||||
// system is too low and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825.
|
||||
//
|
||||
// As such, we instead take the oldest visible time. This is a
|
||||
// bit inefficient, but
|
||||
// since it should only happen rarely, it should be fine.
|
||||
times
|
||||
.iter()
|
||||
.take_while(|t| {
|
||||
last_time.duration_since(**t) < visible_duration
|
||||
})
|
||||
.last()
|
||||
.cloned()
|
||||
.unwrap_or(*last_time)
|
||||
}
|
||||
};
|
||||
|
||||
(0.0, visible_left_bound, visible_left_bound)
|
||||
});
|
||||
|
||||
for (&time, &v) in rx_points
|
||||
.iter_along_base(times)
|
||||
.rev()
|
||||
.take_while(|&(&time, _)| time >= oldest_to_check)
|
||||
{
|
||||
if v > biggest {
|
||||
biggest = v;
|
||||
biggest_time = time;
|
||||
}
|
||||
}
|
||||
|
||||
for (&time, &v) in tx_points
|
||||
.iter_along_base(times)
|
||||
.rev()
|
||||
.take_while(|&(&time, _)| time >= oldest_to_check)
|
||||
{
|
||||
if v > biggest {
|
||||
biggest = v;
|
||||
biggest_time = time;
|
||||
}
|
||||
}
|
||||
|
||||
network_widget_state.height_cache = Some(NetWidgetHeightCache {
|
||||
best_point: (biggest_time, biggest),
|
||||
right_edge: *last_time,
|
||||
period: network_widget_state.current_display_time,
|
||||
});
|
||||
|
||||
biggest
|
||||
let cache = &mut network_widget_state.height_cache;
|
||||
cache.get_or_update(
|
||||
last_time,
|
||||
network_widget_state.current_display_time,
|
||||
[rx_points, tx_points].into_iter(),
|
||||
times,
|
||||
)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
@@ -389,30 +331,6 @@ impl Painter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a cached max value, it's time, and what period it covers if it is
|
||||
/// cached.
|
||||
#[inline]
|
||||
fn check_network_height_cache(
|
||||
network_widget_state: &NetWidgetState, last_time: &std::time::Instant,
|
||||
) -> Option<(f64, std::time::Instant, std::time::Instant)> {
|
||||
let visible_duration = Duration::from_millis(network_widget_state.current_display_time);
|
||||
|
||||
if let Some(NetWidgetHeightCache {
|
||||
best_point,
|
||||
right_edge,
|
||||
period,
|
||||
}) = &network_widget_state.height_cache
|
||||
{
|
||||
if *period == network_widget_state.current_display_time
|
||||
&& last_time.duration_since(best_point.0) < visible_duration
|
||||
{
|
||||
return Some((best_point.1, best_point.0, *right_edge));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the required labels.
|
||||
///
|
||||
/// TODO: This is _really_ ugly... also there might be a bug with certain
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
use tui::{
|
||||
Frame,
|
||||
layout::{Constraint, Rect},
|
||||
symbols::Marker,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{App, AppConfigFields},
|
||||
canvas::{
|
||||
Painter,
|
||||
components::time_graph::{
|
||||
AxisBound, ChartScaling, GraphData, LegendConstraints, TimeGraph,
|
||||
},
|
||||
drawing_utils::should_hide_x_label,
|
||||
},
|
||||
};
|
||||
|
||||
impl Painter {
|
||||
pub fn draw_temperature_graph(
|
||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||
) {
|
||||
if let Some(widget_state) = app_state
|
||||
.states
|
||||
.temp_graph_state
|
||||
.get_mut_widget_state(widget_id)
|
||||
{
|
||||
let shared_data = app_state.data_store.get_data();
|
||||
let points = &(shared_data.timeseries_data.temperature);
|
||||
let times = &(shared_data.timeseries_data.time);
|
||||
let time_start = -(widget_state.current_display_time as f64);
|
||||
|
||||
let border_style = self.get_border_style(widget_id, app_state.current_widget.widget_id);
|
||||
let hide_x_labels = should_hide_x_label(
|
||||
app_state.app_config_fields.hide_time,
|
||||
app_state.app_config_fields.autohide_time,
|
||||
&mut widget_state.autohide_timer,
|
||||
draw_loc,
|
||||
);
|
||||
|
||||
let y_max = {
|
||||
if let Some(last_time) = times.last() {
|
||||
let cache = &mut widget_state.height_cache;
|
||||
cache.get_or_update(
|
||||
last_time,
|
||||
widget_state.current_display_time,
|
||||
points.values(),
|
||||
times,
|
||||
)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let (adjusted_y_max, y_labels) =
|
||||
adjust_temp_data_point(y_max, widget_state.max_temp, &app_state.app_config_fields);
|
||||
let y_bounds = AxisBound::Max(adjusted_y_max);
|
||||
|
||||
// Hide the legend if the width is 90% of the total widget width
|
||||
// or the height is greater than 50% of the total widget height.
|
||||
let legend_constraints = LegendConstraints {
|
||||
width: Constraint::Ratio(9, 10),
|
||||
height: Constraint::Ratio(1, 2),
|
||||
};
|
||||
|
||||
let unit = app_state.app_config_fields.temperature_type.unit();
|
||||
let graph_data: Vec<GraphData<'_, f32>> = points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(itx, (source, values))| {
|
||||
// TODO: Maybe align the value later.
|
||||
let name = match values.last() {
|
||||
Some(latest) => format!("{source}: {latest:.0}{unit}").into(),
|
||||
None => source.as_str().into(),
|
||||
};
|
||||
GraphData::default()
|
||||
.name(name)
|
||||
.style(
|
||||
self.styles.temp_graph_colour_styles
|
||||
[itx % self.styles.temp_graph_colour_styles.len()],
|
||||
)
|
||||
.time(times)
|
||||
.values(values)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let marker = if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
};
|
||||
|
||||
TimeGraph {
|
||||
x_min: time_start,
|
||||
hide_x_labels,
|
||||
y_bounds,
|
||||
y_labels: &(y_labels.into_iter().map(Into::into).collect::<Vec<_>>()),
|
||||
graph_style: self.styles.graph_style,
|
||||
general_widget_style: self.styles.general_widget_style,
|
||||
border_style,
|
||||
border_type: self.styles.border_type,
|
||||
title: " Temperature ".into(),
|
||||
is_selected: app_state.current_widget.widget_id == widget_id,
|
||||
is_expanded: app_state.is_expanded,
|
||||
title_style: self.styles.widget_title_style,
|
||||
legend_position: app_state.app_config_fields.temperature_legend_position,
|
||||
legend_constraints: Some(legend_constraints),
|
||||
marker,
|
||||
scaling: ChartScaling::Linear,
|
||||
}
|
||||
.draw(f, draw_loc, graph_data);
|
||||
}
|
||||
|
||||
// Update draw loc in widget map.
|
||||
if app_state.should_get_widget_bounds() {
|
||||
if let Some(temperature_graph_widget) = app_state.widget_map.get_mut(&widget_id) {
|
||||
temperature_graph_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||
temperature_graph_widget.bottom_right_corner =
|
||||
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the required labels.
|
||||
fn adjust_temp_data_point(
|
||||
max_entry: f64, upper_limit: Option<f32>, config: &AppConfigFields,
|
||||
) -> (f64, [String; 3]) {
|
||||
let default_upper: f64 = config
|
||||
.temperature_type
|
||||
.convert_temp_unit_float(100.0)
|
||||
.into();
|
||||
let unit = config.temperature_type.unit();
|
||||
|
||||
let max_entry = if let Some(limit) = upper_limit {
|
||||
limit as f64
|
||||
} else if max_entry < default_upper {
|
||||
default_upper
|
||||
} else {
|
||||
max_entry
|
||||
};
|
||||
|
||||
let halfway_label = (max_entry / 2.0).ceil() as u32;
|
||||
let max_entry_label = max_entry.ceil() as u32;
|
||||
|
||||
let labels = [
|
||||
format!("0{unit}"),
|
||||
format!("{halfway_label}{unit}"),
|
||||
format!("{max_entry_label}{unit}"),
|
||||
];
|
||||
|
||||
(max_entry, labels)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::data::TemperatureType;
|
||||
|
||||
fn config(temperature_type: TemperatureType) -> AppConfigFields {
|
||||
AppConfigFields {
|
||||
temperature_type,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floors_to_default_upper_when_below() {
|
||||
let cfg = config(TemperatureType::Celsius);
|
||||
let (max, labels) = adjust_temp_data_point(40.0, None, &cfg);
|
||||
assert_eq!(max, 100.0);
|
||||
assert_eq!(labels, ["0°C", "50°C", "100°C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_actual_max_when_above_default() {
|
||||
let cfg = config(TemperatureType::Celsius);
|
||||
let (max, labels) = adjust_temp_data_point(120.0, None, &cfg);
|
||||
assert_eq!(max, 120.0);
|
||||
assert_eq!(labels, ["0°C", "60°C", "120°C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upper_limit_overrides_actual_max() {
|
||||
let cfg = config(TemperatureType::Celsius);
|
||||
let (max, labels) = adjust_temp_data_point(150.0, Some(80.0), &cfg);
|
||||
assert_eq!(max, 80.0);
|
||||
assert_eq!(labels, ["0°C", "40°C", "80°C"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fahrenheit_default_is_212() {
|
||||
let cfg = config(TemperatureType::Fahrenheit);
|
||||
let (max, labels) = adjust_temp_data_point(100.0, None, &cfg);
|
||||
assert_eq!(max, 212.0);
|
||||
assert_eq!(labels, ["0°F", "106°F", "212°F"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kelvin_default_is_373() {
|
||||
let cfg = config(TemperatureType::Kelvin);
|
||||
let (max, labels) = adjust_temp_data_point(100.0, None, &cfg);
|
||||
assert!((max - 373.15).abs() < 1e-3);
|
||||
assert_eq!(labels, ["0K", "187K", "374K"]);
|
||||
}
|
||||
}
|
||||
+17
-9
@@ -328,7 +328,7 @@ impl DataCollector {
|
||||
}
|
||||
}
|
||||
|
||||
if self.widgets_to_harvest.use_temp {
|
||||
if self.widgets_to_harvest.use_temp || self.widgets_to_harvest.use_temp_graph {
|
||||
if self.should_run_less_routine_tasks {
|
||||
self.sys.temps.refresh(true);
|
||||
}
|
||||
@@ -393,9 +393,11 @@ impl DataCollector {
|
||||
let mut local_gpu_total_mem: u64 = 0;
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
if let Some(data) =
|
||||
nvidia::get_nvidia_vecs(&self.filters.temp_filter, &self.widgets_to_harvest)
|
||||
{
|
||||
if let Some(data) = nvidia::get_nvidia_vecs(
|
||||
&self.filters.temp_filter,
|
||||
&self.filters.temp_graph_filter,
|
||||
&self.widgets_to_harvest,
|
||||
) {
|
||||
if let Some(mut temp) = data.temperature {
|
||||
if let Some(sensors) = &mut self.data.temperature_sensors {
|
||||
sensors.append(&mut temp);
|
||||
@@ -458,16 +460,21 @@ impl DataCollector {
|
||||
|
||||
#[inline]
|
||||
fn update_temps(&mut self) {
|
||||
if self.widgets_to_harvest.use_temp {
|
||||
if self.widgets_to_harvest.use_temp || self.widgets_to_harvest.use_temp_graph {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
if let Ok(data) =
|
||||
temperature::get_temperature_data(&self.sys.temps, &self.filters.temp_filter)
|
||||
{
|
||||
if let Ok(data) = temperature::get_temperature_data(
|
||||
&self.sys.temps,
|
||||
&self.filters.temp_filter,
|
||||
&self.filters.temp_graph_filter,
|
||||
) {
|
||||
self.data.temperature_sensors = data;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Ok(data) = temperature::get_temperature_data(&self.filters.temp_filter) {
|
||||
if let Ok(data) = temperature::get_temperature_data(
|
||||
&self.filters.temp_filter,
|
||||
&self.filters.temp_graph_filter,
|
||||
) {
|
||||
self.data.temperature_sensors = data;
|
||||
}
|
||||
}
|
||||
@@ -642,6 +649,7 @@ mod tests {
|
||||
disk_filter: None,
|
||||
mount_filter: None,
|
||||
temp_filter: None,
|
||||
temp_graph_filter: None,
|
||||
net_filter: None,
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ fn init_nvml() -> Result<Nvml, NvmlError> {
|
||||
/// Returns the GPU data from NVIDIA cards.
|
||||
#[inline]
|
||||
pub fn get_nvidia_vecs(
|
||||
filter: &Option<Filter>, widgets_to_harvest: &UsedWidgets,
|
||||
filter: &Option<Filter>, graph_filter: &Option<Filter>, widgets_to_harvest: &UsedWidgets,
|
||||
) -> Option<GpusData> {
|
||||
if let Ok(nvml) = NVML_DATA.get_or_init(init_nvml) {
|
||||
if let Ok(num_gpu) = nvml.device_count() {
|
||||
@@ -71,8 +71,9 @@ pub fn get_nvidia_vecs(
|
||||
}
|
||||
}
|
||||
|
||||
if widgets_to_harvest.use_temp
|
||||
&& Filter::optional_should_keep(filter, &name)
|
||||
if (widgets_to_harvest.use_temp || widgets_to_harvest.use_temp_graph)
|
||||
&& (Filter::optional_should_keep(filter, &name)
|
||||
|| Filter::optional_should_keep(graph_filter, &name))
|
||||
{
|
||||
if let Ok(temperature) = device.temperature(TemperatureSensor::Gpu) {
|
||||
temp_vec.push(TempSensorData {
|
||||
|
||||
@@ -200,7 +200,7 @@ fn finalize_name(
|
||||
/// the device is already in ACPI D0. This has the notable issue that
|
||||
/// once this happens, the device will be *kept* on through the sensor
|
||||
/// reading, and not be able to re-enter ACPI D3cold.
|
||||
fn hwmon_temperatures(filter: &Option<Filter>) -> HwmonResults {
|
||||
fn hwmon_temperatures(filter: &Option<Filter>, graph_filter: &Option<Filter>) -> HwmonResults {
|
||||
let mut temperatures: Vec<TempSensorData> = vec![];
|
||||
let mut seen_names: HashMap<String, u32> = HashMap::default();
|
||||
|
||||
@@ -326,7 +326,9 @@ fn hwmon_temperatures(filter: &Option<Filter>) -> HwmonResults {
|
||||
|
||||
// TODO: It's possible we may want to move the filter check further up to avoid
|
||||
// probing hwmon if not needed?
|
||||
if Filter::optional_should_keep(filter, &name) {
|
||||
if Filter::optional_should_keep(filter, &name)
|
||||
|| Filter::optional_should_keep(graph_filter, &name)
|
||||
{
|
||||
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
||||
temperatures.push(TempSensorData {
|
||||
name,
|
||||
@@ -350,7 +352,9 @@ fn hwmon_temperatures(filter: &Option<Filter>) -> HwmonResults {
|
||||
///
|
||||
/// See [the Linux kernel documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal)
|
||||
/// for more details.
|
||||
fn add_thermal_zone_temperatures(temperatures: &mut Vec<TempSensorData>, filter: &Option<Filter>) {
|
||||
fn add_thermal_zone_temperatures(
|
||||
temperatures: &mut Vec<TempSensorData>, filter: &Option<Filter>, graph_filter: &Option<Filter>,
|
||||
) {
|
||||
let path = Path::new("/sys/class/thermal");
|
||||
let Ok(read_dir) = path.read_dir() else {
|
||||
return;
|
||||
@@ -374,7 +378,9 @@ fn add_thermal_zone_temperatures(temperatures: &mut Vec<TempSensorData>, filter:
|
||||
name
|
||||
};
|
||||
|
||||
if Filter::optional_should_keep(filter, &name) {
|
||||
if Filter::optional_should_keep(filter, &name)
|
||||
|| Filter::optional_should_keep(graph_filter, &name)
|
||||
{
|
||||
let temp_path = file_path.join("temp");
|
||||
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
||||
let name = counted_name(&mut seen_names, name);
|
||||
@@ -391,11 +397,13 @@ fn add_thermal_zone_temperatures(temperatures: &mut Vec<TempSensorData>, filter:
|
||||
}
|
||||
|
||||
/// Gets temperature sensors and data.
|
||||
pub fn get_temperature_data(filter: &Option<Filter>) -> Result<Option<Vec<TempSensorData>>> {
|
||||
let mut results = hwmon_temperatures(filter);
|
||||
pub fn get_temperature_data(
|
||||
filter: &Option<Filter>, graph_filter: &Option<Filter>,
|
||||
) -> Result<Option<Vec<TempSensorData>>> {
|
||||
let mut results = hwmon_temperatures(filter, graph_filter);
|
||||
|
||||
if results.num_hwmon == 0 {
|
||||
add_thermal_zone_temperatures(&mut results.temperatures, filter);
|
||||
add_thermal_zone_temperatures(&mut results.temperatures, filter, graph_filter);
|
||||
}
|
||||
|
||||
Ok(Some(results.temperatures))
|
||||
|
||||
@@ -6,14 +6,16 @@ use super::TempSensorData;
|
||||
use crate::app::filter::Filter;
|
||||
|
||||
pub fn get_temperature_data(
|
||||
components: &sysinfo::Components, filter: &Option<Filter>,
|
||||
components: &sysinfo::Components, filter: &Option<Filter>, graph_filter: &Option<Filter>,
|
||||
) -> Result<Option<Vec<TempSensorData>>> {
|
||||
let mut temperatures: Vec<TempSensorData> = Vec::new();
|
||||
|
||||
for component in components {
|
||||
let name = component.label().to_string();
|
||||
|
||||
if Filter::optional_should_keep(filter, &name) {
|
||||
if Filter::optional_should_keep(filter, &name)
|
||||
|| Filter::optional_should_keep(graph_filter, &name)
|
||||
{
|
||||
temperatures.push(TempSensorData {
|
||||
name,
|
||||
temperature: component.temperature(),
|
||||
|
||||
+36
-5
@@ -430,7 +430,7 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
# By default, there are no mount name filters enabled. An example use case is provided below.
|
||||
@@ -447,7 +447,7 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
@@ -474,7 +474,35 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
# Temperature graph widget configuration
|
||||
#[temperature_graph]
|
||||
|
||||
# Where to place the legend for the temperature graph widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
|
||||
#legend_position = "top-right"
|
||||
|
||||
# An upper temperature value for the graph; entries higher than this will be hidden. If not set,
|
||||
# there is no limit. Is in the unit of `temperature_type`.
|
||||
#max_temp = 100.0
|
||||
|
||||
# By default, there are no temperature sensor filters enabled. An example use case is provided below.
|
||||
#[temperature_graph.sensor_filter]
|
||||
# Whether to ignore any matches. Defaults to true.
|
||||
#is_list_ignored = true
|
||||
|
||||
# A list of filters to try and match.
|
||||
#list = ["cpu", "wifi"]
|
||||
|
||||
# Whether to use regex. Defaults to false.
|
||||
#regex = false
|
||||
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
@@ -494,7 +522,7 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
# Whether to be case-sensitive. Defaults to false.
|
||||
#case_sensitive = false
|
||||
|
||||
# Whether to be require matching the whole word. Defaults to false.
|
||||
# Whether to require matching the whole word. Defaults to false.
|
||||
#whole_word = false
|
||||
|
||||
|
||||
@@ -518,6 +546,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
#avg_entry_color = "red"
|
||||
#cpu_core_colors = ["light magenta", "light yellow", "light cyan", "light green", "light blue", "cyan", "green", "blue"]
|
||||
|
||||
#[styles.temp_graph]
|
||||
#temp_graph_color_styles = ["light magenta", "light yellow", "light cyan", "light green", "light blue", "cyan", "green", "blue"]
|
||||
|
||||
#[styles.memory]
|
||||
#ram_color = "light magenta"
|
||||
#cache_color = "light red"
|
||||
@@ -560,7 +591,7 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
|
||||
# [[row.child]] represents either a widget or a column.
|
||||
# [[row.child.child]] represents a widget.
|
||||
#
|
||||
# All widgets must have the type value set to one of ["cpu", "mem", "proc", "net", "temp", "disk", "empty"].
|
||||
# All widgets must have the type value set to one of ["cpu", "mem", "proc", "net", "temp", "temp_graph", "disk", "empty"].
|
||||
# All layout components have a ratio value - if this is not set, then it defaults to 1.
|
||||
# The default widget layout:
|
||||
#[[row]]
|
||||
|
||||
+52
-12
@@ -22,7 +22,7 @@ use data::TemperatureType;
|
||||
pub(crate) use error::{OptionError, OptionResult};
|
||||
use indexmap::IndexSet;
|
||||
use regex::Regex;
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
#[cfg(feature = "battery")]
|
||||
use starship_battery::Manager;
|
||||
|
||||
@@ -205,7 +205,7 @@ pub(crate) fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result
|
||||
indoc::eprintdoc!(
|
||||
"Note: bottom couldn't create a default config file at '{}', and the \
|
||||
application has fallen back to the default configuration.
|
||||
|
||||
|
||||
Caused by:
|
||||
{err}
|
||||
",
|
||||
@@ -279,14 +279,15 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
// For CPU
|
||||
let default_cpu_selection = get_default_cpu_selection(args, config);
|
||||
|
||||
let mut widget_map = HashMap::default();
|
||||
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::default();
|
||||
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::default();
|
||||
let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::default();
|
||||
let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::default();
|
||||
let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::default();
|
||||
let mut disk_state_map: HashMap<u64, DiskTableWidget> = HashMap::default();
|
||||
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::default();
|
||||
let mut widget_map = FxHashMap::default();
|
||||
let mut cpu_state_map: FxHashMap<u64, CpuWidgetState> = FxHashMap::default();
|
||||
let mut mem_state_map: FxHashMap<u64, MemWidgetState> = FxHashMap::default();
|
||||
let mut net_state_map: FxHashMap<u64, NetWidgetState> = FxHashMap::default();
|
||||
let mut proc_state_map: FxHashMap<u64, ProcWidgetState> = FxHashMap::default();
|
||||
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 battery_state_map: FxHashMap<u64, BatteryWidgetState> = FxHashMap::default();
|
||||
|
||||
let autohide_timer = if autohide_time {
|
||||
Some(Instant::now())
|
||||
@@ -297,7 +298,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
let mut initial_widget_id: u64 = default_widget_id;
|
||||
let mut initial_widget_type = Proc;
|
||||
let is_custom_layout = config.row.is_some();
|
||||
let mut used_widget_set = HashSet::default();
|
||||
let mut used_widget_set = FxHashSet::default();
|
||||
|
||||
let network_unit_type = get_network_unit_type(args, config);
|
||||
let network_scale_type = get_network_scale_type(args, config);
|
||||
@@ -320,6 +321,7 @@ 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)?;
|
||||
|
||||
// TODO: Can probably just reuse the options struct.
|
||||
let app_config_fields = AppConfigFields {
|
||||
@@ -374,6 +376,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
.disk
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.default_sort.to_owned()),
|
||||
temperature_legend_position,
|
||||
};
|
||||
|
||||
let table_config = ProcTableConfig {
|
||||
@@ -483,11 +486,28 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
TempWidgetState::new(&app_config_fields, &styling),
|
||||
);
|
||||
}
|
||||
TempGraph => {
|
||||
let upper_limit = config
|
||||
.temperature_graph
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.max_temp)
|
||||
.map(|v| v as f32);
|
||||
temp_graph_state_map.insert(
|
||||
widget.widget_id,
|
||||
TempGraphWidgetState::new(
|
||||
default_time_value,
|
||||
autohide_timer,
|
||||
upper_limit,
|
||||
),
|
||||
);
|
||||
}
|
||||
Battery => {
|
||||
battery_state_map
|
||||
.insert(widget.widget_id, BatteryWidgetState::default());
|
||||
}
|
||||
_ => {}
|
||||
// FIXME: This is kind of a hack that we have these cases at all.
|
||||
Empty | BasicCpu | BasicMem | BasicNet | BasicTables | CpuLegend
|
||||
| ProcSort | ProcSearch => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,6 +547,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
use_proc: used_widget_set.contains(&Proc),
|
||||
use_disk: used_widget_set.contains(&Disk),
|
||||
use_temp: used_widget_set.contains(&Temp),
|
||||
use_temp_graph: used_widget_set.contains(&TempGraph),
|
||||
use_battery: used_widget_set.contains(&Battery),
|
||||
};
|
||||
|
||||
@@ -548,6 +569,11 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
.context("Update 'temperature.sensor_filter' in your config file")?,
|
||||
None => None,
|
||||
};
|
||||
let temp_graph_sensor_filter = match &config.temperature_graph {
|
||||
Some(cfg) => get_ignore_list(&cfg.sensor_filter)
|
||||
.context("Update 'temperature_graph.sensor_filter' in your config file")?,
|
||||
None => None,
|
||||
};
|
||||
let net_interface_filter = match &config.network {
|
||||
Some(cfg) => get_ignore_list(&cfg.interface_filter)
|
||||
.context("Update 'network.interface_filter' in your config file")?,
|
||||
@@ -560,6 +586,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
net_state: NetState::init(net_state_map),
|
||||
proc_state: ProcState::init(proc_state_map),
|
||||
temp_state: TempState::init(temp_state_map),
|
||||
temp_graph_state: TempGraphStates::init(temp_graph_state_map),
|
||||
disk_state: DiskState::init(disk_state_map),
|
||||
battery_state: AppBatteryState::init(battery_state_map),
|
||||
basic_table_widget_state,
|
||||
@@ -573,6 +600,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
|
||||
disk_filter: disk_name_filter,
|
||||
mount_filter: disk_mount_filter,
|
||||
temp_filter: temp_sensor_filter,
|
||||
temp_graph_filter: temp_graph_sensor_filter,
|
||||
net_filter: net_interface_filter,
|
||||
};
|
||||
let is_expanded = expanded && !use_basic_mode;
|
||||
@@ -1016,6 +1044,7 @@ fn get_retention(args: &BottomArgs, config: &Config) -> OptionResult<u64> {
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_legend_position(
|
||||
arg: Option<&String>, cfg: Option<&String>, setting: &'static str,
|
||||
) -> OptionResult<Option<LegendPosition>> {
|
||||
@@ -1064,6 +1093,17 @@ fn get_memory_legend_position(
|
||||
)
|
||||
}
|
||||
|
||||
fn get_temperature_legend_position(config: &Config) -> OptionResult<Option<LegendPosition>> {
|
||||
parse_legend_position(
|
||||
None,
|
||||
config
|
||||
.temperature_graph
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.legend_position.as_ref()),
|
||||
"legend_position",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use clap::Parser;
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod network;
|
||||
pub mod process;
|
||||
pub mod style;
|
||||
pub mod temperature;
|
||||
pub mod temperature_graph;
|
||||
|
||||
use disk::DiskConfig;
|
||||
use flags::GeneralConfig;
|
||||
@@ -14,6 +15,7 @@ use network::NetworkConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use style::StyleConfig;
|
||||
use temperature::TempConfig;
|
||||
use temperature_graph::TempGraphConfig;
|
||||
|
||||
pub use self::ignore_list::IgnoreList;
|
||||
use self::{cpu::CpuConfig, layout::Row, process::ProcessesConfig};
|
||||
@@ -21,7 +23,7 @@ use self::{cpu::CpuConfig, layout::Row, process::ProcessesConfig};
|
||||
/// Overall config for `bottom`.
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[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))]
|
||||
pub struct Config {
|
||||
pub(crate) flags: Option<GeneralConfig>,
|
||||
pub(crate) styles: Option<StyleConfig>,
|
||||
@@ -29,6 +31,7 @@ pub struct Config {
|
||||
pub(crate) processes: Option<ProcessesConfig>,
|
||||
pub(crate) disk: Option<DiskConfig>,
|
||||
pub(crate) temperature: Option<TempConfig>,
|
||||
pub(crate) temperature_graph: Option<TempGraphConfig>,
|
||||
pub(crate) network: Option<NetworkConfig>,
|
||||
pub(crate) cpu: Option<CpuConfig>,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod graphs;
|
||||
mod memory;
|
||||
mod network;
|
||||
mod tables;
|
||||
mod temp_graph;
|
||||
mod themes;
|
||||
mod utils;
|
||||
mod widgets;
|
||||
@@ -20,6 +21,7 @@ use memory::MemoryStyle;
|
||||
use network::NetworkStyle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tables::TableStyle;
|
||||
use temp_graph::TempGraphStyle;
|
||||
use tui::{style::Style, widgets::BorderType};
|
||||
use utils::{opt, set_colour, set_colour_list, set_style};
|
||||
use widgets::WidgetStyle;
|
||||
@@ -82,6 +84,9 @@ pub(crate) struct StyleConfig {
|
||||
/// Styling for the network widget.
|
||||
pub(crate) network: Option<NetworkStyle>,
|
||||
|
||||
/// Styling for the temperature graph widget.
|
||||
pub(crate) temp_graph: Option<TempGraphStyle>,
|
||||
|
||||
/// Styling for the battery widget.
|
||||
pub(crate) battery: Option<BatteryStyle>,
|
||||
|
||||
@@ -113,6 +118,7 @@ pub struct Styles {
|
||||
pub(crate) all_cpu_colour: Style,
|
||||
pub(crate) avg_cpu_colour: Style,
|
||||
pub(crate) cpu_colour_styles: Vec<Style>,
|
||||
pub(crate) temp_graph_colour_styles: Vec<Style>,
|
||||
pub(crate) border_style: Style,
|
||||
pub(crate) highlighted_border_style: Style,
|
||||
pub(crate) text_style: Style,
|
||||
@@ -178,6 +184,13 @@ impl Styles {
|
||||
set_colour!(self.all_cpu_colour, config.cpu, all_entry_color);
|
||||
set_colour_list!(self.cpu_colour_styles, config.cpu, cpu_core_colors);
|
||||
|
||||
// Temperature graph
|
||||
set_colour_list!(
|
||||
self.temp_graph_colour_styles,
|
||||
config.temp_graph,
|
||||
temp_graph_color_styles
|
||||
);
|
||||
|
||||
// Memory
|
||||
set_colour!(self.ram_style, config.memory, ram_color);
|
||||
set_colour!(self.swap_style, config.memory, swap_color);
|
||||
|
||||
@@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::ColorStr;
|
||||
|
||||
// TODO: Use Canadian spelling for colour with an alias for US later instead since internally I use Canadian spelling.
|
||||
|
||||
/// Styling specific to the CPU widget.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::ColorStr;
|
||||
|
||||
/// Styling specific to the temperature 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 TempGraphStyle {
|
||||
/// Colour of each temperature sensor's graph line. Read in order.
|
||||
#[serde(alias = "temp_graph_colour_styles")]
|
||||
pub(crate) temp_graph_color_styles: Option<Vec<ColorStr>>,
|
||||
}
|
||||
@@ -20,6 +20,17 @@ impl Styles {
|
||||
const DEFAULT_SELECTED_TEXT_STYLE: Style = color!(Color::Black).bg(HIGHLIGHT_COLOUR);
|
||||
const TEXT_COLOUR: Color = Color::Gray;
|
||||
|
||||
let list_colours = vec![
|
||||
color!(Color::LightMagenta),
|
||||
color!(Color::LightYellow),
|
||||
color!(Color::LightCyan),
|
||||
color!(Color::LightGreen),
|
||||
color!(Color::LightBlue),
|
||||
color!(Color::Cyan),
|
||||
color!(Color::Green),
|
||||
color!(Color::Blue),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: color!(FIRST_COLOUR),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -43,16 +54,8 @@ impl Styles {
|
||||
total_tx_style: color!(FOURTH_COLOUR),
|
||||
all_cpu_colour: color!(ALL_COLOUR),
|
||||
avg_cpu_colour: color!(AVG_COLOUR),
|
||||
cpu_colour_styles: vec![
|
||||
color!(Color::LightMagenta),
|
||||
color!(Color::LightYellow),
|
||||
color!(Color::LightCyan),
|
||||
color!(Color::LightGreen),
|
||||
color!(Color::LightBlue),
|
||||
color!(Color::Cyan),
|
||||
color!(Color::Green),
|
||||
color!(Color::Blue),
|
||||
],
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
border_style: color!(TEXT_COLOUR),
|
||||
highlighted_border_style: color!(HIGHLIGHT_COLOUR),
|
||||
text_style: color!(TEXT_COLOUR),
|
||||
@@ -74,6 +77,16 @@ impl Styles {
|
||||
}
|
||||
|
||||
pub fn default_light_palette() -> Self {
|
||||
let list_colours = vec![
|
||||
color!(Color::LightMagenta),
|
||||
color!(Color::LightBlue),
|
||||
color!(Color::LightRed),
|
||||
color!(Color::Cyan),
|
||||
color!(Color::Green),
|
||||
color!(Color::Blue),
|
||||
color!(Color::Red),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: color!(Color::Blue),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -95,15 +108,8 @@ impl Styles {
|
||||
tx_style: color!(Color::Red),
|
||||
total_rx_style: color!(Color::LightBlue),
|
||||
total_tx_style: color!(Color::LightRed),
|
||||
cpu_colour_styles: vec![
|
||||
color!(Color::LightMagenta),
|
||||
color!(Color::LightBlue),
|
||||
color!(Color::LightRed),
|
||||
color!(Color::Cyan),
|
||||
color!(Color::Green),
|
||||
color!(Color::Blue),
|
||||
color!(Color::Red),
|
||||
],
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
border_style: color!(Color::Black),
|
||||
text_style: color!(Color::Black),
|
||||
selected_text_style: color!(Color::White).bg(Color::LightBlue),
|
||||
|
||||
@@ -8,6 +8,29 @@ use crate::options::config::style::{Styles, themes::hex_colour};
|
||||
|
||||
impl Styles {
|
||||
pub(crate) fn gruvbox_palette() -> Self {
|
||||
let list_colours = vec![
|
||||
hex!("#cc241d"),
|
||||
hex!("#98971a"),
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#689d6a"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
hex!("#fabd2f"),
|
||||
hex!("#83a598"),
|
||||
hex!("#d3869b"),
|
||||
hex!("#d65d0e"),
|
||||
hex!("#9d0006"),
|
||||
hex!("#79740e"),
|
||||
hex!("#b57614"),
|
||||
hex!("#076678"),
|
||||
hex!("#8f3f71"),
|
||||
hex!("#427b58"),
|
||||
hex!("#d65d03"),
|
||||
hex!("#af3a03"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#8ec07c"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -31,28 +54,8 @@ impl Styles {
|
||||
total_tx_style: hex!("#d79921"),
|
||||
all_cpu_colour: hex!("#8ec07c"),
|
||||
avg_cpu_colour: hex!("#fb4934"),
|
||||
cpu_colour_styles: vec![
|
||||
hex!("#cc241d"),
|
||||
hex!("#98971a"),
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#689d6a"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
hex!("#fabd2f"),
|
||||
hex!("#83a598"),
|
||||
hex!("#d3869b"),
|
||||
hex!("#d65d0e"),
|
||||
hex!("#9d0006"),
|
||||
hex!("#79740e"),
|
||||
hex!("#b57614"),
|
||||
hex!("#076678"),
|
||||
hex!("#8f3f71"),
|
||||
hex!("#427b58"),
|
||||
hex!("#d65d03"),
|
||||
hex!("#af3a03"),
|
||||
],
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
border_style: hex!("#ebdbb2"),
|
||||
highlighted_border_style: hex!("#fe8019"),
|
||||
text_style: hex!("#ebdbb2"),
|
||||
@@ -74,6 +77,29 @@ impl Styles {
|
||||
}
|
||||
|
||||
pub(crate) fn gruvbox_light_palette() -> Self {
|
||||
let list_colours = vec![
|
||||
hex!("#cc241d"),
|
||||
hex!("#98971a"),
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#689d6a"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
hex!("#fabd2f"),
|
||||
hex!("#83a598"),
|
||||
hex!("#d3869b"),
|
||||
hex!("#d65d0e"),
|
||||
hex!("#9d0006"),
|
||||
hex!("#79740e"),
|
||||
hex!("#b57614"),
|
||||
hex!("#076678"),
|
||||
hex!("#8f3f71"),
|
||||
hex!("#427b58"),
|
||||
hex!("#d65d03"),
|
||||
hex!("#af3a03"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#427b58"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -97,28 +123,8 @@ impl Styles {
|
||||
total_tx_style: hex!("#d79921"),
|
||||
all_cpu_colour: hex!("#8ec07c"),
|
||||
avg_cpu_colour: hex!("#fb4934"),
|
||||
cpu_colour_styles: vec![
|
||||
hex!("#cc241d"),
|
||||
hex!("#98971a"),
|
||||
hex!("#d79921"),
|
||||
hex!("#458588"),
|
||||
hex!("#b16286"),
|
||||
hex!("#689d6a"),
|
||||
hex!("#fe8019"),
|
||||
hex!("#b8bb26"),
|
||||
hex!("#fabd2f"),
|
||||
hex!("#83a598"),
|
||||
hex!("#d3869b"),
|
||||
hex!("#d65d0e"),
|
||||
hex!("#9d0006"),
|
||||
hex!("#79740e"),
|
||||
hex!("#b57614"),
|
||||
hex!("#076678"),
|
||||
hex!("#8f3f71"),
|
||||
hex!("#427b58"),
|
||||
hex!("#d65d03"),
|
||||
hex!("#af3a03"),
|
||||
],
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
border_style: hex!("#3c3836"),
|
||||
highlighted_border_style: hex!("#af3a03"),
|
||||
text_style: hex!("#3c3836"),
|
||||
|
||||
@@ -8,6 +8,17 @@ use crate::options::config::style::{Styles, themes::hex_colour};
|
||||
|
||||
impl Styles {
|
||||
pub(crate) fn nord_palette() -> Self {
|
||||
let list_colours = vec![
|
||||
hex!("#5e81ac"),
|
||||
hex!("#81a1c1"),
|
||||
hex!("#d8dee9"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#d08770"),
|
||||
hex!("#bf616a"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#88c0d0"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -31,16 +42,8 @@ impl Styles {
|
||||
total_tx_style: hex!("#8fbcbb"),
|
||||
all_cpu_colour: hex!("#88c0d0"),
|
||||
avg_cpu_colour: hex!("#8fbcbb"),
|
||||
cpu_colour_styles: vec![
|
||||
hex!("#5e81ac"),
|
||||
hex!("#81a1c1"),
|
||||
hex!("#d8dee9"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#d08770"),
|
||||
hex!("#bf616a"),
|
||||
],
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
border_style: hex!("#88c0d0"),
|
||||
highlighted_border_style: hex!("#5e81ac"),
|
||||
text_style: hex!("#e5e9f0"),
|
||||
@@ -62,6 +65,17 @@ impl Styles {
|
||||
}
|
||||
|
||||
pub(crate) fn nord_light_palette() -> Self {
|
||||
let list_colours = vec![
|
||||
hex!("#5e81ac"),
|
||||
hex!("#88c0d0"),
|
||||
hex!("#4c566a"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#d08770"),
|
||||
hex!("#bf616a"),
|
||||
];
|
||||
|
||||
Self {
|
||||
ram_style: hex!("#81a1c1"),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -85,16 +99,8 @@ impl Styles {
|
||||
total_tx_style: hex!("#8fbcbb"),
|
||||
all_cpu_colour: hex!("#81a1c1"),
|
||||
avg_cpu_colour: hex!("#8fbcbb"),
|
||||
cpu_colour_styles: vec![
|
||||
hex!("#5e81ac"),
|
||||
hex!("#88c0d0"),
|
||||
hex!("#4c566a"),
|
||||
hex!("#b48ead"),
|
||||
hex!("#a3be8c"),
|
||||
hex!("#ebcb8b"),
|
||||
hex!("#d08770"),
|
||||
hex!("#bf616a"),
|
||||
],
|
||||
cpu_colour_styles: list_colours.clone(),
|
||||
temp_graph_colour_styles: list_colours,
|
||||
border_style: hex!("#2e3440"),
|
||||
highlighted_border_style: hex!("#5e81ac"),
|
||||
text_style: hex!("#2e3440"),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::IgnoreList;
|
||||
|
||||
/// Temperature graph configuration.
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq))]
|
||||
pub(crate) struct TempGraphConfig {
|
||||
/// A filter over the sensor names.
|
||||
pub(crate) sensor_filter: Option<IgnoreList>,
|
||||
|
||||
/// The location of the graph's legend.
|
||||
#[serde(default)]
|
||||
pub(crate) legend_position: Option<String>,
|
||||
|
||||
/// An upper temperature value for the graph; entries higher than this will be hidden. If not set,
|
||||
/// there is no limit.
|
||||
///
|
||||
/// Is in the unit of `temperature_type`.
|
||||
#[serde(default)]
|
||||
pub(crate) max_temp: Option<f64>,
|
||||
}
|
||||
@@ -4,12 +4,186 @@ pub mod disk_table;
|
||||
pub mod mem_graph;
|
||||
pub mod network_graph;
|
||||
pub mod process_table;
|
||||
pub mod temperature_graph;
|
||||
pub mod temperature_table;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub use battery_info::*;
|
||||
pub use cpu_graph::*;
|
||||
pub use disk_table::*;
|
||||
pub use mem_graph::*;
|
||||
pub use network_graph::*;
|
||||
pub use process_table::*;
|
||||
pub use temperature_graph::*;
|
||||
pub use temperature_table::*;
|
||||
use timeless::data::ChunkedData;
|
||||
|
||||
struct GraphHeightCacheInner {
|
||||
best_point: (Instant, f64),
|
||||
right_edge: Instant,
|
||||
period: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GraphHeightCache {
|
||||
inner: Option<GraphHeightCacheInner>,
|
||||
}
|
||||
|
||||
impl GraphHeightCache {
|
||||
/// Get the cached height if it exists, or set it otherwise.
|
||||
pub(crate) fn get_or_update<
|
||||
'a,
|
||||
F: Into<f64> + Clone + Copy + 'a,
|
||||
S: Iterator<Item = &'a ChunkedData<F>>,
|
||||
>(
|
||||
&mut self, last_time: &Instant, current_display_time: u64, sources: S, times: &[Instant],
|
||||
) -> f64 {
|
||||
let visible_duration = Duration::from_millis(current_display_time);
|
||||
|
||||
let (mut biggest, mut biggest_time, oldest_to_check) = if let Some(GraphHeightCacheInner {
|
||||
best_point,
|
||||
right_edge,
|
||||
period,
|
||||
}) = self.inner.as_ref()
|
||||
&& *period == current_display_time
|
||||
&& last_time.duration_since(best_point.0) < visible_duration
|
||||
{
|
||||
(best_point.1, best_point.0, *right_edge)
|
||||
} else {
|
||||
let visible_duration = Duration::from_millis(current_display_time);
|
||||
|
||||
let visible_left_bound = match last_time.checked_sub(visible_duration) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
// On some systems (like Windows) it can be possible that the
|
||||
// current display time
|
||||
// causes subtraction to fail if, for example, the uptime of the
|
||||
// system is too low and current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825.
|
||||
//
|
||||
// As such, we instead take the oldest visible time. This is a
|
||||
// bit inefficient, but
|
||||
// since it should only happen rarely, it should be fine.
|
||||
times
|
||||
.iter()
|
||||
.take_while(|t| last_time.duration_since(**t) < visible_duration)
|
||||
.last()
|
||||
.cloned()
|
||||
.unwrap_or(*last_time)
|
||||
}
|
||||
};
|
||||
|
||||
(0.0, visible_left_bound, visible_left_bound)
|
||||
};
|
||||
|
||||
for source in sources {
|
||||
for (&time, &v) in source
|
||||
.iter_along_base(times)
|
||||
.rev()
|
||||
.take_while(|&(&time, _)| time >= oldest_to_check)
|
||||
{
|
||||
let v = v.into();
|
||||
if v > biggest {
|
||||
biggest = v;
|
||||
biggest_time = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.inner = Some(GraphHeightCacheInner {
|
||||
best_point: (biggest_time, biggest),
|
||||
right_edge: *last_time,
|
||||
period: current_display_time,
|
||||
});
|
||||
|
||||
biggest
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn build(times: &[Instant], values: &[f64]) -> ChunkedData<f64> {
|
||||
assert_eq!(times.len(), values.len());
|
||||
let mut data = ChunkedData::default();
|
||||
for &v in values {
|
||||
data.push(v);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_sources_returns_zero() {
|
||||
let mut cache = GraphHeightCache::default();
|
||||
let last_time = Instant::now();
|
||||
let times: Vec<Instant> = vec![];
|
||||
let sources: Vec<ChunkedData<f64>> = vec![];
|
||||
|
||||
let result = cache.get_or_update(&last_time, 1_000, sources.iter(), ×);
|
||||
assert_eq!(result, 0.0);
|
||||
assert!(cache.inner.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_max_across_sources() {
|
||||
let mut cache = GraphHeightCache::default();
|
||||
let now = Instant::now();
|
||||
let times = vec![
|
||||
now - Duration::from_millis(300),
|
||||
now - Duration::from_millis(200),
|
||||
now - Duration::from_millis(100),
|
||||
now,
|
||||
];
|
||||
let a = build(×, &[1.0, 2.0, 3.0, 4.0]);
|
||||
let b = build(×, &[10.0, 5.0, 0.5, 0.25]);
|
||||
|
||||
let result = cache.get_or_update(&now, 1_000, [&a, &b].into_iter(), ×);
|
||||
assert_eq!(result, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_hit_skips_older_points() {
|
||||
// On a cache hit, only points after `right_edge` are rescanned. So if
|
||||
// we pass a fresh source whose only large values are *older* than the
|
||||
// previous `last_time`, the cached max should still win.
|
||||
let mut cache = GraphHeightCache::default();
|
||||
let now = Instant::now();
|
||||
let times = vec![
|
||||
now - Duration::from_millis(200),
|
||||
now - Duration::from_millis(100),
|
||||
now,
|
||||
];
|
||||
let first = build(×, &[3.0, 5.0, 7.0]);
|
||||
|
||||
let first_result = cache.get_or_update(&now, 10_000, [&first].into_iter(), ×);
|
||||
assert_eq!(first_result, 7.0);
|
||||
|
||||
// Older points carry huge values; newest is small. A full rescan would
|
||||
// return 1000.0 — a true cache hit returns the cached 7.0.
|
||||
let second = build(×, &[1000.0, 999.0, 4.0]);
|
||||
let second_result = cache.get_or_update(&now, 10_000, [&second].into_iter(), ×);
|
||||
assert_eq!(second_result, 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_invalidates_on_period_change() {
|
||||
// First call uses a small window that excludes the older high value.
|
||||
// Second call uses a larger window that should include it; a stale
|
||||
// cache would miss it and return the previous max.
|
||||
let mut cache = GraphHeightCache::default();
|
||||
let now = Instant::now();
|
||||
let times = vec![
|
||||
now - Duration::from_millis(500),
|
||||
now - Duration::from_millis(50),
|
||||
now,
|
||||
];
|
||||
let data = build(×, &[100.0, 5.0, 7.0]);
|
||||
|
||||
let first = cache.get_or_update(&now, 100, [&data].into_iter(), ×);
|
||||
assert_eq!(first, 7.0);
|
||||
|
||||
let second = cache.get_or_update(&now, 1_000, [&data].into_iter(), ×);
|
||||
assert_eq!(second, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::widgets::GraphHeightCache;
|
||||
|
||||
pub struct NetWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
pub height_cache: Option<NetWidgetHeightCache>,
|
||||
}
|
||||
|
||||
pub struct NetWidgetHeightCache {
|
||||
pub best_point: (Instant, f64),
|
||||
pub right_edge: Instant,
|
||||
pub period: u64,
|
||||
pub height_cache: GraphHeightCache,
|
||||
}
|
||||
|
||||
impl NetWidgetState {
|
||||
@@ -17,7 +13,7 @@ impl NetWidgetState {
|
||||
NetWidgetState {
|
||||
current_display_time,
|
||||
autohide_timer,
|
||||
height_cache: None,
|
||||
height_cache: GraphHeightCache::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//! Code around a temperature graph widget.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::widgets::GraphHeightCache;
|
||||
|
||||
/// A timeseries graph widget displaying temperature usage over time.
|
||||
pub struct TempGraphWidgetState {
|
||||
pub current_display_time: u64,
|
||||
pub autohide_timer: Option<Instant>,
|
||||
pub height_cache: GraphHeightCache,
|
||||
pub max_temp: Option<f32>,
|
||||
}
|
||||
|
||||
impl TempGraphWidgetState {
|
||||
pub fn new(
|
||||
current_display_time: u64, autohide_timer: Option<Instant>, max_temp: Option<f32>,
|
||||
) -> Self {
|
||||
TempGraphWidgetState {
|
||||
current_display_time,
|
||||
autohide_timer,
|
||||
height_cache: GraphHeightCache::default(),
|
||||
max_temp,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user