use std::sync::Arc;
use parking_lot::Mutex;
use ratatui::{
layout::{Constraint, Rect},
style::Style,
widgets::{Block, BorderType, Borders},
};
use crate::config::AppColors;
use super::{FrameData, GuiState, SelectablePanel, Status, gui_state::Region};
pub mod chart_bandwidth;
pub mod chart_cpu_mem;
pub mod commands;
pub mod containers;
pub mod delete_confirm;
pub mod error;
pub mod filter;
pub mod headers;
pub mod help;
pub mod info;
pub mod inspect;
pub mod logs;
pub mod popup;
pub mod ports;
pub mod search_logs;
pub const NAME_TEXT: &str = r#" 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 "#;
pub const NAME: &str = env!("CARGO_PKG_NAME");
pub const REPO: &str = env!("CARGO_PKG_REPOSITORY");
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
pub const MARGIN: &str = " ";
pub const SELECT_ARROW: &str = "▶ ";
pub const LEFT_ARROW: &str = "←";
pub const RIGHT_ARROW: &str = "→";
pub const DOWN_ARROW: &str = "↓";
pub const UP_ARROW: &str = "↑";
pub const CIRCLE: &str = "⚪ ";
#[cfg(not(test))]
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
pub const VERSION: &str = "0.00.000";
pub const CONSTRAINT_50_50: [Constraint; 2] =
[Constraint::Percentage(50), Constraint::Percentage(50)];
pub const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)];
pub const CONSTRAINT_POPUP: [Constraint; 5] = [
Constraint::Min(2),
Constraint::Max(1),
Constraint::Max(1),
Constraint::Max(3),
Constraint::Min(1),
];
pub const CONSTRAINT_BUTTONS: [Constraint; 5] = [
Constraint::Percentage(10),
Constraint::Percentage(35),
Constraint::Percentage(10),
Constraint::Percentage(35),
Constraint::Percentage(10),
];
pub fn max_line_width(text: &str) -> usize {
text.lines()
.map(|i| i.chars().count())
.max()
.unwrap_or_default()
}
fn generate_block<'a>(
area: Rect,
colors: AppColors,
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(), fd.container_title)
}
SelectablePanel::Logs => {
format!("{}{}", panel.title(), fd.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(ratatui::text::Line::from(title).left_aligned());
if panel == SelectablePanel::Logs
&& let Some(x) = fd.scroll_title.as_ref()
{
block = block
.title_bottom(x.to_owned())
.title_alignment(ratatui::layout::Alignment::Right);
}
if !fd.status.contains(&Status::Filter) {
if fd.selected_panel == panel {
block = block.border_style(Style::default().fg(colors.borders.selected));
} else {
block = block.border_style(Style::default().fg(colors.borders.unselected));
}
}
block
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
pub mod tests {
use std::{
net::{IpAddr, Ipv4Addr},
sync::Arc,
};
use insta::assert_snapshot;
use parking_lot::Mutex;
use ratatui::{Terminal, backend::TestBackend, layout::Rect, style::Color};
use crate::{
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
app_error::AppError,
tests::{gen_appdata, gen_containers},
ui::{GuiState, Rerender, Status, draw_frame},
};
use super::FrameData;
pub struct TuiTestSetup {
pub app_data: Arc<Mutex<AppData>>,
pub gui_state: Arc<Mutex<GuiState>>,
pub fd: FrameData,
pub area: Rect,
pub terminal: Terminal<TestBackend>,
pub ids: Vec<ContainerId>,
}
pub const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
pub const COLOR_RX: Color = Color::Rgb(255, 233, 193);
pub const COLOR_TX: Color = Color::Rgb(205, 140, 140);
pub const COLOR_ORANGE: Color = Color::Rgb(255, 178, 36);
impl From<(&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)> for FrameData {
fn from(data: (&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)) -> Self {
let (mut app_data, gui_data) = (data.0.lock(), data.1.lock());
let (filter_by, filter_term) = app_data.get_filter();
Self {
chart_data: app_data.get_chart_data(),
color_logs: app_data.config.color_logs,
columns: app_data.get_width(),
container_title: app_data.get_container_title(),
delete_confirm: gui_data.get_delete_container(),
filter_by,
filter_term: filter_term.cloned(),
has_containers: app_data.get_container_len() > 0,
has_error: app_data.get_error(),
show_logs: gui_data.get_show_logs(),
info_text: gui_data.info_box_text.clone(),
is_loading: gui_data.is_loading(),
log_search: app_data.gen_log_search(),
loading_icon: gui_data.get_loading().to_string(),
log_height: gui_data.get_log_height(),
log_title: app_data.get_log_title(),
scroll_title: app_data.get_scroll_title(gui_data.get_screen_width()),
port_max_lens: app_data.get_longest_port(),
ports: app_data.get_selected_ports(),
selected_panel: gui_data.get_selected_panel(),
sorted_by: app_data.get_sorted(),
status: gui_data.get_status(),
}
}
}
pub 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 redraw = Arc::new(Rerender::new());
let gui_state = GuiState::new(&redraw, app_data.config.show_logs);
let app_data = Arc::new(Mutex::new(app_data));
let gui_state = Arc::new(Mutex::new(gui_state));
let fd = FrameData::from((&app_data, &gui_state));
let area = Rect::new(0, 0, w, h);
gui_state.lock().set_screen_width(w);
TuiTestSetup {
app_data,
gui_state,
fd,
area,
terminal,
ids,
}
}
pub fn get_result(
setup: &'_ TuiTestSetup,
) -> std::iter::Enumerate<std::slice::Chunks<'_, ratatui::buffer::Cell>> {
setup
.terminal
.backend()
.buffer()
.content
.chunks(usize::from(setup.area.width))
.enumerate()
}
pub 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]);
}
#[allow(clippy::cast_precision_loss)]
pub fn insert_all_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,
);
}
}
#[test]
fn test_draw_blocks_whole_layout() {
let mut setup = test_setup(160, 30, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
#[allow(clippy::too_many_lines)]
fn test_draw_blocks_whole_layout_with_filter_bar() {
let mut setup = test_setup(160, 30, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[1]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
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 fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_long_name() {
let mut setup = test_setup(190, 30, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
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 fd = FrameData::from((&setup.app_data, &setup.gui_state));
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_no_logs() {
let mut setup = test_setup(160, 30, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup.gui_state.lock().log_height_zero();
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_short_height_logs() {
let mut setup = test_setup(160, 30, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup.gui_state.lock().log_height_zero();
for _ in 0..=3 {
setup.gui_state.lock().log_height_increase();
}
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_help_panel() {
let mut setup = test_setup(160, 40, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup.gui_state.lock().status_push(Status::Help);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_error() {
let mut setup = test_setup(160, 40, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup.app_data.lock().set_error(
AppError::DockerCommand(crate::app_data::DockerCommand::Pause),
&setup.gui_state,
Status::Error,
);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_delete() {
let mut setup = test_setup(160, 40, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup
.gui_state
.lock()
.set_delete_container(setup.app_data.lock().get_selected_container_id());
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_whole_layout_info_box() {
let mut setup = test_setup(160, 40, true, true);
insert_all_chart_data(&setup);
insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
private: 8003,
public: Some(8003),
});
let colors = setup.app_data.lock().config.app_colors;
let keymap = setup.app_data.lock().config.keymap.clone();
setup.gui_state.lock().set_info_box("This is a test");
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
}