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
This commit is contained in:
Clement Tsang 2022-10-13 10:17:26 -04:00 committed by GitHub
parent 436dadb683
commit b6a75db1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 349 additions and 222 deletions

View File

@ -1,8 +1,9 @@
use std::cmp::min; use std::cmp::min;
use crate::{ use crate::{
app::App, app::{data_harvester::cpu::CpuDataType, App},
canvas::{drawing_utils::*, Painter}, canvas::Painter,
components::tui_widget::pipe_gauge::{LabelLimit, PipeGauge},
constants::*, constants::*,
data_conversion::CpuWidgetData, data_conversion::CpuWidgetData,
}; };
@ -11,11 +12,11 @@ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
terminal::Frame, terminal::Frame,
text::{Span, Spans}, widgets::Block,
widgets::{Block, Paragraph},
}; };
impl Painter { impl Painter {
/// Inspired by htop.
pub fn draw_basic_cpu<B: Backend>( pub fn draw_basic_cpu<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, &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 { if draw_loc.height > 0 {
let remaining_height = usize::from(draw_loc.height); let remaining_height = usize::from(draw_loc.height);
const REQUIRED_COLUMNS: usize = 4; const REQUIRED_COLUMNS: usize = 4;
let chunk_vec = let col_constraints =
vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS]; vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS];
let chunks = Layout::default() let columns = Layout::default()
.constraints(chunk_vec) .constraints(col_constraints)
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.split(draw_loc); .split(draw_loc);
const CPU_NAME_SPACE: usize = 3; let mut gauge_info = cpu_data.iter().map(|cpu| match cpu {
const BAR_BOUND_SPACE: usize = 2; CpuWidgetData::All => unreachable!(),
const PERCENTAGE_SPACE: usize = 4; CpuWidgetData::Entry {
const MARGIN_SPACE: usize = 2; 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 = (outer, inner, ratio, style)
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();
// Inspired by htop. // Very ugly way to sync the gauge limit across all gauges.
// We do +4 as if it's too few bars in the bar length, it's kinda pointless. let hide_parts = columns
let cpu_bars = if chunk_width >= COMBINED_SPACING + 4 { .get(0)
let bar_length = chunk_width - COMBINED_SPACING; .map(|col| {
cpu_data if col.width >= 12 {
.iter() LabelLimit::None
.enumerate() } else if col.width >= 10 {
.filter_map(|(index, cpu)| match &cpu { LabelLimit::Bars
CpuWidgetData::All => None, } else {
CpuWidgetData::Entry { LabelLimit::StartLabel
data_type: _, }
data: _, })
last_entry, .unwrap_or_default();
} => {
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<_>>()
};
let mut row_counter = num_cpus; let num_entries = cpu_data.len();
let mut start_index = 0; let mut row_counter = num_entries;
for (itx, chunk) in chunks.iter().enumerate() { for (itx, column) in columns.into_iter().enumerate() {
// Explicitly check... don't want an accidental DBZ or underflow, this ensures
// to_divide is > 0
if REQUIRED_COLUMNS > itx { if REQUIRED_COLUMNS > itx {
let to_divide = REQUIRED_COLUMNS - itx; let to_divide = REQUIRED_COLUMNS - itx;
let how_many_cpus = min( let num_taken = min(
remaining_height, remaining_height,
(row_counter / to_divide) (row_counter / to_divide)
+ (if row_counter % to_divide == 0 { 0 } else { 1 }), + (if row_counter % to_divide == 0 { 0 } else { 1 }),
); );
row_counter -= how_many_cpus; row_counter -= num_taken;
let end_index = min(start_index + how_many_cpus, num_cpus); let chunk = (&mut gauge_info).take(num_taken);
let cpu_column = (start_index..end_index) let rows = Layout::default()
.map(|itx| { .direction(Direction::Vertical)
Spans::from(Span { .constraints(vec![Constraint::Length(1); remaining_height])
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)])
.horizontal_margin(1) .horizontal_margin(1)
.split(*chunk)[0]; .split(column);
f.render_widget( for ((start_label, inner_label, ratio, style), row) in chunk.zip(rows) {
Paragraph::new(cpu_column).block(Block::default()), f.render_widget(
margined_loc, PipeGauge::default()
); .gauge_style(style)
.label_style(style)
.inner_label(inner_label)
.start_label(start_label)
.ratio(ratio)
.hide_parts(hide_parts),
row,
);
}
} }
} }
} }

View File

@ -1,16 +1,12 @@
use crate::{ use crate::{
app::App, app::App, canvas::Painter, components::tui_widget::pipe_gauge::PipeGauge, constants::*,
canvas::{drawing_utils::*, Painter},
constants::*,
}; };
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
terminal::Frame, terminal::Frame,
text::Span, widgets::Block,
text::Spans,
widgets::{Block, Paragraph},
}; };
impl Painter { impl Painter {
@ -21,7 +17,18 @@ impl Painter {
let swap_data: &[(f64, f64)] = &app_state.converted_data.swap_data; let swap_data: &[(f64, f64)] = &app_state.converted_data.swap_data;
let margined_loc = Layout::default() 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) .horizontal_margin(1)
.split(draw_loc); .split(draw_loc);
@ -34,118 +41,78 @@ impl Painter {
); );
} }
let ram_use_percentage = if let Some(mem) = mem_data.last() { let ram_ratio = if let Some(mem) = mem_data.last() {
mem.1 mem.1 / 100.0
} else { } else {
0.0 0.0
}; };
let swap_use_percentage = if let Some(swap) = swap_data.last() { let swap_ratio = if let Some(swap) = swap_data.last() {
swap.1 swap.1 / 100.0
} else { } else {
0.0 0.0
}; };
const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B"; const EMPTY_MEMORY_FRAC_STRING: &str = "0.0B/0.0B";
let trimmed_memory_frac = let memory_fraction_label =
if let Some((_label_percent, label_frac)) = &app_state.converted_data.mem_labels { if let Some((_, label_frac)) = &app_state.converted_data.mem_labels {
label_frac.trim() label_frac.trim()
} else { } else {
EMPTY_MEMORY_FRAC_STRING EMPTY_MEMORY_FRAC_STRING
}; };
let trimmed_swap_frac = let swap_fraction_label =
if let Some((_label_percent, label_frac)) = &app_state.converted_data.swap_labels { if let Some((_, label_frac)) = &app_state.converted_data.swap_labels {
label_frac.trim() label_frac.trim()
} else { } else {
EMPTY_MEMORY_FRAC_STRING EMPTY_MEMORY_FRAC_STRING
}; };
// +7 due to 3 + 2 + 2 columns for the name & space + bar bounds + margin spacing f.render_widget(
// Then + length of fraction PipeGauge::default()
let ram_bar_length = .ratio(ram_ratio)
usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_memory_frac.len()); .start_label("RAM")
let swap_bar_length = .inner_label(memory_fraction_label)
usize::from(draw_loc.width.saturating_sub(7)).saturating_sub(trimmed_swap_frac.len()); .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); f.render_widget(
let num_bars_swap = calculate_basic_use_bars(swap_use_percentage, swap_bar_length); PipeGauge::default()
// TODO: Use different styling for the frac. .ratio(swap_ratio)
let mem_label = if app_state.basic_mode_use_percent { .start_label("SWP")
format!( .inner_label(swap_fraction_label)
"RAM[{}{}{:3.0}%]\n", .label_style(self.colours.swap_style)
"|".repeat(num_bars_ram), .gauge_style(self.colours.swap_style),
" ".repeat(ram_bar_length - num_bars_ram + trimmed_memory_frac.len() - 4), margined_loc[1],
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
)
};
let mem_text = vec![ #[cfg(feature = "zfs")]
Spans::from(Span::styled(mem_label, self.colours.ram_style)), {
Spans::from(Span::styled(swap_label, self.colours.swap_style)), let arc_data: &[(f64, f64)] = &app_state.converted_data.arc_data;
#[cfg(feature = "zfs")] let arc_ratio = if let Some(arc) = arc_data.last() {
{ arc.1 / 100.0
let arc_data: &[(f64, f64)] = &app_state.converted_data.arc_data; } else {
let arc_use_percentage = if let Some(arc) = arc_data.last() { 0.0
arc.1 };
} else { let arc_fraction_label =
0.0 if let Some((_, label_frac)) = &app_state.converted_data.arc_labels {
};
let trimmed_arc_frac = if let Some((_label_percent, label_frac)) =
&app_state.converted_data.arc_labels
{
label_frac.trim() label_frac.trim()
} else { } else {
EMPTY_MEMORY_FRAC_STRING 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( f.render_widget(
Paragraph::new(mem_text).block(Block::default()), PipeGauge::default()
margined_loc[0], .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 // Update draw loc in widget map
if app_state.should_get_widget_bounds() { if app_state.should_get_widget_bounds() {

View File

@ -1,4 +1,3 @@
mod tui_widget;
pub mod data_table; pub mod data_table;
pub mod time_graph; pub mod time_graph;
pub mod tui_widget;

View File

@ -1 +1,2 @@
pub mod pipe_gauge;
pub mod time_chart; pub mod time_chart;

View File

@ -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!(),
}
}
}