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 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,
);
}
}
}
}

View File

@ -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() {

View File

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

View File

@ -1 +1,2 @@
pub mod pipe_gauge;
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!(),
}
}
}