use std::{rc::Rc, sync::Arc};
use parking_lot::Mutex;
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style},
widgets::{Block, Paragraph},
};
use super::{CONSTRAINT_100, MARGIN};
use crate::{
app_data::{Header, SortedOrder},
config::{AppColors, Keymap},
ui::{FrameData, GuiState, Status, gui_state::Region},
};
fn gen_header<'a>(
colors: AppColors,
fd: &FrameData,
header: Header,
width: usize,
) -> (Paragraph<'a>, u16) {
let block = gen_header_block(colors, fd, header);
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)
.style(gen_style(None, block.0))
.alignment(Alignment::Left);
(status, count)
}
fn gen_header_block<'a>(colors: AppColors, fd: &FrameData, header: Header) -> (Color, &'a str) {
let mut color = colors.headers_bar.text;
let mut suffix = "";
if let Some((a, b)) = &fd.sorted_by
&& &header == a
{
match b {
SortedOrder::Asc => suffix = " ▲",
SortedOrder::Desc => suffix = " ▼",
}
color = colors.headers_bar.text_selected;
}
(color, suffix)
}
fn gen_style(bg: Option<Color>, fg: Color) -> Style {
bg.map_or_else(
|| Style::default().fg(fg),
|bg| Style::default().bg(bg).fg(fg),
)
}
fn gen_help_text(fd: &FrameData, keymap: &Keymap) -> String {
let suffix = if fd.status.contains(&Status::Help) {
"exit"
} else {
"show"
};
if keymap.toggle_help == Keymap::new().toggle_help {
format!("( h ) {suffix} help{MARGIN}")
} else if let Some(secondary) = keymap.toggle_help.1 {
format!(
" ( {} | {secondary} ) {suffix} help{MARGIN}",
keymap.toggle_help.0
)
} else {
format!(" ( {} ) {suffix} help{MARGIN}", keymap.toggle_help.0)
}
}
fn draw_help(
colors: AppColors,
f: &mut Frame,
fd: &FrameData,
help_text: String,
gui_state: &Arc<Mutex<GuiState>>,
split_bar: &Rc<[Rect]>,
) {
let help_text_color = if fd.status.contains(&Status::Help) {
colors.headers_bar.text
} else {
colors.headers_bar.text_selected
};
let help_paragraph = Paragraph::new(help_text)
.style(gen_style(None, help_text_color))
.alignment(Alignment::Right);
let help_index = if fd.has_containers { 2 } else { 0 };
gui_state
.lock()
.update_region_map(Region::HelpPanel, split_bar[help_index]);
f.render_widget(help_paragraph, split_bar[help_index]);
}
fn draw_loading_spinner(colors: AppColors, f: &mut Frame, fd: &FrameData, rect: Rect) {
let loading_paragraph = Paragraph::new(format!("{:>2}", fd.loading_icon))
.style(gen_style(None, colors.headers_bar.loading_spinner))
.alignment(Alignment::Left);
f.render_widget(loading_paragraph, rect);
}
fn draw_columns(
colors: AppColors,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
split_bar: &Rc<[Rect]>,
) {
if fd.has_containers {
let header_section_width = split_bar[1].width;
let mut counter = 0;
let header_meta = [
(Header::Name, fd.columns.name.1),
(Header::State, fd.columns.state.1),
(Header::Status, fd.columns.status.1),
(Header::Cpu, fd.columns.cpu.1),
(Header::Memory, fd.columns.mem.1 + fd.columns.mem.2 + 3),
(Header::Id, fd.columns.id.1),
(Header::Image, fd.columns.image.1),
(Header::Rx, fd.columns.net_rx.1),
(Header::Tx, fd.columns.net_tx.1),
];
let header_data = header_meta
.into_iter()
.filter_map(|(header, width)| {
let header_block = gen_header(colors, fd, header, usize::from(width));
counter += header_block.1;
if counter <= header_section_width {
Some((header_block.0, header, Constraint::Max(header_block.1)))
} else {
None
}
})
.collect::<Vec<_>>();
let headers_section = Layout::default()
.direction(Direction::Horizontal)
.constraints(header_data.iter().map(|i| i.2))
.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);
f.render_widget(paragraph, rect);
}
}
}
pub fn draw(
area: Rect,
colors: AppColors,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
keymap: &Keymap,
) {
let gen_style = |bg: Option<Color>, fg: Color| {
bg.map_or_else(
|| Style::default().fg(fg),
|bg| Style::default().bg(bg).fg(fg),
)
};
f.render_widget(
Block::default().style(gen_style(Some(colors.headers_bar.background), Color::Reset)),
area,
);
let help_text = gen_help_text(fd, keymap);
let help_width = help_text.chars().count();
let column_width = usize::from(area.width).saturating_sub(help_width);
let column_width = if column_width > 0 { column_width } else { 1 };
let split_bar = Layout::default()
.direction(Direction::Horizontal)
.constraints(if fd.has_containers {
vec![
Constraint::Max(4),
Constraint::Max(column_width.try_into().unwrap_or_default()),
Constraint::Max(help_width.try_into().unwrap_or_default()),
]
} else {
CONSTRAINT_100.to_vec()
})
.split(area);
draw_loading_spinner(colors, f, fd, split_bar[0]);
draw_columns(colors, f, fd, gui_state, &split_bar);
draw_help(colors, f, fd, help_text, gui_state, &split_bar);
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::ops::RangeInclusive;
use crossterm::event::KeyCode;
use insta::assert_snapshot;
use ratatui::style::Color;
use uuid::Uuid;
use crate::{
app_data::{Header, SortedOrder, StatefulList},
config::{AppColors, Keymap},
ui::{
FrameData, Status,
draw_blocks::tests::{TuiTestSetup, get_result, test_setup},
},
};
#[test]
fn test_draw_blocks_headers_no_containers_show_help() {
let mut setup = test_setup(140, 1, true, true);
setup.app_data.lock().containers = StatefulList::new(vec![]);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (_, result_row) in get_result(&setup) {
for result_cell in result_row {
assert_eq!(result_cell.bg, Color::Magenta);
assert_eq!(result_cell.fg, Color::Gray,);
}
}
}
#[test]
fn test_draw_blocks_headers_no_containers_exit_help() {
let mut setup = test_setup(140, 1, true, true);
setup.app_data.lock().containers = StatefulList::new(vec![]);
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
fd.status.insert(Status::Help);
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (_, result_row) in get_result(&setup) {
for result_cell in result_row {
assert_eq!(result_cell.bg, Color::Magenta);
assert_eq!(result_cell.fg, Color::Black);
}
}
}
#[test]
fn test_draw_blocks_headers_some_containers() {
let mut setup = test_setup(140, 1, true, true);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (_, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
assert_eq!(result_cell.bg, Color::Magenta);
assert_eq!(
result_cell.fg,
match result_cell_index {
0..=3 => Color::White,
4..=111 => Color::Black,
112..=121 => Color::Reset,
_ => Color::Gray,
}
);
}
}
}
#[test]
fn test_draw_blocks_headers_some_containers_reduced_width() {
let mut setup = test_setup(80, 1, true, true);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (_, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
assert_eq!(result_cell.bg, Color::Magenta);
assert_eq!(
result_cell.fg,
match result_cell_index {
0..=3 => Color::White,
4..=50 => Color::Black,
51..=61 => Color::Reset,
_ => Color::Gray,
}
);
}
}
}
#[test]
fn test_draw_blocks_headers_animation() {
let mut setup = test_setup(140, 1, true, true);
let uuid = Uuid::new_v4();
setup.gui_state.lock().next_loading(uuid);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (_, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
assert_eq!(result_cell.bg, Color::Magenta);
assert_eq!(
result_cell.fg,
match result_cell_index {
0..=3 => Color::White,
4..=111 => Color::Black,
122..=140 => Color::Gray,
_ => Color::Reset,
}
);
}
}
}
#[test]
fn test_draw_blocks_headers_custom_colors() {
let mut setup = test_setup(140, 1, true, true);
let uuid = Uuid::new_v4();
setup.gui_state.lock().next_loading(uuid);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
let keymap = &setup.app_data.lock().config.keymap;
let mut colors = AppColors::new();
colors.headers_bar.background = Color::Black;
colors.headers_bar.loading_spinner = Color::Green;
colors.headers_bar.text = Color::Blue;
colors.headers_bar.text_selected = Color::Yellow;
setup
.terminal
.draw(|f| {
super::draw(setup.area, colors, f, &fd, &setup.gui_state, keymap);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
for (_, result_row) in get_result(&setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
assert_eq!(result_cell.bg, Color::Black);
assert_eq!(
result_cell.fg,
match result_cell_index {
0..=3 => Color::Green,
4..=111 => Color::Blue,
122..=140 => Color::Yellow,
_ => Color::Reset,
}
);
}
}
}
#[test]
fn test_draw_blocks_headers_custom_keymap_one_definition() {
let mut setup = test_setup(140, 1, true, true);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
let mut keymap = Keymap::new();
keymap.toggle_help = (KeyCode::Char('T'), None);
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
#[test]
fn test_draw_blocks_headers_custom_keymap_two_definitions() {
let mut setup = test_setup(140, 1, true, true);
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
let mut keymap = Keymap::new();
keymap.toggle_help = (KeyCode::Char('T'), Some(KeyCode::Tab));
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&keymap,
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
}
fn check_color(setup: &TuiTestSetup, range: RangeInclusive<usize>) {
for (_, result_row) in get_result(setup) {
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
assert_eq!(result_cell.bg, Color::Magenta);
assert_eq!(
result_cell.fg,
match result_cell_index {
0..=3 => Color::White,
122..=139 => Color::Gray,
x if range.contains(&x) => Color::Gray,
112..=121 => Color::Reset,
_ => Color::Black,
}
);
}
}
}
macro_rules! test_draw_blocks_headers_sort {
($name:ident, $header:expr, $order:expr, $color_range:expr) => {
#[test]
fn $name() {
let mut setup = test_setup(140, 1, true, true);
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
fd.sorted_by = Some(($header, $order));
setup
.terminal
.draw(|f| {
super::draw(
setup.area,
AppColors::new(),
f,
&fd,
&setup.gui_state,
&Keymap::new(),
);
})
.unwrap();
assert_snapshot!(setup.terminal.backend());
check_color(&setup, $color_range);
}
};
}
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_name_asc,
Header::Name,
SortedOrder::Asc,
1..=17
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_name_desc,
Header::Name,
SortedOrder::Desc,
1..=17
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_state_asc,
Header::State,
SortedOrder::Asc,
18..=29
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_state_desc,
Header::State,
SortedOrder::Desc,
18..=29
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_status_asc,
Header::Status,
SortedOrder::Asc,
30..=41
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_status_desc,
Header::Status,
SortedOrder::Desc,
30..=41
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_cpu_asc,
Header::Cpu,
SortedOrder::Asc,
42..=50
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_cpu_desc,
Header::Cpu,
SortedOrder::Desc,
42..=50
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_memory_asc,
Header::Memory,
SortedOrder::Asc,
51..=70
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_memory_desc,
Header::Memory,
SortedOrder::Desc,
51..=70
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_id_asc,
Header::Id,
SortedOrder::Asc,
71..=81
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_id_desc,
Header::Id,
SortedOrder::Desc,
71..=81
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_image_asc,
Header::Image,
SortedOrder::Asc,
82..=91
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_image_desc,
Header::Image,
SortedOrder::Desc,
82..=91
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_rx_asc,
Header::Rx,
SortedOrder::Asc,
92..=101
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_rx_desc,
Header::Rx,
SortedOrder::Desc,
92..=101
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_tx_asc,
Header::Tx,
SortedOrder::Asc,
102..=111
);
test_draw_blocks_headers_sort!(
test_draw_blocks_headers_sort_containers_tx_desc,
Header::Tx,
SortedOrder::Desc,
102..=111
);
}