diff --git a/CHANGELOG.md b/CHANGELOG.md index c04f051e..dba26928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/content/configuration/config-file/disk-table.md b/docs/content/configuration/config-file/disk-table.md index 297ad164..6904c7d8 100644 --- a/docs/content/configuration/config-file/disk-table.md +++ b/docs/content/configuration/config-file/disk-table.md @@ -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 ``` diff --git a/docs/content/configuration/config-file/layout.md b/docs/content/configuration/config-file/layout.md index c5f35465..3f782c7d 100644 --- a/docs/content/configuration/config-file/layout.md +++ b/docs/content/configuration/config-file/layout.md @@ -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. diff --git a/docs/content/configuration/config-file/network.md b/docs/content/configuration/config-file/network.md index badc7f2e..686cce5d 100644 --- a/docs/content/configuration/config-file/network.md +++ b/docs/content/configuration/config-file/network.md @@ -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 ``` diff --git a/docs/content/configuration/config-file/styling.md b/docs/content/configuration/config-file/styling.md index 1aa8181a..861455f7 100644 --- a/docs/content/configuration/config-file/styling.md +++ b/docs/content/configuration/config-file/styling.md @@ -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]`: diff --git a/docs/content/configuration/config-file/temperature-graph.md b/docs/content/configuration/config-file/temperature-graph.md new file mode 100644 index 00000000..cde48985 --- /dev/null +++ b/docs/content/configuration/config-file/temperature-graph.md @@ -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 +``` diff --git a/docs/content/configuration/config-file/temperature-table.md b/docs/content/configuration/config-file/temperature-table.md index 694fc227..e44f8be6 100644 --- a/docs/content/configuration/config-file/temperature-table.md +++ b/docs/content/configuration/config-file/temperature-table.md @@ -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 ``` diff --git a/docs/content/usage/widgets/temperature-graph.md b/docs/content/usage/widgets/temperature-graph.md new file mode 100644 index 00000000..61af02e2 --- /dev/null +++ b/docs/content/usage/widgets/temperature-graph.md @@ -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 | diff --git a/docs/content/usage/widgets/temperature.md b/docs/content/usage/widgets/temperature-table.md similarity index 100% rename from docs/content/usage/widgets/temperature.md rename to docs/content/usage/widgets/temperature-table.md diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 626ef46d..ebff71f8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -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 diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index 80deab27..78032a7e 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -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]] diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index 78761cda..06dd36a4 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -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": [ diff --git a/src/app.rs b/src/app.rs index f864827f..fc1c564d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -80,14 +80,16 @@ pub struct AppConfigFields { pub default_tree_collapse: bool, pub default_temp_sort_column: Option, pub default_disk_sort_column: Option, + pub temperature_legend_position: Option, } /// For filtering out information -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct DataFilters { pub disk_filter: Option, pub mount_filter: Option, pub temp_filter: Option, + pub temp_graph_filter: Option, pub net_filter: Option, } @@ -117,10 +119,13 @@ impl App { widget_map: HashMap, 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(), _ => {} } } diff --git a/src/app/data/store.rs b/src/app/data/store.rs index 054ea76b..6ea69ec8 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -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, pub swap_harvest: Option, #[cfg(not(target_os = "windows"))] @@ -31,8 +37,8 @@ pub struct StoredData { pub arc_harvest: Option, #[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, settings: &AppConfigFields) { + fn eat_data( + &mut self, mut data: Box, 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, settings: &AppConfigFields) { - self.main.eat_data(data, settings); + self.main + .eat_data(data, settings, &self.used_widgets, &self.filters); } /// Clean data. diff --git a/src/app/data/temperature.rs b/src/app/data/temperature.rs index 2b3f0df6..dc313660 100644 --- a/src/app/data/temperature.rs +++ b/src/app/data/temperature.rs @@ -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. diff --git a/src/app/data/time_series.rs b/src/app/data/time_series.rs index 39f1c62a..a6246bac 100644 --- a/src/app/data/time_series.rs +++ b/src/app/data/time_series.rs @@ -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; @@ -58,11 +60,17 @@ pub struct TimeSeriesData { #[cfg(feature = "gpu")] /// GPU memory data. pub gpu_mem: HashMap, + + /// Temperature data. + pub temperature: HashMap>, } 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::>(); + + 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 + } + }); } } diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index ce14d02d..73efce5b 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -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, } diff --git a/src/app/states.rs b/src/app/states.rs index d40aa4d4..b9242a20 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -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, @@ -148,6 +149,24 @@ impl TempState { } } +pub struct TempGraphStates { + pub widget_states: HashMap, +} + +impl TempGraphStates { + pub fn init(widget_states: HashMap) -> 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, } diff --git a/src/canvas.rs b/src/canvas.rs index 8cc8a66e..c6186d57 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -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) + } _ => {} } } diff --git a/src/canvas/components/time_graph/base.rs b/src/canvas/components/time_graph/base.rs index 03d59125..6c458a74 100644 --- a/src/canvas/components/time_graph/base.rs +++ b/src/canvas/components/time_graph/base.rs @@ -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>, style: Style, name: Option>, } -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) -> 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>) { + pub fn draw>( + &self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec>, + ) { // 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>(data: GraphData<'_, F>) -> Dataset<'_, F> { let GraphData { time, values, diff --git a/src/canvas/components/time_graph/vendored.rs b/src/canvas/components/time_graph/vendored.rs index 1cb1ec9d..cdde4295 100644 --- a/src/canvas/components/time_graph/vendored.rs +++ b/src/canvas/components/time_graph/vendored.rs @@ -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, }, #[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> { /// Name of the dataset (used in the legend if shown) name: Option>, /// 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> 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(mut self, name: S) -> Dataset<'a> + pub fn name(mut self, name: S) -> Dataset<'a, F> where S: Into>, { @@ -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) -> 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