use std::fmt::Display;
use ratatui::{
Frame,
layout::{Alignment, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
text::{Line, Span},
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
};
use super::FrameData;
use crate::{
app_data::{State, Stats},
config::AppColors,
};
fn make_chart<'a, T: Stats + Display>(
state: State,
colors: AppColors,
dataset: Vec<Dataset<'a>>,
current_rx: &'a T,
max_rx: &'a T,
current_tx: &'a T,
max_tx: &'a T,
) -> Chart<'a> {
let gen_color = |state: &State, default: Color| {
if state.is_healthy() {
default
} else {
state.get_color(colors)
}
};
let mut labels = [
Span::raw(""),
Span::styled(
format!("{max_rx}"),
Style::default()
.add_modifier(Modifier::BOLD)
.fg(gen_color(&state, colors.chart_bandwidth.max_rx)),
),
Span::styled(
format!("{max_tx}"),
Style::default()
.add_modifier(Modifier::BOLD)
.fg(gen_color(&state, colors.chart_bandwidth.max_tx)),
),
Span::raw(""),
];
if max_rx.get_value() > max_tx.get_value() {
labels.reverse();
}
Chart::new(dataset)
.bg(colors.chart_bandwidth.background)
.block(
Block::default()
.title_alignment(Alignment::Center)
.title(Line::from(vec![
Span::styled(
format!(" rx: {current_rx}"),
Style::default()
.add_modifier(Modifier::BOLD)
.fg(gen_color(&state, colors.chart_bandwidth.title_rx)),
),
Span::styled(
format!(" tx: {current_tx} "),
Style::default()
.add_modifier(Modifier::BOLD)
.fg(gen_color(&state, colors.chart_bandwidth.title_tx)),
),
]))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(colors.chart_bandwidth.border)),
)
.x_axis(Axis::default().bounds([0.0, 60.0]))
.y_axis(
Axis::default()
.labels(labels)
.style(Style::default().fg(colors.chart_bandwidth.y_axis))
.bounds([0.0, (max_rx.get_value()).max(max_tx.get_value()) + 0.01]),
)
}
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
if let Some(x) = fd.chart_data.as_ref() {
let mut dataset = vec![
Dataset::default()
.marker(symbols::Marker::Dot)
.style(Style::default().fg(colors.chart_bandwidth.points_tx))
.graph_type(GraphType::Line)
.marker(Marker::Dot)
.style(Style::default().fg(colors.chart_bandwidth.points_tx))
.data(&x.tx.dataset),
];
dataset.extend(vec![
Dataset::default()
.marker(symbols::Marker::Dot)
.style(Style::default().fg(colors.chart_bandwidth.points_rx))
.marker(Marker::Dot)
.style(Style::default().fg(colors.chart_bandwidth.points_rx))
.graph_type(GraphType::Line)
.data(&x.rx.dataset),
]);
let chart = make_chart(
x.state,
colors,
dataset,
&x.rx.current,
&x.rx.max,
&x.tx.current,
&x.tx.max,
);
f.render_widget(chart, area);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use insta::assert_snapshot;
use ratatui::style::Color;
use crate::{
app_data::{ContainerId, NetworkBandwidth, State},
config::AppColors,
ui::{
FrameData,
draw_blocks::tests::{COLOR_RX, COLOR_TX, get_result, test_setup},
},
};
const TX_DOTS: [(usize, usize); 14] = [
(1, 21),
(2, 19),
(2, 20),
(3, 18),
(3, 19),
(4, 10),
(4, 11),
(4, 17),
(4, 18),
(5, 16),
(6, 14),
(6, 15),
(7, 13),
(7, 14),
];
const RX_DOTS: [(usize, usize); 15] = [
(1, 21),
(2, 19),
(2, 20),
(3, 18),
(3, 19),
(4, 10),
(4, 11),
(4, 17),
(4, 18),
(5, 16),
(6, 16),
(6, 15),
(7, 13),
(7, 14),
(8, 13),
];
const COMBINED_DOTS_RX: [(usize, usize); 15] = [
(1, 21),
(2, 19),
(2, 20),
(3, 18),
(3, 19),
(4, 10),
(4, 11),
(4, 17),
(4, 18),
(5, 16),
(6, 15),
(6, 16),
(7, 13),
(7, 14),
(8, 13),
];
const COMBINED_DOTS_TX: [(usize, usize); 8] = [
(7, 19),
(7, 20),
(7, 21),
(8, 14),
(8, 15),
(8, 16),
(8, 17),
(8, 18),
];
#[test]
fn test_draw_blocks_charts_running_none() {
let mut setup = test_setup(40, 10, true, true);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 0..=7 | 31..=39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 8..=18) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
(0, 19..=30) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
(1..=8, 7) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(4, 1..=6) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
(6, 1..=6) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert_eq!(result_cell.bg, Color::Reset);
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_running_with_data_tx() {
let mut setup = test_setup(40, 10, true, true);
let mut tx = NetworkBandwidth::new();
for i in 0..=20 {
tx.push(1000 * i * (10 + 5 * i));
}
if let Some(item) = setup
.app_data
.lock()
.get_container_by_id(&ContainerId::from("1"))
{
item.tx = tx;
}
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 0..=5 | 34..=39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 6..=16) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
(0, 17..=33) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
(1..=8, 12) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(4, 1..=11) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
(6, 1..=6) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
_x if TX_DOTS.contains(&(row_index, result_cell_index)) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert_eq!(result_cell.bg, Color::Reset);
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_running_with_data_rx() {
let mut setup = test_setup(40, 10, true, true);
let mut rx = NetworkBandwidth::new();
for i in 0..=20 {
rx.push(2000 * i * (10 + 7 * i));
}
if let Some(item) = setup
.app_data
.lock()
.get_container_by_id(&ContainerId::from("1"))
{
item.rx = rx;
}
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 0..=5 | 34..=39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 6..=21) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
(0, 22..=33) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
(1..=8, 12) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(4, 1..=11) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
(6, 1..=6) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
_x if RX_DOTS.contains(&(row_index, result_cell_index)) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert_eq!(result_cell.bg, Color::Reset);
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_running_with_data_tx_and_rx() {
let mut setup = test_setup(40, 10, true, true);
let mut rx = NetworkBandwidth::new();
let mut tx = NetworkBandwidth::new();
for i in 0..=20 {
rx.push(2000 * i * (10 + 7 * i));
tx.push(200 * i * (10 + 7 * i));
}
if let Some(item) = setup
.app_data
.lock()
.get_container_by_id(&ContainerId::from("1"))
{
item.rx = rx;
item.tx = tx;
}
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 0..=3 | 36..=39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 4..=19) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
(0, 20..=35) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
(1..=8, 12) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(4, 1..=9) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
(6, 1..=10) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
_x if COMBINED_DOTS_TX.contains(&(row_index, result_cell_index)) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_TX);
}
_x if COMBINED_DOTS_RX.contains(&(row_index, result_cell_index)) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, COLOR_RX);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert_eq!(result_cell.bg, Color::Reset);
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_paused() {
let mut setup = test_setup(40, 10, true, true);
setup.app_data.lock().containers.items[0].state = State::Paused;
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 0..=7 | 31..=39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 8..=30) | (4 | 6, 1..=6) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Yellow);
}
(1..=8, 7) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert_eq!(result_cell.bg, Color::Reset);
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_dead() {
let mut setup = test_setup(40, 10, true, true);
setup.app_data.lock().containers.items[0].state = State::Dead;
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 0..=7 | 31..=39) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
(0, 8..=30) | (4 | 6, 1..=6) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::Red);
}
(1..=8, 7) => {
assert_eq!(result_cell.bg, Color::Reset);
assert_eq!(result_cell.fg, Color::White);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert_eq!(result_cell.bg, Color::Reset);
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_custom_colors() {
let mut colors = AppColors::new();
colors.chart_bandwidth.background = Color::White;
colors.chart_bandwidth.border = Color::Red;
colors.chart_bandwidth.max_rx = Color::Green;
colors.chart_bandwidth.max_tx = Color::Magenta;
colors.chart_bandwidth.title_rx = Color::LightGreen;
colors.chart_bandwidth.title_tx = Color::LightRed;
colors.chart_bandwidth.points_rx = Color::Black;
colors.chart_bandwidth.points_tx = Color::Blue;
colors.chart_bandwidth.y_axis = Color::Yellow;
let mut setup = test_setup(40, 10, true, true);
let mut rx = NetworkBandwidth::new();
let mut tx = NetworkBandwidth::new();
for i in 0..=20 {
rx.push(2000 * i * (10 + 7 * i));
tx.push(200 * i * (10 + 7 * i));
}
if let Some(item) = setup
.app_data
.lock()
.get_container_by_id(&ContainerId::from("1"))
{
item.rx = rx;
item.tx = tx;
}
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(setup.area, colors, f, &fd);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (row_index, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
match (row_index, result_cell_index) {
(9, _) | (1..=9, 0 | 39) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Red);
}
(0, 0..=3 | 36..=39) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Red);
}
(0, 4..=19) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::LightGreen);
}
(0, 20..=35) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::LightRed);
}
(1..=8, 12) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Yellow);
}
(4, 1..=11) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Green);
}
(6, 1..=10) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Magenta);
}
_x if COMBINED_DOTS_TX.contains(&(row_index, result_cell_index)) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Blue);
}
_x if COMBINED_DOTS_RX.contains(&(row_index, result_cell_index)) => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Black);
}
_ => {
assert_eq!(result_cell.bg, Color::White);
assert_eq!(result_cell.fg, Color::Reset);
}
}
}
}
}
}