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};
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.name.to_string(),
width = widths.name.1.into()
),
colors.containers.text,
),
Span::styled(
format!(
"{:<width$}{MARGIN}",
i.state.to_string(),
width = widths.state.1.into()
),
state_style,
),
Span::styled(
format!(
"{:<width$}{MARGIN}",
i.status.get(),
width = &widths.status.1.into()
),
state_style,
),
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.image.to_string(),
width = widths.image.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),
),
])
}
pub fn draw(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
colors: AppColors,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
) {
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::<Vec<_>>();
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]
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]
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();
}
}
}
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]
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) {
(0 | 5, _) | (1..=4, 0 | 129) => {
assert_eq!(result_cell.fg, Color::LightCyan);
}
(1..=3, 4..=17 | 71..=91) => {
assert_eq!(result_cell.fg, Color::Blue);
}
(1..=3, 18..=70) => {
assert_eq!(result_cell.fg, Color::Green);
}
(1..=3, 92..=101) => {
assert_eq!(result_cell.fg, COLOR_RX);
}
(1..=3, 102..=111) => {
assert_eq!(result_cell.fg, COLOR_TX);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
}
}
}
#[test]
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());
}
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) {
(0 | 5, _) | (1..=4, 0 | 129) => {
assert_eq!(result_cell.fg, Color::LightCyan);
}
(1..=3, 4..=17 | 71..=91) => {
assert_eq!(result_cell.fg, Color::Blue);
}
(1, 18..=70) => {
assert_eq!(result_cell.fg, color);
}
(2..=3, 4..=77) => {
assert_eq!(result_cell.fg, Color::Green);
}
(1..=3, 92..=101) => {
assert_eq!(result_cell.fg, COLOR_RX);
}
(1..=3, 102..=111) => {
assert_eq!(result_cell.fg, COLOR_TX);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
}
}
}
#[test]
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]
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]
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]
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]
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) {
(0 | 5, _) | (1..=4, 0 | 129) => {
assert_eq!(result_cell.fg, Color::LightCyan);
}
(1..=3, 4..=17 | 74..=94) => {
assert_eq!(result_cell.fg, Color::Blue);
}
(1, 18..=73) => {
assert_eq!(result_cell.fg, Color::LightGreen);
}
(2..=3, 18..=73) => {
assert_eq!(result_cell.fg, Color::Green);
}
(1..=3, 95..=104) => {
assert_eq!(result_cell.fg, COLOR_RX);
}
(1..=3, 105..=114) => {
assert_eq!(result_cell.fg, COLOR_TX);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
}
}
}
}
}
#[test]
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) {
(0 | 5, _) | (1..=4, 0 | 129) => {
assert_eq!(result_cell.fg, Color::LightCyan);
}
(1..=3, 4..=17 | 83..=103) => {
assert_eq!(result_cell.fg, Color::Blue);
}
(1, 18..=82) => {
assert_eq!(result_cell.fg, COLOR_ORANGE);
}
(2..=3, 18..=82) => {
assert_eq!(result_cell.fg, Color::Green);
}
(1..=3, 104..=113) => {
assert_eq!(result_cell.fg, COLOR_RX);
}
(1..=3, 114..=123) => {
assert_eq!(result_cell.fg, COLOR_TX);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
}
}
}
#[test]
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]
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() {
if (row_index, result_cell_index) != (1, 2) {
assert_eq!(result_cell.bg, Color::Black);
}
match (row_index, result_cell_index) {
(0 | 5, _) | (1..=4, 0 | 129) => {
assert_eq!(result_cell.fg, Color::Green);
}
(1..=3, 4..=17 | 71..=91) => {
assert_eq!(result_cell.fg, Color::Yellow);
}
(1..=3, 18..=70) => {
assert_eq!(result_cell.fg, Color::Magenta);
}
(1..=3, 92..=101) => {
assert_eq!(result_cell.fg, Color::Red);
}
(1..=3, 102..=111) => {
assert_eq!(result_cell.fg, Color::Blue);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
}
}
}
#[test]
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));
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]
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]
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]
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]
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]
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]
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]
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);
}
}
}
}
}