use std::sync::Arc; use super::MARGIN; use parking_lot::Mutex; use ratatui::{ Frame, layout::{Alignment, Rect}, style::{Modifier, Style, Stylize}, text::{Line, Span}, widgets::{List, ListItem, Paragraph}, }; use crate::{ app_data::{AppData, ByteStats, Columns, ContainerItem, CpuStats}, config::AppColors, ui::{FrameData, GuiState, SelectablePanel}, }; use super::{CIRCLE, generate_block}; /// Format the container data to display nicely on the screen fn format_containers<'a>(colors: AppColors, i: &ContainerItem, widths: &Columns) -> Line<'a> { let state_style = Style::default().fg(i.state.get_color(colors)); Line::from(vec![ Span::styled( format!( "{:width$}{MARGIN}", i.cpu_stats.back().map_or_else(CpuStats::default, |f| *f), width = &widths.cpu.1.into() ), state_style, ), Span::styled( format!( "{:>width_current$} / {:>width_limit$}{MARGIN}", i.mem_stats.back().map_or_else(ByteStats::default, |f| *f), i.mem_limit, width_current = &widths.mem.1.into(), width_limit = &widths.mem.2.into() ), state_style, ), Span::styled( format!( "{:>width$}{MARGIN}", i.id.get_short(), width = &widths.id.1.into() ), colors.containers.text, ), Span::styled( format!( "{:width$}{MARGIN}", i.rx.current_total(), width = widths.net_rx.1.into() ), Style::default().fg(colors.containers.text_rx), ), Span::styled( format!( "{:>width$}{MARGIN}", i.tx.current_total(), width = widths.net_tx.1.into() ), Style::default().fg(colors.containers.text_tx), ), ]) } /// Draw the containers panel pub fn draw( app_data: &Arc>, area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData, gui_state: &Arc>, ) { let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Containers) .bg(colors.containers.background); let items = app_data .lock() .get_container_items() .iter() .map(|i| ListItem::new(format_containers(colors, i, &fd.columns))) .collect::>(); if items.is_empty() { let text = if fd.filter_term.is_some() { "no containers match filter" } else if fd.is_loading { &format!("loading {}", fd.loading_icon) } else { "no containers running" }; let paragraph = Paragraph::new(text) .block(block) .alignment(Alignment::Center); f.render_widget(paragraph, area); } else { let items = List::new(items) .block(block) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol(CIRCLE); f.render_stateful_widget(items, area, app_data.lock().get_container_state()); } } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use insta::assert_snapshot; use ratatui::style::{Color, Modifier}; use crate::{ app_data::{ContainerImage, ContainerName, ContainerStatus, State, StatefulList}, config::AppColors, ui::{ FrameData, draw_blocks::tests::{ BORDER_CHARS, COLOR_ORANGE, COLOR_RX, COLOR_TX, TuiTestSetup, get_result, test_setup, }, }, }; #[test] /// No containers, panel unselected, then selected, border color changes correctly fn test_draw_blocks_containers_none() { let mut setup = test_setup(40, 6, true, true); setup.app_data.lock().containers = StatefulList::new(vec![]); setup .gui_state .lock() .selectable_panel_next(&setup.app_data); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); assert_snapshot!(setup.terminal.backend()); for (_, result_row) in get_result(&setup) { for result_cell in result_row { if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::Gray); } } } setup .gui_state .lock() .selectable_panel_previous(&setup.app_data); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); for (_, result_row) in get_result(&setup) { for result_cell in result_row { if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::LightCyan); } } } } #[test] /// Containers panel drawn, selected line is bold, border is blue fn test_draw_blocks_containers_selected_bold() { let mut setup = test_setup(130, 6, true, true); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &setup.fd, &setup.gui_state, ); }) .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() { if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::LightCyan); } let not_bold = || assert!(result_cell.modifier.is_empty()); if row_index == 1 { match result_cell_index { 0 | 2 | 129 => { not_bold(); } _ => { assert_eq!(result_cell.modifier, Modifier::BOLD); } } } else { not_bold(); } } } // Change selected panel, border is now no longer blue setup .gui_state .lock() .selectable_panel_next(&setup.app_data); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); for (_, result_row) in get_result(&setup) { for result_cell in result_row { if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::Gray); } } } } #[test] /// Columns on all rows are coloured correctly fn test_draw_blocks_containers_colors() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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) { //border (0 | 5, _) | (1..=4, 0 | 129) => { assert_eq!(result_cell.fg, Color::LightCyan); } // name, id, image column (1..=3, 4..=17 | 71..=91) => { assert_eq!(result_cell.fg, Color::Blue); } // state, status, cpu, memory column (1..=3, 18..=70) => { assert_eq!(result_cell.fg, Color::Green); } // rx column (1..=3, 92..=101) => { assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 102..=111) => { assert_eq!(result_cell.fg, COLOR_TX); } _ => assert_eq!(result_cell.fg, Color::Reset), } } } } #[test] /// Long container + image name is truncated correctly fn test_draw_blocks_containers_long_name_image() { let mut setup = test_setup(170, 6, true, true); setup.app_data.lock().containers.items[0].name = ContainerName::from("a_long_container_name_for_the_purposes_of_this_test"); setup.app_data.lock().containers.items[0].image = ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup.app_data.lock().containers.items[0].state = State::Paused; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); assert_snapshot!(setup.terminal.backend()); } // Check that the correct colour is applied to the state/status/cpu/memory section fn check_colour(setup: &TuiTestSetup, color: Color) { 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) { // border (0 | 5, _) | (1..=4, 0 | 129) => { assert_eq!(result_cell.fg, Color::LightCyan); } // name, id, image column (1..=3, 4..=17 | 71..=91) => { assert_eq!(result_cell.fg, Color::Blue); } // state, status, cpu, memory column of the first row (1, 18..=70) => { assert_eq!(result_cell.fg, color); } // state, status, cpu, memory column (2..=3, 4..=77) => { assert_eq!(result_cell.fg, Color::Green); } // rx column (1..=3, 92..=101) => { assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 102..=111) => { assert_eq!(result_cell.fg, COLOR_TX); } _ => assert_eq!(result_cell.fg, Color::Reset), } } } } #[test] /// When container is paused, correct colors displayed fn test_draw_blocks_containers_paused() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup.app_data.lock().containers.items[0].state = State::Paused; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); check_colour(&setup, Color::Yellow); assert_snapshot!(setup.terminal.backend()); } #[test] /// When container is dead, correct colors displayed fn test_draw_blocks_containers_dead() { let mut setup = test_setup(130, 6, true, true); setup.app_data.lock().containers.items[0].state = State::Dead; let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); check_colour(&setup, Color::Red); assert_snapshot!(setup.terminal.backend()); } #[test] /// When container is exited, correct colors displayed fn test_draw_blocks_containers_exited() { let mut setup = test_setup(130, 6, true, true); setup.app_data.lock().containers.items[0].state = State::Exited; let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); check_colour(&setup, Color::Red); assert_snapshot!(setup.terminal.backend()); } #[test] /// When container is paused, correct colors displayed fn test_draw_blocks_containers_removing() { let mut setup = test_setup(130, 6, true, true); setup.app_data.lock().containers.items[0].state = State::Removing; let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); check_colour(&setup, Color::LightRed); assert_snapshot!(setup.terminal.backend()); } #[test] /// When container state is restarting, correct colors displayed fn test_draw_blocks_containers_restarting() { let mut setup = test_setup(130, 6, true, true); setup.app_data.lock().containers.items[0].state = State::Restarting; let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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) { // border (0 | 5, _) | (1..=4, 0 | 129) => { assert_eq!(result_cell.fg, Color::LightCyan); } // name, id, image column (1..=3, 4..=17 | 74..=94) => { assert_eq!(result_cell.fg, Color::Blue); } // state, status, cpu, memory column of the first row (1, 18..=73) => { assert_eq!(result_cell.fg, Color::LightGreen); } // state, status, cpu, memory column (2..=3, 18..=73) => { assert_eq!(result_cell.fg, Color::Green); } // rx column (1..=3, 95..=104) => { assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 105..=114) => { assert_eq!(result_cell.fg, COLOR_TX); } _ => { assert_eq!(result_cell.fg, Color::Reset); } } } } } #[test] /// When container state is unhealthy, correct colors displayed fn test_draw_blocks_containers_unhealthy() { let mut setup = test_setup(130, 6, true, true); let status = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); setup.app_data.lock().containers.items[0].state = State::from(("running", &status)); setup.app_data.lock().containers.items[0].status = status; let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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) { // border (0 | 5, _) | (1..=4, 0 | 129) => { assert_eq!(result_cell.fg, Color::LightCyan); } // name, id, image column (1..=3, 4..=17 | 83..=103) => { assert_eq!(result_cell.fg, Color::Blue); } // state, status, cpu, memory column of the first row (1, 18..=82) => { assert_eq!(result_cell.fg, COLOR_ORANGE); } // state, status, cpu, memory column (2..=3, 18..=82) => { assert_eq!(result_cell.fg, Color::Green); } // rx column (1..=3, 104..=113) => { assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 114..=123) => { assert_eq!(result_cell.fg, COLOR_TX); } _ => assert_eq!(result_cell.fg, Color::Reset), } } } } #[test] /// When container state is unknown, correct colors displayed fn test_draw_blocks_containers_unknown() { let mut setup = test_setup(130, 6, true, true); setup.app_data.lock().containers.items[0].state = State::Unknown; let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let colors = setup.app_data.lock().config.app_colors; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .unwrap(); check_colour(&setup, Color::Red); assert_snapshot!(setup.terminal.backend()); } #[test] /// Custom colors applied correctly fn test_draw_blocks_containers_custom_colors() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.borders.selected = Color::Green; colors.containers.background = Color::Black; colors.containers.text = Color::Yellow; colors.containers.text_rx = Color::Red; colors.containers.text_tx = Color::Blue; colors.container_state.running_healthy = Color::Magenta; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { // The highlight symbol can't correctly be colored if (row_index, result_cell_index) != (1, 2) { assert_eq!(result_cell.bg, Color::Black); } match (row_index, result_cell_index) { //border (0 | 5, _) | (1..=4, 0 | 129) => { assert_eq!(result_cell.fg, Color::Green); } // name, id, image column (1..=3, 4..=17 | 71..=91) => { assert_eq!(result_cell.fg, Color::Yellow); } // state, status, cpu, memory column (1..=3, 18..=70) => { assert_eq!(result_cell.fg, Color::Magenta); } // rx column (1..=3, 92..=101) => { assert_eq!(result_cell.fg, Color::Red); } // tx column (1..=3, 102..=111) => { assert_eq!(result_cell.fg, Color::Blue); } _ => assert_eq!(result_cell.fg, Color::Reset), } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_healthy() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); // Healthy let mut colors = AppColors::new(); colors.container_state.running_healthy = Color::Magenta; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1..=3, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Magenta); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_unhealthy() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.running_unhealthy = Color::Red; let status = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); setup.app_data.lock().containers.items[0].state = State::from(("running", &status)); setup.app_data.lock().containers.items[0].status = status; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Red); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_dead() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.dead = Color::Magenta; setup.app_data.lock().containers.items[0].state = State::Dead; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Magenta); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_exited() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.exited = Color::Gray; setup.app_data.lock().containers.items[0].state = State::Exited; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Gray); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_paused() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.paused = Color::Cyan; setup.app_data.lock().containers.items[0].state = State::Paused; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Cyan); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_removing() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.removing = Color::White; setup.app_data.lock().containers.items[0].state = State::Removing; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::White); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_restarting() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.restarting = Color::LightYellow; setup.app_data.lock().containers.items[0].state = State::Restarting; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::LightYellow); } } } } #[test] /// Make sure that the state has the correctly color applied to it fn test_draw_blocks_containers_custom_colors_state_unknown() { let mut setup = test_setup(130, 6, true, true); let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let mut colors = AppColors::new(); colors.container_state.unknown = COLOR_ORANGE; setup.app_data.lock().containers.items[0].state = State::Unknown; setup .terminal .draw(|f| { super::draw( &setup.app_data, setup.area, colors, f, &fd, &setup.gui_state, ); }) .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() { if let (1, 18..=70) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, COLOR_ORANGE); } } } } }