feat: config file, closes #47
Enable use of a config file, with custom keymap and custom colours
This commit is contained in:
@@ -7,6 +7,7 @@ pub mod log_sanitizer {
|
||||
};
|
||||
|
||||
/// Attempt to colorize the given string to ratatui standards
|
||||
/// TODO this is somewhat slow/cpu intensive
|
||||
pub fn colorize_logs<'a>(input: &str) -> Vec<Line<'a>> {
|
||||
vec![Line::from(
|
||||
categorise_text(input)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,507 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use ratatui::{
|
||||
layout::{Alignment, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
text::Span,
|
||||
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{FrameData, CONSTRAINT_50_50};
|
||||
use crate::{
|
||||
app_data::{ByteStats, CpuStats, State, Stats},
|
||||
config::AppColors,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ChartType {
|
||||
Cpu,
|
||||
Memory,
|
||||
}
|
||||
|
||||
impl ChartType {
|
||||
const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Cpu => "cpu",
|
||||
Self::Memory => "memory",
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_title_color(self, colors: AppColors, state: State) -> Color {
|
||||
if state.is_healthy() {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.title,
|
||||
Self::Memory => colors.chart_memory.title,
|
||||
}
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_bg_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.background,
|
||||
Self::Memory => colors.chart_memory.background,
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_border_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.border,
|
||||
Self::Memory => colors.chart_memory.border,
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_y_axis_color(self, colors: AppColors) -> Color {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.y_axis,
|
||||
Self::Memory => colors.chart_memory.y_axis,
|
||||
}
|
||||
}
|
||||
|
||||
const fn get_max_color(self, colors: AppColors, state: State) -> Color {
|
||||
if state.is_healthy() {
|
||||
match self {
|
||||
Self::Cpu => colors.chart_cpu.max,
|
||||
Self::Memory => colors.chart_memory.max,
|
||||
}
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mem_stats, mem_dataset, mem.1, "", cpu.2
|
||||
// current, dataset, max, name, state
|
||||
/// Create charts
|
||||
fn make_chart<'a, T: Stats + Display>(
|
||||
chart_type: ChartType,
|
||||
colors: AppColors,
|
||||
current: &'a T,
|
||||
dataset: Vec<Dataset<'a>>,
|
||||
max: &'a T,
|
||||
state: State,
|
||||
) -> Chart<'a> {
|
||||
let max_color = chart_type.get_max_color(colors, state);
|
||||
|
||||
Chart::new(dataset)
|
||||
.bg(chart_type.get_bg_color(colors))
|
||||
.block(
|
||||
Block::default()
|
||||
.style(Style::default().bg(chart_type.get_bg_color(colors)))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
format!(" {} {current} ", chart_type.name()),
|
||||
Style::default()
|
||||
.fg(chart_type.get_title_color(colors, state))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
// .bg(chart_type.get_bg_color(colors))
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(chart_type.get_border_color(colors))),
|
||||
)
|
||||
.x_axis(Axis::default().bounds([0.00, 60.0]))
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.labels(vec![
|
||||
Span::styled("", Style::default().fg(max_color)),
|
||||
Span::styled(
|
||||
format!("{max}"),
|
||||
Style::default().add_modifier(Modifier::BOLD).fg(max_color),
|
||||
),
|
||||
])
|
||||
.style(
|
||||
Style::new()
|
||||
// .bg(chart_type.get_bg_color(colors))
|
||||
.fg(chart_type.get_y_axis_color(colors)),
|
||||
)
|
||||
// Add 0.01, so that max point is always visible?
|
||||
.bounds([0.0, max.get_value() + 0.01]),
|
||||
)
|
||||
|
||||
// .style(Style::new().bg(chart_type.get_bg_color(colors)))
|
||||
}
|
||||
|
||||
/// Draw the cpu + mem charts
|
||||
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
||||
if let Some((cpu, mem)) = fd.chart_data.as_ref() {
|
||||
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(colors.chart_cpu.points))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&cpu.0)];
|
||||
let mem_dataset = vec![Dataset::default()
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(colors.chart_memory.points))
|
||||
.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(
|
||||
ChartType::Cpu,
|
||||
colors,
|
||||
&cpu_stats,
|
||||
cpu_dataset,
|
||||
&cpu.1,
|
||||
cpu.2,
|
||||
);
|
||||
let mem_chart = make_chart(
|
||||
ChartType::Memory,
|
||||
colors,
|
||||
&mem_stats,
|
||||
mem_dataset,
|
||||
&mem.1,
|
||||
mem.2,
|
||||
);
|
||||
|
||||
f.render_widget(cpu_chart, area[0]);
|
||||
f.render_widget(mem_chart, area[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::State,
|
||||
config::AppColors,
|
||||
ui::{
|
||||
draw_blocks::tests::{
|
||||
expected_to_vec, get_result, insert_chart_data, test_setup, COLOR_ORANGE,
|
||||
},
|
||||
FrameData,
|
||||
},
|
||||
};
|
||||
|
||||
/// 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);
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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, COLOR_ORANGE);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
(2..=8, 1..=6 | 8..=38 | 49..=78 | 41..=47) | (1, 8..=38 | 49..=78) => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert!(result_cell.modifier.is_empty());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
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);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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, COLOR_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());
|
||||
}
|
||||
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
|
||||
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]
|
||||
/// 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;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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());
|
||||
}
|
||||
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
|
||||
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]
|
||||
/// When dead, text is red
|
||||
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;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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());
|
||||
}
|
||||
(0 | 9, 0..=80) | (1..=9, 0 | 7 | 39 | 40 | 50 | 79) => {
|
||||
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]
|
||||
/// Custom colos correctly applied to each part of the charts
|
||||
fn test_custom_colors() {
|
||||
let mut colors = AppColors::new();
|
||||
|
||||
colors.chart_cpu.background = Color::White;
|
||||
colors.chart_cpu.border = Color::Red;
|
||||
colors.chart_cpu.title = Color::Green;
|
||||
colors.chart_cpu.max = Color::Magenta;
|
||||
colors.chart_cpu.points = Color::Black;
|
||||
colors.chart_cpu.y_axis = Color::Blue;
|
||||
|
||||
colors.chart_memory.background = Color::White;
|
||||
colors.chart_memory.border = Color::Red;
|
||||
colors.chart_memory.title = Color::Green;
|
||||
colors.chart_memory.max = Color::Magenta;
|
||||
colors.chart_memory.points = Color::Black;
|
||||
colors.chart_memory.y_axis = Color::Blue;
|
||||
|
||||
let (w, h) = (80, 10);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_chart_data(&setup);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd);
|
||||
})
|
||||
.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::White);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// border
|
||||
(0, 0..=13 | 26..=50 | 68..=79) | (9, _) | (1..=8, 0 | 39 | 40 | 79) => {
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// title
|
||||
(0, 14..=25 | 51..=67) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
}
|
||||
// max label
|
||||
(1, 1..=6 | 41..=49) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
// data points
|
||||
xy if CPU_XY.contains(&xy) | MEM_XY.contains(&xy) => {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
// y axis
|
||||
(1..=8, 7 | 50) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::RIGHT_ARROW;
|
||||
use crate::{
|
||||
app_data::AppData,
|
||||
config::AppColors,
|
||||
ui::{FrameData, GuiState, SelectablePanel},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{List, ListItem, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::generate_block;
|
||||
|
||||
/// Draw the command panel
|
||||
pub fn draw(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Commands)
|
||||
.bg(colors.commands.background);
|
||||
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(colors)),
|
||||
)]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
tests::gen_container_summary,
|
||||
ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup, BORDER_CHARS},
|
||||
FrameData,
|
||||
},
|
||||
};
|
||||
|
||||
// cusomt border colors
|
||||
#[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);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
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);
|
||||
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
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) {
|
||||
// Borders & delete
|
||||
(0 | 5, _) | (1..=4, 0 | 11) | (4, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change the controls state
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.update_containers(vec![gen_container_summary(1, "paused")]);
|
||||
setup.app_data.lock().docker_controls_next();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│ resume │",
|
||||
"│▶ stop │",
|
||||
"│ delete │",
|
||||
"│ │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
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
|
||||
(0 | 5, _) | (1..=4, 0 | 11) | (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 │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
// Unselected, has a grey border
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
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::Gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control panel now selected, should have a blue border
|
||||
setup.gui_state.lock().next_panel();
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for (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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors are rendered correctlty
|
||||
fn test_draw_blocks_commands_custom_colors() {
|
||||
let (w, h) = (12, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let mut colors = AppColors::new();
|
||||
colors.commands.background = Color::White;
|
||||
colors.commands.pause = Color::Black;
|
||||
colors.commands.restart = Color::Green;
|
||||
colors.commands.stop = Color::Blue;
|
||||
colors.commands.delete = Color::Magenta;
|
||||
colors.commands.resume = Color::Yellow;
|
||||
colors.commands.start = Color::Cyan;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
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::White);
|
||||
match (row_index, result_cell_index) {
|
||||
// pause
|
||||
(1, 3..=7) => {
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
// restart
|
||||
(2, 3..=9) => {
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
}
|
||||
// stop
|
||||
(3, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// delete
|
||||
(4, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Change the controls state
|
||||
setup
|
||||
.app_data
|
||||
.lock()
|
||||
.update_containers(vec![gen_container_summary(1, "paused")]);
|
||||
setup.app_data.lock().docker_controls_next();
|
||||
|
||||
let expected = [
|
||||
"╭──────────╮",
|
||||
"│ resume │",
|
||||
"│▶ stop │",
|
||||
"│ delete │",
|
||||
"│ │",
|
||||
"╰──────────╯",
|
||||
];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
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::White);
|
||||
|
||||
match (row_index, result_cell_index) {
|
||||
// resume
|
||||
(1, 3..=7) => {
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// stop
|
||||
(2, 3..=6) => {
|
||||
assert_eq!(result_cell.fg, Color::Blue);
|
||||
}
|
||||
// delete
|
||||
(3, 3..=8) => {
|
||||
assert_eq!(result_cell.fg, Color::Magenta);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,421 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Direction, Layout},
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_BUTTONS, CONSTRAINT_POPUP};
|
||||
use crate::{
|
||||
app_data::ContainerName,
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
gui_state::{BoxLocation, Region},
|
||||
DeleteButton, GuiState,
|
||||
},
|
||||
};
|
||||
|
||||
use super::popup;
|
||||
|
||||
/// Draw the delete confirm box in the centre of the screen
|
||||
/// take in container id and container name here?
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
keymap: &Keymap,
|
||||
name: &ContainerName,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Confirm Delete ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_delete.background)
|
||||
.fg(colors.popup_delete.text),
|
||||
)
|
||||
.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(colors.popup_delete.text_highlight)
|
||||
.bg(colors.popup_delete.background)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]);
|
||||
|
||||
let yes_text = if keymap.delete_confirm == Keymap::new().delete_confirm {
|
||||
"( y ) yes".to_owned()
|
||||
} else if let Some(secondary) = keymap.delete_confirm.1 {
|
||||
format!("( {} | {} ) yes", keymap.delete_confirm.0, secondary)
|
||||
} else {
|
||||
format!("( {} ) yes", keymap.delete_confirm.0)
|
||||
};
|
||||
|
||||
let no_text = if keymap.delete_deny == Keymap::new().delete_deny {
|
||||
"( n ) no".to_owned()
|
||||
} else if let Some(secondary) = keymap.delete_deny.1 {
|
||||
format!("( {} | {} ) no", keymap.delete_deny.0, secondary)
|
||||
} else {
|
||||
format!("( {} ) no", keymap.delete_deny.0)
|
||||
};
|
||||
|
||||
// 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(colors.popup_delete.background))
|
||||
};
|
||||
|
||||
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::draw(
|
||||
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::Cancel), no_area);
|
||||
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::Confirm), yes_area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::ContainerName,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
};
|
||||
|
||||
#[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 ) no │ │ ( y ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
keymap,
|
||||
&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 ) no │ │ ( y ) yes │ │ ",
|
||||
" │ ╰──────────────────────────────╯ ╰─────────────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(colors, f, &setup.gui_state, keymap, &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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors applied correctly to delete popup
|
||||
fn test_draw_blocks_delete_custom_colors() {
|
||||
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 ) no │ │ ( y ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_delete.background = Color::Black;
|
||||
colors.popup_delete.text = Color::Yellow;
|
||||
colors.popup_delete.text_highlight = Color::Green;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
&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::Black);
|
||||
assert_eq!(result_cell.fg, Color::Green);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap, with multiple definitions for each button, applied correctly to delete popup
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn test_draw_blocks_delete_custom_keymap() {
|
||||
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 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( End ) no │ │ ( F10 ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.delete_confirm = (KeyCode::F(10), None);
|
||||
keymap.delete_deny = (KeyCode::End, None);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
&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]);
|
||||
}
|
||||
}
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( End | Up ) no │ │ ( F10 | L ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.delete_confirm = (KeyCode::F(10), Some(KeyCode::Char('L')));
|
||||
keymap.delete_deny = (KeyCode::End, Some(KeyCode::Up));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
&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]);
|
||||
}
|
||||
}
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Are you sure you want to delete container: container_1 │ ",
|
||||
" │ │ ",
|
||||
" │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
|
||||
" │ │ ( End | Up ) no │ │ ( F10 ) yes │ │ ",
|
||||
" │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
|
||||
" ╰────────────────────────────────────────────────────────────────╯ ",
|
||||
" ",
|
||||
];
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.delete_confirm = (KeyCode::F(10), None);
|
||||
keymap.delete_deny = (KeyCode::End, Some(KeyCode::Up));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
AppColors::new(),
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
&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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
style::Style,
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{max_line_width, NAME, VERSION};
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
ui::gui_state::BoxLocation,
|
||||
};
|
||||
|
||||
use super::popup;
|
||||
|
||||
/// Draw an error popup over whole screen
|
||||
pub fn draw(
|
||||
f: &mut Frame,
|
||||
error: &AppError,
|
||||
keymap: &Keymap,
|
||||
seconds: Option<u8>,
|
||||
colors: AppColors,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Error ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let to_push = if matches!(error, AppError::DockerConnect) {
|
||||
format!(
|
||||
"\n\n {}::v{} closing in {:02} seconds",
|
||||
NAME,
|
||||
VERSION,
|
||||
seconds.unwrap_or(5)
|
||||
)
|
||||
} else {
|
||||
let clear_suffix = "clear error";
|
||||
let clear_text = if keymap.clear == Keymap::new().clear {
|
||||
format!("( {} ) {clear_suffix}", keymap.clear.0)
|
||||
} else if let Some(secondary) = keymap.clear.1 {
|
||||
format!(" ( {} | {secondary} ) {clear_suffix}", keymap.clear.0)
|
||||
} else {
|
||||
format!(" ( {} ) {clear_suffix}", keymap.clear.0)
|
||||
};
|
||||
|
||||
let quit_suffix = "quit oxker";
|
||||
let quit_text = if keymap.quit == Keymap::new().quit {
|
||||
format!("( {} ) {quit_suffix}", keymap.quit.0)
|
||||
} else if let Some(secondary) = keymap.quit.1 {
|
||||
format!(" ( {} | {secondary} ) {quit_suffix}", keymap.quit.0)
|
||||
} else {
|
||||
format!(" ( {} ) {quit_suffix}", keymap.quit.0)
|
||||
};
|
||||
|
||||
format!("\n\n{clear_text}\n\n{quit_text}")
|
||||
};
|
||||
|
||||
let mut text = format!("\n{error}");
|
||||
|
||||
text.push_str(to_push.as_str());
|
||||
|
||||
// Find the maximum line width & height
|
||||
let padded_width = max_line_width(&text) + 8;
|
||||
|
||||
let line_count = text.lines().count();
|
||||
let padded_height = if line_count % 2 == 0 {
|
||||
line_count + 3
|
||||
} else {
|
||||
line_count + 2
|
||||
};
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_error.background)
|
||||
.fg(colors.popup_error.text),
|
||||
)
|
||||
.block(block)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let area = popup::draw(
|
||||
padded_height,
|
||||
padded_width,
|
||||
f.area(),
|
||||
BoxLocation::MiddleCentre,
|
||||
);
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use super::VERSION;
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::Color;
|
||||
|
||||
#[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);
|
||||
let app_colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerConnect, keymap, Some(4), app_colors);
|
||||
})
|
||||
.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, 11);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let app_colors = setup.app_data.lock().config.app_colors;
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, keymap, Some(4), app_colors);
|
||||
})
|
||||
.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 | 10, _) | (1..=9, 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]
|
||||
/// Custom colors applied to the error popup correctly
|
||||
fn test_draw_blocks_clearable_error_custom_colors() {
|
||||
let (w, h) = (39, 11);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let keymap = &setup.app_data.lock().config.keymap;
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_error.background = Color::Yellow;
|
||||
colors.popup_error.text = Color::Black;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, keymap, Some(4), colors);
|
||||
})
|
||||
.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 | 10, _) | (1..=9, 0 | 38) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
}
|
||||
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Yellow);
|
||||
assert_eq!(result_cell.fg, Color::Black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom keymap applied correct with both 1 and 2 definitions
|
||||
fn test_draw_blocks_clearable_error_custom_keymap() {
|
||||
let (w, h) = (39, 11);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.clear = (KeyCode::BackTab, None);
|
||||
keymap.quit = (KeyCode::F(4), None);
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( Back Tab ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( F4 ) 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]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.clear = (KeyCode::BackTab, Some(KeyCode::Char('m')));
|
||||
keymap.quit = (KeyCode::F(4), Some(KeyCode::End));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( Back Tab | m ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( F4 | End ) 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]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut keymap = Keymap::new();
|
||||
keymap.quit = (KeyCode::F(4), Some(KeyCode::End));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
" ",
|
||||
" ╭────────────── Error ──────────────╮ ",
|
||||
" │ │ ",
|
||||
" │ Unable to exec into container │ ",
|
||||
" │ │ ",
|
||||
" │ ( c ) clear error │ ",
|
||||
" │ │ ",
|
||||
" │ ( F4 | End ) 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{app_data::FilterBy, ui::FrameData};
|
||||
|
||||
/// Create the filter_by by spans, coloured dependant on which one is selected
|
||||
fn filter_by_spans(fd: &FrameData) -> [Span; 4] {
|
||||
let selected = Style::default().bg(Color::Gray).fg(Color::Black);
|
||||
let not_selected = Style::default().bg(Color::Reset).fg(Color::Reset);
|
||||
|
||||
let name = [" Name ", " Image ", " Status ", " All "];
|
||||
|
||||
let mut filter_spans = [
|
||||
Span::styled(name[0], not_selected),
|
||||
Span::styled(name[1], not_selected),
|
||||
Span::styled(name[2], not_selected),
|
||||
Span::styled(name[3], not_selected),
|
||||
];
|
||||
|
||||
match fd.filter_by {
|
||||
FilterBy::Name => filter_spans[0] = Span::styled(name[0], selected),
|
||||
FilterBy::Image => filter_spans[1] = Span::styled(name[1], selected),
|
||||
FilterBy::Status => filter_spans[2] = Span::styled(name[2], selected),
|
||||
FilterBy::All => filter_spans[3] = Span::styled(name[3], selected),
|
||||
}
|
||||
filter_spans
|
||||
}
|
||||
|
||||
/// Draw the filter bar
|
||||
pub fn draw(area: Rect, frame: &mut Frame, fd: &FrameData) {
|
||||
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(fd));
|
||||
line.extend_from_slice(&[
|
||||
Span::styled(
|
||||
" term: ",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
fd.filter_term
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone),
|
||||
Style::default().fg(Color::Gray),
|
||||
),
|
||||
]);
|
||||
frame.render_widget(Line::from(line), area);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
FrameData,
|
||||
};
|
||||
|
||||
#[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::draw(setup.area, f, &setup.fd);
|
||||
})
|
||||
.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');
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, f, &fd);
|
||||
})
|
||||
.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();
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, f, &fd);
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use super::{CONSTRAINT_100, MARGIN};
|
||||
use crate::{
|
||||
app_data::{Header, SortedOrder},
|
||||
config::{AppColors, Keymap},
|
||||
ui::{gui_state::Region, FrameData, GuiState, Status},
|
||||
};
|
||||
|
||||
// Draw heading bar at top of program, always visible
|
||||
/// TODO Should separate into loading icon/headers/help functions
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn draw(
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
frame: &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),
|
||||
)
|
||||
};
|
||||
|
||||
frame.render_widget(
|
||||
Block::default().style(gen_style(Some(colors.headers_bar.background), Color::Reset)),
|
||||
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, colors: AppColors| {
|
||||
let mut color = colors.headers_bar.text;
|
||||
let mut suffix = "";
|
||||
if let Some((a, b)) = &fd.sorted_by {
|
||||
if x == a {
|
||||
match b {
|
||||
SortedOrder::Asc => suffix = " ▲",
|
||||
SortedOrder::Desc => suffix = " ▼",
|
||||
}
|
||||
color = colors.headers_bar.text_selected;
|
||||
};
|
||||
};
|
||||
|
||||
(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
|
||||
// TODO - yes this is a mess, needs documenting correctly
|
||||
let gen_header = |header: &Header, width: usize, colors: AppColors| {
|
||||
let block = header_block(header, colors);
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
// Meta data to iterate over to create blocks with correct widths
|
||||
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 suffix = if fd.status.contains(&Status::Help) {
|
||||
"exit"
|
||||
} else {
|
||||
"show"
|
||||
};
|
||||
|
||||
let info_text = 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)
|
||||
};
|
||||
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 fd.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}", fd.loading_icon))
|
||||
.style(gen_style(None, colors.headers_bar.loading_spinner))
|
||||
.alignment(Alignment::Left);
|
||||
frame.render_widget(loading_paragraph, split_bar[0]);
|
||||
if fd.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(), colors);
|
||||
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 help_text_color = if fd.status.contains(&Status::Help) {
|
||||
colors.headers_bar.text
|
||||
} else {
|
||||
colors.headers_bar.text_selected
|
||||
};
|
||||
|
||||
let help_paragraph = Paragraph::new(info_text)
|
||||
.style(gen_style(None, help_text_color))
|
||||
.alignment(Alignment::Right);
|
||||
|
||||
// If no containers, don't display the headers, could maybe do this first?
|
||||
let help_index = if fd.has_containers { 2 } else { 0 };
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::HelpPanel, split_bar[help_index]);
|
||||
frame.render_widget(help_paragraph, split_bar[help_index]);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::Color;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_data::{Header, SortedOrder, StatefulList},
|
||||
config::{AppColors, Keymap},
|
||||
ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
FrameData, Status,
|
||||
},
|
||||
};
|
||||
|
||||
#[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, &setup.gui_state));
|
||||
|
||||
let expected = [" ( h ) show help "];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.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.bg, Color::Magenta);
|
||||
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||
assert_eq!(result_cell.fg, Color::Gray,);
|
||||
}
|
||||
}
|
||||
|
||||
fd.status.insert(Status::Help);
|
||||
let expected = [" ( h ) exit help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.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, &setup.gui_state));
|
||||
|
||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.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 {
|
||||
0..=3 => Color::White,
|
||||
4..=111 => Color::Black,
|
||||
112..=121 => Color::Reset,
|
||||
_ => Color::Gray,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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, &setup.gui_state));
|
||||
|
||||
let expected =
|
||||
[" name state status cpu ( h ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.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 {
|
||||
0..=3 => Color::White,
|
||||
4..=50 => Color::Black,
|
||||
51..=61 => Color::Reset,
|
||||
_ => Color::Gray,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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, &setup.gui_state));
|
||||
|
||||
// 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::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.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 {
|
||||
0..=3 => Color::White,
|
||||
122..=139 => Color::Gray,
|
||||
// given range | help section
|
||||
x if range.contains(&x) => Color::Gray,
|
||||
112..=121 => Color::Reset,
|
||||
_ => 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, &setup.gui_state));
|
||||
|
||||
let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&Keymap::new(),
|
||||
);
|
||||
})
|
||||
.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 {
|
||||
0..=3 => Color::White,
|
||||
4..=111 => Color::Black,
|
||||
122..=140 => Color::Gray,
|
||||
_ => Color::Reset,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Custom colors are applied correctly
|
||||
fn test_draw_blocks_headers_cusomt_colors() {
|
||||
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, &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;
|
||||
|
||||
let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, colors, f, &fd, &setup.gui_state, keymap);
|
||||
})
|
||||
.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::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]
|
||||
/// Custom keymap for help panel is correctly display, with one and two definitions
|
||||
fn test_draw_blocks_headers_custom_keymap() {
|
||||
let (w, h) = (140, 1);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
let mut keymap = Keymap::new();
|
||||
|
||||
keymap.toggle_help = (KeyCode::Char('T'), None);
|
||||
|
||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( T ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.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]);
|
||||
}
|
||||
}
|
||||
|
||||
keymap.toggle_help = (KeyCode::Char('T'), Some(KeyCode::Tab));
|
||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( T | Tab ) show help "];
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
setup.area,
|
||||
AppColors::new(),
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
&keymap,
|
||||
);
|
||||
})
|
||||
.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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,886 @@
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{AppColors, Keymap},
|
||||
ui::gui_state::BoxLocation,
|
||||
};
|
||||
|
||||
use super::{popup, DESCRIPTION, NAME_TEXT, REPO, VERSION};
|
||||
|
||||
/// 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]
|
||||
fn calc_width(lines: &[Line]) -> usize {
|
||||
lines
|
||||
.iter()
|
||||
.map(ratatui::prelude::Line::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 text_span<'a>(input: &str, color: AppColors) -> Span<'a> {
|
||||
Self::span(input, color.popup_help.text)
|
||||
}
|
||||
|
||||
/// &str to white text span
|
||||
fn highlighted_text_span<'a>(input: &str, color: AppColors) -> Span<'a> {
|
||||
Self::span(input, color.popup_help.text_highlight)
|
||||
}
|
||||
|
||||
/// Generate the `oxker` name span + metadata
|
||||
fn gen_name(colors: AppColors) -> Self {
|
||||
let mut lines = NAME_TEXT
|
||||
.lines()
|
||||
.map(|i| Line::from(Self::highlighted_text_span(i, colors)))
|
||||
.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(colors: AppColors) -> Self {
|
||||
let lines = [
|
||||
Self::empty_span(),
|
||||
Line::from(Self::highlighted_text_span(DESCRIPTION, colors)),
|
||||
Self::empty_span(),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the button information span + metadata
|
||||
fn gen_keymap_info(colors: AppColors) -> Self {
|
||||
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
||||
let button_desc = |x: &str| Self::text_span(x, colors);
|
||||
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 - or click heading"),
|
||||
]),
|
||||
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"),
|
||||
]),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the final lines, GitHub link etc, + metadata
|
||||
fn gen_final(colors: AppColors) -> Self {
|
||||
let lines = [
|
||||
Self::empty_span(),
|
||||
Line::from(vec![Self::text_span(
|
||||
"currently an early work in progress, all and any input appreciated",
|
||||
colors,
|
||||
)]),
|
||||
Line::from(vec![Span::styled(
|
||||
REPO,
|
||||
Style::default()
|
||||
.fg(colors.popup_help.text_highlight)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
)]),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the display information when a custom keymap is being used
|
||||
fn gen_custom_keymap_info(colors: AppColors, km: &Keymap) -> Self {
|
||||
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
||||
let button_desc = |x: &str| Self::text_span(x, colors);
|
||||
let or = || button_desc("or");
|
||||
let space = || button_desc(" ");
|
||||
|
||||
let or_secondary = |a: (KeyCode, Option<KeyCode>), desc: &str| {
|
||||
a.1.map_or_else(
|
||||
|| {
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item(&a.0.to_string()),
|
||||
button_desc(desc),
|
||||
])
|
||||
},
|
||||
|secondary| {
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item(&a.0.to_string()),
|
||||
or(),
|
||||
button_item(&secondary.to_string()),
|
||||
button_desc(desc),
|
||||
])
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let lines = [
|
||||
Line::from(vec![Span::from("Custom keymap config in use\n")])
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default().fg(colors.popup_help.text_highlight)),
|
||||
or_secondary(km.select_next_panel, "select next panel"),
|
||||
or_secondary(km.select_previous_panel, "select previous panel"),
|
||||
or_secondary(km.scroll_down_one, "scroll list down by one"),
|
||||
or_secondary(km.scroll_up_one, "scroll list up by one"),
|
||||
or_secondary(km.scroll_down_many, "scroll list down by many"),
|
||||
or_secondary(km.scroll_up_many, "scroll list by up many"),
|
||||
or_secondary(km.scroll_end, "scroll list to end"),
|
||||
or_secondary(km.scroll_start, "scroll list to start"),
|
||||
Line::from(vec![
|
||||
space(),
|
||||
button_item("enter"),
|
||||
button_desc("send docker container command"),
|
||||
]),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
or_secondary(km.exec, "exec into a container"),
|
||||
#[cfg(target_os = "windows")]
|
||||
or_secondary(km.exec, "exec into a container - not available on Windows"),
|
||||
or_secondary(
|
||||
km.toggle_help,
|
||||
"toggle this help information - or click heading",
|
||||
),
|
||||
or_secondary(km.toggle_help, "save logs to file"),
|
||||
or_secondary(
|
||||
km.toggle_mouse_capture,
|
||||
"toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||
),
|
||||
or_secondary(km.filter_mode, "enter filter mode"),
|
||||
or_secondary(km.sort_reset, "reset container sorting"),
|
||||
or_secondary(km.sort_by_name, "sort containers by name"),
|
||||
or_secondary(km.sort_by_state, "sort containers by state"),
|
||||
or_secondary(km.sort_by_status, "sort containers by status"),
|
||||
or_secondary(km.sort_by_cpu, "sort containers by cpu"),
|
||||
or_secondary(km.sort_by_memory, "sort containers by memory"),
|
||||
or_secondary(km.sort_by_id, "sort containers by id"),
|
||||
or_secondary(km.sort_by_image, "sort containers by image"),
|
||||
or_secondary(km.sort_by_rx, "sort containers by rx"),
|
||||
or_secondary(km.sort_by_tx, "sort containers by tx"),
|
||||
or_secondary(km.clear, "close dialog"),
|
||||
or_secondary(km.quit, "quit at any time"),
|
||||
];
|
||||
|
||||
Self {
|
||||
lines: lines.to_vec(),
|
||||
width: Self::calc_width(&lines),
|
||||
height: lines.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the help box in the centre of the screen
|
||||
pub fn draw(f: &mut Frame, colors: AppColors, keymap: &Keymap) {
|
||||
let title = format!(" {VERSION} ");
|
||||
|
||||
let name_info = HelpInfo::gen_name(colors);
|
||||
let description_info = HelpInfo::gen_description(colors);
|
||||
let final_info = HelpInfo::gen_final(colors);
|
||||
|
||||
let button_info = if keymap == &Keymap::new() {
|
||||
HelpInfo::gen_keymap_info(colors)
|
||||
} else {
|
||||
HelpInfo::gen_custom_keymap_info(colors, keymap)
|
||||
};
|
||||
|
||||
let max_line_width = [
|
||||
name_info.width,
|
||||
description_info.width,
|
||||
button_info.width,
|
||||
final_info.width,
|
||||
]
|
||||
.into_iter()
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
+ 2;
|
||||
|
||||
let max_height =
|
||||
name_info.height + description_info.height + button_info.height + final_info.height + 2;
|
||||
|
||||
let area = popup::draw(
|
||||
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(colors.popup_help.background)
|
||||
.fg(colors.popup_help.text_highlight),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let style = || {
|
||||
Style::default()
|
||||
.bg(colors.popup_help.background)
|
||||
.fg(colors.popup_help.text)
|
||||
};
|
||||
let description_paragraph = Paragraph::new(description_info.lines)
|
||||
.style(style())
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let help_paragraph = Paragraph::new(button_info.lines)
|
||||
.style(style())
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
let final_paragraph = Paragraph::new(final_info.lines)
|
||||
.style(style())
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(
|
||||
Style::default()
|
||||
.fg(colors.popup_help.text)
|
||||
.bg(colors.popup_help.background),
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
config::{AppColors, Keymap},
|
||||
ui::draw_blocks::VERSION,
|
||||
};
|
||||
use crossterm::event::KeyCode;
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup};
|
||||
|
||||
#[test]
|
||||
/// This will cause issues once the version has more than the current 5 chars (0.5.0)
|
||||
fn test_draw_blocks_help() {
|
||||
let (w, h) = (87, 33);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &setup.app_data.lock().config.keymap);
|
||||
})
|
||||
.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 - or click heading │ ",
|
||||
" │ ( 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test that the help panel gets drawn with custom colors
|
||||
fn test_draw_blocks_help_custom_colors() {
|
||||
let (w, h) = (87, 33);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let mut colors = AppColors::new();
|
||||
|
||||
colors.popup_help.background = Color::Black;
|
||||
colors.popup_help.text = Color::Red;
|
||||
colors.popup_help.text_highlight = Color::Yellow;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &setup.app_data.lock().config.keymap);
|
||||
})
|
||||
.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 - or click heading │ ",
|
||||
" │ ( 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::Black);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
// 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::Black);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
}
|
||||
// The URL is yellow and underlined
|
||||
(28, 25..=60) => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Yellow);
|
||||
assert_eq!(result_cell.modifier, Modifier::UNDERLINED);
|
||||
}
|
||||
// The rest is red on black
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Black);
|
||||
assert_eq!(result_cell.fg, Color::Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Help panel will show custom keymap if in use, with one definition for each entry
|
||||
fn test_draw_blocks_custom_keymap_one_definition() {
|
||||
let (w, h) = (98, 48);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let input = Keymap {
|
||||
clear: (KeyCode::Char('a'), None),
|
||||
delete_deny: (KeyCode::Char('c'), None),
|
||||
delete_confirm: (KeyCode::Char('e'), None),
|
||||
exec: (KeyCode::Char('g'), None),
|
||||
filter_mode: (KeyCode::Char('i'), None),
|
||||
quit: (KeyCode::Char('k'), None),
|
||||
save_logs: (KeyCode::Char('m'), None),
|
||||
scroll_down_many: (KeyCode::Char('o'), None),
|
||||
scroll_down_one: (KeyCode::Char('q'), None),
|
||||
scroll_end: (KeyCode::Char('s'), None),
|
||||
scroll_start: (KeyCode::Char('u'), None),
|
||||
scroll_up_many: (KeyCode::Char('w'), None),
|
||||
scroll_up_one: (KeyCode::Char('y'), None),
|
||||
select_next_panel: (KeyCode::Char('0'), None),
|
||||
select_previous_panel: (KeyCode::Char('2'), None),
|
||||
sort_by_name: (KeyCode::Char('4'), None),
|
||||
sort_by_state: (KeyCode::Char('6'), None),
|
||||
sort_by_status: (KeyCode::Char('8'), None),
|
||||
sort_by_cpu: (KeyCode::F(1), None),
|
||||
sort_by_memory: (KeyCode::Char('#'), None),
|
||||
sort_by_id: (KeyCode::Char('/'), None),
|
||||
sort_by_image: (KeyCode::Char(','), None),
|
||||
sort_by_rx: (KeyCode::Char('.'), None),
|
||||
sort_by_tx: (KeyCode::Backspace, None),
|
||||
sort_reset: (KeyCode::Up, None),
|
||||
toggle_help: (KeyCode::Home, None),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, None),
|
||||
};
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &input);
|
||||
})
|
||||
.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 │ ",
|
||||
" │ │ ",
|
||||
" │ Custom keymap config in use │ ",
|
||||
" │ ( 0 ) select next panel │ ",
|
||||
" │ ( 2 ) select previous panel │ ",
|
||||
" │ ( q ) scroll list down by one │ ",
|
||||
" │ ( y ) scroll list up by one │ ",
|
||||
" │ ( o ) scroll list down by many │ ",
|
||||
" │ ( w ) scroll list by up many │ ",
|
||||
" │ ( s ) scroll list to end │ ",
|
||||
" │ ( u ) scroll list to start │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( g ) exec into a container │ ",
|
||||
" │ ( Home ) toggle this help information - or click heading │ ",
|
||||
" │ ( Home ) save logs to file │ ",
|
||||
" │ ( Page Down ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( i ) enter filter mode │ ",
|
||||
" │ ( Up ) reset container sorting │ ",
|
||||
" │ ( 4 ) sort containers by name │ ",
|
||||
" │ ( 6 ) sort containers by state │ ",
|
||||
" │ ( 8 ) sort containers by status │ ",
|
||||
" │ ( F1 ) sort containers by cpu │ ",
|
||||
" │ ( # ) sort containers by memory │ ",
|
||||
" │ ( / ) sort containers by id │ ",
|
||||
" │ ( , ) sort containers by image │ ",
|
||||
" │ ( . ) sort containers by rx │ ",
|
||||
" │ ( Backspace ) sort containers by tx │ ",
|
||||
" │ ( a ) close dialog │ ",
|
||||
" │ ( k ) 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]);
|
||||
if row_index == 14 && (36..=62).contains(&result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Help panel will show custom keymap if in use, with two definition for each entry
|
||||
fn test_draw_blocks_custom_keymap_two_definitions() {
|
||||
let (w, h) = (110, 48);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let input = Keymap {
|
||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||
delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))),
|
||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||
exec: (KeyCode::Char('g'), Some(KeyCode::Char('h'))),
|
||||
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
|
||||
quit: (KeyCode::Char('k'), Some(KeyCode::Char('l'))),
|
||||
save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
|
||||
scroll_down_many: (KeyCode::Char('o'), Some(KeyCode::Char('p'))),
|
||||
scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
|
||||
scroll_end: (KeyCode::Char('s'), Some(KeyCode::Char('t'))),
|
||||
scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))),
|
||||
scroll_up_many: (KeyCode::Char('w'), Some(KeyCode::Char('x'))),
|
||||
scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))),
|
||||
select_next_panel: (KeyCode::Char('0'), Some(KeyCode::Char('1'))),
|
||||
select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))),
|
||||
sort_by_name: (KeyCode::Char('4'), Some(KeyCode::Char('5'))),
|
||||
sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))),
|
||||
sort_by_status: (KeyCode::Char('8'), Some(KeyCode::Char('9'))),
|
||||
sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))),
|
||||
sort_by_memory: (KeyCode::Char('#'), Some(KeyCode::Char('-'))),
|
||||
sort_by_id: (KeyCode::Char('/'), Some(KeyCode::Char('='))),
|
||||
sort_by_image: (KeyCode::Char(','), Some(KeyCode::Char('\\'))),
|
||||
sort_by_rx: (KeyCode::Char('.'), Some(KeyCode::Char(']'))),
|
||||
sort_by_tx: (KeyCode::Backspace, Some(KeyCode::BackTab)),
|
||||
sort_reset: (KeyCode::Up, Some(KeyCode::Down)),
|
||||
toggle_help: (KeyCode::Home, Some(KeyCode::Delete)),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||
};
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &input);
|
||||
})
|
||||
.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 │ ",
|
||||
" │ │ ",
|
||||
" │ Custom keymap config in use │ ",
|
||||
" │ ( 0 ) or ( 1 ) select next panel │ ",
|
||||
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
||||
" │ ( q ) or ( r ) scroll list down by one │ ",
|
||||
" │ ( y ) or ( z ) scroll list up by one │ ",
|
||||
" │ ( o ) or ( p ) scroll list down by many │ ",
|
||||
" │ ( w ) or ( x ) scroll list by up many │ ",
|
||||
" │ ( s ) or ( t ) scroll list to end │ ",
|
||||
" │ ( u ) or ( v ) scroll list to start │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( g ) or ( h ) exec into a container │ ",
|
||||
" │ ( Home ) or ( Del ) toggle this help information - or click heading │ ",
|
||||
" │ ( Home ) or ( Del ) save logs to file │ ",
|
||||
" │ ( Page Down ) or ( Page Up ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( i ) or ( j ) enter filter mode │ ",
|
||||
" │ ( Up ) or ( Down ) reset container sorting │ ",
|
||||
" │ ( 4 ) or ( 5 ) sort containers by name │ ",
|
||||
" │ ( 6 ) or ( 7 ) sort containers by state │ ",
|
||||
" │ ( 8 ) or ( 9 ) sort containers by status │ ",
|
||||
" │ ( F1 ) or ( F12 ) sort containers by cpu │ ",
|
||||
" │ ( # ) or ( - ) sort containers by memory │ ",
|
||||
" │ ( / ) or ( = ) sort containers by id │ ",
|
||||
r" │ ( , ) or ( \ ) sort containers by image │ ",
|
||||
" │ ( . ) or ( ] ) sort containers by rx │ ",
|
||||
" │ ( Backspace ) or ( Back Tab ) sort containers by tx │ ",
|
||||
" │ ( a ) or ( b ) close dialog │ ",
|
||||
" │ ( k ) or ( l ) 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Help panel will show custom keymap if in use, with either one or two definition for each entry
|
||||
fn test_draw_blocks_custom_keymap_one_and_two_definitions() {
|
||||
let (w, h) = (110, 48);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let input = Keymap {
|
||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||
delete_deny: (KeyCode::Char('c'), None),
|
||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||
exec: (KeyCode::Char('g'), None),
|
||||
filter_mode: (KeyCode::Char('i'), Some(KeyCode::Char('j'))),
|
||||
quit: (KeyCode::Char('k'), None),
|
||||
save_logs: (KeyCode::Char('m'), Some(KeyCode::Char('n'))),
|
||||
scroll_down_many: (KeyCode::Char('o'), None),
|
||||
scroll_down_one: (KeyCode::Char('q'), Some(KeyCode::Char('r'))),
|
||||
scroll_end: (KeyCode::Char('s'), None),
|
||||
scroll_start: (KeyCode::Char('u'), Some(KeyCode::Char('v'))),
|
||||
scroll_up_many: (KeyCode::Char('w'), None),
|
||||
scroll_up_one: (KeyCode::Char('y'), Some(KeyCode::Char('z'))),
|
||||
select_next_panel: (KeyCode::Char('0'), None),
|
||||
select_previous_panel: (KeyCode::Char('2'), Some(KeyCode::Char('3'))),
|
||||
sort_by_name: (KeyCode::Char('4'), None),
|
||||
sort_by_state: (KeyCode::Char('6'), Some(KeyCode::Char('7'))),
|
||||
sort_by_status: (KeyCode::Char('8'), None),
|
||||
sort_by_cpu: (KeyCode::F(1), Some(KeyCode::F(12))),
|
||||
sort_by_memory: (KeyCode::Char('#'), None),
|
||||
sort_by_id: (KeyCode::Char('/'), Some(KeyCode::Char('='))),
|
||||
sort_by_image: (KeyCode::Char(','), None),
|
||||
sort_by_rx: (KeyCode::Char('.'), Some(KeyCode::Char(']'))),
|
||||
sort_by_tx: (KeyCode::Backspace, None),
|
||||
sort_reset: (KeyCode::Up, Some(KeyCode::Down)),
|
||||
toggle_help: (KeyCode::Home, None),
|
||||
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||
};
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(f, colors, &input);
|
||||
})
|
||||
.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 │ ",
|
||||
" │ │ ",
|
||||
" │ Custom keymap config in use │ ",
|
||||
" │ ( 0 ) select next panel │ ",
|
||||
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
||||
" │ ( q ) or ( r ) scroll list down by one │ ",
|
||||
" │ ( y ) or ( z ) scroll list up by one │ ",
|
||||
" │ ( o ) scroll list down by many │ ",
|
||||
" │ ( w ) scroll list by up many │ ",
|
||||
" │ ( s ) scroll list to end │ ",
|
||||
" │ ( u ) or ( v ) scroll list to start │ ",
|
||||
" │ ( enter ) send docker container command │ ",
|
||||
" │ ( g ) exec into a container │ ",
|
||||
" │ ( Home ) toggle this help information - or click heading │ ",
|
||||
" │ ( Home ) save logs to file │ ",
|
||||
" │ ( Page Down ) or ( Page Up ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||
" │ ( i ) or ( j ) enter filter mode │ ",
|
||||
" │ ( Up ) or ( Down ) reset container sorting │ ",
|
||||
" │ ( 4 ) sort containers by name │ ",
|
||||
" │ ( 6 ) or ( 7 ) sort containers by state │ ",
|
||||
" │ ( 8 ) sort containers by status │ ",
|
||||
" │ ( F1 ) or ( F12 ) sort containers by cpu │ ",
|
||||
" │ ( # ) sort containers by memory │ ",
|
||||
" │ ( / ) or ( = ) sort containers by id │ ",
|
||||
" │ ( , ) sort containers by image │ ",
|
||||
" │ ( . ) or ( ] ) sort containers by rx │ ",
|
||||
" │ ( Backspace ) sort containers by tx │ ",
|
||||
" │ ( a ) or ( b ) close dialog │ ",
|
||||
" │ ( k ) 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
style::Style,
|
||||
widgets::{Block, Borders, Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::{gui_state::BoxLocation, GuiState},
|
||||
};
|
||||
|
||||
use super::{max_line_width, popup};
|
||||
|
||||
/// Draw info box in one of the 9 BoxLocations
|
||||
// TODO is this broken - I don't think so
|
||||
pub fn draw(
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
instant: &Instant,
|
||||
msg: String,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title("")
|
||||
.title_alignment(Alignment::Center)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_info.background)
|
||||
.fg(colors.popup_info.text),
|
||||
)
|
||||
.borders(Borders::NONE);
|
||||
|
||||
let max_line_width = max_line_width(&msg) + 8;
|
||||
let lines = msg.lines().count() + 2;
|
||||
|
||||
let paragraph = Paragraph::new(msg)
|
||||
.block(block)
|
||||
.style(
|
||||
Style::default()
|
||||
.bg(colors.popup_info.background)
|
||||
.fg(colors.popup_info.text),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
let area = popup::draw(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();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ratatui::style::Color;
|
||||
|
||||
use crate::{
|
||||
config::AppColors,
|
||||
ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
};
|
||||
|
||||
#[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 ",
|
||||
" ",
|
||||
];
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&std::time::Instant::now(),
|
||||
"test".to_owned(),
|
||||
);
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Info box drawn in bottom right with custom colors applied
|
||||
fn test_draw_blocks_info_custom_color() {
|
||||
let (w, h) = (45, 9);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let mut colors = AppColors::new();
|
||||
colors.popup_info.background = Color::Red;
|
||||
colors.popup_info.text = Color::Black;
|
||||
let expected = [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" test ",
|
||||
" ",
|
||||
];
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
colors,
|
||||
f,
|
||||
&setup.gui_state,
|
||||
&std::time::Instant::now(),
|
||||
"test".to_owned(),
|
||||
);
|
||||
})
|
||||
.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::Red, Color::Black),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
assert_eq!(result_cell.bg, bg);
|
||||
assert_eq!(result_cell.fg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Modifier, Style},
|
||||
widgets::{List, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_data::AppData,
|
||||
config::AppColors,
|
||||
ui::{FrameData, GuiState, SelectablePanel, Status},
|
||||
};
|
||||
|
||||
use super::{generate_block, RIGHT_ARROW};
|
||||
|
||||
/// Draw the logs panel
|
||||
pub fn draw(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
colors: AppColors,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Logs);
|
||||
if fd.status.contains(&Status::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ratatui::style::{Color, Modifier};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerImage, ContainerName},
|
||||
ui::{
|
||||
draw_blocks::tests::{
|
||||
expected_to_vec, get_result, insert_logs, test_setup, BORDER_CHARS,
|
||||
},
|
||||
FrameData, Status,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
/// No logs, panel unselected, then selected, border color changes correctly
|
||||
fn test_draw_blocks_logs_none() {
|
||||
let (w, h) = (35, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
let expected = [
|
||||
"╭ Logs - container_1 - image_1 ───╮",
|
||||
"│ no logs found │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"│ │",
|
||||
"╰─────────────────────────────────╯",
|
||||
];
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&setup.fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
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 | 5, 0..=34) | (1..=4, 0) | (1..=5, 34) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setup.gui_state.lock().next_panel();
|
||||
setup.gui_state.lock().next_panel();
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
// When selected, has a blue border
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
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, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
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, 0..=31) | (1..=4, 0) | (1..=5, 31) | (5, 0..=30) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, 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, &setup.gui_state));
|
||||
fd.status.insert(Status::Init);
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
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) {
|
||||
(0, 0..=31) | (1..=4, 0) | (1..=5, 31) | (5, 0..=30) => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Logs correct displayed, changing log state also draws correctly
|
||||
fn test_draw_blocks_logs_some() {
|
||||
let (w, h) = (36, 6);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
insert_logs(&setup);
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let expected = [
|
||||
"╭ Logs 3/3 - container_1 - image_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.bg, Color::Reset);
|
||||
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
} else {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
if row_index == 3 && (1..=34).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();
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected = [
|
||||
"╭ Logs 2/3 - container_1 - image_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.bg, Color::Reset);
|
||||
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||
assert_eq!(result_cell.fg, Color::Reset);
|
||||
} else {
|
||||
assert_eq!(result_cell.fg, Color::Gray);
|
||||
}
|
||||
if row_index == 2 && (1..=34).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, &setup.gui_state));
|
||||
let colors = setup.app_data.lock().config.app_colors;
|
||||
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(
|
||||
&setup.app_data,
|
||||
setup.area,
|
||||
colors,
|
||||
f,
|
||||
&fd,
|
||||
&setup.gui_state,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
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::{gui_state::Region, FrameData, GuiState, SelectablePanel, Status};
|
||||
|
||||
pub mod charts;
|
||||
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 logs;
|
||||
pub mod popup;
|
||||
pub mod ports;
|
||||
|
||||
pub 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 "#;
|
||||
|
||||
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
pub const MARGIN: &str = " ";
|
||||
pub const RIGHT_ARROW: &str = "▶ ";
|
||||
pub const CIRCLE: &str = "⚪ ";
|
||||
|
||||
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),
|
||||
];
|
||||
|
||||
/// From a given &str, return the maximum number of chars on a single line
|
||||
pub 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>(
|
||||
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(title);
|
||||
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 parking_lot::Mutex;
|
||||
use ratatui::{backend::TestBackend, layout::Rect, style::Color, Terminal};
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
|
||||
tests::{gen_appdata, gen_containers},
|
||||
ui::{draw_frame, GuiState},
|
||||
};
|
||||
|
||||
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 (app_data, gui_data) = (data.0.lock(), data.1.lock());
|
||||
|
||||
// set max height for container section, needs +5 to deal with docker commands list and borders
|
||||
let height = app_data.get_container_len();
|
||||
let height = if height < 12 {
|
||||
u16::try_from(height + 5).unwrap_or_default()
|
||||
} else {
|
||||
12
|
||||
};
|
||||
|
||||
let (filter_by, filter_term) = app_data.get_filter();
|
||||
Self {
|
||||
chart_data: app_data.get_chart_data(),
|
||||
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(),
|
||||
height,
|
||||
ports: app_data.get_selected_ports(),
|
||||
port_max_lens: app_data.get_longest_port(),
|
||||
info_text: gui_data.info_box_text.clone(),
|
||||
is_loading: gui_data.is_loading(),
|
||||
loading_icon: gui_data.get_loading().to_string(),
|
||||
log_title: app_data.get_log_title(),
|
||||
selected_panel: gui_data.get_selected_panel(),
|
||||
sorted_by: app_data.get_sorted(),
|
||||
status: gui_data.get_status(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate state to be used in *most* gui tests
|
||||
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 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, &gui_state));
|
||||
let area = Rect::new(0, 0, w, h);
|
||||
TuiTestSetup {
|
||||
app_data,
|
||||
gui_state,
|
||||
fd,
|
||||
area,
|
||||
terminal,
|
||||
ids,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a single row of String's from the expected data
|
||||
pub fn expected_to_vec(expected: &[&str], row_index: usize) -> Vec<String> {
|
||||
expected[row_index]
|
||||
.chars()
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub 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()
|
||||
}
|
||||
|
||||
/// Insert some logs into the first container
|
||||
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)]
|
||||
// Add fixed data to the cpu & mem vecdeques
|
||||
pub 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// *************** //
|
||||
// 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(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
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│",
|
||||
"│ │ ││ │ ││ │",
|
||||
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
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(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
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 │",
|
||||
"│ │•• ••• ││ │•• ••• ││ │",
|
||||
"│ │ ││ │ ││ │",
|
||||
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
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 ",
|
||||
];
|
||||
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();
|
||||
|
||||
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(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
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│",
|
||||
"│ │ ││ │ ││ │",
|
||||
"╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
||||
];
|
||||
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();
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use ratatui::layout::{Direction, Layout, Rect};
|
||||
|
||||
use crate::ui::gui_state::BoxLocation;
|
||||
|
||||
/// draw a box in the one of the BoxLocations, based on max line width + number of lines
|
||||
pub fn draw(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]
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use ratatui::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::{app_data::State, config::AppColors, ui::FrameData};
|
||||
|
||||
/// Get the port title color, at the moment the color is only customizable if the container is alive
|
||||
const fn get_port_title_color(colors: AppColors, state: State) -> Color {
|
||||
if state.is_alive() {
|
||||
colors.chart_ports.title
|
||||
} else {
|
||||
state.get_color(colors)
|
||||
}
|
||||
}
|
||||
|
||||
/// Display the ports in a formatted list
|
||||
pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
||||
if let Some(ports) = fd.ports.as_ref() {
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::new().fg(colors.chart_ports.border))
|
||||
// .bg(colors.chart_ports.border))
|
||||
.title_alignment(Alignment::Center)
|
||||
.title(Span::styled(
|
||||
" ports ",
|
||||
Style::default()
|
||||
.fg(get_port_title_color(colors, ports.1))
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
|
||||
let (ip, private, public) = fd.port_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(colors.chart_ports.headings),
|
||||
)];
|
||||
for item in &ports.0 {
|
||||
let strings = item.get_all();
|
||||
|
||||
let line = vec![
|
||||
Span::from(format!("{:>ip$}", strings.0)).fg(colors.chart_ports.text),
|
||||
Span::from(format!("{:>private$}", strings.1)).fg(colors.chart_ports.text),
|
||||
Span::from(format!("{:>public$}", strings.2)).fg(colors.chart_ports.text),
|
||||
];
|
||||
output.push(Line::from(line));
|
||||
}
|
||||
let paragraph = Paragraph::new(output).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use ratatui::style::{Color, Modifier};
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerPorts, State},
|
||||
ui::{
|
||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||
FrameData,
|
||||
},
|
||||
};
|
||||
|
||||
#[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 fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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::White);
|
||||
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result_cell.bg, Color::Reset);
|
||||
assert_eq!(result_cell.fg, Color::White);
|
||||
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 fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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::White);
|
||||
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(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
|
||||
private: 8003,
|
||||
public: Some(8003),
|
||||
});
|
||||
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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) | (0 | 2..=9, 0..=31) | (1, 0 | 29..=31) => {
|
||||
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 fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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;
|
||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||
setup
|
||||
.terminal
|
||||
.draw(|f| {
|
||||
super::draw(setup.area, setup.app_data.lock().config.app_colors, f, &fd);
|
||||
})
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
-29
@@ -49,13 +49,14 @@ impl SelectablePanel {
|
||||
pub enum Region {
|
||||
Panel(SelectablePanel),
|
||||
Header(Header),
|
||||
HelpPanel,
|
||||
Delete(DeleteButton),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum DeleteButton {
|
||||
Yes,
|
||||
No,
|
||||
Confirm,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -173,21 +174,22 @@ pub enum Status {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiState {
|
||||
delete_container: Option<ContainerId>,
|
||||
delete_map: HashMap<DeleteButton, Rect>,
|
||||
heading_map: HashMap<Header, Rect>,
|
||||
exec_mode: Option<ExecMode>,
|
||||
loading_handle: Option<JoinHandle<()>>,
|
||||
loading_index: u8,
|
||||
loading_set: HashSet<Uuid>,
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
intersect_delete: HashMap<DeleteButton, Rect>,
|
||||
intersect_heading: HashMap<Header, Rect>,
|
||||
intersect_help: Option<Rect>,
|
||||
intersect_panel: HashMap<SelectablePanel, Rect>,
|
||||
selected_panel: SelectablePanel,
|
||||
status: HashSet<Status>,
|
||||
exec_mode: Option<ExecMode>,
|
||||
pub info_box_text: Option<(String, Instant)>,
|
||||
}
|
||||
impl GuiState {
|
||||
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||
pub fn clear_area_map(&mut self) {
|
||||
self.panel_map.clear();
|
||||
self.intersect_panel.clear();
|
||||
}
|
||||
|
||||
/// Get the currently selected panel
|
||||
@@ -196,9 +198,9 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn panel_intersect(&mut self, rect: Rect) {
|
||||
pub fn get_intersect_panel(&mut self, rect: Rect) {
|
||||
if let Some(data) = self
|
||||
.panel_map
|
||||
.intersect_panel
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -209,8 +211,8 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
|
||||
pub fn button_intersect(&self, rect: Rect) -> Option<DeleteButton> {
|
||||
self.delete_map
|
||||
pub fn get_intersect_button(&self, rect: Rect) -> Option<DeleteButton> {
|
||||
self.intersect_delete
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -219,8 +221,8 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn header_intersect(&self, rect: Rect) -> Option<Header> {
|
||||
self.heading_map
|
||||
pub fn get_intersect_header(&self, rect: Rect) -> Option<Header> {
|
||||
self.intersect_heading
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -228,24 +230,37 @@ impl GuiState {
|
||||
.map(|data| *data.0)
|
||||
}
|
||||
|
||||
/// Check if a the "show/hide help" section has been clicked
|
||||
pub fn get_intersect_help(&self, rect: Rect) -> bool {
|
||||
self.intersect_help
|
||||
.as_ref()
|
||||
.is_some_and(|i| i.intersects(rect))
|
||||
}
|
||||
|
||||
/// Insert, or updates header area panel into heading_map
|
||||
pub fn update_region_map(&mut self, region: Region, area: Rect) {
|
||||
match region {
|
||||
Region::Header(header) => self
|
||||
.heading_map
|
||||
.entry(header)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
Region::Panel(panel) => self
|
||||
.panel_map
|
||||
.entry(panel)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
Region::Delete(button) => self
|
||||
.delete_map
|
||||
.entry(button)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
Region::Header(header) => {
|
||||
self.intersect_heading
|
||||
.entry(header)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area);
|
||||
}
|
||||
Region::Panel(panel) => {
|
||||
self.intersect_panel
|
||||
.entry(panel)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area);
|
||||
}
|
||||
Region::Delete(button) => {
|
||||
self.intersect_delete
|
||||
.entry(button)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area);
|
||||
}
|
||||
Region::HelpPanel => {
|
||||
self.intersect_help = Some(area);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -260,7 +275,7 @@ impl GuiState {
|
||||
if id.is_some() {
|
||||
self.status.insert(Status::DeleteConfirm);
|
||||
} else {
|
||||
self.delete_map.clear();
|
||||
self.intersect_delete.clear();
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
}
|
||||
self.delete_container = id;
|
||||
|
||||
+36
-17
@@ -32,11 +32,11 @@ use crate::{
|
||||
SortedOrder, State,
|
||||
},
|
||||
app_error::AppError,
|
||||
config::{AppColors, Keymap},
|
||||
exec::TerminalSize,
|
||||
input_handler::InputMessages,
|
||||
};
|
||||
|
||||
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
||||
const POLL_RATE: Duration = std::time::Duration::from_millis(100);
|
||||
|
||||
pub struct Ui {
|
||||
@@ -124,6 +124,8 @@ impl Ui {
|
||||
/// Draw the the error message ui, for 5 seconds, with a countdown
|
||||
fn err_loop(&mut self) -> Result<(), AppError> {
|
||||
let mut seconds = 5;
|
||||
let colors = self.app_data.lock().config.app_colors;
|
||||
let keymap = self.app_data.lock().config.keymap.clone();
|
||||
loop {
|
||||
if self.now.elapsed() >= std::time::Duration::from_secs(1) {
|
||||
seconds -= 1;
|
||||
@@ -135,7 +137,15 @@ impl Ui {
|
||||
|
||||
if self
|
||||
.terminal
|
||||
.draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds)))
|
||||
.draw(|f| {
|
||||
draw_blocks::error::draw(
|
||||
f,
|
||||
&AppError::DockerConnect,
|
||||
&keymap,
|
||||
Some(seconds),
|
||||
colors,
|
||||
);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return Err(AppError::Terminal);
|
||||
@@ -165,6 +175,8 @@ impl Ui {
|
||||
|
||||
/// The loop for drawing the main UI to the terminal
|
||||
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
||||
let colors = self.app_data.lock().config.app_colors;
|
||||
let keymap = self.app_data.lock().config.keymap.clone();
|
||||
while self.is_running.load(Ordering::SeqCst) {
|
||||
let fd = FrameData::from(&*self);
|
||||
let exec = fd.status.contains(&Status::Exec);
|
||||
@@ -174,7 +186,9 @@ impl Ui {
|
||||
|
||||
if self
|
||||
.terminal
|
||||
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state, &fd))
|
||||
.draw(|frame| {
|
||||
draw_frame(&self.app_data, colors, &keymap, frame, &fd, &self.gui_state);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return Err(AppError::Terminal);
|
||||
@@ -223,6 +237,7 @@ impl Ui {
|
||||
/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FrameData {
|
||||
// app_colors: AppColors,
|
||||
chart_data: Option<(CpuTuple, MemTuple)>,
|
||||
columns: Columns,
|
||||
container_title: String,
|
||||
@@ -257,6 +272,7 @@ impl From<&Ui> for FrameData {
|
||||
|
||||
let (filter_by, filter_term) = app_data.get_filter();
|
||||
Self {
|
||||
// app_colors: app_data.config.app_colors,
|
||||
chart_data: app_data.get_chart_data(),
|
||||
columns: app_data.get_width(),
|
||||
container_title: app_data.get_container_title(),
|
||||
@@ -281,10 +297,13 @@ impl From<&Ui> for FrameData {
|
||||
|
||||
/// Draw the main ui to a frame of the terminal
|
||||
fn draw_frame(
|
||||
f: &mut Frame,
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
colors: AppColors,
|
||||
keymap: &Keymap,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
// should pass in the colors here, then I only need to get it once from app+data
|
||||
) {
|
||||
let whole_constraints = if fd.status.contains(&Status::Filter) {
|
||||
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
||||
@@ -326,15 +345,15 @@ fn draw_frame(
|
||||
.constraints(lower_split)
|
||||
.split(upper_main[1]);
|
||||
|
||||
draw_blocks::containers(app_data, top_panel[0], f, fd, gui_state);
|
||||
draw_blocks::containers::draw(app_data, top_panel[0], colors, f, fd, gui_state);
|
||||
|
||||
draw_blocks::logs(app_data, lower_main[0], f, fd, gui_state);
|
||||
draw_blocks::logs::draw(app_data, lower_main[0], colors, f, fd, gui_state);
|
||||
|
||||
draw_blocks::heading_bar(whole_layout[0], f, fd, gui_state);
|
||||
draw_blocks::headers::draw(whole_layout[0], colors, f, fd, gui_state, keymap);
|
||||
|
||||
// Draw filter bar
|
||||
if let Some(rect) = whole_layout.get(2) {
|
||||
draw_blocks::filter_bar(*rect, f, fd);
|
||||
draw_blocks::filter::draw(*rect, f, fd);
|
||||
}
|
||||
|
||||
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||
@@ -345,14 +364,14 @@ fn draw_frame(
|
||||
gui_state.lock().set_delete_container(None);
|
||||
},
|
||||
|name| {
|
||||
draw_blocks::delete_confirm(f, gui_state, name);
|
||||
draw_blocks::delete_confirm::draw(colors, f, gui_state, keymap, name);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// only draw commands + charts if there are containers
|
||||
if let Some(rect) = top_panel.get(1) {
|
||||
draw_blocks::commands(app_data, *rect, f, fd, gui_state);
|
||||
draw_blocks::commands::draw(app_data, *rect, colors, f, fd, gui_state);
|
||||
|
||||
// Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border)
|
||||
let ports_len =
|
||||
@@ -364,20 +383,20 @@ fn draw_frame(
|
||||
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
|
||||
.split(lower_main[1]);
|
||||
|
||||
draw_blocks::chart(f, lower[0], fd);
|
||||
draw_blocks::ports(f, lower[1], fd);
|
||||
draw_blocks::charts::draw(lower[0], colors, f, fd);
|
||||
draw_blocks::ports::draw(lower[1], colors, f, fd);
|
||||
}
|
||||
|
||||
if let Some((text, instant)) = fd.info_text.as_ref() {
|
||||
draw_blocks::info(f, text.to_owned(), instant, gui_state);
|
||||
draw_blocks::info::draw(colors, f, gui_state, instant, text.to_owned());
|
||||
}
|
||||
|
||||
// Check if error, and show popup if so
|
||||
if fd.status.contains(&Status::Help) {
|
||||
draw_blocks::help_box(f);
|
||||
draw_blocks::help::draw(f, colors, keymap);
|
||||
}
|
||||
|
||||
if let Some(error) = fd.has_error {
|
||||
draw_blocks::error(f, error, None);
|
||||
if let Some(error) = fd.has_error.as_ref() {
|
||||
draw_blocks::error::draw(f, error, keymap, None, colors);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user