use std::fmt::Display;
use ratatui::{
Frame,
layout::{Alignment, Direction, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols,
text::Span,
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
};
use super::{CONSTRAINT_50_50, FrameData};
use crate::{
app_data::{State, Stats},
config::AppColors,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ChartVariant {
Cpu,
Memory,
}
impl ChartVariant {
const fn name(self) -> &'static str {
match self {
Self::Cpu => "cpu",
Self::Memory => "memory",
}
}
const fn get_title_color(self, colors: AppColors, state: State) -> Color {
if state.is_healthy() {
match self {
Self::Cpu => colors.chart_cpu.title,
Self::Memory => colors.chart_memory.title,
}
} else {
state.get_color(colors)
}
}
const fn get_bg_color(self, colors: AppColors) -> Color {
match self {
Self::Cpu => colors.chart_cpu.background,
Self::Memory => colors.chart_memory.background,
}
}
const fn get_border_color(self, colors: AppColors) -> Color {
match self {
Self::Cpu => colors.chart_cpu.border,
Self::Memory => colors.chart_memory.border,
}
}
const fn get_y_axis_color(self, colors: AppColors) -> Color {
match self {
Self::Cpu => colors.chart_cpu.y_axis,
Self::Memory => colors.chart_memory.y_axis,
}
}
const fn get_max_color(self, colors: AppColors, state: State) -> Color {
if state.is_healthy() {
match self {
Self::Cpu => colors.chart_cpu.max,
Self::Memory => colors.chart_memory.max,
}
} else {
state.get_color(colors)
}
}
}
fn make_chart<'a, T: Stats + Display>(
chart_variant: ChartVariant,
colors: AppColors,
current: &'a T,
dataset: Vec<Dataset<'a>>,
max: &'a T,
state: State,
) -> Chart<'a> {
let max_color = chart_variant.get_max_color(colors, state);
Chart::new(dataset)
.bg(chart_variant.get_bg_color(colors))
.block(
Block::default()
.style(Style::default().bg(chart_variant.get_bg_color(colors)))
.title_alignment(Alignment::Center)
.title(Span::styled(
format!(" {} {current} ", chart_variant.name()),
Style::default()
.fg(chart_variant.get_title_color(colors, state))
.add_modifier(Modifier::BOLD),
))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(chart_variant.get_border_color(colors))),
)
.x_axis(Axis::default().bounds([0.00, 60.0]))
.y_axis(
Axis::default()
.labels(vec![
Span::styled("", Style::default().fg(max_color)),
Span::styled(
format!("{max}"),
Style::default().add_modifier(Modifier::BOLD).fg(max_color),
),
])
.style(Style::new().fg(chart_variant.get_y_axis_color(colors)))
.bounds([0.0, max.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 area = Layout::default()
.direction(Direction::Horizontal)
.constraints(CONSTRAINT_50_50)
.split(area);
let cpu_dataset = vec![
Dataset::default()
.marker(symbols::Marker::Dot)
.style(Style::default().fg(colors.chart_cpu.points))
.graph_type(GraphType::Line)
.data(&x.cpu.dataset),
];
let mem_dataset = vec![
Dataset::default()
.marker(symbols::Marker::Dot)
.style(Style::default().fg(colors.chart_memory.points))
.graph_type(GraphType::Line)
.data(&x.memory.dataset),
];
let cpu_chart = make_chart(
ChartVariant::Cpu,
colors,
&x.cpu.current,
cpu_dataset,
&x.cpu.max,
x.state,
);
let mem_chart = make_chart(
ChartVariant::Memory,
colors,
&x.memory.current,
mem_dataset,
&x.memory.max,
x.state,
);
f.render_widget(cpu_chart, area[0]);
f.render_widget(mem_chart, area[1]);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use insta::assert_snapshot;
use ratatui::style::{Color, Modifier};
use crate::{
app_data::State,
config::AppColors,
ui::{
FrameData,
draw_blocks::tests::{COLOR_ORANGE, get_result, insert_all_chart_data, test_setup},
},
};
const _EXPECTED: [&str; 10] = [
"â•───────────── cpu 03.00% ─────────────╮â•────────── memory 30.00 kB ───────────╮",
"│10.00%│ • ││100.00 kB│ •• │",
"│ │ •• ││ │ •• │",
"│ │ ••• ││ │ • • │",
"│ │ • • ││ │ • • │",
"│ │ • •• ││ │•• •• │",
"│ │• • ││ │• • │",
"│ │• • ││ │• • │",
"│ │ ││ │ │",
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯",
];
const CPU_XY: [(usize, usize); 16] = [
(1, 13),
(2, 12),
(2, 13),
(3, 11),
(3, 13),
(4, 11),
(4, 13),
(5, 10),
(5, 13),
(6, 9),
(6, 13),
(6, 14),
(7, 8),
(7, 9),
(7, 13),
(7, 14),
];
const MEM_XY: [(usize, usize); 14] = [
(1, 55),
(2, 54),
(2, 55),
(3, 54),
(3, 55),
(4, 53),
(4, 55),
(5, 52),
(5, 53),
(5, 56),
(6, 52),
(6, 56),
(7, 51),
(7, 56),
];
#[test]
fn test_draw_blocks_charts_running_none() {
let mut setup = test_setup(80, 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) {
(0, 14..=25 | 52..=67) => {
assert_eq!(result_cell.fg, Color::Green);
assert_eq!(result_cell.modifier, Modifier::BOLD);
}
(1, 1..=6 | 41..=47) => {
assert_eq!(result_cell.fg, COLOR_ORANGE);
assert_eq!(result_cell.modifier, Modifier::BOLD);
}
(2..=8, 1..=6 | 8..=38 | 49..=78 | 41..=47) | (1, 8..=38 | 49..=78) => {
assert_eq!(result_cell.fg, Color::Reset);
assert!(result_cell.modifier.is_empty());
}
_ => {
assert_eq!(result_cell.fg, Color::White);
assert!(result_cell.modifier.is_empty());
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_running_some() {
let mut setup = test_setup(80, 10, true, true);
insert_all_chart_data(&setup);
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) {
(0, 14..=25 | 51..=67) => {
assert_eq!(result_cell.fg, Color::Green);
assert_eq!(result_cell.modifier, Modifier::BOLD);
}
(1, 1..=6 | 41..=49) => {
assert_eq!(result_cell.fg, COLOR_ORANGE);
assert_eq!(result_cell.modifier, Modifier::BOLD);
}
xy if CPU_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Magenta);
assert!(result_cell.modifier.is_empty());
}
xy if MEM_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Cyan);
assert!(result_cell.modifier.is_empty());
}
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
assert_eq!(result_cell.fg, Color::White);
assert!(result_cell.modifier.is_empty());
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert!(result_cell.modifier.is_empty());
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_paused() {
let mut setup = test_setup(80, 10, true, true);
insert_all_chart_data(&setup);
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) {
(0, 14..=25 | 51..=67) | (1, 1..=6 | 41..=49) => {
assert_eq!(result_cell.fg, Color::Yellow);
assert_eq!(result_cell.modifier, Modifier::BOLD);
}
xy if CPU_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Magenta);
assert!(result_cell.modifier.is_empty());
}
xy if MEM_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Cyan);
assert!(result_cell.modifier.is_empty());
}
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
assert_eq!(result_cell.fg, Color::White);
assert!(result_cell.modifier.is_empty());
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert!(result_cell.modifier.is_empty());
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_dead() {
let mut setup = test_setup(80, 10, true, true);
insert_all_chart_data(&setup);
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) {
(0, 14..=25 | 51..=67) | (1, 1..=6 | 41..=49) => {
assert_eq!(result_cell.fg, Color::Red);
assert_eq!(result_cell.modifier, Modifier::BOLD);
}
xy if CPU_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Magenta);
assert!(result_cell.modifier.is_empty());
}
xy if MEM_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Cyan);
assert!(result_cell.modifier.is_empty());
}
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
assert_eq!(result_cell.fg, Color::White);
assert!(result_cell.modifier.is_empty());
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
assert!(result_cell.modifier.is_empty());
}
}
}
}
}
#[test]
fn test_draw_blocks_charts_custom_colors() {
let mut colors = AppColors::new();
colors.chart_cpu.background = Color::White;
colors.chart_cpu.border = Color::Red;
colors.chart_cpu.title = Color::Green;
colors.chart_cpu.max = Color::Magenta;
colors.chart_cpu.points = Color::Black;
colors.chart_cpu.y_axis = Color::Blue;
colors.chart_memory.background = Color::White;
colors.chart_memory.border = Color::Red;
colors.chart_memory.title = Color::Green;
colors.chart_memory.max = Color::Magenta;
colors.chart_memory.points = Color::Black;
colors.chart_memory.y_axis = Color::Blue;
let mut setup = test_setup(80, 10, true, true);
insert_all_chart_data(&setup);
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() {
assert_eq!(result_cell.bg, Color::White);
match (row_index, result_cell_index) {
(0, 0..=13 | 26..=50 | 68..=79) | (9, _) | (1..=8, 0 | 39 | 40 | 79) => {
assert_eq!(result_cell.fg, Color::Red);
}
(0, 14..=25 | 51..=67) => {
assert_eq!(result_cell.fg, Color::Green);
}
(1, 1..=6 | 41..=49) => {
assert_eq!(result_cell.fg, Color::Magenta);
}
xy if CPU_XY.contains(&xy) | MEM_XY.contains(&xy) => {
assert_eq!(result_cell.fg, Color::Black);
}
(1..=8, 7 | 50) => {
assert_eq!(result_cell.fg, Color::Blue);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
}
}
}
}
}
}