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) } } } /// Create charts fn make_chart<'a, T: Stats + Display>( chart_variant: ChartVariant, colors: AppColors, current: &'a T, dataset: Vec>, 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))) // Add 0.01, so that max point is always visible? .bounds([0.0, max.get_value() + 0.01]), ) } /// Draw the cpu + mem charts 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_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1)); // #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64)); 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}, }, }; /// CPU and Memory charts used in multiple tests, based on data from above insert_chart_data() const _EXPECTED: [&str; 10] = [ "╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮", "│10.00%│ • ││100.00 kB│ •• │", "│ │ •• ││ │ •• │", "│ │ ••• ││ │ • • │", "│ │ • • ││ │ • • │", "│ │ • •• ││ │•• •• │", "│ │• • ││ │• • │", "│ │• • ││ │• • │", "│ │ ││ │ │", "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", ]; // co-ordinates of the dots from the cpu chart 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), ]; // co-ordinates of the dots from the memory chart 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] /// When status is Running, but not data, charts drawn without dots etc, colours correct 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] /// When status is Running, charts correctly drawn 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] /// Whens status paused, some text is now Yellow 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] /// When dead, text is red 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] /// Custom colos correctly applied to each part of the charts 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) { // border (0, 0..=13 | 26..=50 | 68..=79) | (9, _) | (1..=8, 0 | 39 | 40 | 79) => { assert_eq!(result_cell.fg, Color::Red); } // title (0, 14..=25 | 51..=67) => { assert_eq!(result_cell.fg, Color::Green); } // max label (1, 1..=6 | 41..=49) => { assert_eq!(result_cell.fg, Color::Magenta); } // data points xy if CPU_XY.contains(&xy) | MEM_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Black); } // y axis (1..=8, 7 | 50) => { assert_eq!(result_cell.fg, Color::Blue); } _ => { assert_eq!(result_cell.fg, Color::Reset); } } } } } }