578ed9f085
Expect lint was causing issues with crates/docker builds, revert until fix is found
3560 lines
174 KiB
Rust
3560 lines
174 KiB
Rust
use parking_lot::Mutex;
|
|
use ratatui::{
|
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
|
style::{Color, Modifier, Style, Stylize},
|
|
symbols,
|
|
text::{Line, Span},
|
|
widgets::{
|
|
Axis, Block, BorderType, Borders, Chart, Clear, Dataset, GraphType, List, ListItem,
|
|
Paragraph,
|
|
},
|
|
Frame,
|
|
};
|
|
use std::{default::Default, time::Instant};
|
|
use std::{fmt::Display, sync::Arc};
|
|
|
|
use crate::app_data::{ContainerItem, ContainerName, FilterBy, Header, SortedOrder};
|
|
use crate::{
|
|
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
|
app_error::AppError,
|
|
};
|
|
|
|
use super::{
|
|
gui_state::{BoxLocation, DeleteButton, Region},
|
|
FrameData, Status, ORANGE,
|
|
};
|
|
use super::{GuiState, SelectablePanel};
|
|
|
|
const NAME_TEXT: &str = r#"
|
|
88
|
|
88
|
|
88
|
|
,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba,
|
|
a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8
|
|
8b d8 )888( 8888[ 8PP""""""" 88
|
|
"8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88
|
|
`"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 "#;
|
|
|
|
const NAME: &str = env!("CARGO_PKG_NAME");
|
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
|
const MARGIN: &str = " ";
|
|
const RIGHT_ARROW: &str = "▶ ";
|
|
const CIRCLE: &str = "⚪ ";
|
|
|
|
const CONSTRAINT_50_50: [Constraint; 2] = [Constraint::Percentage(50), Constraint::Percentage(50)];
|
|
const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)];
|
|
const CONSTRAINT_POPUP: [Constraint; 5] = [
|
|
Constraint::Min(2),
|
|
Constraint::Max(1),
|
|
Constraint::Max(1),
|
|
Constraint::Max(3),
|
|
Constraint::Min(1),
|
|
];
|
|
|
|
const CONSTRAINT_BUTTONS: [Constraint; 5] = [
|
|
Constraint::Percentage(10),
|
|
Constraint::Percentage(35),
|
|
Constraint::Percentage(10),
|
|
Constraint::Percentage(35),
|
|
Constraint::Percentage(10),
|
|
];
|
|
|
|
/// From a given &str, return the maximum number of chars on a single line
|
|
fn max_line_width(text: &str) -> usize {
|
|
text.lines()
|
|
.map(|i| i.chars().count())
|
|
.max()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Generate block, add a border if is the selected panel,
|
|
/// add custom title based on state of each panel
|
|
fn generate_block<'a>(
|
|
app_data: &Arc<Mutex<AppData>>,
|
|
area: Rect,
|
|
fd: &FrameData,
|
|
gui_state: &Arc<Mutex<GuiState>>,
|
|
panel: SelectablePanel,
|
|
) -> Block<'a> {
|
|
gui_state
|
|
.lock()
|
|
.update_region_map(Region::Panel(panel), area);
|
|
let mut title = match panel {
|
|
SelectablePanel::Containers => {
|
|
format!("{}{}", panel.title(), app_data.lock().container_title())
|
|
}
|
|
SelectablePanel::Logs => {
|
|
format!("{}{}", panel.title(), app_data.lock().get_log_title())
|
|
}
|
|
SelectablePanel::Commands => String::new(),
|
|
};
|
|
if !title.is_empty() {
|
|
title = format!(" {title} ");
|
|
}
|
|
let mut block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded)
|
|
.title(title);
|
|
if fd.selected_panel == panel && !gui_state.lock().status_contains(&[Status::Filter]) {
|
|
block = block.border_style(Style::default().fg(Color::LightCyan));
|
|
}
|
|
block
|
|
}
|
|
|
|
/// Draw the command panel
|
|
pub fn commands(
|
|
app_data: &Arc<Mutex<AppData>>,
|
|
area: Rect,
|
|
f: &mut Frame,
|
|
fd: &FrameData,
|
|
gui_state: &Arc<Mutex<GuiState>>,
|
|
) {
|
|
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
|
|
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
|
|
i.iter()
|
|
.map(|c| {
|
|
let lines = Line::from(vec![Span::styled(
|
|
c.to_string(),
|
|
Style::default().fg(c.get_color()),
|
|
)]);
|
|
ListItem::new(lines)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
});
|
|
|
|
if let Some(i) = app_data.lock().get_control_state() {
|
|
let items = List::new(items)
|
|
.block(block)
|
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
|
.highlight_symbol(RIGHT_ARROW);
|
|
f.render_stateful_widget(items, area, i);
|
|
} else {
|
|
let paragraph = Paragraph::new("").block(block).alignment(Alignment::Center);
|
|
f.render_widget(paragraph, area);
|
|
}
|
|
}
|
|
|
|
/// Format the container data to display nicely on the screen
|
|
fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
|
let state_style = Style::default().fg(i.state.get_color());
|
|
let blue = Style::default().fg(Color::Blue);
|
|
|
|
// Truncate?
|
|
Line::from(vec![
|
|
Span::styled(
|
|
format!(
|
|
"{:<width$}{MARGIN}",
|
|
i.name.to_string(),
|
|
width = widths.name.1.into()
|
|
),
|
|
blue,
|
|
),
|
|
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().unwrap_or(&CpuStats::default()),
|
|
width = &widths.cpu.1.into()
|
|
),
|
|
state_style,
|
|
),
|
|
Span::styled(
|
|
format!(
|
|
"{:>width_current$} / {:>width_limit$}{MARGIN}",
|
|
i.mem_stats.back().unwrap_or(&ByteStats::default()),
|
|
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()
|
|
),
|
|
blue,
|
|
),
|
|
Span::styled(
|
|
format!(
|
|
"{:<width$}{MARGIN}",
|
|
i.image.to_string(),
|
|
width = widths.image.1.into()
|
|
),
|
|
blue,
|
|
),
|
|
Span::styled(
|
|
format!("{:>width$}{MARGIN}", i.rx, width = widths.net_rx.1.into()),
|
|
Style::default().fg(Color::Rgb(255, 233, 193)),
|
|
),
|
|
Span::styled(
|
|
format!("{:>width$}{MARGIN}", i.tx, width = widths.net_tx.1.into()),
|
|
Style::default().fg(Color::Rgb(205, 140, 140)),
|
|
),
|
|
])
|
|
}
|
|
|
|
/// Draw the containers panel
|
|
pub fn containers(
|
|
app_data: &Arc<Mutex<AppData>>,
|
|
area: Rect,
|
|
f: &mut Frame,
|
|
fd: &FrameData,
|
|
gui_state: &Arc<Mutex<GuiState>>,
|
|
) {
|
|
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
|
|
|
|
let items = app_data
|
|
.lock()
|
|
.get_container_items()
|
|
.iter()
|
|
.map(|i| ListItem::new(format_containers(i, &fd.columns)))
|
|
.collect::<Vec<_>>();
|
|
|
|
if items.is_empty() {
|
|
let text = if app_data.lock().get_filter_term().is_some() {
|
|
"no containers match filter"
|
|
} else if gui_state.lock().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());
|
|
}
|
|
}
|
|
|
|
/// Draw the logs panel
|
|
pub fn logs(
|
|
app_data: &Arc<Mutex<AppData>>,
|
|
area: Rect,
|
|
f: &mut Frame,
|
|
fd: &FrameData,
|
|
gui_state: &Arc<Mutex<GuiState>>,
|
|
) {
|
|
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs);
|
|
if fd.init {
|
|
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
|
.style(Style::default())
|
|
.block(block)
|
|
.alignment(Alignment::Center);
|
|
f.render_widget(paragraph, area);
|
|
} else {
|
|
let logs = app_data.lock().get_logs();
|
|
|
|
if logs.is_empty() {
|
|
let paragraph = Paragraph::new("no logs found")
|
|
.block(block)
|
|
.alignment(Alignment::Center);
|
|
f.render_widget(paragraph, area);
|
|
} else {
|
|
let items = List::new(logs)
|
|
.block(block)
|
|
.highlight_symbol(RIGHT_ARROW)
|
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
|
// This should always return Some, as logs is not empty
|
|
if let Some(log_state) = app_data.lock().get_log_state() {
|
|
f.render_stateful_widget(items, area, log_state);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display the ports in a formatted list
|
|
pub fn ports(
|
|
f: &mut Frame,
|
|
area: Rect,
|
|
app_data: &Arc<Mutex<AppData>>,
|
|
max_lens: (usize, usize, usize),
|
|
) {
|
|
let ports = app_data.lock().get_selected_ports();
|
|
if let Some(ports) = ports {
|
|
let block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded)
|
|
.title_alignment(Alignment::Center)
|
|
.title(Span::styled(
|
|
" ports ",
|
|
Style::default()
|
|
.fg(ports.1.get_color())
|
|
.add_modifier(Modifier::BOLD),
|
|
));
|
|
|
|
let (ip, private, public) = max_lens;
|
|
|
|
if ports.0.is_empty() {
|
|
let text = match ports.1 {
|
|
State::Running(_) | State::Paused | State::Restarting => "no ports",
|
|
_ => "",
|
|
};
|
|
let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
|
|
.alignment(Alignment::Center)
|
|
.block(block);
|
|
f.render_widget(paragraph, area);
|
|
} else {
|
|
let mut output = vec![Line::from(
|
|
Span::from(format!(
|
|
"{:>ip$}{:>private$}{:>public$}",
|
|
"ip", "private", "public"
|
|
))
|
|
.fg(Color::Yellow),
|
|
)];
|
|
for item in &ports.0 {
|
|
let fg = Color::White;
|
|
let strings = item.print();
|
|
|
|
let line = vec![
|
|
Span::from(format!("{:>ip$}", strings.0)).fg(fg),
|
|
Span::from(format!("{:>private$}", strings.1)).fg(fg),
|
|
Span::from(format!("{:>public$}", strings.2)).fg(fg),
|
|
];
|
|
output.push(Line::from(line));
|
|
}
|
|
let paragraph = Paragraph::new(output).block(block);
|
|
f.render_widget(paragraph, area);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Draw the cpu + mem charts
|
|
pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) {
|
|
let cpu_mem = app_data.lock().get_chart_data();
|
|
if let Some((cpu, mem)) = cpu_mem {
|
|
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(Color::Magenta))
|
|
.graph_type(GraphType::Line)
|
|
.data(&cpu.0)];
|
|
let mem_dataset = vec![Dataset::default()
|
|
.marker(symbols::Marker::Dot)
|
|
.style(Style::default().fg(Color::Cyan))
|
|
.graph_type(GraphType::Line)
|
|
.data(&mem.0)];
|
|
|
|
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(cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1);
|
|
let mem_chart = make_chart(mem.2, "memory", mem_dataset, &mem_stats, &mem.1);
|
|
|
|
f.render_widget(cpu_chart, area[0]);
|
|
f.render_widget(mem_chart, area[1]);
|
|
}
|
|
}
|
|
|
|
/// Create charts
|
|
fn make_chart<'a, T: Stats + Display>(
|
|
state: State,
|
|
name: &'a str,
|
|
dataset: Vec<Dataset<'a>>,
|
|
current: &'a T,
|
|
max: &'a T,
|
|
) -> Chart<'a> {
|
|
let title_color = state.get_color();
|
|
let label_color = match state {
|
|
State::Running(_) => ORANGE,
|
|
_ => state.get_color(),
|
|
};
|
|
Chart::new(dataset)
|
|
.block(
|
|
Block::default()
|
|
.title_alignment(Alignment::Center)
|
|
.title(Span::styled(
|
|
format!(" {name} {current} "),
|
|
Style::default()
|
|
.fg(title_color)
|
|
.add_modifier(Modifier::BOLD),
|
|
))
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded),
|
|
)
|
|
.x_axis(
|
|
Axis::default()
|
|
.style(Style::default().fg(title_color))
|
|
.bounds([0.00, 60.0]),
|
|
)
|
|
.y_axis(
|
|
Axis::default()
|
|
.labels(vec![
|
|
Span::styled("", Style::default().fg(label_color)),
|
|
Span::styled(
|
|
format!("{max}"),
|
|
Style::default()
|
|
.add_modifier(Modifier::BOLD)
|
|
.fg(label_color),
|
|
),
|
|
])
|
|
// Add 0.01, so that max point is always visible?
|
|
.bounds([0.0, max.get_value() + 0.01]),
|
|
)
|
|
}
|
|
|
|
/// Create the filter_by by spans, coloured dependant on which one is selected
|
|
fn filter_by_spans(app_data: &Arc<Mutex<AppData>>) -> [Span; 4] {
|
|
let filter_by = app_data.lock().get_filter_by();
|
|
|
|
let selected = Style::default().bg(Color::Gray).fg(Color::Black);
|
|
let not_selected = Style::default().bg(Color::Reset).fg(Color::Reset);
|
|
|
|
// This should be refactored somehow
|
|
let name = [" Name ", " Image ", " Status ", " All "];
|
|
|
|
match filter_by {
|
|
FilterBy::Name => [
|
|
Span::styled(name[0], selected),
|
|
Span::styled(name[1], not_selected),
|
|
Span::styled(name[2], not_selected),
|
|
Span::styled(name[3], not_selected),
|
|
],
|
|
FilterBy::Image => [
|
|
Span::styled(name[0], not_selected),
|
|
Span::styled(name[1], selected),
|
|
Span::styled(name[2], not_selected),
|
|
Span::styled(name[3], not_selected),
|
|
],
|
|
FilterBy::Status => [
|
|
Span::styled(name[0], not_selected),
|
|
Span::styled(name[1], not_selected),
|
|
Span::styled(name[2], selected),
|
|
Span::styled(name[3], not_selected),
|
|
],
|
|
FilterBy::All => [
|
|
Span::styled(name[0], not_selected),
|
|
Span::styled(name[1], not_selected),
|
|
Span::styled(name[2], not_selected),
|
|
Span::styled(name[3], selected),
|
|
],
|
|
}
|
|
}
|
|
|
|
/// Draw the filter bar
|
|
pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc<Mutex<AppData>>) {
|
|
let style_but = Style::default().fg(Color::Black).bg(Color::Magenta);
|
|
let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset);
|
|
|
|
let mut line = vec![
|
|
Span::styled(" Esc ", style_but),
|
|
Span::styled(" clear ", style_desc),
|
|
Span::styled(" ← by → ", style_but),
|
|
Span::from(" "),
|
|
];
|
|
line.extend_from_slice(&filter_by_spans(app_data));
|
|
line.extend_from_slice(&[
|
|
Span::styled(
|
|
" term: ",
|
|
Style::default()
|
|
.fg(Color::Magenta)
|
|
.add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::styled(
|
|
app_data
|
|
.lock()
|
|
.get_filter_term()
|
|
.map_or(String::new(), std::borrow::ToOwned::to_owned),
|
|
Style::default().fg(Color::Gray),
|
|
),
|
|
]);
|
|
frame.render_widget(Line::from(line), area);
|
|
}
|
|
|
|
/// Draw heading bar at top of program, always visible
|
|
/// TODO Should separate into loading icon/headers/help functions
|
|
pub fn heading_bar(
|
|
area: Rect,
|
|
frame: &mut Frame,
|
|
data: &FrameData,
|
|
gui_state: &Arc<Mutex<GuiState>>,
|
|
) {
|
|
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
|
|
|
|
frame.render_widget(block(Color::Black), area);
|
|
|
|
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
|
let header_block = |x: &Header| {
|
|
let mut color = Color::Black;
|
|
let mut suffix = "";
|
|
if let Some((a, b)) = &data.sorted_by {
|
|
if x == a {
|
|
match b {
|
|
SortedOrder::Asc => suffix = " ▲",
|
|
SortedOrder::Desc => suffix = " ▼",
|
|
}
|
|
color = Color::Gray;
|
|
};
|
|
};
|
|
|
|
(Block::default().style(Style::default().fg(color)), suffix)
|
|
};
|
|
|
|
// Generate block for the headers, state and status has a specific layout, others all equal
|
|
// width is dependant on it that column is selected to sort - or not
|
|
let gen_header = |header: &Header, width: usize| {
|
|
let block = header_block(header);
|
|
|
|
// Yes this is a mess, needs documenting correctly
|
|
|
|
let text = format!(
|
|
"{x:<width$}{MARGIN}",
|
|
x = format!("{header}{ic}", ic = block.1),
|
|
);
|
|
let count = u16::try_from(text.chars().count()).unwrap_or_default();
|
|
let status = Paragraph::new(text)
|
|
.block(block.0)
|
|
.alignment(Alignment::Left);
|
|
(status, count)
|
|
};
|
|
|
|
// Meta data to iterate over to create blocks with correct widths
|
|
let header_meta = [
|
|
(Header::Name, data.columns.name.1),
|
|
(Header::State, data.columns.state.1),
|
|
(Header::Status, data.columns.status.1),
|
|
(Header::Cpu, data.columns.cpu.1),
|
|
(Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3),
|
|
(Header::Id, data.columns.id.1),
|
|
(Header::Image, data.columns.image.1),
|
|
(Header::Rx, data.columns.net_rx.1),
|
|
(Header::Tx, data.columns.net_tx.1),
|
|
];
|
|
|
|
// Need to add widths to this
|
|
|
|
let suffix = if data.help_visible { "exit" } else { "show" };
|
|
let info_text = format!("( h ) {suffix} help{MARGIN}",);
|
|
let info_width = info_text.chars().count();
|
|
|
|
let column_width = usize::from(area.width).saturating_sub(info_width);
|
|
let column_width = if column_width > 0 { column_width } else { 1 };
|
|
let splits = if data.has_containers {
|
|
vec![
|
|
Constraint::Max(4),
|
|
Constraint::Max(column_width.try_into().unwrap_or_default()),
|
|
Constraint::Max(info_width.try_into().unwrap_or_default()),
|
|
]
|
|
} else {
|
|
CONSTRAINT_100.to_vec()
|
|
};
|
|
|
|
let split_bar = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(splits)
|
|
.split(area);
|
|
|
|
// Draw loading icon, or not, and a prefix with a single space
|
|
let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
|
|
.block(block(Color::White))
|
|
.alignment(Alignment::Left);
|
|
frame.render_widget(loading_paragraph, split_bar[0]);
|
|
if data.has_containers {
|
|
let header_section_width = split_bar[1].width;
|
|
|
|
let mut counter = 0;
|
|
|
|
// Only show a header if the header cumulative header width is less than the header section width
|
|
let header_data = header_meta
|
|
.iter()
|
|
.filter_map(|i| {
|
|
let header_block = gen_header(&i.0, i.1.into());
|
|
counter += header_block.1;
|
|
if counter <= header_section_width {
|
|
Some((header_block.0, i.0, Constraint::Max(header_block.1)))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
|
let headers_section = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(container_splits)
|
|
.split(split_bar[1]);
|
|
|
|
for (index, (paragraph, header, _)) in header_data.into_iter().enumerate() {
|
|
let rect = headers_section[index];
|
|
gui_state
|
|
.lock()
|
|
.update_region_map(Region::Header(header), rect);
|
|
frame.render_widget(paragraph, rect);
|
|
}
|
|
}
|
|
|
|
// show/hide help
|
|
let color = if data.help_visible {
|
|
Color::Black
|
|
} else {
|
|
Color::White
|
|
};
|
|
let help_paragraph = Paragraph::new(info_text)
|
|
.block(block(color))
|
|
.alignment(Alignment::Right);
|
|
|
|
// If no containers, don't display the headers, could maybe do this first?
|
|
let help_index = if data.has_containers { 2 } else { 0 };
|
|
frame.render_widget(help_paragraph, split_bar[help_index]);
|
|
}
|
|
|
|
/// Help popup box needs these three pieces of information
|
|
struct HelpInfo {
|
|
lines: Vec<Line<'static>>,
|
|
width: usize,
|
|
height: usize,
|
|
}
|
|
|
|
impl HelpInfo {
|
|
/// Find the max width of a Span in &[Line], although it isn't calculating it correctly
|
|
fn calc_width(lines: &[Line]) -> usize {
|
|
lines
|
|
.iter()
|
|
.flat_map(|x| x.spans.iter())
|
|
.map(ratatui::text::Span::width)
|
|
.max()
|
|
.unwrap_or(1)
|
|
}
|
|
|
|
/// Just an empty span, i.e. a new line
|
|
fn empty_span<'a>() -> Line<'a> {
|
|
Line::from(String::new())
|
|
}
|
|
|
|
/// generate a span, of given &str and given color
|
|
fn span<'a>(input: &str, color: Color) -> Span<'a> {
|
|
Span::styled(input.to_owned(), Style::default().fg(color))
|
|
}
|
|
|
|
/// &str to black text span
|
|
fn black_span<'a>(input: &str) -> Span<'a> {
|
|
Self::span(input, Color::Black)
|
|
}
|
|
|
|
/// &str to white text span
|
|
fn white_span<'a>(input: &str) -> Span<'a> {
|
|
Self::span(input, Color::White)
|
|
}
|
|
|
|
/// Generate the `oxker` name span + metadata
|
|
fn gen_name() -> Self {
|
|
let mut lines = NAME_TEXT
|
|
.lines()
|
|
.map(|i| Line::from(Self::white_span(i)))
|
|
.collect::<Vec<_>>();
|
|
lines.insert(0, Self::empty_span());
|
|
let width = Self::calc_width(&lines);
|
|
let height = lines.len();
|
|
Self {
|
|
lines,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
/// Generate the description span + metadata
|
|
fn gen_description() -> Self {
|
|
let lines = [
|
|
Self::empty_span(),
|
|
Line::from(Self::white_span(DESCRIPTION)),
|
|
Self::empty_span(),
|
|
];
|
|
let width = Self::calc_width(&lines);
|
|
let height = lines.len();
|
|
Self {
|
|
lines: lines.to_vec(),
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
/// Generate the button information span + metadata
|
|
fn gen_button() -> Self {
|
|
let button_item = |x: &str| Self::white_span(&format!(" ( {x} ) "));
|
|
let button_desc = |x: &str| Self::black_span(x);
|
|
let or = || button_desc("or");
|
|
let space = || button_desc(" ");
|
|
|
|
let lines = [
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("tab"),
|
|
or(),
|
|
button_item("shift+tab"),
|
|
button_desc("change panels"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("↑ ↓"),
|
|
or(),
|
|
button_item("j k"),
|
|
or(),
|
|
button_item("PgUp PgDown"),
|
|
or(),
|
|
button_item("Home End"),
|
|
button_desc("change selected line"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("enter"),
|
|
button_desc("send docker container command"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("e"),
|
|
button_desc("exec into a container"),
|
|
#[cfg(target_os = "windows")]
|
|
button_desc(" - not available on Windows"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("h"),
|
|
button_desc("toggle this help information"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("s"),
|
|
button_desc("save logs to file"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("m"),
|
|
button_desc(
|
|
"toggle mouse capture - if disabled, text on screen can be selected & copied",
|
|
),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("F1"),
|
|
or(),
|
|
button_item("/"),
|
|
button_desc("enter filter mode"),
|
|
]),
|
|
Line::from(vec![space(), button_item("0"), button_desc("stop sort")]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("1 - 9"),
|
|
button_desc("sort by header - or click header"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("esc"),
|
|
button_desc("close dialog"),
|
|
]),
|
|
Line::from(vec![
|
|
space(),
|
|
button_item("q"),
|
|
button_desc("quit at any time"),
|
|
]),
|
|
];
|
|
|
|
let height = lines.len();
|
|
let width = Self::calc_width(&lines);
|
|
Self {
|
|
lines: lines.to_vec(),
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
/// Generate the final lines, GitHub link etc, + metadata
|
|
fn gen_final() -> Self {
|
|
let lines = [
|
|
Self::empty_span(),
|
|
Line::from(vec![Self::black_span(
|
|
"currently an early work in progress, all and any input appreciated",
|
|
)]),
|
|
Line::from(vec![Span::styled(
|
|
REPO.to_owned(),
|
|
Style::default()
|
|
.fg(Color::White)
|
|
.add_modifier(Modifier::UNDERLINED),
|
|
)]),
|
|
];
|
|
let height = lines.len();
|
|
let width = Self::calc_width(&lines);
|
|
Self {
|
|
lines: lines.to_vec(),
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Draw the help box in the centre of the screen
|
|
pub fn help_box(f: &mut Frame) {
|
|
let title = format!(" {VERSION} ");
|
|
|
|
let name_info = HelpInfo::gen_name();
|
|
let description_info = HelpInfo::gen_description();
|
|
let button_info = HelpInfo::gen_button();
|
|
let final_info = HelpInfo::gen_final();
|
|
|
|
// have to add 10, but shouldn't need to, is an error somewhere
|
|
let max_line_width = [
|
|
name_info.width,
|
|
description_info.width,
|
|
button_info.width,
|
|
final_info.width,
|
|
]
|
|
.into_iter()
|
|
.max()
|
|
.unwrap_or_default()
|
|
+ 10;
|
|
let max_height =
|
|
name_info.height + description_info.height + button_info.height + final_info.height + 2;
|
|
|
|
let area = popup(
|
|
max_height,
|
|
max_line_width,
|
|
f.area(),
|
|
BoxLocation::MiddleCentre,
|
|
);
|
|
|
|
let split_popup = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Max(name_info.height.try_into().unwrap_or_default()),
|
|
Constraint::Max(description_info.height.try_into().unwrap_or_default()),
|
|
Constraint::Max(button_info.height.try_into().unwrap_or_default()),
|
|
Constraint::Min(final_info.height.try_into().unwrap_or_default()),
|
|
])
|
|
.split(area);
|
|
|
|
let name_paragraph = Paragraph::new(name_info.lines)
|
|
.style(Style::default().bg(Color::Magenta).fg(Color::White))
|
|
.block(Block::default())
|
|
.alignment(Alignment::Center);
|
|
|
|
let description_paragraph = Paragraph::new(description_info.lines)
|
|
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
|
.block(Block::default())
|
|
.alignment(Alignment::Center);
|
|
|
|
let help_paragraph = Paragraph::new(button_info.lines)
|
|
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
|
.block(Block::default())
|
|
.alignment(Alignment::Left);
|
|
|
|
let final_paragraph = Paragraph::new(final_info.lines)
|
|
.style(Style::default().bg(Color::Magenta).fg(Color::Black))
|
|
.block(Block::default())
|
|
.alignment(Alignment::Center);
|
|
|
|
let block = Block::default()
|
|
.title(title)
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded)
|
|
.border_style(Style::default().fg(Color::Black).bg(Color::Magenta));
|
|
|
|
// Order is important here
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(name_paragraph, split_popup[0]);
|
|
f.render_widget(description_paragraph, split_popup[1]);
|
|
f.render_widget(help_paragraph, split_popup[2]);
|
|
f.render_widget(final_paragraph, split_popup[3]);
|
|
f.render_widget(block, area);
|
|
}
|
|
|
|
/// Draw the delete confirm box in the centre of the screen
|
|
/// take in container id and container name here?
|
|
pub fn delete_confirm(f: &mut Frame, gui_state: &Arc<Mutex<GuiState>>, name: &ContainerName) {
|
|
let block = Block::default()
|
|
.title(" Confirm Delete ")
|
|
.border_type(BorderType::Rounded)
|
|
.style(Style::default().bg(Color::White).fg(Color::Black))
|
|
.title_alignment(Alignment::Center)
|
|
.borders(Borders::ALL);
|
|
|
|
let confirm = Line::from(vec![
|
|
Span::from("Are you sure you want to delete container: "),
|
|
Span::styled(
|
|
name.get(),
|
|
Style::default()
|
|
.fg(Color::Red)
|
|
.bg(Color::White)
|
|
.add_modifier(Modifier::BOLD),
|
|
),
|
|
]);
|
|
|
|
let yes_text = " (Y)es ";
|
|
let no_text = " (N)o ";
|
|
|
|
// Find the maximum line width & height, and add some padding
|
|
let max_line_width = u16::try_from(confirm.width()).unwrap_or(64) + 12;
|
|
let lines = 8;
|
|
|
|
let confirm_para = Paragraph::new(confirm).alignment(Alignment::Center);
|
|
|
|
let button_block = || {
|
|
Block::default()
|
|
.border_type(BorderType::Rounded)
|
|
.borders(Borders::ALL)
|
|
.style(Style::default().bg(Color::White))
|
|
};
|
|
|
|
let yes_para = Paragraph::new(yes_text)
|
|
.alignment(Alignment::Center)
|
|
.block(button_block());
|
|
|
|
let no_para = Paragraph::new(no_text)
|
|
.alignment(Alignment::Center)
|
|
.block(button_block());
|
|
|
|
let area = popup(
|
|
lines,
|
|
max_line_width.into(),
|
|
f.area(),
|
|
BoxLocation::MiddleCentre,
|
|
);
|
|
|
|
let split_popup = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(CONSTRAINT_POPUP)
|
|
.split(area);
|
|
|
|
let split_buttons = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(CONSTRAINT_BUTTONS)
|
|
.split(split_popup[3]);
|
|
|
|
let no_area = split_buttons[1];
|
|
let yes_area = split_buttons[3];
|
|
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(block, area);
|
|
f.render_widget(confirm_para, split_popup[1]);
|
|
f.render_widget(no_para, no_area);
|
|
f.render_widget(yes_para, yes_area);
|
|
// Insert button areas into region map, so can interact with them on click
|
|
gui_state
|
|
.lock()
|
|
.update_region_map(Region::Delete(DeleteButton::No), no_area);
|
|
|
|
gui_state
|
|
.lock()
|
|
.update_region_map(Region::Delete(DeleteButton::Yes), yes_area);
|
|
}
|
|
|
|
/// Draw an error popup over whole screen
|
|
pub fn error(f: &mut Frame, error: AppError, seconds: Option<u8>) {
|
|
let block = Block::default()
|
|
.title(" Error ")
|
|
.border_type(BorderType::Rounded)
|
|
.title_alignment(Alignment::Center)
|
|
.borders(Borders::ALL);
|
|
|
|
let to_push = match error {
|
|
AppError::DockerConnect => {
|
|
format!(
|
|
"\n\n {}::v{} closing in {:02} seconds",
|
|
NAME,
|
|
VERSION,
|
|
seconds.unwrap_or(5)
|
|
)
|
|
}
|
|
_ => String::from("\n\n ( c ) clear error\n ( q ) quit oxker "),
|
|
};
|
|
|
|
let mut text = format!("\n{error}");
|
|
|
|
text.push_str(to_push.as_str());
|
|
|
|
// Find the maximum line width & height
|
|
let mut max_line_width = max_line_width(&text);
|
|
let mut lines = text.lines().count();
|
|
|
|
// Add some horizontal & vertical margins
|
|
max_line_width += 8;
|
|
lines += 3;
|
|
|
|
let paragraph = Paragraph::new(text)
|
|
.style(Style::default().bg(Color::Red).fg(Color::White))
|
|
.block(block)
|
|
.alignment(Alignment::Center);
|
|
|
|
let area = popup(lines, max_line_width, f.area(), BoxLocation::MiddleCentre);
|
|
|
|
// let (paragraph, area) = gen_error(f, error, seconds);
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(paragraph, area);
|
|
}
|
|
|
|
/// Draw info box in one of the 9 BoxLocations
|
|
// TODO is this broken - I don't think so
|
|
pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc<Mutex<GuiState>>) {
|
|
let block = Block::default()
|
|
.title("")
|
|
.title_alignment(Alignment::Center)
|
|
.borders(Borders::NONE);
|
|
|
|
let mut max_line_width = max_line_width(text);
|
|
let mut lines = text.lines().count();
|
|
|
|
// Add some horizontal & vertical margins
|
|
max_line_width += 8;
|
|
lines += 2;
|
|
|
|
let paragraph = Paragraph::new(text)
|
|
.style(Style::default().bg(Color::Blue).fg(Color::White))
|
|
.block(block)
|
|
.alignment(Alignment::Center);
|
|
|
|
let area = popup(lines, max_line_width, f.area(), BoxLocation::BottomRight);
|
|
f.render_widget(Clear, area);
|
|
f.render_widget(paragraph, area);
|
|
if instant.elapsed().as_millis() > 4000 {
|
|
gui_state.lock().reset_info_box();
|
|
}
|
|
}
|
|
|
|
/// draw a box in the one of the BoxLocations, based on max line width + number of lines
|
|
fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocation) -> Rect {
|
|
// Make sure blank_space can't be an negative, as will crash
|
|
let calc = |x: u16, y: usize| usize::from(x).saturating_sub(y).saturating_div(2);
|
|
|
|
let blank_vertical = calc(r.height, text_lines);
|
|
let blank_horizontal = calc(r.width, text_width);
|
|
|
|
let (h_constraints, v_constraints) = box_location.get_constraints(
|
|
blank_horizontal.try_into().unwrap_or_default(),
|
|
blank_vertical.try_into().unwrap_or_default(),
|
|
text_lines.try_into().unwrap_or_default(),
|
|
text_width.try_into().unwrap_or_default(),
|
|
);
|
|
|
|
let indexes = box_location.get_indexes();
|
|
|
|
let popup_layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(v_constraints)
|
|
.split(r);
|
|
|
|
Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(h_constraints)
|
|
.split(popup_layout[indexes.0])[indexes.1]
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used)]
|
|
mod tests {
|
|
|
|
use std::{ops::RangeInclusive, sync::Arc};
|
|
|
|
use parking_lot::Mutex;
|
|
use ratatui::{
|
|
backend::TestBackend,
|
|
layout::Rect,
|
|
style::{Color, Modifier},
|
|
Terminal,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{
|
|
app_data::{
|
|
AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, ContainerStatus,
|
|
Header, SortedOrder, State, StatefulList,
|
|
},
|
|
app_error::AppError,
|
|
tests::{gen_appdata, gen_container_summary, gen_containers},
|
|
ui::{draw_frame, GuiState},
|
|
};
|
|
|
|
use super::{FrameData, ORANGE, VERSION};
|
|
|
|
struct TuiTestSetup {
|
|
app_data: Arc<Mutex<AppData>>,
|
|
gui_state: Arc<Mutex<GuiState>>,
|
|
fd: FrameData,
|
|
area: Rect,
|
|
terminal: Terminal<TestBackend>,
|
|
ids: Vec<ContainerId>,
|
|
}
|
|
|
|
const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
|
|
|
|
/// Generate state to be used in *most* gui tests
|
|
fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup {
|
|
let backend = TestBackend::new(w, h);
|
|
let terminal = Terminal::new(backend).unwrap();
|
|
|
|
let (ids, containers) = gen_containers();
|
|
let mut app_data = gen_appdata(&containers);
|
|
if control_start {
|
|
app_data.docker_controls_start();
|
|
}
|
|
if container_start {
|
|
app_data.containers_start();
|
|
}
|
|
|
|
let gui_state = GuiState::default();
|
|
|
|
let app_data = Arc::new(Mutex::new(app_data));
|
|
let gui_state = Arc::new(Mutex::new(gui_state));
|
|
|
|
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
|
let area = Rect::new(0, 0, w, h);
|
|
TuiTestSetup {
|
|
app_data,
|
|
gui_state,
|
|
fd,
|
|
area,
|
|
terminal,
|
|
ids,
|
|
}
|
|
}
|
|
|
|
/// Insert some logs into the first container
|
|
fn insert_logs(setup: &TuiTestSetup) {
|
|
let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::<Vec<_>>();
|
|
setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]);
|
|
}
|
|
|
|
/// Get a single row of String's from the expected data
|
|
fn expected_to_vec(expected: &[&str], row_index: usize) -> Vec<String> {
|
|
expected[row_index]
|
|
.chars()
|
|
.map(|i| i.to_string())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
fn get_result(
|
|
setup: &TuiTestSetup,
|
|
w: u16,
|
|
) -> std::iter::Enumerate<std::slice::Chunks<ratatui::buffer::Cell>> {
|
|
setup
|
|
.terminal
|
|
.backend()
|
|
.buffer()
|
|
.content
|
|
.chunks(usize::from(w))
|
|
.enumerate()
|
|
}
|
|
|
|
// ******************** //
|
|
// DockerControls panel //
|
|
// ******************** //
|
|
|
|
#[test]
|
|
/// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
|
fn test_draw_blocks_commands_none() {
|
|
let (w, h) = (12, 6);
|
|
let mut setup = test_setup(w, h, false, false);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭──────────╮",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────╯",
|
|
];
|
|
|
|
for (row_index, row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (cell_index, cell) in row.iter().enumerate() {
|
|
assert_eq!(cell.symbol(), expected_row[cell_index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
// Test that when DockerCommands are available, they are drawn correctly, dependant on container state
|
|
fn test_draw_blocks_commands_some() {
|
|
let (w, h) = (12, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭──────────╮",
|
|
"│▶ pause │",
|
|
"│ restart │",
|
|
"│ stop │",
|
|
"│ delete │",
|
|
"╰──────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
match (row_index, result_cell_index) {
|
|
// pause
|
|
(1, 3..=7) => {
|
|
assert_eq!(result_cell.fg, Color::Yellow);
|
|
}
|
|
// restart
|
|
(2, 3..=9) => {
|
|
assert_eq!(result_cell.fg, Color::Magenta);
|
|
}
|
|
// stop
|
|
(3, 3..=6) => {
|
|
assert_eq!(result_cell.fg, Color::Red);
|
|
}
|
|
// delete
|
|
(4, 3..=8) => {
|
|
assert_eq!(result_cell.fg, Color::Gray);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Change the controls state
|
|
setup
|
|
.app_data
|
|
.lock()
|
|
.update_containers(&mut vec![gen_container_summary(1, "paused")]);
|
|
setup.app_data.lock().docker_controls_next();
|
|
|
|
let expected = [
|
|
"╭──────────╮",
|
|
"│ resume │",
|
|
"│▶ stop │",
|
|
"│ delete │",
|
|
"│ │",
|
|
"╰──────────╯",
|
|
];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
match (row_index, result_cell_index) {
|
|
// resume
|
|
(1, 3..=8) => {
|
|
assert_eq!(result_cell.fg, Color::Blue);
|
|
}
|
|
// stop
|
|
(2, 3..=6) => {
|
|
assert_eq!(result_cell.fg, Color::Red);
|
|
}
|
|
// delete
|
|
(3, 3..=8) => {
|
|
assert_eq!(result_cell.fg, Color::Gray);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// When control panel is selected, the border is blue, if not then white, selected text is highlighted
|
|
fn test_draw_blocks_commands_panel_selected_color() {
|
|
let (w, h) = (12, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let expected = [
|
|
"╭──────────╮",
|
|
"│▶ pause │",
|
|
"│ restart │",
|
|
"│ stop │",
|
|
"│ delete │",
|
|
"╰──────────╯",
|
|
];
|
|
|
|
// Unselected, has a grey border
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
if BORDER_CHARS.contains(&result_cell.symbol()) {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Control panel now selected, should have a blue border
|
|
setup.gui_state.lock().next_panel();
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::commands(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
if row_index == 0
|
|
|| row_index == 5
|
|
|| result_cell_index == 0
|
|
|| result_cell_index == 11
|
|
{
|
|
assert_eq!(result_cell.fg, Color::LightCyan);
|
|
}
|
|
if row_index == 1 && result_cell_index > 0 && result_cell_index < 11 {
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
} else {
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// *********************** //
|
|
// Container summary panel //
|
|
// *********************** //
|
|
|
|
#[test]
|
|
/// No containers, panel unselected, then selected, border color changes correctly
|
|
fn test_draw_blocks_containers_none() {
|
|
let (w, h) = (25, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
setup.app_data.lock().containers = StatefulList::new(vec![]);
|
|
|
|
let expected = [
|
|
"╭ Containers ───────────╮",
|
|
"│ no containers running │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰───────────────────────╯",
|
|
];
|
|
|
|
setup.gui_state.lock().next_panel();
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
|
|
setup.gui_state.lock().previous_panel();
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
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 (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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().next_panel();
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
if BORDER_CHARS.contains(&result_cell.symbol()) {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Columns on all rows are coloured correctly
|
|
fn test_draw_blocks_containers_colors() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
|
];
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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::Rgb(255, 233, 193));
|
|
}
|
|
// tx column
|
|
(1..=3, 102..=111) => {
|
|
assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
|
|
}
|
|
_ => assert_eq!(result_cell.fg, Color::Reset),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Long container + image name is truncated correctly
|
|
fn test_draw_blocks_containers_long_name_image() {
|
|
let (w, h) = (170, 6);
|
|
let mut setup = test_setup(w, h, 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 expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
setup.app_data.lock().containers.items[0].state = State::Paused;
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check that the correct colour is applied to the state/status/cpu/memory section
|
|
fn check_expected(expected: [&str; 6], w: u16, _h: u16, setup: &TuiTestSetup, color: Color) {
|
|
for (row_index, result_row) in get_result(setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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::Rgb(255, 233, 193));
|
|
}
|
|
// tx column
|
|
(1..=3, 102..=111) => {
|
|
assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
|
|
}
|
|
_ => assert_eq!(result_cell.fg, Color::Reset),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// When container is paused, correct colors displayed
|
|
fn test_draw_blocks_containers_paused() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
setup.app_data.lock().containers.items[0].state = State::Paused;
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
check_expected(expected, w, h, &setup, Color::Yellow);
|
|
}
|
|
|
|
#[test]
|
|
/// When container is dead, correct colors displayed
|
|
fn test_draw_blocks_containers_dead() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ✖ dead Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
setup.app_data.lock().containers.items[0].state = State::Dead;
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
check_expected(expected, w, h, &setup, Color::Red);
|
|
}
|
|
|
|
#[test]
|
|
/// When container is exited, correct colors displayed
|
|
fn test_draw_blocks_containers_exited() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ✖ exited Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
setup.app_data.lock().containers.items[0].state = State::Exited;
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
check_expected(expected, w, h, &setup, Color::Red);
|
|
}
|
|
#[test]
|
|
/// When container is paused, correct colors displayed
|
|
fn test_draw_blocks_containers_removing() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 removing Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
setup.app_data.lock().containers.items[0].state = State::Removing;
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
check_expected(expected, w, h, &setup, Color::LightRed);
|
|
}
|
|
|
|
#[test]
|
|
/// When container state is restarting, correct colors displayed
|
|
fn test_draw_blocks_containers_restarting() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ↻ restarting Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
setup.app_data.lock().containers.items[0].state = State::Restarting;
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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::Rgb(255, 233, 193));
|
|
}
|
|
// tx column
|
|
(1..=3, 105..=114) => {
|
|
assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// When container state is unknown, correct colors displayed
|
|
fn test_draw_blocks_containers_unhealthy() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, 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 expected= [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ! running Up 1 hour (unhealthy) 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
|
];
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
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, 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::Rgb(255, 233, 193));
|
|
}
|
|
// tx column
|
|
(1..=3, 114..=123) => {
|
|
assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
|
|
}
|
|
_ => assert_eq!(result_cell.fg, Color::Reset),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// When container state is unknown, correct colors displayed
|
|
fn test_draw_blocks_containers_unknown() {
|
|
let (w, h) = (130, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│⚪ container_1 ? unknown Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
setup.app_data.lock().containers.items[0].state = State::Unknown;
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
check_expected(expected, w, h, &setup, Color::Red);
|
|
}
|
|
|
|
// ********** //
|
|
// Logs panel //
|
|
// ********** //
|
|
|
|
#[test]
|
|
/// No logs, panel unselected, then selected, border color changes correctly
|
|
fn test_draw_blocks_logs_none() {
|
|
let (w, h) = (25, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
setup.app_data.lock().containers = StatefulList::new(vec![]);
|
|
|
|
let expected = [
|
|
"╭ Logs ─────────────────╮",
|
|
"│ no logs found │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰───────────────────────╯",
|
|
];
|
|
|
|
let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
|
|
setup.gui_state.lock().next_panel();
|
|
setup.gui_state.lock().next_panel();
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
// When selected, has a blue border
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
if BORDER_CHARS.contains(&result_cell.symbol()) {
|
|
assert_eq!(result_cell.fg, Color::LightCyan);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Parsing logs, spinner visible, and then animates by one frame
|
|
fn test_draw_blocks_logs_parsing() {
|
|
let (w, h) = (32, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let uuid = Uuid::new_v4();
|
|
setup.gui_state.lock().next_loading(uuid);
|
|
|
|
let expected = [
|
|
"╭ Logs - container_1 - image_1 ╮",
|
|
"│ parsing logs ⠙ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────╯",
|
|
];
|
|
|
|
let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
fd.init = true;
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
|
|
// animation moved by one frame
|
|
setup.gui_state.lock().next_loading(uuid);
|
|
|
|
let expected = [
|
|
"╭ Logs - container_1 - image_1 ╮",
|
|
"│ parsing logs ⠹ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────╯",
|
|
];
|
|
|
|
let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
fd.init = true;
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Logs correct displayed, changing log state also draws correctly
|
|
fn test_draw_blocks_logs_some() {
|
|
let (w, h) = (25, 6);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
insert_logs(&setup);
|
|
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
let expected = [
|
|
"╭ Logs 3/3 - container_1╮",
|
|
"│ line 1 │",
|
|
"│ line 2 │",
|
|
"│▶ line 3 │",
|
|
"│ │",
|
|
"╰───────────────────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
|
|
if row_index == 3 && (1..=23).contains(&result_cell_index) {
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
} else {
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Change selected log line
|
|
setup.app_data.lock().log_previous();
|
|
_ = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭ Logs 2/3 - container_1╮",
|
|
"│ line 1 │",
|
|
"│▶ line 2 │",
|
|
"│ line 3 │",
|
|
"│ │",
|
|
"╰───────────────────────╯",
|
|
];
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
|
|
if row_index == 2 && (1..=23).contains(&result_cell_index) {
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
} else {
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Full (long) name displayed in logs border
|
|
fn test_draw_blocks_logs_long_name() {
|
|
let (w, h) = (80, 6);
|
|
let mut setup = test_setup(w, h, 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");
|
|
|
|
insert_logs(&setup);
|
|
|
|
let expected = [
|
|
"╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image╮",
|
|
"│ line 1 │",
|
|
"│ line 2 │",
|
|
"│▶ line 3 │",
|
|
"│ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────╯",
|
|
];
|
|
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ************ //
|
|
// Charts panel //
|
|
// ************ //
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
// Add fixed data to the cpu & mem vecdeques
|
|
fn insert_chart_data(setup: &TuiTestSetup) {
|
|
for i in 1..=10 {
|
|
setup.app_data.lock().update_stats_by_id(
|
|
&setup.ids[0],
|
|
Some(i as f64),
|
|
Some(i * 10000),
|
|
i * 10000,
|
|
i,
|
|
i,
|
|
);
|
|
}
|
|
for i in 1..=3 {
|
|
setup.app_data.lock().update_stats_by_id(
|
|
&setup.ids[0],
|
|
Some(i as f64),
|
|
Some(i * 10000),
|
|
i * 10000,
|
|
i,
|
|
i,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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); 15] = [
|
|
(1, 12),
|
|
(2, 11),
|
|
(2, 12),
|
|
(3, 10),
|
|
(3, 11),
|
|
(3, 12),
|
|
(4, 10),
|
|
(4, 12),
|
|
(5, 9),
|
|
(5, 13),
|
|
(5, 14),
|
|
(6, 8),
|
|
(6, 13),
|
|
(7, 8),
|
|
(7, 13),
|
|
];
|
|
|
|
// co-ordinates of the dots from the memory chart
|
|
const MEM_XY: [(usize, usize); 16] = [
|
|
(1, 54),
|
|
(1, 55),
|
|
(2, 54),
|
|
(2, 55),
|
|
(3, 53),
|
|
(3, 55),
|
|
(4, 52),
|
|
(4, 55),
|
|
(5, 51),
|
|
(5, 52),
|
|
(5, 55),
|
|
(5, 56),
|
|
(6, 51),
|
|
(6, 55),
|
|
(7, 51),
|
|
(7, 55),
|
|
];
|
|
|
|
#[test]
|
|
/// When status is Running, but not data, charts drawn without dots etc, colours correct
|
|
fn test_draw_blocks_charts_running_none() {
|
|
let (w, h) = (80, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::chart(f, setup.area, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮",
|
|
"│00.00%│ ││0.00 kB│ │",
|
|
"│ │ ││ │ │",
|
|
"│ │ ││ │ │",
|
|
"│ │ ││ │ │",
|
|
"│ │ ││ │ │",
|
|
"│ │ ││ │ │",
|
|
"│ │ ││ │ │",
|
|
"│ │ ││ │ │",
|
|
"╰──────────────────────────────────────╯╰──────────────────────────────────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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, ORANGE);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// When status is Running, charts correctly drawn
|
|
fn test_draw_blocks_charts_running_some() {
|
|
let (w, h) = (80, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
insert_chart_data(&setup);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::chart(f, setup.area, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&EXPECTED, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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, 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());
|
|
}
|
|
_ => {
|
|
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 (w, h) = (80, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
insert_chart_data(&setup);
|
|
setup.app_data.lock().containers.items[0].state = State::Paused;
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::chart(f, setup.area, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&EXPECTED, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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());
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// When dead, text is read
|
|
fn test_draw_blocks_charts_dead() {
|
|
let (w, h) = (80, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
insert_chart_data(&setup);
|
|
setup.app_data.lock().containers.items[0].state = State::Dead;
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::chart(f, setup.area, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&EXPECTED, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
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());
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ******* //
|
|
// Headers //
|
|
// ******* //
|
|
|
|
#[test]
|
|
/// Heading back only has show/exit help when no containers, correctly coloured
|
|
fn test_draw_blocks_headers_no_containers() {
|
|
let (w, h) = (140, 1);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
setup.app_data.lock().containers = StatefulList::new(vec![]);
|
|
|
|
let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
let expected = [" ( h ) show help "];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::heading_bar(setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::White);
|
|
}
|
|
}
|
|
|
|
fd.help_visible = true;
|
|
let expected = [" ( h ) exit help "];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::heading_bar(setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Show all headings when containers present, colors valid
|
|
fn test_draw_blocks_headers_some_containers() {
|
|
let (w, h) = (140, 1);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::heading_bar(setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(
|
|
result_cell.fg,
|
|
match result_cell_index {
|
|
(4..=121) => Color::Black,
|
|
_ => Color::White,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Only show the headings that fit the reduced-in-size header section
|
|
fn test_draw_blocks_headers_some_containers_reduced_width() {
|
|
let (w, h) = (80, 1);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
let expected =
|
|
[" name state status cpu ( h ) show help "];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::heading_bar(setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(
|
|
result_cell.fg,
|
|
match result_cell_index {
|
|
(4..=61) => Color::Black,
|
|
_ => Color::White,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Test all combination of headers & sort by
|
|
fn test_draw_blocks_headers_sort_containers() {
|
|
let (w, h) = (140, 1);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
// Actual test, used for each header and sorted type
|
|
let mut test =
|
|
|expected: &[&str], range: RangeInclusive<usize>, x: (Header, SortedOrder)| {
|
|
fd.sorted_by = Some(x);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::heading_bar(setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(
|
|
result_cell.fg,
|
|
match result_cell_index {
|
|
0..=3 | 122..=139 => Color::White,
|
|
// given range | help section
|
|
x if range.contains(&x) => Color::Gray,
|
|
_ => Color::Black,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Name
|
|
test(&[" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Asc));
|
|
test(&[" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Desc));
|
|
// state
|
|
test(&[" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "],18..=29, (Header::State, SortedOrder::Asc));
|
|
test(&[" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 18..=29, (Header::State, SortedOrder::Desc));
|
|
// status
|
|
test(&[" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Asc));
|
|
test(&[" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Desc));
|
|
// cpu
|
|
test(&[" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Asc));
|
|
test(&[" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Desc));
|
|
// memory
|
|
test(&[" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Asc));
|
|
test(&[" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Desc));
|
|
//id
|
|
test(&[" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Asc));
|
|
test(&[" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Desc));
|
|
// image
|
|
test(&[" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Asc));
|
|
test(&[" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Desc));
|
|
// rx
|
|
test(&[" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Asc));
|
|
test(&[" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Desc));
|
|
// tx
|
|
test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▲ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Asc));
|
|
test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▼ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Desc));
|
|
}
|
|
|
|
#[test]
|
|
/// Show animation
|
|
fn test_draw_blocks_headers_animation() {
|
|
let (w, h) = (140, 1);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let uuid = Uuid::new_v4();
|
|
setup.gui_state.lock().next_loading(uuid);
|
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
|
|
|
let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::heading_bar(setup.area, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(
|
|
result_cell.fg,
|
|
match result_cell_index {
|
|
(4..=121) => Color::Black,
|
|
_ => Color::White,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ********** //
|
|
// Help popup //
|
|
// ********** //
|
|
#[test]
|
|
/// This will cause issues once the version has more than the current 5 chars (0.5.0)
|
|
// Help popup is drawn correctly
|
|
fn test_draw_blocks_help() {
|
|
let (w, h) = (87, 33);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::help_box(f);
|
|
})
|
|
.unwrap();
|
|
let version_row = format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ ");
|
|
let expected = [
|
|
" ",
|
|
version_row.as_str(),
|
|
" │ │ ",
|
|
" │ 88 │ ",
|
|
" │ 88 │ ",
|
|
" │ 88 │ ",
|
|
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
|
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
|
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
|
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
|
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
|
" │ │ ",
|
|
" │ A simple tui to view & control docker containers │ ",
|
|
" │ │ ",
|
|
" │ ( tab ) or ( shift+tab ) change panels │ ",
|
|
" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ",
|
|
" │ ( enter ) send docker container command │ ",
|
|
" │ ( e ) exec into a container │ ",
|
|
" │ ( h ) toggle this help information │ ",
|
|
" │ ( s ) save logs to file │ ",
|
|
" │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
|
" │ ( F1 ) or ( / ) enter filter mode │ ",
|
|
" │ ( 0 ) stop sort │ ",
|
|
" │ ( 1 - 9 ) sort by header - or click header │ ",
|
|
" │ ( esc ) close dialog │ ",
|
|
" │ ( q ) quit at any time │ ",
|
|
" │ │ ",
|
|
" │ currently an early work in progress, all and any input appreciated │ ",
|
|
" │ https://github.com/mrjackwills/oxker │ ",
|
|
" │ │ ",
|
|
" │ │ ",
|
|
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
|
" "
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match (row_index, result_cell_index) {
|
|
// first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area
|
|
(0 | 32, _) | (0..=33, 0 | 86) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
// border is black on magenta
|
|
(1 | 31, _) | (1..=31, 1 | 85) => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
// oxker logo && description
|
|
(2..=10, 2..=85) | (12, 19..=66)
|
|
// button in the brackets
|
|
| (14, 2..=10 | 13..=27)
|
|
| (15, 2..=10 | 13..=21 | 24..=40 | 43..=56)
|
|
| (16 | 23, 2..=12)
|
|
| (17..=20 | 22 | 25, 2..=8)
|
|
| (21, 2..=9 | 12..=18)
|
|
| (24, 2..=10) => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::White);
|
|
}
|
|
// The URL is white and underlined
|
|
(28, 25..=60) => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::White);
|
|
assert_eq!(result_cell.modifier, Modifier::UNDERLINED);
|
|
}
|
|
// The rest is black on magenta
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ************ //
|
|
// Delete popup //
|
|
// ************ //
|
|
|
|
#[test]
|
|
/// Delete container popup is drawn correctly
|
|
fn test_draw_blocks_delete() {
|
|
let (w, h) = (82, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
" ",
|
|
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
|
" │ │ ",
|
|
" │ Are you sure you want to delete container: container_1 │ ",
|
|
" │ │ ",
|
|
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
|
" │ │ (N)o │ │ (Y)es │ │ ",
|
|
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
|
" ╰────────────────────────────────────────────────────────────────╯ ",
|
|
" ",
|
|
];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::delete_confirm(f, &setup.gui_state, &ContainerName::from("container_1"));
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match (row_index, result_cell_index) {
|
|
(0 | 9, _) | (1..=8, 0..=7 | 74..=81) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
(3, 57..=67) => {
|
|
assert_eq!(result_cell.bg, Color::White);
|
|
assert_eq!(result_cell.fg, Color::Red);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::White);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Delete container popup is drawn correctly
|
|
fn test_draw_blocks_delete_long_name() {
|
|
let (w, h) = (106, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let name = ContainerName::from("container_1_container_1_container_1");
|
|
setup.app_data.lock().containers.items[0].name = name.clone();
|
|
|
|
let expected = [
|
|
" ",
|
|
" ╭──────────────────────────────────── Confirm Delete ────────────────────────────────────╮ ",
|
|
" │ │ ",
|
|
" │ Are you sure you want to delete container: container_1_container_1_container_1 │ ",
|
|
" │ │ ",
|
|
" │ ╭──────────────────────────────╮ ╭─────────────────────────────╮ │ ",
|
|
" │ │ (N)o │ │ (Y)es │ │ ",
|
|
" │ ╰──────────────────────────────╯ ╰─────────────────────────────╯ │ ",
|
|
" ╰────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
|
" ",
|
|
];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::delete_confirm(f, &setup.gui_state, &name);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match (row_index, result_cell_index) {
|
|
(0 | 9, _) | (1..=8, 0..=7 | 98..=106) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
(3, 57..=91) => {
|
|
assert_eq!(result_cell.bg, Color::White);
|
|
assert_eq!(result_cell.fg, Color::Red);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::White);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ***** //
|
|
// popup //
|
|
// ***** //
|
|
|
|
#[test]
|
|
/// Info box drawn in bottom right
|
|
fn test_draw_blocks_info() {
|
|
let (w, h) = (45, 9);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
let expected = [
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" test ",
|
|
" ",
|
|
];
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::info(f, "test", std::time::Instant::now(), &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
let (bg, fg) = match (row_index, result_cell_index) {
|
|
(6..=8, 32..=44) => (Color::Blue, Color::White),
|
|
_ => (Color::Reset, Color::Reset),
|
|
};
|
|
assert_eq!(result_cell.bg, bg);
|
|
assert_eq!(result_cell.fg, fg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ********** //
|
|
// Filter Row //
|
|
// ********** //
|
|
|
|
#[test]
|
|
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
|
/// Filter row is drawn correctly & colors are correct
|
|
/// Colours change when filter_by option is changed
|
|
fn test_draw_blocks_filter_row() {
|
|
let (w, h) = (140, 1);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
setup
|
|
.gui_state
|
|
.lock()
|
|
.status_push(crate::ui::Status::Filter);
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::filter_bar(setup.area, f, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
" Esc clear ← by → Name Image Status All term: "
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
match result_cell_index {
|
|
0..=4 | 12..=19 => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
5..=11 => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Gray);
|
|
}
|
|
21..=26 => {
|
|
assert_eq!(result_cell.bg, Color::Gray);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
47..=53 => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Magenta);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test when char added to search term
|
|
setup.app_data.lock().filter_term_push('c');
|
|
setup.app_data.lock().filter_term_push('d');
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::filter_bar(setup.area, f, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
" Esc clear ← by → Name Image Status All term: cd "
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match result_cell_index {
|
|
0..=4 | 12..=19 => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
5..=11 | 54..=55 => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Gray);
|
|
}
|
|
21..=26 => {
|
|
assert_eq!(result_cell.bg, Color::Gray);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
47..=53 => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Magenta);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test when filter_by chances
|
|
setup.app_data.lock().filter_by_next();
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::filter_bar(setup.area, f, &setup.app_data);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
" Esc clear ← by → Name Image Status All term: cd "
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match result_cell_index {
|
|
0..=4 | 12..=19 => {
|
|
assert_eq!(result_cell.bg, Color::Magenta);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
5..=11 | 54..=55 => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Gray);
|
|
}
|
|
27..=33 => {
|
|
assert_eq!(result_cell.bg, Color::Gray);
|
|
assert_eq!(result_cell.fg, Color::Black);
|
|
}
|
|
47..=53 => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Magenta);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// *********** //
|
|
// Error popup //
|
|
// *********** //
|
|
|
|
#[test]
|
|
/// Test that the error popup is centered, red background, white border, white text, and displays the correct text
|
|
fn test_draw_blocks_docker_connect_error() {
|
|
let (w, h) = (46, 9);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::error(f, AppError::DockerConnect, Some(4));
|
|
})
|
|
.unwrap();
|
|
|
|
let version_row = format!(" │ oxker::v{VERSION} closing in 04 seconds │ ");
|
|
let expected = [
|
|
" ",
|
|
" ╭───────────────── Error ──────────────────╮ ",
|
|
" │ │ ",
|
|
" │ Unable to access docker daemon │ ",
|
|
" │ │ ",
|
|
version_row.as_str(),
|
|
" │ │ ",
|
|
" ╰──────────────────────────────────────────╯ ",
|
|
" ",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match (row_index, result_cell_index) {
|
|
(0 | 8, _) | (1..=7, 0 | 45) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Red);
|
|
assert_eq!(result_cell.fg, Color::White);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text
|
|
fn test_draw_blocks_clearable_error() {
|
|
let (w, h) = (39, 10);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::error(f, AppError::DockerExec, Some(4));
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
" ",
|
|
" ╭────────────── Error ──────────────╮ ",
|
|
" │ │ ",
|
|
" │ Unable to exec into container │ ",
|
|
" │ │ ",
|
|
" │ ( c ) clear error │ ",
|
|
" │ ( q ) quit oxker │ ",
|
|
" │ │ ",
|
|
" ╰───────────────────────────────────╯ ",
|
|
" ",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
|
|
match (row_index, result_cell_index) {
|
|
(0 | 9, _) | (1..=8, 0 | 38) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Red);
|
|
assert_eq!(result_cell.fg, Color::White);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Port section when container has no ports
|
|
fn test_draw_blocks_ports_no_ports() {
|
|
let (w, h) = (30, 8);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
setup.app_data.lock().containers.items[0].ports = vec![];
|
|
|
|
let max_lens = setup.app_data.lock().get_longest_port();
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::ports(f, setup.area, &setup.app_data, max_lens);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭────────── ports ───────────╮",
|
|
"│ no ports │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰────────────────────────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
match (row_index, result_cell_index) {
|
|
(0, 11..=17) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Green);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
(1, 11..=18) => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
_ => {
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// When state is "State::Running | State::Paused | State::Restarting, won't show "no ports"
|
|
setup.app_data.lock().containers.items[0].state = State::Dead;
|
|
let max_lens = setup.app_data.lock().get_longest_port();
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::ports(f, setup.area, &setup.app_data, max_lens);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭────────── ports ───────────╮",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰────────────────────────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
if let (0, 11..=17) = (row_index, result_cell_index) {
|
|
assert_eq!(result_cell.fg, Color::Red);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
} else {
|
|
assert_eq!(result_cell.fg, Color::Reset);
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Port section when container has multiple ports
|
|
fn test_draw_blocks_ports_multiple_ports() {
|
|
let (w, h) = (32, 8);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: None,
|
|
private: 8002,
|
|
public: None,
|
|
});
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some("127.0.0.1".to_owned()),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
|
|
let max_lens = setup.app_data.lock().get_longest_port();
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::ports(f, setup.area, &setup.app_data, max_lens);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭─────────── ports ────────────╮",
|
|
"│ ip private public │",
|
|
"│ 8001 │",
|
|
"│ 8002 │",
|
|
"│127.0.0.1 8003 8003 │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
|
|
match (row_index, result_cell_index) {
|
|
(0, 12..=18) => {
|
|
assert_eq!(result_cell.fg, Color::Green);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
(1, 1..=28) => {
|
|
assert_eq!(result_cell.fg, Color::Yellow);
|
|
assert!(result_cell.modifier.is_empty());
|
|
}
|
|
(2..=4, 1..=28) => {
|
|
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]
|
|
/// Port section title color correct dependant on state
|
|
fn test_draw_blocks_ports_container_state() {
|
|
let (w, h) = (32, 8);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
let max_lens = setup.app_data.lock().get_longest_port();
|
|
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::ports(f, setup.area, &setup.app_data, max_lens);
|
|
})
|
|
.unwrap();
|
|
|
|
let expected = [
|
|
"╭─────────── ports ────────────╮",
|
|
"│ ip private public │",
|
|
"│ 8001 │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────╯",
|
|
];
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
if let (0, 12..=18) = (row_index, result_cell_index) {
|
|
assert_eq!(result_cell.fg, Color::Green);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
}
|
|
}
|
|
|
|
setup.app_data.lock().containers.items[0].state = State::Paused;
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::ports(f, setup.area, &setup.app_data, max_lens);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
if let (0, 12..=18) = (row_index, result_cell_index) {
|
|
assert_eq!(result_cell.fg, Color::Yellow);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
}
|
|
}
|
|
|
|
setup.app_data.lock().containers.items[0].state = State::Exited;
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
super::ports(f, setup.area, &setup.app_data, max_lens);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
assert_eq!(result_cell.bg, Color::Reset);
|
|
if let (0, 12..=18) = (row_index, result_cell_index) {
|
|
assert_eq!(result_cell.fg, Color::Red);
|
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// *************** //
|
|
// The whole layout //
|
|
// **************** //
|
|
#[test]
|
|
/// Check that the whole layout is drawn correctly
|
|
fn test_draw_blocks_whole_layout() {
|
|
let (w, h) = (160, 30);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
insert_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some("127.0.0.1".to_owned()),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
|
|
let expected = [
|
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
|
"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮",
|
|
"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │",
|
|
"│ ││ delete │",
|
|
"│ ││ │",
|
|
"│ ││ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
|
|
"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│ line 1 │",
|
|
"│ line 2 │",
|
|
"│▶ line 3 │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
"╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮",
|
|
"│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
|
|
"│ │ ••• • ││ │ ••• • ││ 8001 │",
|
|
"│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003│",
|
|
"│ │ ││ │ ││ │",
|
|
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
|
];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(f, &setup.app_data, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::too_many_lines)]
|
|
/// Check that the whole layout is drawn correctly
|
|
fn test_draw_blocks_whole_layout_with_filter() {
|
|
let (w, h) = (160, 30);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
insert_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
|
|
setup.app_data.lock().containers.items[1]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some("127.0.0.1".to_owned()),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
|
|
let expected = [
|
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
|
"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮",
|
|
"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │",
|
|
"│ ││ delete │",
|
|
"│ ││ │",
|
|
"│ ││ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
|
|
"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│ line 1 │",
|
|
"│ line 2 │",
|
|
"│▶ line 3 │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
"╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮",
|
|
"│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
|
|
"│ │ ••• • ││ │ ••• • ││ 8001 │",
|
|
"│ │•• ••• ││ │•• ••• ││ │",
|
|
"│ │ ││ │ ││ │",
|
|
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
|
];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(f, &setup.app_data, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
}
|
|
}
|
|
|
|
setup
|
|
.gui_state
|
|
.lock()
|
|
.status_push(crate::ui::Status::Filter);
|
|
setup.app_data.lock().filter_term_push('r');
|
|
setup.app_data.lock().filter_term_push('_');
|
|
setup.app_data.lock().filter_term_push('1');
|
|
|
|
let expected = [
|
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
|
"╭ Containers 1/1 - filtered ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮",
|
|
"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │",
|
|
"│ ││ restart │",
|
|
"│ ││ stop │",
|
|
"│ ││ delete │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
|
|
"╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
|
"│ line 1 │",
|
|
"│ line 2 │",
|
|
"│▶ line 3 │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
"╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮",
|
|
"│10.00%│ ••• ││100.00 kB│ •• ││ ip private public│",
|
|
"│ │ •• • ││ │ •• • ││ 8001 │",
|
|
"│ │ ••• • • ││ │ ••• • • ││ │",
|
|
"│ │• •• ││ │• •• ││ │",
|
|
"│ │ ││ │ ││ │",
|
|
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
|
" Esc clear ← by → Name Image Status All term: r_1 ",
|
|
];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(f, &setup.app_data, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn correctly when have long container name and long image name
|
|
fn test_draw_blocks_whole_layout_long_name() {
|
|
let (w, h) = (190, 30);
|
|
let mut setup = test_setup(w, h, true, true);
|
|
|
|
insert_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some("127.0.0.1".to_owned()),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
|
|
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 expected = [
|
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
|
"╭ Containers 1/3 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭─────────────────╮",
|
|
"│⚪ a_long_container_name_for_the… ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB ││▶ pause │",
|
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │",
|
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │",
|
|
"│ ││ delete │",
|
|
"│ ││ │",
|
|
"│ ││ │",
|
|
"╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰─────────────────╯",
|
|
"╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image_name_for_the_purposes_of_this_test ──────────────────────────────────────────────────────────────────────────╮",
|
|
"│ line 1 │",
|
|
"│ line 2 │",
|
|
"│▶ line 3 │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"│ │",
|
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
|
"╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮╭────────── ports ───────────╮",
|
|
"│10.00%│ •••• ││100.00 kB│ ••••• ││ ip private public│",
|
|
"│ │ •••• • ││ │ ••• • ││ 8001 │",
|
|
"│ │••• •••• ││ │••• ••• ││127.0.0.1 8003 8003│",
|
|
"│ │ ││ │ ││ │",
|
|
"╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
|
];
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(f, &setup.app_data, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
for (row_index, result_row) in get_result(&setup, w) {
|
|
let expected_row = expected_to_vec(&expected, row_index);
|
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
|
}
|
|
}
|
|
}
|
|
}
|