summaryrefslogtreecommitdiffstats
path: root/src/app/widgets/bottom_widgets
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/widgets/bottom_widgets')
-rw-r--r--src/app/widgets/bottom_widgets/basic_cpu.rs205
-rw-r--r--src/app/widgets/bottom_widgets/basic_mem.rs164
-rw-r--r--src/app/widgets/bottom_widgets/basic_net.rs134
-rw-r--r--src/app/widgets/bottom_widgets/battery.rs121
-rw-r--r--src/app/widgets/bottom_widgets/carousel.rs209
-rw-r--r--src/app/widgets/bottom_widgets/cpu.rs333
-rw-r--r--src/app/widgets/bottom_widgets/disk.rs162
-rw-r--r--src/app/widgets/bottom_widgets/empty.rs58
-rw-r--r--src/app/widgets/bottom_widgets/mem.rs179
-rw-r--r--src/app/widgets/bottom_widgets/net.rs782
-rw-r--r--src/app/widgets/bottom_widgets/process.rs1529
-rw-r--r--src/app/widgets/bottom_widgets/temp.rs164
12 files changed, 4040 insertions, 0 deletions
diff --git a/src/app/widgets/bottom_widgets/basic_cpu.rs b/src/app/widgets/bottom_widgets/basic_cpu.rs
new file mode 100644
index 00000000..5b862367
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/basic_cpu.rs
@@ -0,0 +1,205 @@
+use std::cmp::max;
+
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Direction, Layout, Rect},
+ widgets::Block,
+ Frame,
+};
+
+use crate::{
+ app::{widgets::tui_widgets::PipeGauge, AppConfigFields, Component, DataCollection, Widget},
+ canvas::Painter,
+ constants::SIDE_BORDERS,
+ options::layout_options::LayoutRule,
+};
+
+const REQUIRED_COLUMNS: usize = 4;
+
+#[derive(Debug)]
+pub struct BasicCpu {
+ bounds: Rect,
+ display_data: Vec<(f64, String, String)>,
+ width: LayoutRule,
+ showing_avg: bool,
+}
+
+impl BasicCpu {
+ /// Creates a new [`BasicCpu`] given a [`AppConfigFields`].
+ pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
+ Self {
+ bounds: Default::default(),
+ display_data: Default::default(),
+ width: Default::default(),
+ showing_avg: app_config_fields.show_average_cpu,
+ }
+ }
+
+ /// Sets the width.
+ pub fn width(mut self, width: LayoutRule) -> Self {
+ self.width = width;
+ self
+ }
+}
+
+impl Component for BasicCpu {
+ fn bounds(&self) -> Rect {
+ self.bounds
+ }
+
+ fn set_bounds(&mut self, new_bounds: Rect) {
+ self.bounds = new_bounds;
+ }
+}
+
+impl Widget for BasicCpu {
+ fn get_pretty_name(&self) -> &'static str {
+ "CPU"
+ }
+
+ fn draw<B: Backend>(
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
+ ) {
+ const CONSTRAINTS: [Constraint; 2 * REQUIRED_COLUMNS - 1] = [
+ Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
+ Constraint::Length(2),
+ Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
+ Constraint::Length(2),
+ Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
+ Constraint::Length(2),
+ Constraint::Ratio(1, REQUIRED_COLUMNS as u32),
+ ];
+ let block = Block::default()
+ .borders(*SIDE_BORDERS)
+ .border_style(painter.colours.highlighted_border_style);
+ let inner_area = block.inner(area);
+ let split_area = Layout::default()
+ .direction(Direction::Horizontal)
+ .constraints(CONSTRAINTS)
+ .split(inner_area)
+ .into_iter()
+ .enumerate()
+ .filter_map(
+ |(index, rect)| {
+ if index % 2 == 0 {
+ Some(rect)
+ } else {
+ None
+ }
+ },
+ );
+
+ let display_data_len = self.display_data.len();
+ let length = display_data_len / REQUIRED_COLUMNS;
+ let largest_height = max(
+ 1,
+ length
+ + (if display_data_len % REQUIRED_COLUMNS == 0 {
+ 0
+ } else {
+ 1
+ }),
+ );
+ let mut leftover = display_data_len % REQUIRED_COLUMNS;
+ let column_heights = (0..REQUIRED_COLUMNS).map(|_| {
+ if leftover > 0 {
+ leftover -= 1;
+ length + 1
+ } else {
+ length
+ }
+ });
+
+ if selected {
+ f.render_widget(block, area);
+ }
+
+ let mut index_offset = 0;
+ split_area
+ .into_iter()
+ .zip(column_heights)
+ .for_each(|(area, height)| {
+ let column_areas = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints(vec![Constraint::Length(1); largest_height])
+ .split(area);
+
+ let num_entries = if index_offset + height < display_data_len {
+ height
+ } else {
+ display_data_len - index_offset
+ };
+ let end = index_offset + num_entries;
+
+ self.display_data[index_offset..end]
+ .iter()
+ .zip(column_areas)
+ .enumerate()
+ .for_each(|(column_index, ((percent, label, usage_label), area))| {
+ let cpu_index = index_offset + column_index;
+ let style = if cpu_index == 0 {
+ painter.colours.avg_colour_style
+ } else {
+ let cpu_style_index = if self.showing_avg {
+ cpu_index - 1
+ } else {
+ cpu_index
+ };
+ painter.colours.cpu_colour_styles
+ [cpu_style_index % painter.colours.cpu_colour_styles.len()]
+ };
+
+ f.render_widget(
+ PipeGauge::default()
+ .ratio(*percent)
+ .style(style)
+ .gauge_style(style)
+ .start_label(label.clone())
+ .end_label(usage_label.clone()),
+ area,
+ );
+ });
+
+ index_offset = end;
+ });
+ }
+
+ fn update_data(&mut self, data_collection: &DataCollection) {
+ self.display_data = data_collection
+ .cpu_harvest
+ .iter()
+ .map(|data| {
+ (
+ data.cpu_usage / 100.0,
+ format!(
+ "{:3}",
+ data.cpu_count
+ .map(|c| c.to_string())
+ .unwrap_or(data.cpu_prefix.clone())
+ ),
+ format!("{:3.0}%", data.cpu_usage.round()),
+ )
+ })
+ .collect::<Vec<_>>();
+
+ }
+
+ fn width(&self) -> LayoutRule {
+ self.width
+ }
+
+ fn height(&self) -> LayoutRule {
+ let display_data_len = self.display_data.len();
+ let length = max(
+ 1,
+ (display_data_len / REQUIRED_COLUMNS) as u16
+ + (if display_data_len % REQUIRED_COLUMNS == 0 {
+ 0
+ } else {
+ 1
+ }),
+ );
+
+ LayoutRule::Length { length }
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/basic_mem.rs b/src/app/widgets/bottom_widgets/basic_mem.rs
new file mode 100644
index 00000000..7fbd1fd0
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/basic_mem.rs
@@ -0,0 +1,164 @@
+use crossterm::event::{KeyCode, KeyEvent};
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Layout, Rect},
+ widgets::Block,
+ Frame,
+};
+
+use crate::{
+ app::{
+ event::WidgetEventResult, widgets::tui_widgets::PipeGauge, Component, DataCollection,
+ Widget,
+ },
+ canvas::Painter,
+ constants::SIDE_BORDERS,
+ data_conversion::{convert_mem_data_points, convert_mem_labels, convert_swap_data_points},
+ options::layout_options::LayoutRule,
+};
+
+#[derive(Debug)]
+pub struct BasicMem {
+ bounds: Rect,
+ width: LayoutRule,
+ mem_data: (f64, String, String),
+ swap_data: Option<(f64, String, String)>,
+ use_percent: bool,
+}
+
+impl Default for BasicMem {
+ fn default() -> Self {
+ Self {
+ bounds: Default::default(),
+ width: Default::default(),
+ mem_data: (0.0, "0.0B/0.0B".to_string(), "0%".to_string()),
+ swap_data: None,
+ use_percent: false,
+ }
+ }
+}
+
+impl BasicMem {
+ /// Sets the width.
+ pub fn width(mut self, width: LayoutRule) -> Self {
+ self.width = width;
+ self
+ }
+}
+
+impl Component for BasicMem {
+ fn bounds(&self) -> Rect {
+ self.bounds
+ }
+
+ fn set_bounds(&mut self, new_bounds: Rect) {
+ self.bounds = new_bounds;
+ }
+
+ fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
+ match event.code {
+ KeyCode::Char('%') if event.modifiers.is_empty() => {
+ self.use_percent = !self.use_percent;
+ WidgetEventResult::Redraw
+ }
+ _ => WidgetEventResult::NoRedraw,
+ }
+ }
+}
+
+impl Widget for BasicMem {
+ fn get_pretty_name(&self) -> &'static str {
+ "Memory"
+ }
+
+ fn draw<B: Backend>(
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
+ ) {
+ let block = Block::default()
+ .borders(*SIDE_BORDERS)
+ .border_style(painter.colours.highlighted_border_style);
+ let inner_area = block.inner(area);
+ const CONSTRAINTS: [Constraint; 2] = [Constraint::Ratio(1, 2); 2];
+ let split_area = Layout::default()
+ .direction(tui::layout::Direction::Vertical)
+ .constraints(CONSTRAINTS)
+ .split(inner_area);
+
+ if selected {
+ f.render_widget(block, area);
+ }
+
+ let mut use_percentage =
+ self.use_percent || (split_area[0].width as usize) < self.mem_data.1.len() + 7;
+
+ if let Some(swap_data) = &self.swap_data {
+ use_percentage =
+ use_percentage || (split_area[1].width as usize) < swap_data.1.len() + 7;
+
+ f.render_widget(
+ PipeGauge::default()
+ .ratio(swap_data.0)
+ .style(painter.colours.swap_style)
+ .gauge_style(painter.colours.swap_style)
+ .start_label("SWP")
+ .end_label(if use_percentage {
+ swap_data.2.clone()
+ } else {
+ swap_data.1.clone()
+ }),
+ split_area[1],
+ );
+ }
+ f.render_widget(
+ PipeGauge::default()
+ .ratio(self.mem_data.0)
+ .style(painter.colours.ram_style)
+ .gauge_style(painter.colours.ram_style)
+ .start_label("RAM")
+ .end_label(if use_percentage {
+ self.mem_data.2.clone()
+ } else {
+ self.mem_data.1.clone()
+ }),
+ split_area[0],
+ );
+ }
+
+ fn update_data(&mut self, data_collection: &DataCollection) {
+ let (memory_labels, swap_labels) = convert_mem_labels(data_collection);
+
+ // TODO: [Data update optimization] Probably should just make another function altogether for basic mode.
+ self.mem_data = if let (Some(data), Some((_, fraction))) = (
+ convert_mem_data_points(data_collection, false).last(),
+ memory_labels,
+ ) {
+ (
+ data.1 / 100.0,
+ fraction.trim().to_string(),
+ format!("{:3.0}%", data.1.round()),
+ )
+ } else {
+ (0.0, "0.0B/0.0B".to_string(), "0%".to_string())
+ };
+ self.swap_data = if let (Some(data), Some((_, fraction))) = (
+ convert_swap_data_points(data_collection, false).last(),
+ swap_labels,
+ ) {
+ Some((
+ data.1 / 100.0,
+ fraction.trim().to_string(),
+ format!("{:3.0}%", data.1.round()),
+ ))
+ } else {
+ None
+ };
+ }
+
+ fn width(&self) -> LayoutRule {
+ self.width
+ }
+
+ fn height(&self) -> LayoutRule {
+ LayoutRule::Length { length: 2 }
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/basic_net.rs b/src/app/widgets/bottom_widgets/basic_net.rs
new file mode 100644
index 00000000..60b0c44b
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/basic_net.rs
@@ -0,0 +1,134 @@
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Layout, Rect},
+ text::{Span, Spans},
+ widgets::{Block, Paragraph},
+ Frame,
+};
+
+use crate::{
+ app::{AppConfigFields, AxisScaling, Component, DataCollection, Widget},
+ canvas::Painter,
+ constants::SIDE_BORDERS,
+ data_conversion::convert_network_data_points,
+ options::layout_options::LayoutRule,
+ units::data_units::DataUnit,
+};
+
+#[derive(Debug)]
+pub struct BasicNet {
+ bounds: Rect,
+ width: LayoutRule,
+
+ rx_display: String,
+ tx_display: String,
+ total_rx_display: String,
+ total_tx_display: String,
+
+ pub unit_type: DataUnit,
+ pub use_binary_prefix: bool,
+}
+
+impl BasicNet {
+ /// Creates a new [`BasicNet`] given a [`AppConfigFields`].
+ pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
+ Self {
+ bounds: Default::default(),
+ width: Default::default(),
+ rx_display: "RX: 0b/s".to_string(),
+ tx_display: "TX: 0b/s".to_string(),
+ total_rx_display: "Total RX: 0B".to_string(),
+ total_tx_display: "Total TX: 0B".to_string(),
+ unit_type: app_config_fields.network_unit_type.clone(),
+ use_binary_prefix: app_config_fields.network_use_binary_prefix,
+ }
+ }
+
+ /// Sets the width.
+ pub fn width(mut self, width: LayoutRule) -> Self {
+ self.width = width;
+ self
+ }
+}
+
+impl Component for BasicNet {
+ fn bounds(&self) -> Rect {
+ self.bounds
+ }
+
+ fn set_bounds(&mut self, new_bounds: Rect) {
+ self.bounds = new_bounds;
+ }
+}
+
+impl Widget for BasicNet {
+ fn get_pretty_name(&self) -> &'static str {
+ "Network"
+ }
+
+ fn draw<B: Backend>(
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
+ ) {
+ let block = Block::default()
+ .borders(*SIDE_BORDERS)
+ .border_style(painter.colours.highlighted_border_style);
+
+ let inner_area = block.inner(area);
+ const CONSTRAINTS: [Constraint; 2] = [Constraint::Ratio(1, 2); 2];
+ let split_area = Layout::default()
+ .direction(tui::layout::Direction::Horizontal)
+ .constraints(CONSTRAINTS)
+ .split(inner_area);
+ let texts = [
+ [
+ Spans::from(Span::styled(&self.rx_display, painter.colours.rx_style)),
+ Spans::from(Span::styled(&self.tx_display, painter.colours.tx_style)),
+ ],
+ [
+ Spans::from(Span::styled(
+ &self.total_rx_display,
+ painter.colours.total_rx_style,
+ )),
+ Spans::from(Span::styled(
+ &self.total_tx_display,
+ painter.colours.total_tx_style,
+ )),
+ ],
+ ];
+
+ if selected {
+ f.render_widget(block, area);
+ }
+
+ IntoIterator::into_iter(texts)
+ .zip(split_area)
+ .for_each(|(text, area)| f.render_widget(Paragraph::new(text.to_vec()), 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
+ true,
+ &AxisScaling::Linear,
+ &self.unit_type,
+ self.use_binary_prefix,
+ );
+ self.rx_display = format!("RX: {}", network_data.rx_display);
+ self.tx_display = format!("TX: {}", network_data.tx_display);
+ if let Some(total_rx_display) = network_data.total_rx_display {
+ self.total_rx_display = format!("Total RX: {}", total_rx_display);
+ }
+ if let Some(total_tx_display) = network_data.total_tx_display {
+ self.total_tx_display = format!("Total TX: {}", total_tx_display);
+ }
+ }
+
+ fn width(&self) -> LayoutRule {
+ self.width
+ }
+
+ fn height(&self) -> LayoutRule {
+ LayoutRule::Length { length: 2 }
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/battery.rs b/src/app/widgets/bottom_widgets/battery.rs
new file mode 100644
index 00000000..2680a8d7
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/battery.rs
@@ -0,0 +1,121 @@
+use std::collections::HashMap;
+
+use tui::{layout::Rect, widgets::Borders};
+
+use crate::{
+ app::{data_farmer::DataCollection, Component, Widget},
+ data_conversion::{convert_battery_harvest, ConvertedBatteryData},
+ options::layout_options::LayoutRule,
+};
+
+#[derive(Default)]
+pub struct BatteryWidgetState {
+ pub currently_selected_battery_index: usize,
+ pub tab_click_locs: Option<Vec<((u16, u16), (u16, u16))>>,
+}
+
+#[derive(Default)]
+pub struct BatteryState {
+ pub widget_states: HashMap<u64, BatteryWidgetState>,
+}
+
+impl BatteryState {
+ pub fn init(widget_states: HashMap<u64, BatteryWidgetState>) -> Self {
+ BatteryState { widget_states }
+ }
+
+ pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut BatteryWidgetState> {
+ self.widget_states.get_mut(&widget_id)
+ }
+
+ pub fn get_widget_state(&self, widget_id: u64) -> Option<&BatteryWidgetState> {
+ self.widget_states.get(&widget_id)
+ }
+}
+
+// TODO: Implement battery widget.
+/// A table displaying battery information on a per-battery basis.
+pub struct BatteryTable {
+ bounds: Rect,
+ selected_index: usize,
+ batteries: Vec<String>,
+ battery_data: Vec<ConvertedBatteryData>,
+ width: LayoutRule,
+ height: LayoutRule,
+ block_border: Borders,
+}
+
+impl Default for BatteryTable {
+ fn default() -> Self {
+ Self {
+ batteries: vec![],
+ bounds: Default::default(),
+ selected_index: 0,
+ battery_data: Default::default(),
+ width: LayoutRule::default(),
+ height: LayoutRule::default(),
+ block_border: Borders::ALL,
+ }
+ }
+}
+
+impl BatteryTable {
+ /// Sets the width.
+ pub fn width(mut self, width: LayoutRule) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height.
+ pub fn height(mut self, height: LayoutRule) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Returns the index of the currently selected battery.
+ pub fn index(&self) -> usize {
+ self.selected_index
+ }
+
+ /// Returns a reference to the battery names.
+ pub fn batteries(&self) -> &[String] {
+ &self.batteries
+ }
+
+ /// Sets the block border style.
+ pub fn basic_mode(mut self, basic_mode: bool) -> Self {
+ if basic_mode {
+ self.block_border = *crate::constants::SIDE_BORDERS;
+ }
+
+ self
+ }
+}
+
+impl Component for BatteryTable {
+ fn bounds(&self) -> tui::layout::Rect {
+ self.bounds
+ }
+
+ fn set_bounds(&mut self, new_bounds: tui::layout::Rect) {
+ self.bounds = new_bounds;
+ }
+}
+
+impl Widget for BatteryTable {
+ fn get_pretty_name(&self) -> &'static str {
+ "Battery"
+ }
+
+ fn update_data(&mut self, data_collection: &DataCollection) {
+ self.battery_data = convert_battery_harvest(data_collection);
+ }
+
+ fn width(&self) -> LayoutRule {
+ self.width
+ }
+
+ fn height(&self) -> LayoutRule {
+ self.height
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/carousel.rs b/src/app/widgets/bottom_widgets/carousel.rs
new file mode 100644
index 00000000..0f91c212
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/carousel.rs
@@ -0,0 +1,209 @@
+use std::borrow::Cow;
+
+use crossterm::event::MouseEvent;
+use indextree::NodeId;
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Layout, Rect},
+ text::{Span, Spans},
+ widgets::Paragraph,
+ Frame,
+};
+
+use crate::{
+ app::{
+ does_bound_intersect_coordinate, event::WidgetEventResult, Component, SelectableType,
+ Widget,
+ },
+ canvas::Painter,
+ options::layout_options::LayoutRule,
+};
+
+/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s.
+#[derive(PartialEq, Eq)]
+pub struct Carousel {
+ index: usize,
+ children: Vec<(NodeId, Cow<'static, str>)>,
+ bounds: Rect,
+ width: LayoutRule,
+ height: LayoutRule,
+ left_button_bounds: Rect,
+ right_button_bounds: Rect,
+}
+
+impl Carousel {
+ /// Creates a new [`Carousel`] with the specified children.
+ pub fn new(children: Vec<(NodeId, Cow<'static, str>)>) -> Self {
+ Self {
+ index: 0,
+ children,
+ bounds: Default::default(),
+ width: Default::default(),
+ height: Default::default(),
+ left_button_bounds: Default::default(),
+ right_button_bounds: Default::default(),
+ }
+ }
+
+ /// Sets the width.
+ pub fn width(mut self, width: LayoutRule) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height.
+ pub fn height(mut self, height: LayoutRule) -> Self {
+ self.height = height;
+ self
+ }
+
+ /// Adds a new child to a [`Carousel`].
+ pub fn add_child(&mut self, child: NodeId, name: Cow<'static, str>) {
+ self.children.push((child, name));
+ }
+
+ /// Returns the currently selected [`NodeId`] if possible.
+ pub fn get_currently_selected(&self) -> Option<NodeId> {
+ self.children.get(self.index).map(|i| i.0.clone())
+ }
+
+ fn get_next(&self) -> Option<&(NodeId, Cow<'static, str>)> {
+ self.children.get(if self.index + 1 == self.children.len() {
+ 0
+ } else {
+ self.index + 1
+ })
+ }
+
+ fn get_prev(&self) -> Option<&(NodeId, Cow<'static, str>)> {
+ self.children.get(if self.index > 0 {
+ self.index - 1
+ } else {
+ self.children.len().saturating_sub(1)
+ })
+ }
+
+ fn increment_index(&mut self) {
+ if self.index + 1 == self.children.len() {
+ self.index = 0;
+ } else {
+ self.index += 1;
+ }
+ }
+
+ fn decrement_index(&mut self) {
+ if self.index > 0 {
+ self.index -= 1;
+ } else {
+ self.index = self.children.len().saturating_sub(1);
+ }
+ }
+
+ /// Draws the [`Carousel`] arrows, and returns back the remaining [`Rect`] to draw the child with.
+ pub fn draw_carousel<B: Backend>(
+ &mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect,
+ ) -> Rect {
+ const CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1), Constraint::Min(0)];
+ let split_area = Layout::default()
+ .constraints(CONSTRAINTS)
+ .direction(tui::layout::Direction::Vertical)
+ .split(area);
+
+ self.set_bounds(split_area[0]);
+
+ if let Some((_prev_id, prev_element_name)) = self.get_prev() {
+ let prev_arrow_text = Spans::from(Span::styled(
+ format!("◄ {}", prev_element_name),
+ painter.colours.text_style,
+ ));
+
+ self.left_button_bounds = Rect::new(
+ split_area[0].x,
+ split_area[0].y,
+ prev_arrow_text.width() as u16,
+ split_area[0].height,
+ );
+
+ f.render_widget(
+ Paragraph::new(vec![prev_arrow_text]).alignment(tui::layout::Alignment::Left),
+ split_area[0],
+ );
+ }
+
+ if let Some((_next_id, next_element_name)) = self.get_next() {
+ let next_arrow_text = Spans::from(Span::styled(
+ format!("{} ►", next_element_name),
+ painter.colours.text_style,
+ ));
+
+ let width = next_arrow_text.width() as u16;
+
+ self.right_button_bounds = Rect::new(
+ split_area[0].right().saturating_sub(width + 1),
+ split_area[0].y,
+ width,
+ split_area[0].height,
+ );
+
+ f.render_widget(
+ Paragraph::new(vec![next_arrow_text]).alignment(tui::layout::Alignment::Right),
+ split_area[0],
+ );
+ }
+
+ split_area[1]
+ }
+}
+
+impl Component for Carousel {
+ fn bounds(&self) -> Rect {
+ self.bounds
+ }
+
+ fn set_bounds(&mut self, new_bounds: Rect) {
+ self.bounds = new_bounds;
+ }
+
+ fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
+ match event.kind {
+ crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
+ let x = event.column;
+ let y = event.row;
+
+ if does_bound_intersect_coordinate(x, y, self.left_button_bounds) {
+ self.decrement_index();
+ WidgetEventResult::Redraw
+ } else if does_bound_intersect_coordinate(x, y, self.right_button_bounds) {
+ self.increment_index();
+ WidgetEventResult::Redraw
+ } else {
+ WidgetEventResult::NoRedraw
+ }
+ }
+ _ => WidgetEventResult::NoRedraw,
+ }
+ }
+}
+
+impl Widget for Carousel {
+ fn get_pretty_name(&self) -> &'static str {
+ "Carousel"
+ }
+
+ fn width(&self) -> LayoutRule {
+ self.width
+ }
+
+ fn height(&self) -> LayoutRule {
+ self.height
+ }
+
+ fn selectable_type(&self) -> SelectableType {
+ if let Some(node) = self.get_currently_selected() {
+ debug!("node: {:?}", node);
+ SelectableType::Redirect(node)
+ } else {
+ SelectableType::Unselectable
+ }
+ }
+}
diff --git a/src/app/widgets/bottom_widgets/cpu.rs b/src/app/widgets/bottom_widgets/cpu.rs
new file mode 100644
index 00000000..753142f5
--- /dev/null
+++ b/src/app/widgets/bottom_widgets/cpu.rs
@@ -0,0 +1,333 @@
+use std::{borrow::Cow, collections::HashMap, time::Instant};
+
+use crossterm::event::{KeyEvent, MouseEvent};
+use tui::{
+ backend::Backend,
+ layout::{Constraint, Direction, Layout, Rect},
+ widgets::{Block, Borders},
+ Frame,
+};
+
+use crate::{
+ app::{
+ event::WidgetEventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData,
+ AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection,
+ TextTable, TimeGraph, Widget,
+ },
+ canvas::Painter,
+ data_conversion::{convert_cpu_data_points, ConvertedCpuData},
+ options::layout_options::LayoutRule,
+};
+
+pub struct CpuWidgetState {
+ pub current_display_time: u64,
+ pub is_legend_hidden: bool,
+ pub autohide_timer: Option<Instant>,
+ pub scroll_state: AppScrollWidgetState,
+ pub is_multi_graph_mode: bool,
+ pub table_width_state: CanvasTableWidthState,
+}
+
+impl CpuWidgetState {
+ pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
+ CpuWidgetState {
+ current_display_time,
+ is_legend_hidden: false,
+ autohide_timer,
+ scroll_state: AppScrollWidgetState::default(),
+ is_multi_graph_mode: false,
+ table_width_state: CanvasTableWidthState::default(),
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct CpuState {
+ pub force_update: Option<u64>,
+ pub widget_states: HashMap<u64, CpuWidgetState>,
+}
+
+impl CpuState {
+ pub fn init(widget_states: HashMap<u64, CpuWidgetState>) -> Self {
+ CpuState {
+ force_update: None,
+ widget_states,
+ }
+ }
+
+ pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut CpuWidgetState> {
+ self.widget_states.get_mut(&widget_id)
+ }
+
+ pub fn get_widget_state(&self, widget_id: u64) -> Option<&CpuWidgetState> {
+ self.widget_states.get(&widget_id)
+ }
+}
+
+enum CpuGraphSelection {
+ Graph,
+ Legend,
+ None,
+}
+
+/// Whether the [`CpuGraph`]'s legend is placed on the left or right.
+pub enum CpuGraphLegendPosition {
+ Left,
+ Right,
+}
+
+/// A widget designed to show CPU usage via a graph, along with a side legend in a table.
+pub struct CpuGraph {
+ graph: TimeGraph,
+ legend: TextTable<SimpleSortableColumn>,
+ legend_position: CpuGraphLegendPosition,
+ showing_avg: bool,
+
+ bounds: Rect,
+ selected: CpuGraphSelection,
+
+ display_data: Vec<ConvertedCpuData>,
+ load_avg_data: [f32; 3],
+
+ width: LayoutRule,
+ height: LayoutRule,
+}
+
+impl CpuGraph {
+ /// Creates a new [`CpuGraph`] from a config.
+ pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
+ let graph = TimeGraph::from_config(app_config_fields);
+ let legend = TextTable::new(vec![
+ SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5),
+ SimpleSortableColumn::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 {
+ graph,
+ legend,
+ legend_position,
+ showing_avg,
+ bounds: Rect::default(),
+ selected: CpuGraphSelection::None,
+ display_data: Default::default(),
+ load_avg_data: [0.0; 3],
+ width: LayoutRule::default(),
+ height: LayoutRule::default(),
+ }
+ }
+
+ /// Sets the width.
+ pub fn width(mut self, width: LayoutRule) -> Self {
+ self.width = width;
+ self
+ }
+
+ /// Sets the height.
+ pub fn height(mut self, height: LayoutRule) -> Self {
+ self.height = height;
+ self
+ }
+}
+
+impl Component for CpuGraph {
+ fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
+ match self.selected {