diff options
Diffstat (limited to 'src/app/widgets')
21 files changed, 1229 insertions, 85 deletions
diff --git a/src/app/widgets/base.rs b/src/app/widgets/base.rs index c5bf80cf..3e63e3e6 100644 --- a/src/app/widgets/base.rs +++ b/src/app/widgets/base.rs @@ -15,8 +15,5 @@ pub use scrollable::Scrollable; pub mod text_input; pub use text_input::TextInput; -pub mod carousel; -pub use carousel::Carousel; - pub mod sort_menu; pub use sort_menu::SortMenu; diff --git a/src/app/widgets/base/carousel.rs b/src/app/widgets/base/carousel.rs deleted file mode 100644 index 4454a847..00000000 --- a/src/app/widgets/base/carousel.rs +++ /dev/null @@ -1,42 +0,0 @@ -use indextree::NodeId; -use tui::layout::Rect; - -use crate::app::Component; - -/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s. -pub struct Carousel { - index: usize, - children: Vec<NodeId>, - bounds: Rect, -} - -impl Carousel { - /// Creates a new [`Carousel`] with the specified children. - pub fn new(children: Vec<NodeId>) -> Self { - Self { - index: 0, - children, - bounds: Rect::default(), - } - } - - /// Adds a new child to a [`Carousel`]. - pub fn add_child(&mut self, child: NodeId) { - self.children.push(child); - } - - /// Returns the currently selected [`NodeId`] if possible. - pub fn get_currently_selected(&self) -> Option<&NodeId> { - self.children.get(self.index) - } -} - -impl Component for Carousel { - fn bounds(&self) -> tui::layout::Rect { - self.bounds - } - - fn set_bounds(&mut self, new_bounds: tui::layout::Rect) { - self.bounds = new_bounds; - } -} diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index 03111566..6cf460f9 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -384,6 +384,9 @@ where use tui::widgets::Row; let inner_area = block.inner(block_area); + if inner_area.height < 2 { + return; + } let table_gap = if !self.show_gap || inner_area.height < TABLE_GAP_HEIGHT_LIMIT { 0 } else { diff --git a/src/app/widgets/basic_cpu.rs b/src/app/widgets/basic_cpu.rs deleted file mode 100644 index e69de29b..00000000 --- a/src/app/widgets/basic_cpu.rs +++ /dev/null diff --git a/src/app/widgets/basic_mem.rs b/src/app/widgets/basic_mem.rs deleted file mode 100644 index e69de29b..00000000 --- a/src/app/widgets/basic_mem.rs +++ /dev/null diff --git a/src/app/widgets/basic_net.rs b/src/app/widgets/basic_net.rs deleted file mode 100644 index e69de29b..00000000 --- a/src/app/widgets/basic_net.rs +++ /dev/null diff --git a/src/app/widgets/bottom_widgets.rs b/src/app/widgets/bottom_widgets.rs new file mode 100644 index 00000000..fbc7c78c --- /dev/null +++ b/src/app/widgets/bottom_widgets.rs @@ -0,0 +1,35 @@ +pub mod process; +pub use process::*; + +pub mod net; +pub use net::*; + +pub mod mem; +pub use mem::*; + +pub mod cpu; +pub use cpu::*; + +pub mod disk; +pub use disk::*; + +pub mod battery; +pub use self::battery::*; + +pub mod temp; +pub use temp::*; + +pub mod basic_cpu; +pub use basic_cpu::BasicCpu; + +pub mod basic_mem; +pub use basic_mem::BasicMem; + +pub mod basic_net; +pub use basic_net::BasicNet; + +pub mod carousel; +pub use carousel::Carousel; + +pub mod empty; +pub use empty::Empty; 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/battery.rs b/src/app/widgets/bottom_widgets/battery.rs index e12e2fe6..2680a8d7 100644 --- a/src/app/widgets/battery.rs +++ b/src/app/widgets/bottom_widgets/battery.rs @@ -1,14 +1,13 @@ use std::collections::HashMap; -use tui::layout::Rect; +use tui::{layout::Rect, widgets::Borders}; use crate::{ - app::data_farmer::DataCollection, + app::{data_farmer::DataCollection, Component, Widget}, data_conversion::{convert_battery_harvest, ConvertedBatteryData}, + options::layout_options::LayoutRule, }; -use super::{Component, Widget}; - #[derive(Default)] pub struct BatteryWidgetState { pub currently_selected_battery_index: usize, @@ -36,30 +35,61 @@ impl BatteryState { // TODO: Implement battery widget. /// A table displaying battery information on a per-battery basis. -#[derive(Default)] pub struct BatteryTable { bounds: Rect, selected_index: usize, batteries: Vec<String>, battery_data: Vec<ConvertedBatteryData>, + width: LayoutRule, + height: LayoutRule, + block_border: Borders, } -impl BatteryTable { - /// Creates a new [`BatteryTable`]. - pub fn new(batteries: Vec<String>) -> Self { +impl Default for BatteryTable { + fn default() -> Self { Self { - batteries, - ..Default::default() + 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 { @@ -80,4 +110,12 @@ impl Widget for BatteryTable { 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 { + |