From b6a75db1b48e19ca85c00e88554b5132b543f1c7 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Thu, 13 Oct 2022 10:17:26 -0400 Subject: [PATCH] refactor: switch to pipe gauge implementation for basic cpu + mem (#829) * refactor: switch to pipe gauge implementation for basic cpu + mem * fix incorrect new basic cpu chunking scheme, revert to old one --- src/canvas/widgets/cpu_basic.rs | 189 +++++++------------- src/canvas/widgets/mem_basic.rs | 155 +++++++--------- src/components.rs | 3 +- src/components/tui_widget.rs | 1 + src/components/tui_widget/pipe_gauge.rs | 223 ++++++++++++++++++++++++ 5 files changed, 349 insertions(+), 222 deletions(-) create mode 100644 src/components/tui_widget/pipe_gauge.rs diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/widgets/cpu_basic.rs index 4296b967..1ca75a90 100644 --- a/src/canvas/widgets/cpu_basic.rs +++ b/src/canvas/widgets/cpu_basic.rs @@ -1,8 +1,9 @@ use std::cmp::min; use crate::{ - app::App, - canvas::{drawing_utils::*, Painter}, + app::{data_harvester::cpu::CpuDataType, App}, + canvas::Painter, + components::tui_widget::pipe_gauge::{LabelLimit, PipeGauge}, constants::*, data_conversion::CpuWidgetData, }; @@ -11,11 +12,11 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, terminal::Frame, - text::{Span, Spans}, - widgets::{Block, Paragraph}, + widgets::Block, }; impl Painter { + /// Inspired by htop. pub fn draw_basic_cpu<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { @@ -42,148 +43,84 @@ impl Painter { ); } - let num_cpus = cpu_data.len(); - let show_avg_cpu = app_state.app_config_fields.show_average_cpu; - if draw_loc.height > 0 { let remaining_height = usize::from(draw_loc.height); const REQUIRED_COLUMNS: usize = 4; - let chunk_vec = + let col_constraints = vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS]; - let chunks = Layout::default() - .constraints(chunk_vec) + let columns = Layout::default() + .constraints(col_constraints) .direction(Direction::Horizontal) .split(draw_loc); - const CPU_NAME_SPACE: usize = 3; - const BAR_BOUND_SPACE: usize = 2; - const PERCENTAGE_SPACE: usize = 4; - const MARGIN_SPACE: usize = 2; + let mut gauge_info = cpu_data.iter().map(|cpu| match cpu { + CpuWidgetData::All => unreachable!(), + CpuWidgetData::Entry { + data_type, + data: _, + last_entry, + } => { + let (outer, style) = match data_type { + CpuDataType::Avg => ("AVG".to_string(), self.colours.avg_colour_style), + CpuDataType::Cpu(index) => ( + format!("{index:<3}",), + self.colours.cpu_colour_styles + [index % self.colours.cpu_colour_styles.len()], + ), + }; + let inner = format!("{:>3.0}%", last_entry.round()); + let ratio = last_entry / 100.0; - const COMBINED_SPACING: usize = - CPU_NAME_SPACE + BAR_BOUND_SPACE + PERCENTAGE_SPACE + MARGIN_SPACE; - const REDUCED_SPACING: usize = CPU_NAME_SPACE + PERCENTAGE_SPACE + MARGIN_SPACE; - let chunk_width: usize = chunks[0].width.into(); + (outer, inner, ratio, style) + } + }); - // Inspired by htop. - // We do +4 as if it's too few bars in the bar length, it's kinda pointless. - let cpu_bars = if chunk_width >= COMBINED_SPACING + 4 { - let bar_length = chunk_width - COMBINED_SPACING; - cpu_data - .iter() - .enumerate() - .filter_map(|(index, cpu)| match &cpu { - CpuWidgetData::All => None, - CpuWidgetData::Entry { - data_type: _, - data: _, - last_entry, - } => { - let num_bars = calculate_basic_use_bars(*last_entry, bar_length); - Some(format!( - "{:3}[{}{}{:3.0}%]", - if app_state.app_config_fields.show_average_cpu { - if index == 0 { - "AVG".to_string() - } else { - (index - 1).to_string() - } - } else { - index.to_string() - }, - "|".repeat(num_bars), - " ".repeat(bar_length - num_bars), - last_entry.round(), - )) - } - }) - .collect::<Vec<_>>() - } else if chunk_width >= REDUCED_SPACING { - cpu_data - .iter() - .enumerate() - .filter_map(|(index, cpu)| match &cpu { - CpuWidgetData::All => None, - CpuWidgetData::Entry { - data_type: _, - data: _, - last_entry, - } => Some(format!( - "{:3} {:3.0}%", - if app_state.app_config_fields.show_average_cpu { - if index == 0 { - "AVG".to_string() - } else { - (index - 1).to_string() - } - } else { - index.to_string() - }, - last_entry.round(), - )), - }) - .collect::<Vec<_>>() - } else { - cpu_data - .iter() - .filter_map(|cpu| match &cpu { - CpuWidgetData::All => None, - CpuWidgetData::Entry { - data_type: _, - data: _, - last_entry, - } => Some(format!("{:3.0}%", last_entry.round())), - }) - .collect::<Vec<_>>() - }; + // Very ugly way to sync the gauge limit across all gauges. + let hide_parts = columns + .get(0) + .map(|col| { + if col.width >= 12 { + LabelLimit::None + } else if col.width >= 10 { + LabelLimit::Bars + } else { + LabelLimit::StartLabel + } + }) + .unwrap_or_default(); - let mut row_counter = num_cpus; - let mut start_index = 0; - for (itx, chunk) in chunks.iter().enumerate() { - // Explicitly check... don't want an accidental DBZ or underflow, this ensures - // to_divide is > 0 + let num_entries = cpu_data.len(); + let mut row_counter = num_entries; + for (itx, column) in columns.into_iter().enumerate() { if REQUIRED_COLUMNS > itx { let to_divide = REQUIRED_COLUMNS - itx; - let how_many_cpus = min( + let num_taken = min( remaining_height, (row_counter / to_divide) + (if row_counter % to_divide == 0 { 0 } else { 1 }), ); - row_counter -= how_many_cpus; - let end_index = min(start_index + how_many_cpus, num_cpus); + row_counter -= num_taken; + let chunk = (&mut gauge_info).take(num_taken); - let cpu_column = (start_index..end_index) - .map(|itx| { - Spans::from(Span { - content: (&cpu_bars[itx]).into(), - style: if show_avg_cpu { - if itx == 0 { - self.colours.avg_colour_style - } else { - self.colours.cpu_colour_styles - [(itx - 1) % self.colours.cpu_colour_styles.len()] - } - } else { - self.colours.cpu_colour_styles - [itx % self.colours.cpu_colour_styles.len()] - }, - }) - }) - .collect::<Vec<_>>(); - - start_index += how_many_cpus; - - let margined_loc = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(100)]) + let rows = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(1); remaining_height]) .horizontal_margin(1) - .split(*chunk)[0]; + .split(column); - f.render_widget( - Paragraph::new(cpu_column).block(Block::default()), - margined_loc, - ); + for ((start_label, inner_label, ratio, style), row) in chunk.zip(rows) { + f.render_widget( + PipeGauge::default() + .gauge_style(style) + .label_style(style) + .inner_label(inner_label) + .start_label(start_label) + .ratio(ratio) + .hide_parts(hide_parts), + row, + ); + } } } } diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/widgets/mem_basic.rs index 60f35f09..1d3c9969 100644 --- a/src/canvas/widgets/mem_basic.rs +++ b/src/canvas/widgets/mem_basic.rs @@ -1,16 +1,12 @@ use crate::{ - app::App, - canvas::{drawing_utils::*, Painter}, - constants::*, + app::App, canvas::Painter, components::tui_widget::pipe_gauge::PipeGauge, constants::*, }; use tui::{ backend::Backend, - layout::{Constraint, Layout, Rect}, + layout::{Constraint, Direction, Layout, Rect}, terminal::Frame, - text::Span, - text::Spans, - widgets::{Block, Paragraph}, + widgets::Block, }; impl Painter { @@ -21,7 +17,18 @@ impl Painter { let swap_data: &[(f64, f64)] = &app_state.converted_data.swap_data; let margined_loc = Layout::default() - .constraints([Constraint::Percentage(100)]) + .constraints({ + #[cfg(feature = "zfs")] + { + [Constraint::Length(1); 3] + } + + #[cfg(not(feature = "zfs"))] + { + [Constraint::Length(1); 2] + } + }) + .direction(Direction::Vertical) .horizontal_margin(1) .split(draw_loc); @@ -34,118 +41,78 @@ impl Painter { ); } - let ram_use_percentage = if let Some(mem) = mem_data.last() { - mem.1 + let ram_ratio = if let Some(mem) = mem_data.last() { + mem.1 / 100.0 } else { 0.0 }; - let swap_use_percentage = if let Some(swap) = swap_data.last() { - swap.1 + let swap_ratio = if let Some(swap) = swap_data.last() { + swap.1 / 100.0 } else { 0.0 }; const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B"; - let trimmed_memory_frac = - if let Some((_label_percent, label_frac)) = &app_state.converted_data.mem_labels { + let memory_fraction_label = + if let Some((_, label_frac)) = &app_state.converted_data.mem_labels { label_frac.trim() } else { EMPTY_MEMORY_FRAC_STRING }; - let trimmed_swap_frac = - if let Some((_label_percent, label_frac)) = &app_state.converted_data.swap_labels { + let swap_fraction_label = + if let Some((_, label_frac)) = &app_state.converted_data.swap_labels { label_frac.trim() } else { EMPTY_MEMORY_FRAC_STRING }; - // +7 due to 3 + 2 + 2 columns for the name & space + bar bounds + margin spacing - // Then + length of fraction - let ram_bar_length = - usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_memory_frac.len()); - let swap_bar_length = - usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_swap_frac.len()); + f.render_widget( + PipeGauge::default() + .ratio(ram_ratio) + .start_label("RAM") + .inner_label(memory_fraction_label) + .label_style(self.colours.ram_style) + .gauge_style(self.colours.ram_style), + margined_loc[0], + ); - let num_bars_ram = calculate_basic_use_bars(ram_use_percentage, ram_bar_length); - let num_bars_swap = calculate_basic_use_bars(swap_use_percentage, swap_bar_length); - // TODO: Use different styling for the frac. - let mem_label = if app_state.basic_mode_use_percent { - format!( - "RAM[{}{}{:3.0}%]\n", - "|".repeat(num_bars_ram), - " ".repeat(ram_bar_length - num_bars_ram + trimmed_memory_frac.len() - 4), - ram_use_percentage.round() - ) - } else { - format!( - "RAM[{}{}{}]\n", - "|".repeat(num_bars_ram), - " ".repeat(ram_bar_length - num_bars_ram), - trimmed_memory_frac - ) - }; - let swap_label = if app_state.basic_mode_use_percent { - format!( - "SWP[{}{}{:3.0}%]", - "|".repeat(num_bars_swap), - " ".repeat(swap_bar_length - num_bars_swap + trimmed_swap_frac.len() - 4), - swap_use_percentage.round() - ) - } else { - format!( - "SWP[{}{}{}]", - "|".repeat(num_bars_swap), - " ".repeat(swap_bar_length - num_bars_swap), - trimmed_swap_frac - ) - }; + f.render_widget( + PipeGauge::default() + .ratio(swap_ratio) + .start_label("SWP") + .inner_label(swap_fraction_label) + .label_style(self.colours.swap_style) + .gauge_style(self.colours.swap_style), + margined_loc[1], + ); - let mem_text = vec![ - Spans::from(Span::styled(mem_label, self.colours.ram_style)), - Spans::from(Span::styled(swap_label, self.colours.swap_style)), - #[cfg(feature = "zfs")] - { - let arc_data: &[(f64, f64)] = &app_state.converted_data.arc_data; - let arc_use_percentage = if let Some(arc) = arc_data.last() { - arc.1 - } else { - 0.0 - }; - let trimmed_arc_frac = if let Some((_label_percent, label_frac)) = - &app_state.converted_data.arc_labels - { + #[cfg(feature = "zfs")] + { + let arc_data: &[(f64, f64)] = &app_state.converted_data.arc_data; + let arc_ratio = if let Some(arc) = arc_data.last() { + arc.1 / 100.0 + } else { + 0.0 + }; + let arc_fraction_label = + if let Some((_, label_frac)) = &app_state.converted_data.arc_labels { label_frac.trim() } else { EMPTY_MEMORY_FRAC_STRING }; - let arc_bar_length = usize::from(draw_loc.width.saturating_sub(7)) - .saturating_sub(trimmed_arc_frac.len()); - let num_bars_arc = calculate_basic_use_bars(arc_use_percentage, arc_bar_length); - let arc_label = if app_state.basic_mode_use_percent { - format!( - "ARC[{}{}{:3.0}%]", - "|".repeat(num_bars_arc), - " ".repeat(arc_bar_length - num_bars_arc + trimmed_arc_frac.len() - 4), - arc_use_percentage.round() - ) - } else { - format!( - "ARC[{}{}{}]", - "|".repeat(num_bars_arc), - " ".repeat(arc_bar_length - num_bars_arc), - trimmed_arc_frac - ) - }; - Spans::from(Span::styled(arc_label, self.colours.arc_style)) - }, - ]; - f.render_widget( - Paragraph::new(mem_text).block(Block::default()), - margined_loc[0], - ); + f.render_widget( + PipeGauge::default() + .ratio(arc_ratio) + .start_label("ARC") + .inner_label(arc_fraction_label) + .label_style(self.colours.arc_style) + .gauge_style(self.colours.arc_style), + margined_loc[2], + ); + } // Update draw loc in widget map if app_state.should_get_widget_bounds() { diff --git a/src/components.rs b/src/components.rs index 1c4c453c..c2ee5aae 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,4 +1,3 @@ -mod tui_widget; - pub mod data_table; pub mod time_graph; +pub mod tui_widget; diff --git a/src/components/tui_widget.rs b/src/components/tui_widget.rs index a4e0978a..93c0509d 100644 --- a/src/components/tui_widget.rs +++ b/src/components/tui_widget.rs @@ -1 +1,2 @@ +pub mod pipe_gauge; pub mod time_chart; diff --git a/src/components/tui_widget/pipe_gauge.rs b/src/components/tui_widget/pipe_gauge.rs new file mode 100644 index 00000000..aa933185 --- /dev/null +++ b/src/components/tui_widget/pipe_gauge.rs @@ -0,0 +1,223 @@ +use tui::{ + buffer::Buffer, + layout::Rect, + style::Style, + text::Spans, + widgets::{Block, Widget}, +}; + +#[derive(Debug, Clone, Copy)] +pub enum LabelLimit { + None, + Auto(u16), + Bars, + StartLabel, +} + +impl Default for LabelLimit { + fn default() -> Self { + Self::None + } +} + +/// A widget to measure something, using pipe characters ('|') as a unit. +#[derive(Debug, Clone)] +pub struct PipeGauge<'a> { + block: Option<Block<'a>>, + ratio: f64, + start_label: Option<Spans<'a>>, + inner_label: Option<Spans<'a>>, + label_style: Style, + gauge_style: Style, + hide_parts: LabelLimit, +} + +impl<'a> Default for PipeGauge<'a> { + fn default() -> Self { + Self { + block: None, + ratio: 0.0, + start_label: None, + inner_label: None, + label_style: Style::default(), + gauge_style: Style::default(), + hide_parts: LabelLimit::default(), + } + } +} + +impl<'a> PipeGauge<'a> { + /// The ratio, a value from 0.0 to 1.0 (any other greater or less will be clamped) + /// represents the portion of the pipe gauge to fill. + /// + /// Note: passing in NaN will potentially cause problems. + pub fn ratio(mut self, ratio: f64) -> Self { + self.ratio = ratio.clamp(0.0, 1.0); + + self + } + + /// The label displayed before the bar. + pub fn start_label<T>(mut self, start_label: T) -> Self + where + T: Into<Spans<'a>>, + { + self.start_label = Some(start_label.into()); + self + } + + /// The label displayed inside the bar. + pub fn inner_label<T>(mut self, inner_label: T) -> Self + where + T: Into<Spans<'a>>, + { + self.inner_label = Some(inner_label.into()); + self + } + + /// The style of the labels. + pub fn label_style(mut self, label_style: Style) -> Self { + self.label_style = label_style; + self + } + + /// The style of the gauge itself. + pub fn gauge_style(mut self, style: Style) -> Self { + self.gauge_style = style; + self + } + + /// Whether to hide parts of the gauge/label if the inner label wouldn't fit. + pub fn hide_parts(mut self, hide_parts: LabelLimit) -> Self { + self.hide_parts = hide_parts; + self + } +} + +impl<'a> Widget for PipeGauge<'a> { + fn render(mut self, area: Rect, buf: &mut Buffer) { + buf.set_style(area, self.label_style); + let gauge_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); + b.render(area, buf); + inner_area + } + None => area, + }; + + if gauge_area.height < 1 { + return; + } + + let (col, row) = { + let inner_label_width = self + .inner_label + .as_ref() + .map(|l| l.width()) + .unwrap_or_default(); + + let start_label_width = self + .start_label + .as_ref() + .map(|l| l.width()) + .unwrap_or_default(); + + match self.hide_parts { + LabelLimit::StartLabel => { + let inner_label = self.inner_label.unwrap_or_else(|| Spans::from("")); + let _ = buf.set_spans( + gauge_area.left(), + gauge_area.top(), + &inner_label, + inner_label.width() as u16, + ); + + // Short circuit. + return; + } + LabelLimit::Auto(_) + if gauge_area.width < (inner_label_width + start_label_width + 1) as u16 => + { + let inner_label = self.inner_label.unwrap_or_else(|| Spans::from("")); + let _ = buf.set_spans( + gauge_area.left(), + gauge_area.top(), + &inner_label, + inner_label.width() as u16, + ); + + // Short circuit. + return; + } + _ => { + let start_label = self.start_label.unwrap_or_else(|| Spans::from("")); + buf.set_spans( + gauge_area.left(), + gauge_area.top(), + &start_label, + start_label.width() as u16, + ) + } + } + }; + + let end_label = self.inner_label.unwrap_or_else(|| Spans::from("")); + match self.hide_parts { + LabelLimit::Bars => { + let _ = buf.set_spans( + gauge_area + .right() + .saturating_sub(end_label.width() as u16 + 1), + row, + &end_label, + end_label.width() as u16, + ); + } + LabelLimit::Auto(width_limit) + if gauge_area.right().saturating_sub(col) < width_limit => + { + let _ = buf.set_spans( + gauge_area + .right() + .saturating_sub(end_label.width() as u16 + 1), + row, + &end_label, + 1, + ); + } + LabelLimit::Auto(_) | LabelLimit::None => { + let (start, _) = buf.set_spans(col, row, &Spans::from("["), gauge_area.width); + if start >= gauge_area.right() { + return; + } + + let (end, _) = buf.set_spans( + (gauge_area.x + gauge_area.width).saturating_sub(1), + row, + &Spans::from("]"), + gauge_area.width, + ); + + let pipe_end = + start + (f64::from(end.saturating_sub(start)) * self.ratio).floor() as u16; + for col in start..pipe_end { + buf.get_mut(col, row).set_symbol("|").set_style(Style { + fg: self.gauge_style.fg, + bg: None, + add_modifier: self.gauge_style.add_modifier, + sub_modifier: self.gauge_style.sub_modifier, + }); + } + + if (end_label.width() as u16) < end.saturating_sub(start) { + let gauge_end = gauge_area + .right() + .saturating_sub(end_label.width() as u16 + 1); + buf.set_spans(gauge_end, row, &end_label, end_label.width() as u16); + } + } + LabelLimit::StartLabel => unreachable!(), + } + } +}