mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 07:34:27 +02:00
refactor: port over graph widgets
Things working as of now: - Actually drawing - Interpolation - Styling
This commit is contained in:
parent
b72e76aa71
commit
2bff04d8a4
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -246,6 +246,7 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"enum_dispatch",
|
"enum_dispatch",
|
||||||
"fern",
|
"fern",
|
||||||
|
"float-ord",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-timer",
|
"futures-timer",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
@ -584,6 +585,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float-ord"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
|
@ -44,6 +44,7 @@ clap = "2.33"
|
|||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
dirs = "3.0.2"
|
dirs = "3.0.2"
|
||||||
enum_dispatch = "0.3.7"
|
enum_dispatch = "0.3.7"
|
||||||
|
float-ord = "0.3.2"
|
||||||
futures = "0.3.14"
|
futures = "0.3.14"
|
||||||
futures-timer = "3.0.2"
|
futures-timer = "3.0.2"
|
||||||
fxhash = "0.2.1"
|
fxhash = "0.2.1"
|
||||||
|
92
src/app.rs
92
src/app.rs
@ -19,7 +19,7 @@ use indextree::{Arena, NodeId};
|
|||||||
use unicode_segmentation::GraphemeCursor;
|
use unicode_segmentation::GraphemeCursor;
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
use data_farmer::*;
|
pub use data_farmer::*;
|
||||||
use data_harvester::{processes, temperature};
|
use data_harvester::{processes, temperature};
|
||||||
pub use filter::*;
|
pub use filter::*;
|
||||||
use layout_manager::*;
|
use layout_manager::*;
|
||||||
@ -28,9 +28,7 @@ pub use widgets::*;
|
|||||||
use crate::{
|
use crate::{
|
||||||
canvas,
|
canvas,
|
||||||
constants::{self, MAX_SIGNAL},
|
constants::{self, MAX_SIGNAL},
|
||||||
data_conversion::*,
|
|
||||||
units::data_units::DataUnit,
|
units::data_units::DataUnit,
|
||||||
update_final_process_list,
|
|
||||||
utils::error::{BottomError, Result},
|
utils::error::{BottomError, Result},
|
||||||
BottomEvent, Pid,
|
BottomEvent, Pid,
|
||||||
};
|
};
|
||||||
@ -367,7 +365,10 @@ impl AppState {
|
|||||||
self.data_collection.eat_data(new_data);
|
self.data_collection.eat_data(new_data);
|
||||||
|
|
||||||
if !self.is_frozen {
|
if !self.is_frozen {
|
||||||
self.convert_data();
|
let data_collection = &self.data_collection;
|
||||||
|
self.widget_lookup_map
|
||||||
|
.iter_mut()
|
||||||
|
.for_each(|(_id, widget)| widget.update_data(data_collection));
|
||||||
|
|
||||||
EventResult::Redraw
|
EventResult::Redraw
|
||||||
} else {
|
} else {
|
||||||
@ -395,89 +396,6 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_data(&mut self) {
|
|
||||||
// TODO: Probably refactor this.
|
|
||||||
|
|
||||||
// Network
|
|
||||||
if self.used_widgets.use_net {
|
|
||||||
let network_data = convert_network_data_points(
|
|
||||||
&self.data_collection,
|
|
||||||
false,
|
|
||||||
self.app_config_fields.use_basic_mode
|
|
||||||
|| self.app_config_fields.use_old_network_legend,
|
|
||||||
&self.app_config_fields.network_scale_type,
|
|
||||||
&self.app_config_fields.network_unit_type,
|
|
||||||
self.app_config_fields.network_use_binary_prefix,
|
|
||||||
);
|
|
||||||
self.canvas_data.network_data_rx = network_data.rx;
|
|
||||||
self.canvas_data.network_data_tx = network_data.tx;
|
|
||||||
self.canvas_data.rx_display = network_data.rx_display;
|
|
||||||
self.canvas_data.tx_display = network_data.tx_display;
|
|
||||||
if let Some(total_rx_display) = network_data.total_rx_display {
|
|
||||||
self.canvas_data.total_rx_display = total_rx_display;
|
|
||||||
}
|
|
||||||
if let Some(total_tx_display) = network_data.total_tx_display {
|
|
||||||
self.canvas_data.total_tx_display = total_tx_display;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disk
|
|
||||||
if self.used_widgets.use_disk {
|
|
||||||
self.canvas_data.disk_data = convert_disk_row(&self.data_collection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temperatures
|
|
||||||
if self.used_widgets.use_temp {
|
|
||||||
self.canvas_data.temp_sensor_data = convert_temp_row(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memory
|
|
||||||
if self.used_widgets.use_mem {
|
|
||||||
self.canvas_data.mem_data = convert_mem_data_points(&self.data_collection, false);
|
|
||||||
self.canvas_data.swap_data = convert_swap_data_points(&self.data_collection, false);
|
|
||||||
let (memory_labels, swap_labels) = convert_mem_labels(&self.data_collection);
|
|
||||||
|
|
||||||
self.canvas_data.mem_labels = memory_labels;
|
|
||||||
self.canvas_data.swap_labels = swap_labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.used_widgets.use_cpu {
|
|
||||||
// CPU
|
|
||||||
convert_cpu_data_points(&self.data_collection, &mut self.canvas_data.cpu_data, false);
|
|
||||||
self.canvas_data.load_avg_data = self.data_collection.load_avg_harvest;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processes
|
|
||||||
if self.used_widgets.use_proc {
|
|
||||||
self.update_all_process_lists();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Battery
|
|
||||||
if self.used_widgets.use_battery {
|
|
||||||
self.canvas_data.battery_data = convert_battery_harvest(&self.data_collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::needless_collect)]
|
|
||||||
fn update_all_process_lists(&mut self) {
|
|
||||||
// TODO: Probably refactor this.
|
|
||||||
|
|
||||||
// According to clippy, I can avoid a collect... but if I follow it,
|
|
||||||
// I end up conflicting with the borrow checker since app is used within the closure... hm.
|
|
||||||
if !self.is_frozen {
|
|
||||||
let widget_ids = self
|
|
||||||
.proc_state
|
|
||||||
.widget_states
|
|
||||||
.keys()
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
widget_ids.into_iter().for_each(|widget_id| {
|
|
||||||
update_final_process_list(self, widget_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_esc(&mut self) {
|
pub fn on_esc(&mut self) {
|
||||||
self.reset_multi_tap_keys();
|
self.reset_multi_tap_keys();
|
||||||
if self.is_in_dialog() {
|
if self.is_in_dialog() {
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::{
|
app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable},
|
||||||
sort_text_table::SortableColumn, DiskTable, MemGraph, NetGraph, OldNetGraph,
|
|
||||||
ProcessManager, TempTable,
|
|
||||||
},
|
|
||||||
error::{BottomError, Result},
|
error::{BottomError, Result},
|
||||||
options::layout_options::{Row, RowChildren},
|
options::layout_options::{Row, RowChildren},
|
||||||
};
|
};
|
||||||
@ -16,7 +13,7 @@ use crate::app::widgets::Widget;
|
|||||||
use crate::constants::DEFAULT_WIDGET_ID;
|
use crate::constants::DEFAULT_WIDGET_ID;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
event::SelectionAction, CpuGraph, SortableTextTable, TimeGraph, TmpBottomWidget, UsedWidgets,
|
event::SelectionAction, AppConfigFields, CpuGraph, TimeGraph, TmpBottomWidget, UsedWidgets,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Represents a more usable representation of the layout, derived from the
|
/// Represents a more usable representation of the layout, derived from the
|
||||||
@ -1051,44 +1048,43 @@ pub struct LayoutCreationOutput {
|
|||||||
// FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this!
|
// FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this!
|
||||||
pub fn create_layout_tree(
|
pub fn create_layout_tree(
|
||||||
rows: &[Row], process_defaults: crate::options::ProcessDefaults,
|
rows: &[Row], process_defaults: crate::options::ProcessDefaults,
|
||||||
app_config_fields: &super::AppConfigFields,
|
app_config_fields: &AppConfigFields,
|
||||||
) -> Result<LayoutCreationOutput> {
|
) -> Result<LayoutCreationOutput> {
|
||||||
fn add_widget_to_map(
|
fn add_widget_to_map(
|
||||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType,
|
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType,
|
||||||
widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults,
|
widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults,
|
||||||
app_config_fields: &super::AppConfigFields,
|
app_config_fields: &AppConfigFields,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
match widget_type {
|
match widget_type {
|
||||||
BottomWidgetType::Cpu => {
|
BottomWidgetType::Cpu => {
|
||||||
let graph = TimeGraph::from_config(app_config_fields);
|
widget_lookup_map
|
||||||
let legend = SortableTextTable::new(vec![
|
.insert(widget_id, CpuGraph::from_config(app_config_fields).into());
|
||||||
SortableColumn::new_flex("CPU".into(), None, false, 0.5),
|
|
||||||
SortableColumn::new_flex("Use%".into(), None, false, 0.5),
|
|
||||||
]);
|
|
||||||
let legend_position = super::CpuGraphLegendPosition::Right;
|
|
||||||
|
|
||||||
widget_lookup_map.insert(
|
|
||||||
widget_id,
|
|
||||||
CpuGraph::new(graph, legend, legend_position).into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
BottomWidgetType::Mem => {
|
BottomWidgetType::Mem => {
|
||||||
let graph = TimeGraph::from_config(app_config_fields);
|
let graph = TimeGraph::from_config(app_config_fields);
|
||||||
widget_lookup_map.insert(widget_id, MemGraph::new(graph).into());
|
widget_lookup_map.insert(widget_id, MemGraph::new(graph).into());
|
||||||
}
|
}
|
||||||
BottomWidgetType::Net => {
|
BottomWidgetType::Net => {
|
||||||
let graph = TimeGraph::from_config(app_config_fields);
|
|
||||||
if app_config_fields.use_old_network_legend {
|
if app_config_fields.use_old_network_legend {
|
||||||
widget_lookup_map.insert(widget_id, OldNetGraph::new(graph).into());
|
widget_lookup_map.insert(
|
||||||
|
widget_id,
|
||||||
|
OldNetGraph::from_config(app_config_fields).into(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
widget_lookup_map.insert(widget_id, NetGraph::new(graph).into());
|
widget_lookup_map
|
||||||
|
.insert(widget_id, NetGraph::from_config(app_config_fields).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BottomWidgetType::Proc => {
|
BottomWidgetType::Proc => {
|
||||||
widget_lookup_map.insert(widget_id, ProcessManager::new(process_defaults).into());
|
widget_lookup_map.insert(widget_id, ProcessManager::new(process_defaults).into());
|
||||||
}
|
}
|
||||||
BottomWidgetType::Temp => {
|
BottomWidgetType::Temp => {
|
||||||
widget_lookup_map.insert(widget_id, TempTable::default().into());
|
widget_lookup_map.insert(
|
||||||
|
widget_id,
|
||||||
|
TempTable::default()
|
||||||
|
.set_temp_type(app_config_fields.temperature_type.clone())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
BottomWidgetType::Disk => {
|
BottomWidgetType::Disk => {
|
||||||
widget_lookup_map.insert(widget_id, DiskTable::default().into());
|
widget_lookup_map.insert(widget_id, DiskTable::default().into());
|
||||||
|
@ -9,10 +9,12 @@ use crate::{
|
|||||||
event::{EventResult, SelectionAction},
|
event::{EventResult, SelectionAction},
|
||||||
layout_manager::BottomWidgetType,
|
layout_manager::BottomWidgetType,
|
||||||
},
|
},
|
||||||
canvas::{DisplayableData, Painter},
|
canvas::Painter,
|
||||||
constants,
|
constants,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod tui_widgets;
|
||||||
|
|
||||||
pub mod base;
|
pub mod base;
|
||||||
pub use base::*;
|
pub use base::*;
|
||||||
|
|
||||||
@ -37,6 +39,8 @@ pub use self::battery::*;
|
|||||||
pub mod temp;
|
pub mod temp;
|
||||||
pub use temp::*;
|
pub use temp::*;
|
||||||
|
|
||||||
|
use super::data_farmer::DataCollection;
|
||||||
|
|
||||||
/// A trait for things that are drawn with state.
|
/// A trait for things that are drawn with state.
|
||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@ -75,9 +79,6 @@ pub trait Component {
|
|||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub trait Widget {
|
pub trait Widget {
|
||||||
/// Updates a [`Widget`] given some data. Defaults to doing nothing.
|
|
||||||
fn update(&mut self) {}
|
|
||||||
|
|
||||||
/// Handles what to do when trying to respond to a widget selection movement to the left.
|
/// Handles what to do when trying to respond to a widget selection movement to the left.
|
||||||
/// Defaults to just moving to the next-possible widget in that direction.
|
/// Defaults to just moving to the next-possible widget in that direction.
|
||||||
fn handle_widget_selection_left(&mut self) -> SelectionAction {
|
fn handle_widget_selection_left(&mut self) -> SelectionAction {
|
||||||
@ -107,12 +108,13 @@ pub trait Widget {
|
|||||||
|
|
||||||
/// Draws a [`Widget`]. Defaults to doing nothing.
|
/// Draws a [`Widget`]. Defaults to doing nothing.
|
||||||
fn draw<B: Backend>(
|
fn draw<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
selected: bool,
|
|
||||||
) {
|
) {
|
||||||
// TODO: Remove the default implementation in the future!
|
// TODO: Remove the default implementation in the future!
|
||||||
// TODO: Do another pass on ALL of the draw code - currently it's just glue, it should eventually be done properly!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How a [`Widget`] updates its internal displayed data. Defaults to doing nothing.
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The "main" widgets that are used by bottom to display information!
|
/// The "main" widgets that are used by bottom to display information!
|
||||||
|
@ -61,7 +61,7 @@ impl Scrollable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the currently selected index of the [`Scrollable`].
|
/// Returns the currently selected index of the [`Scrollable`].
|
||||||
pub fn index(&self) -> usize {
|
pub fn current_index(&self) -> usize {
|
||||||
self.current_index
|
self.current_index
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,8 +195,8 @@ impl Scrollable {
|
|||||||
self.num_items
|
self.num_items
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tui_state(&self) -> TableState {
|
pub fn tui_state(&mut self) -> &mut TableState {
|
||||||
self.tui_state.clone()
|
&mut self.tui_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,13 +2,17 @@ use std::borrow::Cow;
|
|||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
||||||
use tui::{
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
widgets::{Table, TableState},
|
widgets::{Block, Table, TableState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::{event::EventResult, Component, TextTable};
|
use crate::{
|
||||||
|
app::{event::EventResult, Component, TextTable},
|
||||||
|
canvas::Painter,
|
||||||
|
};
|
||||||
|
|
||||||
use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn};
|
use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn, TextTableData};
|
||||||
|
|
||||||
fn get_shortcut_name(e: &KeyEvent) -> String {
|
fn get_shortcut_name(e: &KeyEvent) -> String {
|
||||||
let modifier = if e.modifiers.is_empty() {
|
let modifier = if e.modifiers.is_empty() {
|
||||||
@ -182,6 +186,10 @@ impl SortableTextTable {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_index(&self) -> usize {
|
||||||
|
self.table.current_index()
|
||||||
|
}
|
||||||
|
|
||||||
fn set_sort_index(&mut self, new_index: usize) {
|
fn set_sort_index(&mut self, new_index: usize) {
|
||||||
if new_index == self.sort_index {
|
if new_index == self.sort_index {
|
||||||
if let Some(column) = self.table.columns.get_mut(self.sort_index) {
|
if let Some(column) = self.table.columns.get_mut(self.sort_index) {
|
||||||
@ -218,6 +226,18 @@ impl SortableTextTable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws a [`Table`] given the [`TextTable`] and the given data.
|
||||||
|
///
|
||||||
|
/// Note if the number of columns don't match in the [`TextTable`] and data,
|
||||||
|
/// it will only create as many columns as it can grab data from both sources from.
|
||||||
|
pub fn draw_tui_table<B: Backend>(
|
||||||
|
&mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, data: &TextTableData,
|
||||||
|
block: Block<'_>, block_area: Rect, show_selected_entry: bool,
|
||||||
|
) {
|
||||||
|
self.table
|
||||||
|
.draw_tui_table(painter, f, data, block, block_area, show_selected_entry);
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a [`Table`] representing the sort list.
|
/// Creates a [`Table`] representing the sort list.
|
||||||
pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) {
|
pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) {
|
||||||
todo!()
|
todo!()
|
||||||
|
@ -5,9 +5,12 @@ use std::{
|
|||||||
|
|
||||||
use crossterm::event::{KeyEvent, MouseEvent};
|
use crossterm::event::{KeyEvent, MouseEvent};
|
||||||
use tui::{
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
layout::{Constraint, Rect},
|
layout::{Constraint, Rect},
|
||||||
|
style::Style,
|
||||||
text::Text,
|
text::Text,
|
||||||
widgets::{Table, TableState},
|
widgets::{Block, Table},
|
||||||
|
Frame,
|
||||||
};
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
@ -36,6 +39,8 @@ pub trait TableColumn {
|
|||||||
fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>);
|
fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type TextTableData = Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>, Option<Style>)>>;
|
||||||
|
|
||||||
/// A [`SimpleColumn`] represents some column in a [`TextTable`].
|
/// A [`SimpleColumn`] represents some column in a [`TextTable`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SimpleColumn {
|
pub struct SimpleColumn {
|
||||||
@ -128,6 +133,9 @@ where
|
|||||||
|
|
||||||
/// Whether we draw columns from left-to-right.
|
/// Whether we draw columns from left-to-right.
|
||||||
pub left_to_right: bool,
|
pub left_to_right: bool,
|
||||||
|
|
||||||
|
/// Whether to enable selection.
|
||||||
|
pub selectable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> TextTable<C>
|
impl<C> TextTable<C>
|
||||||
@ -142,6 +150,7 @@ where
|
|||||||
show_gap: true,
|
show_gap: true,
|
||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
left_to_right: true,
|
left_to_right: true,
|
||||||
|
selectable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +164,11 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unselectable(mut self) -> Self {
|
||||||
|
self.selectable = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn displayed_column_names(&self) -> Vec<Cow<'static, str>> {
|
pub fn displayed_column_names(&self) -> Vec<Cow<'static, str>> {
|
||||||
self.columns
|
self.columns
|
||||||
.iter()
|
.iter()
|
||||||
@ -172,8 +186,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_index(&self) -> usize {
|
||||||
|
self.scrollable.current_index()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_desired_column_widths(
|
pub fn get_desired_column_widths(
|
||||||
columns: &[C], data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
|
columns: &[C], data: &TextTableData,
|
||||||
) -> Vec<DesiredColumnWidth> {
|
) -> Vec<DesiredColumnWidth> {
|
||||||
columns
|
columns
|
||||||
.iter()
|
.iter()
|
||||||
@ -183,22 +201,22 @@ where
|
|||||||
let max_len = data
|
let max_len = data
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| c.get(column_index))
|
.filter_map(|c| c.get(column_index))
|
||||||
.max_by(|(x, short_x), (y, short_y)| {
|
.max_by(|(a, short_a, _a_style), (b, short_b, _b_style)| {
|
||||||
let x = if let Some(short_x) = short_x {
|
let a_len = if let Some(short_a) = short_a {
|
||||||
short_x
|
short_a.len()
|
||||||
} else {
|
} else {
|
||||||
x
|
a.len()
|
||||||
};
|
};
|
||||||
|
|
||||||
let y = if let Some(short_y) = short_y {
|
let b_len = if let Some(short_b) = short_b {
|
||||||
short_y
|
short_b.len()
|
||||||
} else {
|
} else {
|
||||||
y
|
b.len()
|
||||||
};
|
};
|
||||||
|
|
||||||
x.len().cmp(&y.len())
|
a_len.cmp(&b_len)
|
||||||
})
|
})
|
||||||
.map(|(s, _)| s.len())
|
.map(|(longest_data_str, _, _)| longest_data_str.len())
|
||||||
.unwrap_or(0) as u16;
|
.unwrap_or(0) as u16;
|
||||||
|
|
||||||
DesiredColumnWidth::Hard(max(max_len, *width))
|
DesiredColumnWidth::Hard(max(max_len, *width))
|
||||||
@ -211,9 +229,7 @@ where
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cache(
|
fn get_cache(&mut self, area: Rect, data: &TextTableData) -> Vec<u16> {
|
||||||
&mut self, area: Rect, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
|
|
||||||
) -> Vec<u16> {
|
|
||||||
fn calculate_column_widths(
|
fn calculate_column_widths(
|
||||||
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
|
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
|
||||||
) -> Vec<u16> {
|
) -> Vec<u16> {
|
||||||
@ -326,37 +342,37 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a [`Table`] given the [`TextTable`] and the given data, along with its
|
/// Draws a [`Table`] given the [`TextTable`] and the given data.
|
||||||
/// widths (because for some reason a [`Table`] only borrows the constraints...?)
|
|
||||||
/// and [`TableState`] (so we know which row is selected).
|
|
||||||
///
|
///
|
||||||
/// Note if the number of columns don't match in the [`TextTable`] and data,
|
/// Note if the number of columns don't match in the [`TextTable`] and data,
|
||||||
/// it will only create as many columns as it can grab data from both sources from.
|
/// it will only create as many columns as it can grab data from both sources from.
|
||||||
pub fn create_draw_table(
|
pub fn draw_tui_table<B: Backend>(
|
||||||
&mut self, painter: &Painter, data: &[Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>],
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
|
||||||
area: Rect,
|
block_area: Rect, show_selected_entry: bool,
|
||||||
) -> (Table<'_>, Vec<Constraint>, TableState) {
|
) {
|
||||||
use tui::widgets::Row;
|
use tui::widgets::Row;
|
||||||
|
|
||||||
let table_gap = if !self.show_gap || area.height < TABLE_GAP_HEIGHT_LIMIT {
|
let inner_area = block.inner(block_area);
|
||||||
|
|
||||||
|
let table_gap = if !self.show_gap || inner_area.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
};
|
};
|
||||||
|
|
||||||
self.update_num_items(data.len());
|
self.update_num_items(data.len());
|
||||||
self.set_bounds(area);
|
self.set_bounds(inner_area);
|
||||||
let table_extras = 1 + table_gap;
|
let table_extras = 1 + table_gap;
|
||||||
let scrollable_height = area.height.saturating_sub(table_extras);
|
let scrollable_height = inner_area.height.saturating_sub(table_extras);
|
||||||
self.scrollable.set_bounds(Rect::new(
|
self.scrollable.set_bounds(Rect::new(
|
||||||
area.x,
|
inner_area.x,
|
||||||
area.y + table_extras,
|
inner_area.y + table_extras,
|
||||||
area.width,
|
inner_area.width,
|
||||||
scrollable_height,
|
scrollable_height,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Calculate widths first, since we need them later.
|
// Calculate widths first, since we need them later.
|
||||||
let calculated_widths = self.get_cache(area, data);
|
let calculated_widths = self.get_cache(inner_area, data);
|
||||||
let widths = calculated_widths
|
let widths = calculated_widths
|
||||||
.iter()
|
.iter()
|
||||||
.map(|column| Constraint::Length(*column))
|
.map(|column| Constraint::Length(*column))
|
||||||
@ -373,25 +389,28 @@ where
|
|||||||
&data[start..end]
|
&data[start..end]
|
||||||
};
|
};
|
||||||
let rows = data_slice.iter().map(|row| {
|
let rows = data_slice.iter().map(|row| {
|
||||||
Row::new(
|
Row::new(row.iter().zip(&calculated_widths).map(
|
||||||
row.iter()
|
|((text, shrunk_text, opt_style), width)| {
|
||||||
.zip(&calculated_widths)
|
let text_style = opt_style.unwrap_or(painter.colours.text_style);
|
||||||
.map(|((text, shrunk_text), width)| {
|
|
||||||
let width = *width as usize;
|
let width = *width as usize;
|
||||||
let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true)
|
let graphemes =
|
||||||
.collect::<Vec<&str>>();
|
UnicodeSegmentation::graphemes(text.as_ref(), true).collect::<Vec<&str>>();
|
||||||
let grapheme_width = graphemes.len();
|
let grapheme_width = graphemes.len();
|
||||||
if width < grapheme_width && width > 1 {
|
if width < grapheme_width && width > 1 {
|
||||||
if let Some(shrunk_text) = shrunk_text {
|
if let Some(shrunk_text) = shrunk_text {
|
||||||
Text::raw(shrunk_text.clone())
|
Text::styled(shrunk_text.clone(), text_style)
|
||||||
} else {
|
} else {
|
||||||
Text::raw(format!("{}…", graphemes[..(width - 1)].concat()))
|
Text::styled(
|
||||||
}
|
format!("{}…", graphemes[..(width - 1)].concat()),
|
||||||
} else {
|
text_style,
|
||||||
Text::raw(text.to_owned())
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text::styled(text.to_owned(), text_style)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now build up our headers...
|
// Now build up our headers...
|
||||||
@ -399,21 +418,24 @@ where
|
|||||||
.style(painter.colours.table_header_style)
|
.style(painter.colours.table_header_style)
|
||||||
.bottom_margin(table_gap);
|
.bottom_margin(table_gap);
|
||||||
|
|
||||||
// And return tui-rs's [`TableState`].
|
let table = Table::new(rows)
|
||||||
let tui_state = self.scrollable.tui_state();
|
|
||||||
|
|
||||||
(
|
|
||||||
Table::new(rows)
|
|
||||||
.header(header)
|
.header(header)
|
||||||
.style(painter.colours.text_style),
|
.style(painter.colours.text_style)
|
||||||
widths,
|
.highlight_style(if show_selected_entry {
|
||||||
tui_state,
|
painter.colours.currently_selected_text_style
|
||||||
)
|
} else {
|
||||||
}
|
painter.colours.text_style
|
||||||
|
});
|
||||||
|
|
||||||
/// Creates a [`Table`] representing the sort list.
|
if self.selectable {
|
||||||
pub fn create_sort_list(&mut self) -> (Table<'_>, TableState) {
|
f.render_stateful_widget(
|
||||||
todo!()
|
table.block(block).widths(&widths),
|
||||||
|
block_area,
|
||||||
|
self.scrollable.tui_state(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
f.render_widget(table.block(block).widths(&widths), block_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,11 +444,19 @@ where
|
|||||||
C: TableColumn,
|
C: TableColumn,
|
||||||
{
|
{
|
||||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||||
|
if self.selectable {
|
||||||
self.scrollable.handle_key_event(event)
|
self.scrollable.handle_key_event(event)
|
||||||
|
} else {
|
||||||
|
EventResult::NoRedraw
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||||
|
if self.selectable {
|
||||||
self.scrollable.handle_mouse_event(event)
|
self.scrollable.handle_mouse_event(event)
|
||||||
|
} else {
|
||||||
|
EventResult::NoRedraw
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bounds(&self) -> Rect {
|
fn bounds(&self) -> Rect {
|
||||||
|
@ -1,10 +1,28 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent};
|
use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
|
||||||
use tui::layout::Rect;
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Rect},
|
||||||
|
style::Style,
|
||||||
|
symbols::Marker,
|
||||||
|
text::Span,
|
||||||
|
widgets::{Block, GraphType},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{event::EventResult, AppConfigFields, Component},
|
app::{
|
||||||
|
event::EventResult,
|
||||||
|
widgets::tui_widgets::{
|
||||||
|
custom_legend_chart::{Axis, Dataset},
|
||||||
|
TimeChart,
|
||||||
|
},
|
||||||
|
AppConfigFields, Component,
|
||||||
|
},
|
||||||
|
canvas::Painter,
|
||||||
constants::{AUTOHIDE_TIMEOUT_MILLISECONDS, STALE_MAX_MILLISECONDS, STALE_MIN_MILLISECONDS},
|
constants::{AUTOHIDE_TIMEOUT_MILLISECONDS, STALE_MAX_MILLISECONDS, STALE_MIN_MILLISECONDS},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,6 +42,7 @@ pub enum AutohideTimer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: [AUTOHIDE] Not a fan of how this is done, as this should really "trigger" a draw when it's done.
|
||||||
impl AutohideTimer {
|
impl AutohideTimer {
|
||||||
fn start_display_timer(&mut self) {
|
fn start_display_timer(&mut self) {
|
||||||
match self {
|
match self {
|
||||||
@ -57,6 +76,27 @@ impl AutohideTimer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_showing(&mut self) -> bool {
|
||||||
|
self.update_display_timer();
|
||||||
|
match self {
|
||||||
|
AutohideTimer::AlwaysShow => true,
|
||||||
|
AutohideTimer::AlwaysHide => false,
|
||||||
|
AutohideTimer::Enabled {
|
||||||
|
state,
|
||||||
|
show_duration: _,
|
||||||
|
} => match state {
|
||||||
|
AutohideTimerState::Hidden => false,
|
||||||
|
AutohideTimerState::Running(_) => true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TimeGraphData<'d> {
|
||||||
|
pub data: &'d [(f64, f64)],
|
||||||
|
pub label: Option<Cow<'static, str>>,
|
||||||
|
pub style: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A graph widget with controllable time ranges along the x-axis.
|
/// A graph widget with controllable time ranges along the x-axis.
|
||||||
@ -71,13 +111,15 @@ pub struct TimeGraph {
|
|||||||
time_interval: u64,
|
time_interval: u64,
|
||||||
|
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
|
|
||||||
|
use_dot: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimeGraph {
|
impl TimeGraph {
|
||||||
/// Creates a new [`TimeGraph`]. All time values are in milliseconds.
|
/// Creates a new [`TimeGraph`]. All time values are in milliseconds.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
start_value: u64, autohide_timer: AutohideTimer, min_duration: u64, max_duration: u64,
|
start_value: u64, autohide_timer: AutohideTimer, min_duration: u64, max_duration: u64,
|
||||||
time_interval: u64,
|
time_interval: u64, use_dot: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_display_time: start_value,
|
current_display_time: start_value,
|
||||||
@ -87,6 +129,7 @@ impl TimeGraph {
|
|||||||
max_duration,
|
max_duration,
|
||||||
time_interval,
|
time_interval,
|
||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
|
use_dot,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +150,7 @@ impl TimeGraph {
|
|||||||
STALE_MIN_MILLISECONDS,
|
STALE_MIN_MILLISECONDS,
|
||||||
STALE_MAX_MILLISECONDS,
|
STALE_MAX_MILLISECONDS,
|
||||||
app_config_fields.time_interval,
|
app_config_fields.time_interval,
|
||||||
|
app_config_fields.use_dot,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +209,89 @@ impl TimeGraph {
|
|||||||
EventResult::Redraw
|
EventResult::Redraw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_x_axis_labels(&self, painter: &Painter) -> Vec<Span<'_>> {
|
||||||
|
vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{}s", self.current_display_time / 1000),
|
||||||
|
painter.colours.graph_style,
|
||||||
|
),
|
||||||
|
Span::styled("0s", painter.colours.graph_style),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_display_time(&self) -> u64 {
|
||||||
|
self.current_display_time
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`Chart`].
|
||||||
|
///
|
||||||
|
/// The `reverse_order` parameter is mostly used for cases where you want the first entry to be drawn on
|
||||||
|
/// top - note that this will also reverse the naturally generated legend, if shown!
|
||||||
|
pub fn draw_tui_chart<B: Backend>(
|
||||||
|
&mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, data: &'_ [TimeGraphData<'_>],
|
||||||
|
y_bound_labels: &[Cow<'static, str>], y_bounds: [f64; 2], reverse_order: bool,
|
||||||
|
block: Block<'_>, block_area: Rect,
|
||||||
|
) {
|
||||||
|
let inner_area = block.inner(block_area);
|
||||||
|
|
||||||
|
self.set_bounds(inner_area);
|
||||||
|
|
||||||
|
let time_start = -(self.current_display_time as f64);
|
||||||
|
let x_axis = {
|
||||||
|
let x_axis = Axis::default()
|
||||||
|
.bounds([time_start, 0.0])
|
||||||
|
.style(painter.colours.graph_style);
|
||||||
|
if self.autohide_timer.is_showing() {
|
||||||
|
x_axis.labels(self.get_x_axis_labels(painter))
|
||||||
|
} else {
|
||||||
|
x_axis
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let y_axis = Axis::default()
|
||||||
|
.bounds(y_bounds)
|
||||||
|
.style(painter.colours.graph_style)
|
||||||
|
.labels(
|
||||||
|
y_bound_labels
|
||||||
|
.into_iter()
|
||||||
|
.map(|label| Span::styled(label.clone(), painter.colours.graph_style))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut datasets: Vec<Dataset<'_>> = data
|
||||||
|
.iter()
|
||||||
|
.map(|time_graph_data| {
|
||||||
|
let mut dataset = Dataset::default()
|
||||||
|
.data(time_graph_data.data)
|
||||||
|
.style(time_graph_data.style)
|
||||||
|
.marker(if self.use_dot {
|
||||||
|
Marker::Dot
|
||||||
|
} else {
|
||||||
|
Marker::Braille
|
||||||
|
})
|
||||||
|
.graph_type(GraphType::Line);
|
||||||
|
|
||||||
|
if let Some(label) = &time_graph_data.label {
|
||||||
|
dataset = dataset.name(label.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
dataset
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if reverse_order {
|
||||||
|
datasets.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
let chart = TimeChart::new(datasets)
|
||||||
|
.x_axis(x_axis)
|
||||||
|
.y_axis(y_axis)
|
||||||
|
.style(painter.colours.graph_style)
|
||||||
|
.legend_style(painter.colours.graph_style)
|
||||||
|
.hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4)));
|
||||||
|
|
||||||
|
f.render_widget(chart.block(block), block_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for TimeGraph {
|
impl Component for TimeGraph {
|
||||||
@ -183,8 +310,8 @@ impl Component for TimeGraph {
|
|||||||
|
|
||||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||||
match event.kind {
|
match event.kind {
|
||||||
crossterm::event::MouseEventKind::ScrollDown => self.zoom_out(),
|
MouseEventKind::ScrollDown => self.zoom_out(),
|
||||||
crossterm::event::MouseEventKind::ScrollUp => self.zoom_in(),
|
MouseEventKind::ScrollUp => self.zoom_in(),
|
||||||
_ => EventResult::NoRedraw,
|
_ => EventResult::NoRedraw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,11 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use tui::layout::Rect;
|
use tui::layout::Rect;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::data_farmer::DataCollection,
|
||||||
|
data_conversion::{convert_battery_harvest, ConvertedBatteryData},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{Component, Widget};
|
use super::{Component, Widget};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -36,6 +41,7 @@ pub struct BatteryTable {
|
|||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
batteries: Vec<String>,
|
batteries: Vec<String>,
|
||||||
|
battery_data: Vec<ConvertedBatteryData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BatteryTable {
|
impl BatteryTable {
|
||||||
@ -70,4 +76,8 @@ impl Widget for BatteryTable {
|
|||||||
fn get_pretty_name(&self) -> &'static str {
|
fn get_pretty_name(&self) -> &'static str {
|
||||||
"Battery"
|
"Battery"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
|
self.battery_data = convert_battery_harvest(data_collection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
use std::{collections::HashMap, time::Instant};
|
use std::{borrow::Cow, collections::HashMap, time::Instant};
|
||||||
|
|
||||||
use crossterm::event::{KeyEvent, MouseEvent};
|
use crossterm::event::{KeyEvent, MouseEvent};
|
||||||
use tui::layout::Rect;
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
widgets::{Block, Borders},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::event::EventResult;
|
use crate::{
|
||||||
|
app::{
|
||||||
|
event::EventResult, sort_text_table::SortableColumn, time_graph::TimeGraphData,
|
||||||
|
AppConfigFields, DataCollection,
|
||||||
|
},
|
||||||
|
canvas::Painter,
|
||||||
|
data_conversion::{convert_cpu_data_points, ConvertedCpuData},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppScrollWidgetState, CanvasTableWidthState, Component, SortableTextTable, TimeGraph, Widget,
|
AppScrollWidgetState, CanvasTableWidthState, Component, SortableTextTable, TimeGraph, Widget,
|
||||||
@ -70,23 +81,40 @@ pub enum CpuGraphLegendPosition {
|
|||||||
pub struct CpuGraph {
|
pub struct CpuGraph {
|
||||||
graph: TimeGraph,
|
graph: TimeGraph,
|
||||||
legend: SortableTextTable,
|
legend: SortableTextTable,
|
||||||
pub legend_position: CpuGraphLegendPosition,
|
legend_position: CpuGraphLegendPosition,
|
||||||
|
showing_avg: bool,
|
||||||
|
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
selected: CpuGraphSelection,
|
selected: CpuGraphSelection,
|
||||||
|
|
||||||
|
display_data: Vec<ConvertedCpuData>,
|
||||||
|
load_avg_data: [f32; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CpuGraph {
|
impl CpuGraph {
|
||||||
/// Creates a new [`CpuGraph`].
|
/// Creates a new [`CpuGraph`] from a config.
|
||||||
pub fn new(
|
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
|
||||||
graph: TimeGraph, legend: SortableTextTable, legend_position: CpuGraphLegendPosition,
|
let graph = TimeGraph::from_config(app_config_fields);
|
||||||
) -> Self {
|
let legend = SortableTextTable::new(vec![
|
||||||
|
SortableColumn::new_flex("CPU".into(), None, false, 0.5),
|
||||||
|
SortableColumn::new_flex("Use%".into(), None, false, 0.5),
|
||||||
|
]);
|
||||||
|
let legend_position = if app_config_fields.left_legend {
|
||||||
|
CpuGraphLegendPosition::Left
|
||||||
|
} else {
|
||||||
|
CpuGraphLegendPosition::Right
|
||||||
|
};
|
||||||
|
let showing_avg = app_config_fields.show_average_cpu;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
graph,
|
graph,
|
||||||
legend,
|
legend,
|
||||||
legend_position,
|
legend_position,
|
||||||
|
showing_avg,
|
||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
selected: CpuGraphSelection::None,
|
selected: CpuGraphSelection::None,
|
||||||
|
display_data: Default::default(),
|
||||||
|
load_avg_data: [0.0; 3],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,11 +130,21 @@ impl Component for CpuGraph {
|
|||||||
|
|
||||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||||
if self.graph.does_intersect_mouse(&event) {
|
if self.graph.does_intersect_mouse(&event) {
|
||||||
self.selected = CpuGraphSelection::Graph;
|
if let CpuGraphSelection::Graph = self.selected {
|
||||||
self.graph.handle_mouse_event(event)
|
self.graph.handle_mouse_event(event)
|
||||||
|
} else {
|
||||||
|
self.selected = CpuGraphSelection::Graph;
|
||||||
|
self.graph.handle_mouse_event(event);
|
||||||
|
EventResult::Redraw
|
||||||
|
}
|
||||||
} else if self.legend.does_intersect_mouse(&event) {
|
} else if self.legend.does_intersect_mouse(&event) {
|
||||||
self.selected = CpuGraphSelection::Legend;
|
if let CpuGraphSelection::Legend = self.selected {
|
||||||
self.legend.handle_mouse_event(event)
|
self.legend.handle_mouse_event(event)
|
||||||
|
} else {
|
||||||
|
self.selected = CpuGraphSelection::Legend;
|
||||||
|
self.legend.handle_mouse_event(event);
|
||||||
|
EventResult::Redraw
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
EventResult::NoRedraw
|
EventResult::NoRedraw
|
||||||
}
|
}
|
||||||
@ -125,4 +163,145 @@ impl Widget for CpuGraph {
|
|||||||
fn get_pretty_name(&self) -> &'static str {
|
fn get_pretty_name(&self) -> &'static str {
|
||||||
"CPU"
|
"CPU"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&mut self, painter: &Painter, f: &mut tui::Frame<'_, B>, area: Rect, selected: bool,
|
||||||
|
) {
|
||||||
|
let constraints = match self.legend_position {
|
||||||
|
CpuGraphLegendPosition::Left => {
|
||||||
|
[Constraint::Percentage(15), Constraint::Percentage(85)]
|
||||||
|
}
|
||||||
|
CpuGraphLegendPosition::Right => {
|
||||||
|
[Constraint::Percentage(85), Constraint::Percentage(15)]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let split_area = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(constraints)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
|
||||||
|
let y_bound_labels: [Cow<'static, str>; 2] = ["0%".into(), "100%".into()];
|
||||||
|
|
||||||
|
let current_index = self.legend.current_index();
|
||||||
|
let sliced_cpu_data = if current_index == 0 {
|
||||||
|
&self.display_data[..]
|
||||||
|
} else {
|
||||||
|
&self.display_data[current_index..current_index + 1]
|
||||||
|
};
|
||||||
|
|
||||||
|
let cpu_data = sliced_cpu_data
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(cpu_index, core_data)| TimeGraphData {
|
||||||
|
data: &core_data.cpu_data,
|
||||||
|
label: None,
|
||||||
|
style: {
|
||||||
|
let offset_cpu_index = cpu_index + current_index;
|
||||||
|
if offset_cpu_index == 0 {
|
||||||
|
painter.colours.all_colour_style
|
||||||
|
} else if self.showing_avg && offset_cpu_index == 1 {
|
||||||
|
painter.colours.avg_colour_style
|
||||||
|
} else {
|
||||||
|
let cpu_style_index = if self.showing_avg {
|
||||||
|
// No underflow should occur, as if offset_cpu_index was
|
||||||
|
// 1 and avg is showing, it's caught by the above case!
|
||||||
|
offset_cpu_index - 2
|
||||||
|
} else {
|
||||||
|
offset_cpu_index - 1
|
||||||
|
};
|
||||||
|
painter.colours.cpu_colour_styles
|
||||||
|
[cpu_style_index % painter.colours.cpu_colour_styles.len()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let legend_data = self
|
||||||
|
.display_data
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(cpu_index, core_data)| {
|
||||||
|
let style = Some(if cpu_index == 0 {
|
||||||
|
painter.colours.all_colour_style
|
||||||
|
} else if self.showing_avg && cpu_index == 1 {
|
||||||
|
painter.colours.avg_colour_style
|
||||||
|
} else {
|
||||||
|
let cpu_style_index = if self.showing_avg {
|
||||||
|
// No underflow should occur, as if cpu_index was
|
||||||
|
// 1 and avg is showing, it's caught by the above case!
|
||||||
|
cpu_index - 2
|
||||||
|
} else {
|
||||||
|
cpu_index - 1
|
||||||
|
};
|
||||||
|
painter.colours.cpu_colour_styles
|
||||||
|
[cpu_style_index % painter.colours.cpu_colour_styles.len()]
|
||||||
|
});
|
||||||
|
|
||||||
|
vec![
|
||||||
|
(
|
||||||
|
core_data.cpu_name.clone().into(),
|
||||||
|
Some(core_data.short_cpu_name.clone().into()),
|
||||||
|
style,
|
||||||
|
),
|
||||||
|
(core_data.legend_value.clone().into(), None, style),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let graph_block = Block::default()
|
||||||
|
.border_style(if selected {
|
||||||
|
if let CpuGraphSelection::Graph = &self.selected {
|
||||||
|
painter.colours.highlighted_border_style
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
})
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
|
let legend_block = Block::default()
|
||||||
|
.border_style(if selected {
|
||||||
|
if let CpuGraphSelection::Legend = &self.selected {
|
||||||
|
painter.colours.highlighted_border_style
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
})
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
|
let (graph_block_area, legend_block_area) = match self.legend_position {
|
||||||
|
CpuGraphLegendPosition::Left => (split_area[1], split_area[0]),
|
||||||
|
CpuGraphLegendPosition::Right => (split_area[0], split_area[1]),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.graph.draw_tui_chart(
|
||||||
|
painter,
|
||||||
|
f,
|
||||||
|
&cpu_data,
|
||||||
|
&y_bound_labels,
|
||||||
|
Y_BOUNDS,
|
||||||
|
true,
|
||||||
|
graph_block,
|
||||||
|
graph_block_area,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.legend.draw_tui_table(
|
||||||
|
painter,
|
||||||
|
f,
|
||||||
|
&legend_data,
|
||||||
|
legend_block,
|
||||||
|
legend_block_area,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
|
convert_cpu_data_points(data_collection, &mut self.display_data, false); // TODO: Again, the "is_frozen" is probably useless
|
||||||
|
self.load_avg_data = data_collection.load_avg_harvest;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
335
src/app/widgets/custom_tui/custom_legend_chart.rs
Normal file
335
src/app/widgets/custom_tui/custom_legend_chart.rs
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Chart<'a> {
|
||||||
|
/// A block to display around the widget eventually
|
||||||
|
block: Option<Block<'a>>,
|
||||||
|
/// The horizontal axis
|
||||||
|
x_axis: Axis<'a>,
|
||||||
|
/// The vertical axis
|
||||||
|
y_axis: Axis<'a>,
|
||||||
|
/// A reference to the datasets
|
||||||
|
datasets: Vec<Dataset<'a>>,
|
||||||
|
/// The widget base style
|
||||||
|
style: Style,
|
||||||
|
/// Constraints used to determine whether the legend should be shown or not
|
||||||
|
hidden_legend_constraints: (Constraint, Constraint),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Chart<'a> {
|
||||||
|
pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
|
||||||
|
Chart {
|
||||||
|
block: None,
|
||||||
|
x_axis: Axis::default(),
|
||||||
|
y_axis: Axis::default(),
|
||||||
|
style: Default::default(),
|
||||||
|
datasets,
|
||||||
|
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
|
||||||
|
self.block = Some(block);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> Chart<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
|
||||||
|
self.x_axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
|
||||||
|
self.y_axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the constraints used to determine whether the legend should be shown or not.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use tui::widgets::Chart;
|
||||||
|
/// # use tui::layout::Constraint;
|
||||||
|
/// let constraints = (
|
||||||
|
/// Constraint::Ratio(1, 3),
|
||||||
|
/// Constraint::Ratio(1, 4)
|
||||||
|
/// );
|
||||||
|
/// // Hide the legend when either its width is greater than 33% of the total widget width
|
||||||
|
/// // or if its height is greater than 25% of the total widget height.
|
||||||
|
/// let _chart: Chart = Chart::new(vec![])
|
||||||
|
/// .hidden_legend_constraints(constraints);
|
||||||
|
/// ```
|
||||||
|
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
|
||||||
|
self.hidden_legend_constraints = constraints;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the internal layout of the chart given the area. If the area is too small some
|
||||||
|
/// elements may be automatically hidden
|
||||||
|
fn layout(&self, area: Rect) -> ChartLayout {
|
||||||
|
let mut layout = ChartLayout::default();
|
||||||
|
if area.height == 0 || area.width == 0 {
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
let mut x = area.left();
|
||||||
|
let mut y = area.bottom() - 1;
|
||||||
|
|
||||||
|
if self.x_axis.labels.is_some() && y > area.top() {
|
||||||
|
layout.label_x = Some(y);
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||||
|
x += self.max_width_of_labels_left_of_y_axis(area);
|
||||||
|
|
||||||
|
if self.x_axis.labels.is_some() && y > area.top() {
|
||||||
|
layout.axis_x = Some(y);
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.y_axis.labels.is_some() && x + 1 < area.right() {
|
||||||
|
layout.axis_y = Some(x);
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if x < area.right() && y > 1 {
|
||||||
|
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref title) = self.x_axis.title {
|
||||||
|
let w = title.width() as u16;
|
||||||
|
if w < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||||
|
layout.title_x = Some((x + layout.graph_area.width - w, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref title) = self.y_axis.title {
|
||||||
|
let w = title.width() as u16;
|
||||||
|
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||||
|
layout.title_y = Some((x, area.top()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
|
||||||
|
let legend_width = inner_width + 2;
|
||||||
|
let legend_height = self.datasets.len() as u16 + 2;
|
||||||
|
let max_legend_width = self
|
||||||
|
.hidden_legend_constraints
|
||||||
|
.0
|
||||||
|
.apply(layout.graph_area.width);
|
||||||
|
let max_legend_height = self
|
||||||
|
.hidden_legend_constraints
|
||||||
|
.1
|
||||||
|
.apply(layout.graph_area.height);
|
||||||
|
if inner_width > 0
|
||||||
|
&& legend_width < max_legend_width
|
||||||
|
&& legend_height < max_legend_height
|
||||||
|
{
|
||||||
|
layout.legend_area = Some(Rect::new(
|
||||||
|
layout.graph_area.right() - legend_width,
|
||||||
|
layout.graph_area.top(),
|
||||||
|
legend_width,
|
||||||
|
legend_height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_width_of_labels_left_of_y_axis(&self, area: Rect) -> u16 {
|
||||||
|
let mut max_width = self
|
||||||
|
.y_axis
|
||||||
|
.labels
|
||||||
|
.as_ref()
|
||||||
|
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(ref x_labels) = self.x_axis.labels {
|
||||||
|
if !x_labels.is_empty() {
|
||||||
|
max_width = max(max_width, x_labels[0].content.width() as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||||
|
max_width.min(area.width / 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_x_labels(
|
||||||
|
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
|
||||||
|
) {
|
||||||
|
let y = match layout.label_x {
|
||||||
|
Some(y) => y,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||||
|
let labels_len = labels.len() as u16;
|
||||||
|
if labels_len < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let width_between_ticks = graph_area.width / (labels_len - 1);
|
||||||
|
for (i, label) in labels.iter().enumerate() {
|
||||||
|
let label_width = label.width() as u16;
|
||||||
|
let label_width = if i == 0 {
|
||||||
|
// the first label is put between the left border of the chart and the y axis.
|
||||||
|
graph_area
|
||||||
|
.left()
|
||||||
|
.saturating_sub(chart_area.left())
|
||||||
|
.min(label_width)
|
||||||
|
} else {
|
||||||
|
// other labels are put on the left of each tick on the x axis
|
||||||
|
width_between_ticks.min(label_width)
|
||||||
|
};
|
||||||
|
buf.set_span(
|
||||||
|
graph_area.left() + i as u16 * width_between_ticks - label_width,
|
||||||
|
y,
|
||||||
|
label,
|
||||||
|
label_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_y_labels(
|
||||||
|
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
|
||||||
|
) {
|
||||||
|
let x = match layout.label_y {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||||
|
let labels_len = labels.len() as u16;
|
||||||
|
let label_width = graph_area.left().saturating_sub(chart_area.left());
|
||||||
|
for (i, label) in labels.iter().enumerate() {
|
||||||
|
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||||
|
if dy < graph_area.bottom() {
|
||||||
|
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label_width as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for Chart<'a> {
|
||||||
|
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.area() == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buf.set_style(area, self.style);
|
||||||
|
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||||
|
// the cells that are part of the components put on top of the graph area (i.e legend and
|
||||||
|
// axis names).
|
||||||
|
let original_style = buf.get(area.left(), area.top()).style();
|
||||||
|
|
||||||
|
let chart_area = match self.block.take() {
|
||||||
|
Some(b) => {
|
||||||
|
let inner_area = b.inner(area);
|
||||||
|
b.render(area, buf);
|
||||||
|
inner_area
|
||||||
|
}
|
||||||
|
None => area,
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = self.layout(chart_area);
|
||||||
|
let graph_area = layout.graph_area;
|
||||||
|
if graph_area.width < 1 || graph_area.height < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||||
|
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||||
|
|
||||||
|
if let Some(y) = layout.axis_x {
|
||||||
|
for x in graph_area.left()..graph_area.right() {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::HORIZONTAL)
|
||||||
|
.set_style(self.x_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = layout.axis_y {
|
||||||
|
for y in graph_area.top()..graph_area.bottom() {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::VERTICAL)
|
||||||
|
.set_style(self.y_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = layout.axis_x {
|
||||||
|
if let Some(x) = layout.axis_y {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::BOTTOM_LEFT)
|
||||||
|
.set_style(self.x_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dataset in &self.datasets {
|
||||||
|
Canvas::default()
|
||||||
|
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||||
|
.x_bounds(self.x_axis.bounds)
|
||||||
|
.y_bounds(self.y_axis.bounds)
|
||||||
|
.marker(dataset.marker)
|
||||||
|
.paint(|ctx| {
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: dataset.data,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
if let GraphType::Line = dataset.graph_type {
|
||||||
|
for data in dataset.data.windows(2) {
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: data[0].0,
|
||||||
|
y1: data[0].1,
|
||||||
|
x2: data[1].0,
|
||||||
|
y2: data[1].1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.render(graph_area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(legend_area) = layout.legend_area {
|
||||||
|
buf.set_style(legend_area, original_style);
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.render(legend_area, buf);
|
||||||
|
for (i, dataset) in self.datasets.iter().enumerate() {
|
||||||
|
buf.set_string(
|
||||||
|
legend_area.x + 1,
|
||||||
|
legend_area.y + 1 + i as u16,
|
||||||
|
&dataset.name,
|
||||||
|
dataset.style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = layout.title_x {
|
||||||
|
let title = self.x_axis.title.unwrap();
|
||||||
|
let width = graph_area.right().saturating_sub(x);
|
||||||
|
buf.set_style(
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
original_style,
|
||||||
|
);
|
||||||
|
buf.set_spans(x, y, &title, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = layout.title_y {
|
||||||
|
let title = self.y_axis.title.unwrap();
|
||||||
|
let width = graph_area.right().saturating_sub(x);
|
||||||
|
buf.set_style(
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
original_style,
|
||||||
|
);
|
||||||
|
buf.set_spans(x, y, &title, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,11 +9,15 @@ use tui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{event::EventResult, sort_text_table::SortableColumn},
|
app::{data_farmer::DataCollection, event::EventResult, sort_text_table::SortableColumn},
|
||||||
canvas::{DisplayableData, Painter},
|
canvas::Painter,
|
||||||
|
data_conversion::convert_disk_row,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, SortableTextTable, Widget};
|
use super::{
|
||||||
|
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component,
|
||||||
|
SortableTextTable, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct DiskWidgetState {
|
pub struct DiskWidgetState {
|
||||||
pub scroll_state: AppScrollWidgetState,
|
pub scroll_state: AppScrollWidgetState,
|
||||||
@ -52,6 +56,8 @@ impl DiskState {
|
|||||||
pub struct DiskTable {
|
pub struct DiskTable {
|
||||||
table: SortableTextTable,
|
table: SortableTextTable,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
|
|
||||||
|
display_data: TextTableData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DiskTable {
|
impl Default for DiskTable {
|
||||||
@ -69,6 +75,7 @@ impl Default for DiskTable {
|
|||||||
Self {
|
Self {
|
||||||
table,
|
table,
|
||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
|
display_data: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,8 +104,7 @@ impl Widget for DiskTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw<B: Backend>(
|
fn draw<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
selected: bool,
|
|
||||||
) {
|
) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.border_style(if selected {
|
.border_style(if selected {
|
||||||
@ -108,19 +114,12 @@ impl Widget for DiskTable {
|
|||||||
})
|
})
|
||||||
.borders(Borders::ALL);
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
self.set_bounds(area);
|
|
||||||
let draw_area = block.inner(area);
|
|
||||||
let (table, widths, mut tui_state) =
|
|
||||||
self.table
|
self.table
|
||||||
.table
|
.table
|
||||||
.create_draw_table(painter, &data.disk_data, draw_area);
|
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
|
||||||
|
}
|
||||||
|
|
||||||
let table = table.highlight_style(if selected {
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
painter.colours.currently_selected_text_style
|
self.display_data = convert_disk_row(data_collection);
|
||||||
} else {
|
|
||||||
painter.colours.text_style
|
|
||||||
});
|
|
||||||
|
|
||||||
f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
use std::{collections::HashMap, time::Instant};
|
use std::{borrow::Cow, collections::HashMap, time::Instant};
|
||||||
|
|
||||||
use crossterm::event::{KeyEvent, MouseEvent};
|
use crossterm::event::{KeyEvent, MouseEvent};
|
||||||
use tui::layout::Rect;
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::Rect,
|
||||||
|
widgets::{Block, Borders},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::event::EventResult;
|
use crate::{
|
||||||
|
app::{event::EventResult, time_graph::TimeGraphData, DataCollection},
|
||||||
|
data_conversion::{convert_mem_data_points, convert_mem_labels, convert_swap_data_points},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{Component, TimeGraph, Widget};
|
use super::{Component, TimeGraph, Widget};
|
||||||
|
|
||||||
@ -48,12 +55,22 @@ impl MemState {
|
|||||||
/// around [`TimeGraph`] as of now.
|
/// around [`TimeGraph`] as of now.
|
||||||
pub struct MemGraph {
|
pub struct MemGraph {
|
||||||
graph: TimeGraph,
|
graph: TimeGraph,
|
||||||
|
mem_labels: Option<(String, String)>,
|
||||||
|
swap_labels: Option<(String, String)>,
|
||||||
|
mem_data: Vec<(f64, f64)>,
|
||||||
|
swap_data: Vec<(f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemGraph {
|
impl MemGraph {
|
||||||
/// Creates a new [`MemGraph`].
|
/// Creates a new [`MemGraph`].
|
||||||
pub fn new(graph: TimeGraph) -> Self {
|
pub fn new(graph: TimeGraph) -> Self {
|
||||||
Self { graph }
|
Self {
|
||||||
|
graph,
|
||||||
|
mem_labels: Default::default(),
|
||||||
|
swap_labels: Default::default(),
|
||||||
|
mem_data: Default::default(),
|
||||||
|
swap_data: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,4 +96,58 @@ impl Widget for MemGraph {
|
|||||||
fn get_pretty_name(&self) -> &'static str {
|
fn get_pretty_name(&self) -> &'static str {
|
||||||
"Memory"
|
"Memory"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&mut self, painter: &crate::canvas::Painter, f: &mut tui::Frame<'_, B>, area: Rect,
|
||||||
|
selected: bool,
|
||||||
|
) {
|
||||||
|
let block = Block::default()
|
||||||
|
.border_style(if selected {
|
||||||
|
painter.colours.highlighted_border_style
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
})
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
|
let mut chart_data = Vec::with_capacity(2);
|
||||||
|
if let Some((label_percent, label_frac)) = &self.mem_labels {
|
||||||
|
let mem_label = format!("RAM:{}{}", label_percent, label_frac);
|
||||||
|
chart_data.push(TimeGraphData {
|
||||||
|
data: &self.mem_data,
|
||||||
|
label: Some(mem_label.into()),
|
||||||
|
style: painter.colours.ram_style,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some((label_percent, label_frac)) = &self.swap_labels {
|
||||||
|
let swap_label = format!("SWP:{}{}", label_percent, label_frac);
|
||||||
|
chart_data.push(TimeGraphData {
|
||||||
|
data: &self.swap_data,
|
||||||
|
label: Some(swap_label.into()),
|
||||||
|
style: painter.colours.swap_style,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
|
||||||
|
let y_bound_labels: [Cow<'static, str>; 2] = ["0%".into(), "100%".into()];
|
||||||
|
|
||||||
|
self.graph.draw_tui_chart(
|
||||||
|
painter,
|
||||||
|
f,
|
||||||
|
&chart_data,
|
||||||
|
&y_bound_labels,
|
||||||
|
Y_BOUNDS,
|
||||||
|
false,
|
||||||
|
block,
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
|
self.mem_data = convert_mem_data_points(data_collection, false); // TODO: I think the "is_frozen" part is useless... it's always false now.
|
||||||
|
self.swap_data = convert_swap_data_points(data_collection, false);
|
||||||
|
let (memory_labels, swap_labels) = convert_mem_labels(data_collection);
|
||||||
|
|
||||||
|
self.mem_labels = memory_labels;
|
||||||
|
self.swap_labels = swap_labels;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,24 @@
|
|||||||
use std::{collections::HashMap, time::Instant};
|
use std::{collections::HashMap, time::Instant};
|
||||||
|
|
||||||
use tui::layout::Rect;
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
widgets::{Block, Borders},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{Component, TimeGraph, Widget};
|
use crate::{
|
||||||
|
app::{
|
||||||
|
data_farmer::DataCollection, text_table::SimpleColumn, time_graph::TimeGraphData,
|
||||||
|
AppConfigFields, AxisScaling,
|
||||||
|
},
|
||||||
|
canvas::Painter,
|
||||||
|
data_conversion::convert_network_data_points,
|
||||||
|
units::data_units::DataUnit,
|
||||||
|
utils::gen_util::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Component, TextTable, TimeGraph, Widget};
|
||||||
|
|
||||||
pub struct NetWidgetState {
|
pub struct NetWidgetState {
|
||||||
pub current_display_time: u64,
|
pub current_display_time: u64,
|
||||||
@ -57,6 +73,331 @@ impl NetState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- NEW STUFF BELOW ---
|
||||||
|
|
||||||
|
/// Returns the max data point and time given a time.
|
||||||
|
fn get_max_entry(
|
||||||
|
rx: &[(f64, f64)], tx: &[(f64, f64)], time_start: f64, network_scale_type: &AxisScaling,
|
||||||
|
network_use_binary_prefix: bool,
|
||||||
|
) -> (f64, f64) {
|
||||||
|
/// Determines a "fake" max value in circumstances where we couldn't find one from the data.
|
||||||
|
fn calculate_missing_max(
|
||||||
|
network_scale_type: &AxisScaling, network_use_binary_prefix: bool,
|
||||||
|
) -> f64 {
|
||||||
|
match network_scale_type {
|
||||||
|
AxisScaling::Log => {
|
||||||
|
if network_use_binary_prefix {
|
||||||
|
LOG_KIBI_LIMIT
|
||||||
|
} else {
|
||||||
|
LOG_KILO_LIMIT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AxisScaling::Linear => {
|
||||||
|
if network_use_binary_prefix {
|
||||||
|
KIBI_LIMIT_F64
|
||||||
|
} else {
|
||||||
|
KILO_LIMIT_F64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays
|
||||||
|
// are sorted, so we can short-circuit our search to filter out only the relevant data points...
|
||||||
|
let filtered_rx = if let (Some(rx_start), Some(rx_end)) = (
|
||||||
|
rx.iter().position(|(time, _data)| *time >= time_start),
|
||||||
|
rx.iter().rposition(|(time, _data)| *time <= 0.0),
|
||||||
|
) {
|
||||||
|
Some(&rx[rx_start..=rx_end])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let filtered_tx = if let (Some(tx_start), Some(tx_end)) = (
|
||||||
|
tx.iter().position(|(time, _data)| *time >= time_start),
|
||||||
|
tx.iter().rposition(|(time, _data)| *time <= 0.0),
|
||||||
|
) {
|
||||||
|
Some(&tx[tx_start..=tx_end])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then, find the maximal rx/tx so we know how to scale, and return it.
|
||||||
|
match (filtered_rx, filtered_tx) {
|
||||||
|
(None, None) => (
|
||||||
|
time_start,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
),
|
||||||
|
(None, Some(filtered_tx)) => {
|
||||||
|
match filtered_tx
|
||||||
|
.iter()
|
||||||
|
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
|
||||||
|
{
|
||||||
|
Some((best_time, max_val)) => {
|
||||||
|
if *max_val == 0.0 {
|
||||||
|
(
|
||||||
|
time_start,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(*best_time, *max_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
time_start,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(filtered_rx), None) => {
|
||||||
|
match filtered_rx
|
||||||
|
.iter()
|
||||||
|
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
|
||||||
|
{
|
||||||
|
Some((best_time, max_val)) => {
|
||||||
|
if *max_val == 0.0 {
|
||||||
|
(
|
||||||
|
time_start,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(*best_time, *max_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
time_start,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(filtered_rx), Some(filtered_tx)) => {
|
||||||
|
match filtered_rx
|
||||||
|
.iter()
|
||||||
|
.chain(filtered_tx)
|
||||||
|
.max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false))
|
||||||
|
{
|
||||||
|
Some((best_time, max_val)) => {
|
||||||
|
if *max_val == 0.0 {
|
||||||
|
(
|
||||||
|
*best_time,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(*best_time, *max_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
time_start,
|
||||||
|
calculate_missing_max(network_scale_type, network_use_binary_prefix),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the required max data point and labels.
|
||||||
|
fn adjust_network_data_point(
|
||||||
|
max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
|
||||||
|
network_use_binary_prefix: bool,
|
||||||
|
) -> (f64, Vec<String>) {
|
||||||
|
// So, we're going with an approach like this for linear data:
|
||||||
|
// - Main goal is to maximize the amount of information displayed given a specific height.
|
||||||
|
// We don't want to drown out some data if the ranges are too far though! Nor do we want to filter
|
||||||
|
// out too much data...
|
||||||
|
// - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load.
|
||||||
|
//
|
||||||
|
// The idea is we take the top value, build our scale such that each "point" is a scaled version of that.
|
||||||
|
// So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and
|
||||||
|
// probably something like 438.75?
|
||||||
|
//
|
||||||
|
// So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max
|
||||||
|
// value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will
|
||||||
|
// properly space them all out... we just work with that and space it out properly.
|
||||||
|
//
|
||||||
|
// Dynamic chart idea based off of FreeNAS's chart design.
|
||||||
|
//
|
||||||
|
// ===
|
||||||
|
//
|
||||||
|
// For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple.
|
||||||
|
|
||||||
|
// Now just check the largest unit we correspond to... then proceed to build some entries from there!
|
||||||
|
|
||||||
|
let unit_char = match network_unit_type {
|
||||||
|
DataUnit::Byte => "B",
|
||||||
|
DataUnit::Bit => "b",
|
||||||
|
};
|
||||||
|
|
||||||
|
match network_scale_type {
|
||||||
|
AxisScaling::Linear => {
|
||||||
|
let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix {
|
||||||
|
(
|
||||||
|
KIBI_LIMIT_F64,
|
||||||
|
MEBI_LIMIT_F64,
|
||||||
|
GIBI_LIMIT_F64,
|
||||||
|
TEBI_LIMIT_F64,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
KILO_LIMIT_F64,
|
||||||
|
MEGA_LIMIT_F64,
|
||||||
|
GIGA_LIMIT_F64,
|
||||||
|
TERA_LIMIT_F64,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type.
|
||||||
|
let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) =
|
||||||
|
if bumped_max_entry < k_limit {
|
||||||
|
(max_entry, "", unit_char)
|
||||||
|
} else if bumped_max_entry < m_limit {
|
||||||
|
(
|
||||||
|
max_entry / k_limit,
|
||||||
|
if network_use_binary_prefix { "Ki" } else { "K" },
|
||||||
|
unit_char,
|
||||||
|
)
|
||||||
|
} else if bumped_max_entry < g_limit {
|
||||||
|
(
|
||||||
|
max_entry / m_limit,
|
||||||
|
if network_use_binary_prefix { "Mi" } else { "M" },
|
||||||
|
unit_char,
|
||||||
|
)
|
||||||
|
} else if bumped_max_entry < t_limit {
|
||||||
|
(
|
||||||
|
max_entry / g_limit,
|
||||||
|
if network_use_binary_prefix { "Gi" } else { "G" },
|
||||||
|
unit_char,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
max_entry / t_limit,
|
||||||
|
if network_use_binary_prefix { "Ti" } else { "T" },
|
||||||
|
unit_char,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finally, build an acceptable range starting from there, using the given height!
|
||||||
|
// Note we try to put more of a weight on the bottom section vs. the top, since the top has less data.
|
||||||
|
|
||||||
|
let base_unit = max_value_scaled;
|
||||||
|
let labels: Vec<String> = vec![
|
||||||
|
format!("0{}{}", unit_prefix, unit_type),
|
||||||
|
format!("{:.1}", base_unit * 0.5),
|
||||||
|
format!("{:.1}", base_unit),
|
||||||
|
format!("{:.1}", base_unit * 1.5),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(bumped_max_entry, labels)
|
||||||
|
}
|
||||||
|
AxisScaling::Log => {
|
||||||
|
let (m_limit, g_limit, t_limit) = if network_use_binary_prefix {
|
||||||
|
(LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT)
|
||||||
|
} else {
|
||||||
|
(LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT)
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{}0{}",
|
||||||
|
if network_use_binary_prefix { " " } else { " " },
|
||||||
|
unit_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"1{}{}",
|
||||||
|
if network_use_binary_prefix { "Ki" } else { "K" },
|
||||||
|
unit_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"1{}{}",
|
||||||
|
if network_use_binary_prefix { "Mi" } else { "M" },
|
||||||
|
unit_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"1{}{}",
|
||||||
|
if network_use_binary_prefix { "Gi" } else { "G" },
|
||||||
|
unit_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"1{}{}",
|
||||||
|
if network_use_binary_prefix { "Ti" } else { "T" },
|
||||||
|
unit_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"1{}{}",
|
||||||
|
if network_use_binary_prefix { "Pi" } else { "P" },
|
||||||
|
unit_char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if max_entry < m_limit {
|
||||||
|
(
|
||||||
|
m_limit,
|
||||||
|
vec![
|
||||||
|
get_zero(network_use_binary_prefix, unit_char),
|
||||||
|
get_k(network_use_binary_prefix, unit_char),
|
||||||
|
get_m(network_use_binary_prefix, unit_char),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else if max_entry < g_limit {
|
||||||
|
(
|
||||||
|
g_limit,
|
||||||
|
vec![
|
||||||
|
get_zero(network_use_binary_prefix, unit_char),
|
||||||
|
get_k(network_use_binary_prefix, unit_char),
|
||||||
|
get_m(network_use_binary_prefix, unit_char),
|
||||||
|
get_g(network_use_binary_prefix, unit_char),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else if max_entry < t_limit {
|
||||||
|
(
|
||||||
|
t_limit,
|
||||||
|
vec![
|
||||||
|
get_zero(network_use_binary_prefix, unit_char),
|
||||||
|
get_k(network_use_binary_prefix, unit_char),
|
||||||
|
get_m(network_use_binary_prefix, unit_char),
|
||||||
|
get_g(network_use_binary_prefix, unit_char),
|
||||||
|
get_t(network_use_binary_prefix, unit_char),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// I really doubt anyone's transferring beyond petabyte speeds...
|
||||||
|
(
|
||||||
|
if network_use_binary_prefix {
|
||||||
|
LOG_PEBI_LIMIT
|
||||||
|
} else {
|
||||||
|
LOG_PETA_LIMIT
|
||||||
|
},
|
||||||
|
vec![
|
||||||
|
get_zero(network_use_binary_prefix, unit_char),
|
||||||
|
get_k(network_use_binary_prefix, unit_char),
|
||||||
|
get_m(network_use_binary_prefix, unit_char),
|
||||||
|
get_g(network_use_binary_prefix, unit_char),
|
||||||
|
get_t(network_use_binary_prefix, unit_char),
|
||||||
|
get_p(network_use_binary_prefix, unit_char),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A struct containing useful cached information for a [`NetGraph`].
|
/// A struct containing useful cached information for a [`NetGraph`].
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct NetGraphCache {
|
pub struct NetGraphCache {
|
||||||
@ -83,17 +424,48 @@ pub struct NetGraph {
|
|||||||
|
|
||||||
// Cached details for drawing purposes; probably want to move at some point...
|
// Cached details for drawing purposes; probably want to move at some point...
|
||||||
draw_cache: NetGraphCacheState,
|
draw_cache: NetGraphCacheState,
|
||||||
|
|
||||||
|
pub rx_display: String,
|
||||||
|
pub tx_display: String,
|
||||||
|
pub total_rx_display: String,
|
||||||
|
pub total_tx_display: String,
|
||||||
|
pub network_data_rx: Vec<(f64, f64)>,
|
||||||
|
pub network_data_tx: Vec<(f64, f64)>,
|
||||||
|
|
||||||
|
pub scale_type: AxisScaling,
|
||||||
|
pub unit_type: DataUnit,
|
||||||
|
pub use_binary_prefix: bool,
|
||||||
|
|
||||||
|
hide_legend: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NetGraph {
|
impl NetGraph {
|
||||||
/// Creates a new [`NetGraph`].
|
/// Creates a new [`NetGraph`] given a [`AppConfigFields`].
|
||||||
pub fn new(graph: TimeGraph) -> Self {
|
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
|
||||||
|
let graph = TimeGraph::from_config(app_config_fields);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
graph,
|
graph,
|
||||||
draw_cache: NetGraphCacheState::Uncached,
|
draw_cache: NetGraphCacheState::Uncached,
|
||||||
|
rx_display: Default::default(),
|
||||||
|
tx_display: Default::default(),
|
||||||
|
total_rx_display: Default::default(),
|
||||||
|
total_tx_display: Default::default(),
|
||||||
|
network_data_rx: Default::default(),
|
||||||
|
network_data_tx: Default::default(),
|
||||||
|
scale_type: app_config_fields.network_scale_type.clone(),
|
||||||
|
unit_type: app_config_fields.network_unit_type.clone(),
|
||||||
|
use_binary_prefix: app_config_fields.network_use_binary_prefix,
|
||||||
|
hide_legend: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hides the legend. Only really useful for [`OldNetGraph`].
|
||||||
|
pub fn hide_legend(mut self) -> Self {
|
||||||
|
self.hide_legend = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the associated cache on a [`NetGraph`].
|
/// Updates the associated cache on a [`NetGraph`].
|
||||||
pub fn set_cache(&mut self, area: Rect, max_range: f64, labels: Vec<String>, time_start: f64) {
|
pub fn set_cache(&mut self, area: Rect, max_range: f64, labels: Vec<String>, time_start: f64) {
|
||||||
self.draw_cache = NetGraphCacheState::Cached {
|
self.draw_cache = NetGraphCacheState::Cached {
|
||||||
@ -171,20 +543,110 @@ impl Widget for NetGraph {
|
|||||||
fn get_pretty_name(&self) -> &'static str {
|
fn get_pretty_name(&self) -> &'static str {
|
||||||
"Network"
|
"Network"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
|
) {
|
||||||
|
let block = Block::default()
|
||||||
|
.border_style(if selected {
|
||||||
|
painter.colours.highlighted_border_style
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
})
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
|
let chart_data = vec![
|
||||||
|
TimeGraphData {
|
||||||
|
data: &self.network_data_rx,
|
||||||
|
label: if self.hide_legend {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.rx_display.clone().into())
|
||||||
|
},
|
||||||
|
style: painter.colours.rx_style,
|
||||||
|
},
|
||||||
|
TimeGraphData {
|
||||||
|
data: &self.network_data_tx,
|
||||||
|
label: if self.hide_legend {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.tx_display.clone().into())
|
||||||
|
},
|
||||||
|
style: painter.colours.tx_style,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let (_best_time, max_entry) = get_max_entry(
|
||||||
|
&self.network_data_rx,
|
||||||
|
&self.network_data_tx,
|
||||||
|
-(self.graph.get_current_display_time() as f64),
|
||||||
|
&self.scale_type,
|
||||||
|
self.use_binary_prefix,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (max_range, labels) = adjust_network_data_point(
|
||||||
|
max_entry,
|
||||||
|
&self.scale_type,
|
||||||
|
&self.unit_type,
|
||||||
|
self.use_binary_prefix,
|
||||||
|
);
|
||||||
|
let y_bounds = [0.0, max_range];
|
||||||
|
let y_bound_labels = labels.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.graph.draw_tui_chart(
|
||||||
|
painter,
|
||||||
|
f,
|
||||||
|
&chart_data,
|
||||||
|
&y_bound_labels,
|
||||||
|
y_bounds,
|
||||||
|
false,
|
||||||
|
block,
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
|
let network_data = convert_network_data_points(
|
||||||
|
data_collection,
|
||||||
|
false, // TODO: I think the is_frozen here is also useless; see mem and cpu
|
||||||
|
false,
|
||||||
|
&self.scale_type,
|
||||||
|
&self.unit_type,
|
||||||
|
self.use_binary_prefix,
|
||||||
|
);
|
||||||
|
self.network_data_rx = network_data.rx;
|
||||||
|
self.network_data_tx = network_data.tx;
|
||||||
|
self.rx_display = network_data.rx_display;
|
||||||
|
self.tx_display = network_data.tx_display;
|
||||||
|
if let Some(total_rx_display) = network_data.total_rx_display {
|
||||||
|
self.total_rx_display = total_rx_display;
|
||||||
|
}
|
||||||
|
if let Some(total_tx_display) = network_data.total_tx_display {
|
||||||
|
self.total_tx_display = total_tx_display;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`],
|
/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`],
|
||||||
/// and the main difference is that it also contains a bounding box for the graph + text.
|
/// and the main difference is that it also contains a bounding box for the graph + text.
|
||||||
pub struct OldNetGraph {
|
pub struct OldNetGraph {
|
||||||
net_graph: NetGraph,
|
net_graph: NetGraph,
|
||||||
|
table: TextTable,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OldNetGraph {
|
impl OldNetGraph {
|
||||||
/// Creates a new [`OldNetGraph`].
|
/// Creates a new [`OldNetGraph`] from a [`AppConfigFields`].
|
||||||
pub fn new(graph: TimeGraph) -> Self {
|
pub fn from_config(config: &AppConfigFields) -> Self {
|
||||||
Self {
|
Self {
|
||||||
net_graph: NetGraph::new(graph),
|
net_graph: NetGraph::from_config(config).hide_legend(),
|
||||||
|
table: TextTable::new(vec![
|
||||||
|
SimpleColumn::new_flex("RX".into(), 0.25),
|
||||||
|
SimpleColumn::new_flex("TX".into(), 0.25),
|
||||||
|
SimpleColumn::new_flex("Total RX".into(), 0.25),
|
||||||
|
SimpleColumn::new_flex("Total TX".into(), 0.25),
|
||||||
|
])
|
||||||
|
.unselectable(),
|
||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,4 +678,70 @@ impl Widget for OldNetGraph {
|
|||||||
fn get_pretty_name(&self) -> &'static str {
|
fn get_pretty_name(&self) -> &'static str {
|
||||||
"Network"
|
"Network"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
|
) {
|
||||||
|
const CONSTRAINTS: [Constraint; 2] = [Constraint::Min(0), Constraint::Length(4)];
|
||||||
|
|
||||||
|
let split_area = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(CONSTRAINTS)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let graph_area = split_area[0];
|
||||||
|
let table_area = split_area[1];
|
||||||
|
|
||||||
|
self.net_graph.draw(painter, f, graph_area, selected);
|
||||||
|
|
||||||
|
let table_block = Block::default()
|
||||||
|
.border_style(if selected {
|
||||||
|
painter.colours.highlighted_border_style
|
||||||
|
} else {
|
||||||
|
painter.colours.border_style
|
||||||
|
})
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
self.table.draw_tui_table(
|
||||||
|
painter,
|
||||||
|
f,
|
||||||
|
&vec![vec![
|
||||||
|
(
|
||||||
|
self.net_graph.rx_display.clone().into(),
|
||||||
|
None,
|
||||||
|
Some(painter.colours.rx_style),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
self.net_graph.tx_display.clone().into(),
|
||||||
|
None,
|
||||||
|
Some(painter.colours.tx_style),
|
||||||
|
),
|
||||||
|
(self.net_graph.total_rx_display.clone().into(), None, None),
|
||||||
|
(self.net_graph.total_tx_display.clone().into(), None, None),
|
||||||
|
]],
|
||||||
|
table_block,
|
||||||
|
table_area,
|
||||||
|
selected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
|
let network_data = convert_network_data_points(
|
||||||
|
data_collection,
|
||||||
|
false, // TODO: I think the is_frozen here is also useless; see mem and cpu
|
||||||
|
true,
|
||||||
|
&self.net_graph.scale_type,
|
||||||
|
&self.net_graph.unit_type,
|
||||||
|
self.net_graph.use_binary_prefix,
|
||||||
|
);
|
||||||
|
self.net_graph.network_data_rx = network_data.rx;
|
||||||
|
self.net_graph.network_data_tx = network_data.tx;
|
||||||
|
self.net_graph.rx_display = network_data.rx_display;
|
||||||
|
self.net_graph.tx_display = network_data.tx_display;
|
||||||
|
if let Some(total_rx_display) = network_data.total_rx_display {
|
||||||
|
self.net_graph.total_rx_display = total_rx_display;
|
||||||
|
}
|
||||||
|
if let Some(total_tx_display) = network_data.total_tx_display {
|
||||||
|
self.net_graph.total_tx_display = total_tx_display;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,16 +14,17 @@ use crate::{
|
|||||||
app::{
|
app::{
|
||||||
event::{EventResult, MultiKey, MultiKeyResult},
|
event::{EventResult, MultiKey, MultiKeyResult},
|
||||||
query::*,
|
query::*,
|
||||||
|
DataCollection,
|
||||||
},
|
},
|
||||||
canvas::{DisplayableData, Painter},
|
canvas::Painter,
|
||||||
data_harvester::processes::{self, ProcessSorting},
|
data_harvester::processes::{self, ProcessSorting},
|
||||||
options::ProcessDefaults,
|
options::ProcessDefaults,
|
||||||
};
|
};
|
||||||
use ProcessSorting::*;
|
use ProcessSorting::*;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
|
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component,
|
||||||
SortableTextTable, TextInput, TextTable, Widget,
|
CursorDirection, ScrollDirection, SortableTextTable, TextInput, TextTable, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// AppSearchState deals with generic searching (I might do this in the future).
|
/// AppSearchState deals with generic searching (I might do this in the future).
|
||||||
@ -653,6 +654,8 @@ pub struct ProcessManager {
|
|||||||
show_search: bool,
|
show_search: bool,
|
||||||
|
|
||||||
search_modifiers: SearchModifiers,
|
search_modifiers: SearchModifiers,
|
||||||
|
|
||||||
|
display_data: TextTableData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessManager {
|
impl ProcessManager {
|
||||||
@ -671,6 +674,7 @@ impl ProcessManager {
|
|||||||
show_sort: false,
|
show_sort: false,
|
||||||
show_search: false,
|
show_search: false,
|
||||||
search_modifiers: SearchModifiers::default(),
|
search_modifiers: SearchModifiers::default(),
|
||||||
|
display_data: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.set_tree_mode(process_defaults.is_tree);
|
manager.set_tree_mode(process_defaults.is_tree);
|
||||||
@ -813,14 +817,29 @@ impl Component for ProcessManager {
|
|||||||
match &event.kind {
|
match &event.kind {
|
||||||
MouseEventKind::Down(MouseButton::Left) => {
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
if self.process_table.does_intersect_mouse(&event) {
|
if self.process_table.does_intersect_mouse(&event) {
|
||||||
self.selected = ProcessManagerSelection::Processes;
|
if let ProcessManagerSelection::Processes = self.selected {
|
||||||
self.process_table.handle_mouse_event(event)
|
self.process_table.handle_mouse_event(event)
|
||||||
|
} else {
|
||||||
|
self.selected = ProcessManagerSelection::Processes;
|
||||||
|
self.process_table.handle_mouse_event(event);
|
||||||
|
EventResult::Redraw
|
||||||
|
}
|
||||||
} else if self.sort_table.does_intersect_mouse(&event) {
|
} else if self.sort_table.does_intersect_mouse(&event) {
|
||||||
self.selected = ProcessManagerSelection::Sort;
|
if let ProcessManagerSelection::Sort = self.selected {
|
||||||
self.sort_table.handle_mouse_event(event)
|
self.sort_table.handle_mouse_event(event)
|
||||||
|
} else {
|
||||||
|
self.selected = ProcessManagerSelection::Sort;
|
||||||
|
self.sort_table.handle_mouse_event(event);
|
||||||
|
EventResult::Redraw
|
||||||
|
}
|
||||||
} else if self.search_input.does_intersect_mouse(&event) {
|
} else if self.search_input.does_intersect_mouse(&event) {
|
||||||
self.selected = ProcessManagerSelection::Search;
|
if let ProcessManagerSelection::Search = self.selected {
|
||||||
self.search_input.handle_mouse_event(event)
|
self.search_input.handle_mouse_event(event)
|
||||||
|
} else {
|
||||||
|
self.selected = ProcessManagerSelection::Search;
|
||||||
|
self.search_input.handle_mouse_event(event);
|
||||||
|
EventResult::Redraw
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
EventResult::NoRedraw
|
EventResult::NoRedraw
|
||||||
}
|
}
|
||||||
@ -841,8 +860,7 @@ impl Widget for ProcessManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw<B: Backend>(
|
fn draw<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
selected: bool,
|
|
||||||
) {
|
) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.border_style(if selected {
|
.border_style(if selected {
|
||||||
@ -852,24 +870,15 @@ impl Widget for ProcessManager {
|
|||||||
})
|
})
|
||||||
.borders(Borders::ALL);
|
.borders(Borders::ALL);
|
||||||
|
|
||||||
self.set_bounds(area);
|
self.process_table.table.draw_tui_table(
|
||||||
let draw_area = block.inner(area);
|
|
||||||
let (process_table, widths, mut tui_state) = self.process_table.table.create_draw_table(
|
|
||||||
painter,
|
painter,
|
||||||
&vec![], // TODO: Fix this
|
f,
|
||||||
draw_area,
|
&self.display_data, // TODO: Fix this
|
||||||
);
|
block,
|
||||||
|
|
||||||
let process_table = process_table.highlight_style(if selected {
|
|
||||||
painter.colours.currently_selected_text_style
|
|
||||||
} else {
|
|
||||||
painter.colours.text_style
|
|
||||||
});
|
|
||||||
|
|
||||||
f.render_stateful_widget(
|
|
||||||
process_table.block(block).widths(&widths),
|
|
||||||
area,
|
area,
|
||||||
&mut tui_state,
|
selected,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self, data_collection: &DataCollection) {}
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,18 @@ use tui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{event::EventResult, sort_text_table::SortableColumn},
|
app::{
|
||||||
canvas::{DisplayableData, Painter},
|
data_farmer::DataCollection, data_harvester::temperature::TemperatureType,
|
||||||
|
event::EventResult, sort_text_table::SortableColumn,
|
||||||
|
},
|
||||||
|
canvas::Painter,
|
||||||
|
data_conversion::convert_temp_row,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, SortableTextTable, Widget};
|
use super::{
|
||||||
|
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component,
|
||||||
|
SortableTextTable, Widget,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct TempWidgetState {
|
pub struct TempWidgetState {
|
||||||
pub scroll_state: AppScrollWidgetState,
|
pub scroll_state: AppScrollWidgetState,
|
||||||
@ -52,6 +59,8 @@ impl TempState {
|
|||||||
pub struct TempTable {
|
pub struct TempTable {
|
||||||
table: SortableTextTable,
|
table: SortableTextTable,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
|
display_data: TextTableData,
|
||||||
|
temp_type: TemperatureType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TempTable {
|
impl Default for TempTable {
|
||||||
@ -65,10 +74,20 @@ impl Default for TempTable {
|
|||||||
Self {
|
Self {
|
||||||
table,
|
table,
|
||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
|
display_data: Default::default(),
|
||||||
|
temp_type: TemperatureType::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TempTable {
|
||||||
|
/// Sets the [`TemperatureType`] for the [`TempTable`].
|
||||||
|
pub fn set_temp_type(mut self, temp_type: TemperatureType) -> Self {
|
||||||
|
self.temp_type = temp_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Component for TempTable {
|
impl Component for TempTable {
|
||||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||||
self.table.handle_key_event(event)
|
self.table.handle_key_event(event)
|
||||||
@ -93,8 +112,7 @@ impl Widget for TempTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw<B: Backend>(
|
fn draw<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, data: &DisplayableData,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
selected: bool,
|
|
||||||
) {
|
) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.border_style(if selected {
|
.border_style(if selected {
|
||||||
@ -104,19 +122,12 @@ impl Widget for TempTable {
|
|||||||
})
|
})
|
||||||
.borders(Borders::ALL); // TODO: Also do the scrolling indicator!
|
.borders(Borders::ALL); // TODO: Also do the scrolling indicator!
|
||||||
|
|
||||||
self.set_bounds(area);
|
|
||||||
let draw_area = block.inner(area);
|
|
||||||
let (table, widths, mut tui_state) =
|
|
||||||
self.table
|
self.table
|
||||||
.table
|
.table
|
||||||
.create_draw_table(painter, &data.temp_sensor_data, draw_area);
|
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
|
||||||
|
}
|
||||||
|
|
||||||
let table = table.highlight_style(if selected {
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
painter.colours.currently_selected_text_style
|
self.display_data = convert_temp_row(data_collection, &self.temp_type);
|
||||||
} else {
|
|
||||||
painter.colours.text_style
|
|
||||||
});
|
|
||||||
|
|
||||||
f.render_stateful_widget(table.block(block).widths(&widths), area, &mut tui_state);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
src/app/widgets/tui_widgets.rs
Normal file
2
src/app/widgets/tui_widgets.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod custom_legend_chart;
|
||||||
|
pub use custom_legend_chart::TimeChart;
|
596
src/app/widgets/tui_widgets/custom_legend_chart.rs
Normal file
596
src/app/widgets/tui_widgets/custom_legend_chart.rs
Normal file
@ -0,0 +1,596 @@
|
|||||||
|
use float_ord::FloatOrd;
|
||||||
|
use std::{borrow::Cow, cmp::max};
|
||||||
|
use tui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
symbols,
|
||||||
|
text::{Span, Spans},
|
||||||
|
widgets::{
|
||||||
|
canvas::{Canvas, Line, Points},
|
||||||
|
Block, Borders, GraphType, Widget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
/// An X or Y axis for the chart widget
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Axis<'a> {
|
||||||
|
/// Title displayed next to axis end
|
||||||
|
title: Option<Spans<'a>>,
|
||||||
|
/// Bounds for the axis (all data points outside these limits will not be represented)
|
||||||
|
bounds: [f64; 2],
|
||||||
|
/// A list of labels to put to the left or below the axis
|
||||||
|
labels: Option<Vec<Span<'a>>>,
|
||||||
|
/// The style used to draw the axis itself
|
||||||
|
style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Axis<'a> {
|
||||||
|
fn default() -> Axis<'a> {
|
||||||
|
Axis {
|
||||||
|
title: None,
|
||||||
|
bounds: [0.0, 0.0],
|
||||||
|
labels: None,
|
||||||
|
style: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<'a> Axis<'a> {
|
||||||
|
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
||||||
|
where
|
||||||
|
T: Into<Spans<'a>>,
|
||||||
|
{
|
||||||
|
self.title = Some(title.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
||||||
|
self.bounds = bounds;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
|
||||||
|
self.labels = Some(labels);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> Axis<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A group of data points
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Dataset<'a> {
|
||||||
|
/// Name of the dataset (used in the legend if shown)
|
||||||
|
name: Cow<'a, str>,
|
||||||
|
/// A reference to the actual data
|
||||||
|
data: &'a [(f64, f64)],
|
||||||
|
/// Symbol used for each points of this dataset
|
||||||
|
marker: symbols::Marker,
|
||||||
|
/// Determines graph type used for drawing points
|
||||||
|
graph_type: GraphType,
|
||||||
|
/// Style used to plot this dataset
|
||||||
|
style: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Default for Dataset<'a> {
|
||||||
|
fn default() -> Dataset<'a> {
|
||||||
|
Dataset {
|
||||||
|
name: Cow::from(""),
|
||||||
|
data: &[],
|
||||||
|
marker: symbols::Marker::Dot,
|
||||||
|
graph_type: GraphType::Scatter,
|
||||||
|
style: Style::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Dataset<'a> {
|
||||||
|
pub fn name<S>(mut self, name: S) -> Dataset<'a>
|
||||||
|
where
|
||||||
|
S: Into<Cow<'a, str>>,
|
||||||
|
{
|
||||||
|
self.name = name.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
|
||||||
|
self.data = data;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||||
|
self.marker = marker;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||||
|
self.graph_type = graph_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> Dataset<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||||
|
/// labels, legend, ...).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
struct ChartLayout {
|
||||||
|
/// Location of the title of the x axis
|
||||||
|
title_x: Option<(u16, u16)>,
|
||||||
|
/// Location of the title of the y axis
|
||||||
|
title_y: Option<(u16, u16)>,
|
||||||
|
/// Location of the first label of the x axis
|
||||||
|
label_x: Option<u16>,
|
||||||
|
/// Location of the first label of the y axis
|
||||||
|
label_y: Option<u16>,
|
||||||
|
/// Y coordinate of the horizontal axis
|
||||||
|
axis_x: Option<u16>,
|
||||||
|
/// X coordinate of the vertical axis
|
||||||
|
axis_y: Option<u16>,
|
||||||
|
/// Area of the legend
|
||||||
|
legend_area: Option<Rect>,
|
||||||
|
/// Area of the graph
|
||||||
|
graph_area: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChartLayout {
|
||||||
|
fn default() -> ChartLayout {
|
||||||
|
ChartLayout {
|
||||||
|
title_x: None,
|
||||||
|
title_y: None,
|
||||||
|
label_x: None,
|
||||||
|
label_y: None,
|
||||||
|
axis_x: None,
|
||||||
|
axis_y: None,
|
||||||
|
legend_area: None,
|
||||||
|
graph_area: Rect::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A "custom" chart, just a slightly tweaked [`tui::widgets::Chart`] from tui-rs, but with greater control over the
|
||||||
|
/// legend, and built with the idea of drawing data points relative to a time-based x-axis.
|
||||||
|
///
|
||||||
|
/// Main changes:
|
||||||
|
/// - Styling option for the legend box
|
||||||
|
/// - Automatically optimizing out redundant draws in the x-bounds.
|
||||||
|
/// - Automatic interpolation to points that fall *just* outside of the screen.
|
||||||
|
///
|
||||||
|
/// To add:
|
||||||
|
/// - Support for putting the legend on the left side.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TimeChart<'a> {
|
||||||
|
/// A block to display around the widget eventually
|
||||||
|
block: Option<Block<'a>>,
|
||||||
|
/// The horizontal axis
|
||||||
|
x_axis: Axis<'a>,
|
||||||
|
/// The vertical axis
|
||||||
|
y_axis: Axis<'a>,
|
||||||
|
/// A reference to the datasets
|
||||||
|
datasets: Vec<Dataset<'a>>,
|
||||||
|
/// 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
|
||||||
|
hidden_legend_constraints: (Constraint, Constraint),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TimeChart<'a> {
|
||||||
|
pub fn new(datasets: Vec<Dataset<'a>>) -> TimeChart<'a> {
|
||||||
|
TimeChart {
|
||||||
|
block: None,
|
||||||
|
x_axis: Axis::default(),
|
||||||
|
y_axis: Axis::default(),
|
||||||
|
style: Default::default(),
|
||||||
|
legend_style: Default::default(),
|
||||||
|
datasets,
|
||||||
|
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block(mut self, block: Block<'a>) -> TimeChart<'a> {
|
||||||
|
self.block = Some(block);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style(mut self, style: Style) -> TimeChart<'a> {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legend_style(mut self, legend_style: Style) -> TimeChart<'a> {
|
||||||
|
self.legend_style = legend_style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn x_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {
|
||||||
|
self.x_axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn y_axis(mut self, axis: Axis<'a>) -> TimeChart<'a> {
|
||||||
|
self.y_axis = axis;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the constraints used to determine whether the legend should be shown or not.
|
||||||
|
pub fn hidden_legend_constraints(
|
||||||
|
mut self, constraints: (Constraint, Constraint),
|
||||||
|
) -> TimeChart<'a> {
|
||||||
|
self.hidden_legend_constraints = constraints;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the internal layout of the chart given the area. If the area is too small some
|
||||||
|
/// elements may be automatically hidden
|
||||||
|
fn layout(&self, area: Rect) -> ChartLayout {
|
||||||
|
let mut layout = ChartLayout::default();
|
||||||
|
if area.height == 0 || area.width == 0 {
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
let mut x = area.left();
|
||||||
|
let mut y = area.bottom() - 1;
|
||||||
|
|
||||||
|
if self.x_axis.labels.is_some() && y > area.top() {
|
||||||
|
layout.label_x = Some(y);
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||||
|
x += self.max_width_of_labels_left_of_y_axis(area);
|
||||||
|
|
||||||
|
if self.x_axis.labels.is_some() && y > area.top() {
|
||||||
|
layout.axis_x = Some(y);
|
||||||
|
y -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.y_axis.labels.is_some() && x + 1 < area.right() {
|
||||||
|
layout.axis_y = Some(x);
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if x < area.right() && y > 1 {
|
||||||
|
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref title) = self.x_axis.title {
|
||||||
|
let w = title.width() as u16;
|
||||||
|
if w < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||||
|
layout.title_x = Some((x + layout.graph_area.width - w, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref title) = self.y_axis.title {
|
||||||
|
let w = title.width() as u16;
|
||||||
|
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||||
|
layout.title_y = Some((x, area.top()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
|
||||||
|
let legend_width = inner_width + 2;
|
||||||
|
let legend_height = self.datasets.len() as u16 + 2;
|
||||||
|
let max_legend_width = self
|
||||||
|
.hidden_legend_constraints
|
||||||
|
.0
|
||||||
|
.apply(layout.graph_area.width);
|
||||||
|
let max_legend_height = self
|
||||||
|
.hidden_legend_constraints
|
||||||
|
.1
|
||||||
|
.apply(layout.graph_area.height);
|
||||||
|
if inner_width > 0
|
||||||
|
&& legend_width < max_legend_width
|
||||||
|
&& legend_height < max_legend_height
|
||||||
|
{
|
||||||
|
layout.legend_area = Some(Rect::new(
|
||||||
|
layout.graph_area.right() - legend_width,
|
||||||
|
layout.graph_area.top(),
|
||||||
|
legend_width,
|
||||||
|
legend_height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_width_of_labels_left_of_y_axis(&self, area: Rect) -> u16 {
|
||||||
|
let mut max_width = self
|
||||||
|
.y_axis
|
||||||
|
.labels
|
||||||
|
.as_ref()
|
||||||
|
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Some(ref x_labels) = self.x_axis.labels {
|
||||||
|
if !x_labels.is_empty() {
|
||||||
|
max_width = max(max_width, x_labels[0].content.width() as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
||||||
|
max_width.min(area.width / 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_x_labels(
|
||||||
|
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
|
||||||
|
) {
|
||||||
|
let y = match layout.label_x {
|
||||||
|
Some(y) => y,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let labels = self.x_axis.labels.as_ref().unwrap();
|
||||||
|
let labels_len = labels.len() as u16;
|
||||||
|
if labels_len < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let width_between_ticks = graph_area.width / (labels_len - 1);
|
||||||
|
for (i, label) in labels.iter().enumerate() {
|
||||||
|
let label_width = label.width() as u16;
|
||||||
|
let label_width = if i == 0 {
|
||||||
|
// the first label is put between the left border of the chart and the y axis.
|
||||||
|
graph_area
|
||||||
|
.left()
|
||||||
|
.saturating_sub(chart_area.left())
|
||||||
|
.min(label_width)
|
||||||
|
} else {
|
||||||
|
// other labels are put on the left of each tick on the x axis
|
||||||
|
width_between_ticks.min(label_width)
|
||||||
|
};
|
||||||
|
buf.set_span(
|
||||||
|
graph_area.left() + i as u16 * width_between_ticks - label_width,
|
||||||
|
y,
|
||||||
|
label,
|
||||||
|
label_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_y_labels(
|
||||||
|
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
|
||||||
|
) {
|
||||||
|
let x = match layout.label_y {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
let labels = self.y_axis.labels.as_ref().unwrap();
|
||||||
|
let labels_len = labels.len() as u16;
|
||||||
|
let label_width = graph_area.left().saturating_sub(chart_area.left());
|
||||||
|
for (i, label) in labels.iter().enumerate() {
|
||||||
|
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
|
||||||
|
if dy < graph_area.bottom() {
|
||||||
|
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label_width as u16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for TimeChart<'a> {
|
||||||
|
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||||
|
/// Returns the y-axis value for a given `x`, given two points to draw a line between.
|
||||||
|
fn interpolate_point(older_point: &(f64, f64), newer_point: &(f64, f64), x: f64) -> f64 {
|
||||||
|
let delta_x = newer_point.0 - older_point.0;
|
||||||
|
let delta_y = newer_point.1 - older_point.1;
|
||||||
|
let slope = delta_y / delta_x;
|
||||||
|
|
||||||
|
(older_point.1 + (x - older_point.0) * slope).max(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if area.area() == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buf.set_style(area, self.style);
|
||||||
|
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||||
|
// the cells that are part of the components put on top of the graph area (i.e legend and
|
||||||
|
// axis names).
|
||||||
|
let original_style = buf.get(area.left(), area.top()).style();
|
||||||
|
|
||||||
|
let chart_area = match self.block.take() {
|
||||||
|
Some(b) => {
|
||||||
|
let inner_area = b.inner(area);
|
||||||
|
b.render(area, buf);
|
||||||
|
inner_area
|
||||||
|
}
|
||||||
|
None => area,
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = self.layout(chart_area);
|
||||||
|
let graph_area = layout.graph_area;
|
||||||
|
if graph_area.width < 1 || graph_area.height < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||||
|
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||||
|
|
||||||
|
if let Some(y) = layout.axis_x {
|
||||||
|
for x in graph_area.left()..graph_area.right() {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::HORIZONTAL)
|
||||||
|
.set_style(self.x_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = layout.axis_y {
|
||||||
|
for y in graph_area.top()..graph_area.bottom() {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::VERTICAL)
|
||||||
|
.set_style(self.y_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(y) = layout.axis_x {
|
||||||
|
if let Some(x) = layout.axis_y {
|
||||||
|
buf.get_mut(x, y)
|
||||||
|
.set_symbol(symbols::line::BOTTOM_LEFT)
|
||||||
|
.set_style(self.x_axis.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for dataset in &self.datasets {
|
||||||
|
Canvas::default()
|
||||||
|
.background_color(self.style.bg.unwrap_or(Color::Reset))
|
||||||
|
.x_bounds(self.x_axis.bounds)
|
||||||
|
.y_bounds(self.y_axis.bounds)
|
||||||
|
.marker(dataset.marker)
|
||||||
|
.paint(|ctx| {
|
||||||
|
let start_bound = FloatOrd(self.x_axis.bounds[0]);
|
||||||
|
let end_bound = FloatOrd(self.x_axis.bounds[1]);
|
||||||
|
let (start_index, interpolate_start) = match dataset
|
||||||
|
.data
|
||||||
|
.binary_search_by(|(x, _y)| FloatOrd(*x).cmp(&start_bound))
|
||||||
|
{
|
||||||
|
Ok(index) => (index, None),
|
||||||
|
Err(index) => (index, index.checked_sub(1)),
|
||||||
|
};
|
||||||
|
let (end_index, interpolate_end) = match dataset
|
||||||
|
.data
|
||||||
|
.binary_search_by(|(x, _y)| FloatOrd(*x).cmp(&end_bound))
|
||||||
|
{
|
||||||
|
Ok(index) => (index, None),
|
||||||
|
Err(index) => (
|
||||||
|
index,
|
||||||
|
if index + 1 < dataset.data.len() {
|
||||||
|
Some(index + 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
// debug!("Start: {}, inter: {:?}", start_index, interpolate_start);
|
||||||
|
// debug!("End: {}, inter: {:?}", end_index, interpolate_end);
|
||||||
|
|
||||||
|
let data_slice = &dataset.data[start_index..end_index];
|
||||||
|
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: data_slice,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(interpolate_start) = interpolate_start {
|
||||||
|
if let (Some(older_point), Some(newer_point)) = (
|
||||||
|
dataset.data.get(interpolate_start),
|
||||||
|
dataset.data.get(interpolate_start + 1),
|
||||||
|
) {
|
||||||
|
let interpolated_point = (
|
||||||
|
self.x_axis.bounds[0],
|
||||||
|
interpolate_point(
|
||||||
|
&older_point,
|
||||||
|
&newer_point,
|
||||||
|
self.x_axis.bounds[0],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: &[interpolated_point],
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: interpolated_point.0,
|
||||||
|
y1: interpolated_point.1,
|
||||||
|
x2: newer_point.0,
|
||||||
|
y2: newer_point.1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let GraphType::Line = dataset.graph_type {
|
||||||
|
for data in data_slice.windows(2) {
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: data[0].0,
|
||||||
|
y1: data[0].1,
|
||||||
|
x2: data[1].0,
|
||||||
|
y2: data[1].1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(interpolate_end) = interpolate_end {
|
||||||
|
if let (Some(older_point), Some(newer_point)) = (
|
||||||
|
dataset.data.get(interpolate_end - 1),
|
||||||
|
dataset.data.get(interpolate_end),
|
||||||
|
) {
|
||||||
|
let interpolated_point = (
|
||||||
|
self.x_axis.bounds[1],
|
||||||
|
interpolate_point(
|
||||||
|
&older_point,
|
||||||
|
&newer_point,
|
||||||
|
self.x_axis.bounds[1],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: &[interpolated_point],
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.draw(&Line {
|
||||||
|
x1: older_point.0,
|
||||||
|
y1: older_point.1,
|
||||||
|
x2: interpolated_point.0,
|
||||||
|
y2: interpolated_point.1,
|
||||||
|
color: dataset.style.fg.unwrap_or(Color::Reset),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.render(graph_area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(legend_area) = layout.legend_area {
|
||||||
|
buf.set_style(legend_area, original_style);
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(self.legend_style)
|
||||||
|
.render(legend_area, buf);
|
||||||
|
for (i, dataset) in self.datasets.iter().enumerate() {
|
||||||
|
buf.set_string(
|
||||||
|
legend_area.x + 1,
|
||||||
|
legend_area.y + 1 + i as u16,
|
||||||
|
&dataset.name,
|
||||||
|
dataset.style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = layout.title_x {
|
||||||
|
let title = self.x_axis.title.unwrap();
|
||||||
|
let width = graph_area.right().saturating_sub(x);
|
||||||
|
buf.set_style(
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
original_style,
|
||||||
|
);
|
||||||
|
buf.set_spans(x, y, &title, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((x, y)) = layout.title_y {
|
||||||
|
let title = self.y_axis.title.unwrap();
|
||||||
|
let width = graph_area.right().saturating_sub(x);
|
||||||
|
buf.set_style(
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
original_style,
|
||||||
|
);
|
||||||
|
buf.set_spans(x, y, &title, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
use std::{borrow::Cow, collections::HashMap, str::FromStr};
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
use fxhash::FxHashMap;
|
use fxhash::FxHashMap;
|
||||||
use indextree::{Arena, NodeId};
|
use indextree::{Arena, NodeId};
|
||||||
@ -16,7 +16,13 @@ use canvas_colours::*;
|
|||||||
use dialogs::*;
|
use dialogs::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{self, layout_manager::LayoutNode, widgets::Widget, TmpBottomWidget},
|
app::{
|
||||||
|
self,
|
||||||
|
layout_manager::LayoutNode,
|
||||||
|
text_table::TextTableData,
|
||||||
|
widgets::{Component, Widget},
|
||||||
|
TmpBottomWidget,
|
||||||
|
},
|
||||||
constants::*,
|
constants::*,
|
||||||
data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
|
data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
|
||||||
options::Config,
|
options::Config,
|
||||||
@ -41,16 +47,16 @@ pub struct DisplayableData {
|
|||||||
pub total_tx_display: String,
|
pub total_tx_display: String,
|
||||||
pub network_data_rx: Vec<Point>,
|
pub network_data_rx: Vec<Point>,
|
||||||
pub network_data_tx: Vec<Point>,
|
pub network_data_tx: Vec<Point>,
|
||||||
pub disk_data: Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>>,
|
pub disk_data: TextTableData,
|
||||||
pub temp_sensor_data: Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>>,
|
pub temp_sensor_data: TextTableData,
|
||||||
pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
|
pub single_process_data: HashMap<Pid, ConvertedProcessData>, // Contains single process data, key is PID
|
||||||
pub stringified_process_data_map: HashMap<NodeId, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
|
pub stringified_process_data_map: HashMap<NodeId, Vec<(Vec<(String, Option<String>)>, bool)>>, // Represents the row and whether it is disabled, key is the widget ID
|
||||||
|
|
||||||
pub mem_labels: Option<(String, String)>,
|
pub mem_labels: Option<(String, String)>,
|
||||||
pub swap_labels: Option<(String, String)>,
|
pub swap_labels: Option<(String, String)>,
|
||||||
|
pub mem_data: Vec<Point>,
|
||||||
pub mem_data: Vec<Point>, // TODO: Switch this and all data points over to a better data structure...
|
|
||||||
pub swap_data: Vec<Point>,
|
pub swap_data: Vec<Point>,
|
||||||
|
|
||||||
pub load_avg_data: [f32; 3],
|
pub load_avg_data: [f32; 3],
|
||||||
pub cpu_data: Vec<ConvertedCpuData>,
|
pub cpu_data: Vec<ConvertedCpuData>,
|
||||||
pub battery_data: Vec<ConvertedBatteryData>,
|
pub battery_data: Vec<ConvertedBatteryData>,
|
||||||
@ -336,12 +342,12 @@ impl Painter {
|
|||||||
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
let canvas_data = &app_state.canvas_data;
|
|
||||||
if let Some(current_widget) = app_state
|
if let Some(current_widget) = app_state
|
||||||
.widget_lookup_map
|
.widget_lookup_map
|
||||||
.get_mut(&app_state.selected_widget)
|
.get_mut(&app_state.selected_widget)
|
||||||
{
|
{
|
||||||
current_widget.draw(self, f, draw_area, canvas_data, true);
|
current_widget.set_bounds(draw_area);
|
||||||
|
current_widget.draw(self, f, draw_area, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/// A simple traversal through the `arena`.
|
/// A simple traversal through the `arena`.
|
||||||
@ -392,7 +398,8 @@ impl Painter {
|
|||||||
}
|
}
|
||||||
LayoutNode::Widget => {
|
LayoutNode::Widget => {
|
||||||
if let Some(widget) = lookup_map.get_mut(&node) {
|
if let Some(widget) = lookup_map.get_mut(&node) {
|
||||||
widget.draw(painter, f, area, canvas_data, selected_id == node);
|
widget.set_bounds(area);
|
||||||
|
widget.draw(painter, f, area, selected_id == node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
//! This mainly concerns converting collected data into things that the canvas
|
//! This mainly concerns converting collected data into things that the canvas
|
||||||
//! can actually handle.
|
//! can actually handle.
|
||||||
|
use crate::app::data_harvester::temperature::TemperatureType;
|
||||||
|
use crate::app::text_table::TextTableData;
|
||||||
|
use crate::app::DataCollection;
|
||||||
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{data_farmer, data_harvester, AppState, ProcWidgetState},
|
app::{data_harvester, ProcWidgetState},
|
||||||
utils::{self, gen_util::*},
|
utils::{self, gen_util::*},
|
||||||
};
|
};
|
||||||
use data_harvester::processes::ProcessSorting;
|
use data_harvester::processes::ProcessSorting;
|
||||||
@ -85,15 +88,12 @@ pub struct ConvertedCpuData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_temp_row(
|
pub fn convert_temp_row(
|
||||||
app: &AppState,
|
current_data: &DataCollection, temp_type: &TemperatureType,
|
||||||
) -> Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>> {
|
) -> TextTableData {
|
||||||
let current_data = &app.data_collection;
|
|
||||||
let temp_type = &app.app_config_fields.temperature_type;
|
|
||||||
|
|
||||||
if current_data.temp_harvest.is_empty() {
|
if current_data.temp_harvest.is_empty() {
|
||||||
vec![vec![
|
vec![vec![
|
||||||
("No Sensors Found".into(), Some("N/A".into())),
|
("No Sensors Found".into(), Some("N/A".into()), None),
|
||||||
("".into(), None),
|
("".into(), None, None),
|
||||||
]]
|
]]
|
||||||
} else {
|
} else {
|
||||||
let (unit_long, unit_short) = match temp_type {
|
let (unit_long, unit_short) = match temp_type {
|
||||||
@ -108,10 +108,11 @@ pub fn convert_temp_row(
|
|||||||
.map(|temp_harvest| {
|
.map(|temp_harvest| {
|
||||||
let val = temp_harvest.temperature.ceil().to_string();
|
let val = temp_harvest.temperature.ceil().to_string();
|
||||||
vec![
|
vec![
|
||||||
(temp_harvest.name.clone().into(), None),
|
(temp_harvest.name.clone().into(), None, None),
|
||||||
(
|
(
|
||||||
format!("{}{}", val, unit_long).into(),
|
format!("{}{}", val, unit_long).into(),
|
||||||
Some(format!("{}{}", val, unit_short).into()),
|
Some(format!("{}{}", val, unit_short).into()),
|
||||||
|
None,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@ -119,13 +120,11 @@ pub fn convert_temp_row(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_disk_row(
|
pub fn convert_disk_row(current_data: &DataCollection) -> TextTableData {
|
||||||
current_data: &data_farmer::DataCollection,
|
|
||||||
) -> Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>)>> {
|
|
||||||
if current_data.disk_harvest.is_empty() {
|
if current_data.disk_harvest.is_empty() {
|
||||||
vec![vec![
|
vec![vec![
|
||||||
("No Disks Found".into(), Some("N/A".into())),
|
("No Disks Found".into(), Some("N/A".into()), None),
|
||||||
("".into(), None),
|
("".into(), None, None),
|
||||||
]]
|
]]
|
||||||
} else {
|
} else {
|
||||||
current_data
|
current_data
|
||||||
@ -164,13 +163,13 @@ pub fn convert_disk_row(
|
|||||||
};
|
};
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
(disk.name.clone().into(), None),
|
(disk.name.clone().into(), None, None),
|
||||||
(disk.mount_point.clone().into(), None),
|
(disk.mount_point.clone().into(), None, None),
|
||||||
(usage_fmt, None),
|
(usage_fmt, None, None),
|
||||||
(free_space_fmt, None),
|
(free_space_fmt, None, None),
|
||||||
(total_space_fmt, None),
|
(total_space_fmt, None, None),
|
||||||
(io_read.clone().into(), None),
|
(io_read.clone().into(), None, None),
|
||||||
(io_write.clone().into(), None),
|
(io_write.clone().into(), None, None),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
@ -178,8 +177,7 @@ pub fn convert_disk_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_cpu_data_points(
|
pub fn convert_cpu_data_points(
|
||||||
current_data: &data_farmer::DataCollection, existing_cpu_data: &mut Vec<ConvertedCpuData>,
|
current_data: &DataCollection, existing_cpu_data: &mut Vec<ConvertedCpuData>, is_frozen: bool,
|
||||||
is_frozen: bool,
|
|
||||||
) {
|
) {
|
||||||
let current_time = if is_frozen {
|
let current_time = if is_frozen {
|
||||||
if let Some(frozen_instant) = current_data.frozen_instant {
|
if let Some(frozen_instant) = current_data.frozen_instant {
|
||||||
@ -257,9 +255,7 @@ pub fn convert_cpu_data_points(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_mem_data_points(
|
pub fn convert_mem_data_points(current_data: &DataCollection, is_frozen: bool) -> Vec<Point> {
|
||||||
current_data: &data_farmer::DataCollection, is_frozen: bool,
|
|
||||||
) -> Vec<Point> {
|
|
||||||
let mut result: Vec<Point> = Vec::new();
|
let mut result: Vec<Point> = Vec::new();
|
||||||
let current_time = if is_frozen {
|
let current_time = if is_frozen {
|
||||||
if let Some(frozen_instant) = current_data.frozen_instant {
|
if let Some(frozen_instant) = current_data.frozen_instant {
|
||||||
@ -285,9 +281,7 @@ pub fn convert_mem_data_points(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_swap_data_points(
|
pub fn convert_swap_data_points(current_data: &DataCollection, is_frozen: bool) -> Vec<Point> {
|
||||||
current_data: &data_farmer::DataCollection, is_frozen: bool,
|
|
||||||
) -> Vec<Point> {
|
|
||||||
let mut result: Vec<Point> = Vec::new();
|
let mut result: Vec<Point> = Vec::new();
|
||||||
let current_time = if is_frozen {
|
let current_time = if is_frozen {
|
||||||
if let Some(frozen_instant) = current_data.frozen_instant {
|
if let Some(frozen_instant) = current_data.frozen_instant {
|
||||||
@ -314,7 +308,7 @@ pub fn convert_swap_data_points(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_mem_labels(
|
pub fn convert_mem_labels(
|
||||||
current_data: &data_farmer::DataCollection,
|
current_data: &DataCollection,
|
||||||
) -> (Option<(String, String)>, Option<(String, String)>) {
|
) -> (Option<(String, String)>, Option<(String, String)>) {
|
||||||
/// Returns the unit type and denominator for given total amount of memory in kibibytes.
|
/// Returns the unit type and denominator for given total amount of memory in kibibytes.
|
||||||
fn return_unit_and_denominator_for_mem_kib(mem_total_kib: u64) -> (&'static str, f64) {
|
fn return_unit_and_denominator_for_mem_kib(mem_total_kib: u64) -> (&'static str, f64) {
|
||||||
@ -333,6 +327,7 @@ pub fn convert_mem_labels(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Should probably make this only return none if no data is left/visible?
|
||||||
(
|
(
|
||||||
if current_data.memory_harvest.mem_total_in_kib > 0 {
|
if current_data.memory_harvest.mem_total_in_kib > 0 {
|
||||||
Some((
|
Some((
|
||||||
@ -384,7 +379,7 @@ pub fn convert_mem_labels(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rx_tx_data_points(
|
pub fn get_rx_tx_data_points(
|
||||||
current_data: &data_farmer::DataCollection, is_frozen: bool, network_scale_type: &AxisScaling,
|
current_data: &DataCollection, is_frozen: bool, network_scale_type: &AxisScaling,
|
||||||
network_unit_type: &DataUnit, network_use_binary_prefix: bool,
|
network_unit_type: &DataUnit, network_use_binary_prefix: bool,
|
||||||
) -> (Vec<Point>, Vec<Point>) {
|
) -> (Vec<Point>, Vec<Point>) {
|
||||||
let mut rx: Vec<Point> = Vec::new();
|
let mut rx: Vec<Point> = Vec::new();
|
||||||
@ -439,7 +434,7 @@ pub fn get_rx_tx_data_points(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_network_data_points(
|
pub fn convert_network_data_points(
|
||||||
current_data: &data_farmer::DataCollection, is_frozen: bool, need_four_points: bool,
|
current_data: &DataCollection, is_frozen: bool, need_four_points: bool,
|
||||||
network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
|
network_scale_type: &AxisScaling, network_unit_type: &DataUnit,
|
||||||
network_use_binary_prefix: bool,
|
network_use_binary_prefix: bool,
|
||||||
) -> ConvertedNetworkData {
|
) -> ConvertedNetworkData {
|
||||||
@ -620,7 +615,7 @@ fn get_disk_io_strings(
|
|||||||
/// Because we needed to UPDATE data entries rather than REPLACING entries, we instead update
|
/// Because we needed to UPDATE data entries rather than REPLACING entries, we instead update
|
||||||
/// the existing vector.
|
/// the existing vector.
|
||||||
pub fn convert_process_data(
|
pub fn convert_process_data(
|
||||||
current_data: &data_farmer::DataCollection,
|
current_data: &DataCollection,
|
||||||
existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
|
existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
|
||||||
#[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable,
|
#[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable,
|
||||||
) {
|
) {
|
||||||
@ -1379,9 +1374,7 @@ pub fn group_process_data(
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_battery_harvest(
|
pub fn convert_battery_harvest(current_data: &DataCollection) -> Vec<ConvertedBatteryData> {
|
||||||
current_data: &data_farmer::DataCollection,
|
|
||||||
) -> Vec<ConvertedBatteryData> {
|
|
||||||
current_data
|
current_data
|
||||||
.battery_harvest
|
.battery_harvest
|
||||||
.iter()
|
.iter()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user