refactor: Multiple UI improvements;

Use FrameData struct to store commonly accessed data, in order to
reduce mutex locks.

rename unpause to resume

use get_selected_panel() function instead of directly
gui_state.selected_panel

debug mode now shows some usefull information
This commit is contained in:
Jack Wills
2023-11-16 10:54:01 +00:00
parent 650aa0fc91
commit 40090865fd
8 changed files with 338 additions and 264 deletions
+125 -115
View File
@@ -13,14 +13,16 @@ use ratatui::{
use std::default::Default;
use std::{fmt::Display, sync::Arc};
use crate::app_data::{Header, SortedOrder};
use crate::ui::Status;
use crate::app_data::{ContainerItem, Header, SortedOrder};
use crate::{
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
app_error::AppError,
};
use super::gui_state::{BoxLocation, DeleteButton, Region};
use super::{
gui_state::{BoxLocation, DeleteButton, Region},
FrameData,
};
use super::{GuiState, SelectablePanel};
const NAME_TEXT: &str = r#"
@@ -39,7 +41,7 @@ const REPO: &str = env!("CARGO_PKG_REPOSITORY");
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
const ORANGE: Color = Color::Rgb(255, 178, 36);
const MARGIN: &str = " ";
const ARROW: &str = "";
const RIGHT_ARROW: &str = "";
const CIRCLE: &str = "";
/// From a given &str, return the maximum number of chars on a single line
@@ -55,6 +57,7 @@ fn max_line_width(text: &str) -> usize {
fn generate_block<'a>(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
panel: SelectablePanel,
) -> Block<'a> {
@@ -77,7 +80,7 @@ fn generate_block<'a>(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title);
if gui_state.lock().selected_panel == panel {
if fd.selected_panel == panel {
block = block.border_style(Style::default().fg(Color::LightCyan));
}
block
@@ -88,9 +91,10 @@ pub fn commands(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
) {
let block = || generate_block(app_data, area, gui_state, SelectablePanel::Commands);
let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
i.iter()
.map(|c| {
@@ -106,7 +110,7 @@ pub fn commands(
let items = List::new(items)
.block(block())
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol(ARROW);
.highlight_symbol(RIGHT_ARROW);
if let Some(i) = app_data.lock().get_control_state() {
f.render_stateful_widget(items, area, i);
@@ -118,88 +122,91 @@ pub fn commands(
}
}
/// Format the container data to display nicely on the screen
fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
let state_style = Style::default().fg(i.state.get_color());
let blue = Style::default().fg(Color::Blue);
Line::from(vec![
Span::styled(
format!(
"{:<width$}",
i.state.to_string(),
width = widths.state.1.into()
),
state_style,
),
Span::styled(
format!(
"{MARGIN}{:>width$}",
i.status,
width = &widths.status.1.into()
),
state_style,
),
Span::styled(
format!(
"{}{:>width$}",
MARGIN,
i.cpu_stats.back().unwrap_or(&CpuStats::default()),
width = &widths.cpu.1.into()
),
state_style,
),
Span::styled(
format!(
"{MARGIN}{:>width_current$} / {:>width_limit$}",
i.mem_stats.back().unwrap_or(&ByteStats::default()),
i.mem_limit,
width_current = &widths.mem.1.into(),
width_limit = &widths.mem.2.into()
),
state_style,
),
Span::styled(
format!(
"{}{:>width$}",
MARGIN,
i.id.get_short(),
width = &widths.id.1.into()
),
blue,
),
Span::styled(
format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()),
blue,
),
Span::styled(
format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()),
blue,
),
Span::styled(
format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()),
Style::default().fg(Color::Rgb(255, 233, 193)),
),
Span::styled(
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
Style::default().fg(Color::Rgb(205, 140, 140)),
),
])
}
/// Draw the containers panel
pub fn containers(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
widths: &Columns,
) {
let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
let items = app_data
.lock()
.get_container_items()
.iter()
.map(|i| {
let state_style = Style::default().fg(i.state.get_color());
let blue = Style::default().fg(Color::Blue);
let lines = Line::from(vec![
Span::styled(
format!(
"{:<width$}",
i.state.to_string(),
width = widths.state.1.into()
),
state_style,
),
Span::styled(
format!(
"{MARGIN}{:>width$}",
i.status,
width = &widths.status.1.into()
),
state_style,
),
Span::styled(
format!(
"{}{:>width$}",
MARGIN,
i.cpu_stats.back().unwrap_or(&CpuStats::default()),
width = &widths.cpu.1.into()
),
state_style,
),
Span::styled(
format!(
"{MARGIN}{:>width_current$} / {:>width_limit$}",
i.mem_stats.back().unwrap_or(&ByteStats::default()),
i.mem_limit,
width_current = &widths.mem.1.into(),
width_limit = &widths.mem.2.into()
),
state_style,
),
Span::styled(
format!(
"{}{:>width$}",
MARGIN,
i.id.get().chars().take(8).collect::<String>(),
width = &widths.id.1.into()
),
blue,
),
Span::styled(
format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()),
blue,
),
Span::styled(
format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()),
blue,
),
Span::styled(
format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()),
Style::default().fg(Color::Rgb(255, 233, 193)),
),
Span::styled(
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
Style::default().fg(Color::Rgb(205, 140, 140)),
),
]);
ListItem::new(lines)
})
.map(|i| ListItem::new(format_containers(i, widths)))
.collect::<Vec<_>>();
if items.is_empty() {
@@ -212,7 +219,6 @@ pub fn containers(
.block(block)
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol(CIRCLE);
f.render_stateful_widget(items, area, app_data.lock().get_container_state());
}
}
@@ -222,12 +228,12 @@ pub fn logs(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
loading_icon: &str,
) {
let block = || generate_block(app_data, area, gui_state, SelectablePanel::Logs);
if gui_state.lock().status_contains(&[Status::Init]) {
let paragraph = Paragraph::new(format!("parsing logs {loading_icon}"))
let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs);
if fd.init {
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
.style(Style::default())
.block(block())
.alignment(Alignment::Center);
@@ -243,12 +249,11 @@ pub fn logs(
} else {
let items = List::new(logs)
.block(block())
.highlight_symbol(ARROW)
.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(i) = app_data.lock().get_log_state() {
f.render_stateful_widget(items, area, i);
if let Some(log_state) = app_data.lock().get_log_state() {
f.render_stateful_widget(items, area, log_state);
}
}
}
@@ -338,24 +343,20 @@ fn make_chart<'a, T: Stats + Display>(
#[allow(clippy::too_many_lines)]
pub fn heading_bar(
area: Rect,
columns: &Columns,
f: &mut Frame,
has_containers: bool,
loading_icon: &str,
sorted_by: Option<(Header, SortedOrder)>,
frame: &mut Frame,
data: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
) {
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
let help_visible = gui_state.lock().status_contains(&[Status::Help]);
f.render_widget(block(Color::Black), area);
frame.render_widget(block(Color::Black), area);
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
let header_block = |x: &Header| {
let mut color = Color::Black;
let mut suffix = "";
let mut suffix_margin = 0;
if let Some((a, b)) = sorted_by.as_ref() {
if let Some((a, b)) = data.sorted_by.as_ref() {
if x == a {
match b {
SortedOrder::Asc => suffix = "",
@@ -407,15 +408,15 @@ pub fn heading_bar(
// Meta data to iterate over to create blocks with correct widths
let header_meta = [
(Header::State, columns.state.1),
(Header::Status, columns.status.1),
(Header::Cpu, columns.cpu.1),
(Header::Memory, columns.mem.1 + columns.mem.2 + 3),
(Header::Id, columns.id.1),
(Header::Name, columns.name.1),
(Header::Image, columns.image.1),
(Header::Rx, columns.net_rx.1),
(Header::Tx, columns.net_tx.1),
(Header::State, data.columns.state.1),
(Header::Status, data.columns.status.1),
(Header::Cpu, data.columns.cpu.1),
(Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3),
(Header::Id, data.columns.id.1),
(Header::Name, data.columns.name.1),
(Header::Image, data.columns.image.1),
(Header::Rx, data.columns.net_rx.1),
(Header::Tx, data.columns.net_tx.1),
];
let header_data = header_meta
@@ -426,13 +427,13 @@ pub fn heading_bar(
})
.collect::<Vec<_>>();
let suffix = if help_visible { "exit" } else { "show" };
let suffix = if data.help_visible { "exit" } else { "show" };
let info_text = format!("( h ) {suffix} help {MARGIN}",);
let info_width = info_text.chars().count();
let column_width = usize::from(area.width).saturating_sub(info_width);
let column_width = if column_width > 0 { column_width } else { 1 };
let splits = if has_containers {
let splits = if data.has_containers {
vec![
Constraint::Min(2),
Constraint::Min(column_width.try_into().unwrap_or_default()),
@@ -446,13 +447,12 @@ pub fn heading_bar(
.direction(Direction::Horizontal)
.constraints(splits)
.split(area);
if has_containers {
if data.has_containers {
// Draw loading icon, or not, and a prefix with a single space
let loading_icon = format!("{loading_icon:>2}");
let loading_paragraph = Paragraph::new(loading_icon)
let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
.block(block(Color::White))
.alignment(Alignment::Center);
f.render_widget(loading_paragraph, split_bar[0]);
frame.render_widget(loading_paragraph, split_bar[0]);
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
let headers_section = Layout::default()
@@ -466,12 +466,12 @@ pub fn heading_bar(
gui_state
.lock()
.update_region_map(Region::Header(header), rect);
f.render_widget(paragraph, rect);
frame.render_widget(paragraph, rect);
}
}
// show/hide help
let color = if help_visible {
let color = if data.help_visible {
Color::Black
} else {
Color::White
@@ -481,8 +481,8 @@ pub fn heading_bar(
.alignment(Alignment::Right);
// If no containers, don't display the headers, could maybe do this first?
let help_index = if has_containers { 2 } else { 0 };
f.render_widget(help_paragraph, split_bar[help_index]);
let help_index = if data.has_containers { 2 } else { 0 };
frame.render_widget(help_paragraph, split_bar[help_index]);
}
/// Help popup box needs these three pieces of information
@@ -586,7 +586,7 @@ impl HelpInfo {
button_item("enter"),
button_desc("to send docker container command"),
]),
Line::from(vec![
Line::from(vec![
space(),
button_item("e"),
button_desc("exec into a container"),
@@ -876,13 +876,13 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option<u8>) {
}
/// Draw info box in one of the 9 BoxLocations
pub fn info(f: &mut Frame, text: String) {
pub fn info(f: &mut Frame, text: &str) {
let block = Block::default()
.title("")
.title_alignment(Alignment::Center)
.borders(Borders::NONE);
let mut max_line_width = max_line_width(&text);
let mut max_line_width = max_line_width(text);
let mut lines = text.lines().count();
// Add some horizontal & vertical margins
@@ -927,6 +927,16 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio
.split(popup_layout[indexes.0])[indexes.1]
}
#[cfg(debug_assertions)]
// Single row at the top of the screen for debugging
pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) {
let block = Block::default().style(Style::default().bg(Color::Red));
let paragraph = Paragraph::new(debug_string)
.style(Style::default().fg(Color::White))
.block(block);
f.render_widget(paragraph, area);
}
// Draw nothing, as in a blank screen
// pub fn nothing(f: &mut Frame) {
// let whole_layout = Layout::default()
+12 -7
View File
@@ -166,15 +166,15 @@ pub enum Status {
/// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug, Default, Clone)]
pub struct GuiState {
delete_container: Option<ContainerId>,
delete_map: HashMap<DeleteButton, Rect>,
heading_map: HashMap<Header, Rect>,
is_loading: HashSet<Uuid>,
loading_index: u8,
panel_map: HashMap<SelectablePanel, Rect>,
delete_map: HashMap<DeleteButton, Rect>,
selected_panel: SelectablePanel,
status: HashSet<Status>,
delete_container: Option<ContainerId>,
pub info_box_text: Option<String>,
pub selected_panel: SelectablePanel,
}
impl GuiState {
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
@@ -182,6 +182,11 @@ impl GuiState {
self.panel_map.clear();
}
/// Get the currently selected panel
pub const fn get_selected_panel(&self) -> SelectablePanel {
self.selected_panel
}
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
pub fn panel_intersect(&mut self, rect: Rect) {
if let Some(data) = self
@@ -293,7 +298,7 @@ impl GuiState {
}
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
pub fn get_loading(&mut self) -> char {
pub fn get_loading(&self) -> char {
if self.is_loading.is_empty() {
' '
} else {
@@ -314,11 +319,11 @@ impl GuiState {
gui_state: &Arc<Mutex<Self>>,
loading_uuid: Uuid,
) -> JoinHandle<()> {
gui_state.lock().next_loading(loading_uuid);
let gui_state = Arc::clone(gui_state);
gui_state.lock().next_loading(loading_uuid);
let gui_state = Arc::clone(gui_state);
tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
gui_state.lock().next_loading(loading_uuid);
}
})
+95 -74
View File
@@ -4,7 +4,7 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use parking_lot::Mutex;
use parking_lot::{Mutex, MutexGuard};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
@@ -26,16 +26,16 @@ mod gui_state;
pub use self::color_match::*;
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
use crate::{
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
input_handler::InputMessages, parse_args::CliArgs,
app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
app_error::AppError,
input_handler::InputMessages,
};
pub const DOCKER_COMMAND: &str = "docker";
pub struct Ui {
args: CliArgs,
// args: CliArgs,
app_data: Arc<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
input_poll_rate: Duration,
is_running: Arc<AtomicBool>,
@@ -60,17 +60,14 @@ impl Ui {
/// Create a new Ui struct, and execute the drawing loop
pub async fn create(
app_data: Arc<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
sender: Sender<InputMessages>,
) {
if let Ok(terminal) = Self::setup_terminal() {
let args = app_data.lock().args.clone();
// let args = app_data.lock().args.clone();
let mut ui = Self {
args,
app_data,
docker_sx,
gui_state,
input_poll_rate: std::time::Duration::from_millis(100),
is_running,
@@ -158,10 +155,8 @@ impl Ui {
if child.kill().is_err() {
std::process::exit(1)
}
// }
}
}
self.terminal.clear().ok();
self.reset_terminal().ok();
Self::init_terminal().ok();
@@ -170,16 +165,10 @@ impl Ui {
/// The loop for drawing the main UI to the terminal
async fn gui_loop(&mut self) -> Result<(), AppError> {
let update_duration =
std::time::Duration::from_millis(u64::from(self.args.docker_interval));
while self.is_running.load(Ordering::SeqCst) {
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
if exec {
self.exec();
self.docker_sx.send(DockerMessage::Update).await.ok();
continue;
}
if self
@@ -212,12 +201,6 @@ impl Ui {
}
}
}
// Should this be done in the docker thread instead?
if self.now.elapsed() >= update_duration {
self.docker_sx.send(DockerMessage::Update).await.ok();
self.now = Instant::now();
}
}
Ok(())
}
@@ -237,48 +220,94 @@ impl Ui {
}
}
#[macro_export]
/// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
macro_rules! value_capture {
($name:ident, $lock_expr:expr) => {
let $name = || $lock_expr;
let $name = $name();
};
// #[macro_export]
// /// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
// macro_rules! value_capture {
// ($name:ident, $lock_expr:expr) => {
// let $name = || $lock_expr;
// let $name = $name();
// };
// }
#[cfg(not(debug_assertions))]
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
.split(f.size())
}
#[cfg(debug_assertions)]
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref())
.split(f.size())
}
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
#[derive(Debug)]
pub struct FrameData {
columns: Columns,
delete_confirm: Option<ContainerId>,
has_containers: bool,
has_error: Option<AppError>,
height: u16,
help_visible: bool,
init: bool,
info_text: Option<String>,
loading_icon: String,
selected_panel: SelectablePanel,
sorted_by: Option<(Header, SortedOrder)>,
}
impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self {
// set max height for container section, needs +5 to deal with docker commands list and borders
let height = data.0.get_container_len();
let height = if height < 12 {
u16::try_from(height + 5).unwrap_or_default()
} else {
12
};
Self {
columns: data.0.get_width(),
delete_confirm: data.1.get_delete_container(),
has_containers: data.0.get_container_len() > 1,
has_error: data.0.get_error(),
height,
help_visible: data.1.status_contains(&[Status::Help]),
init: data.1.status_contains(&[Status::Init]),
info_text: data.1.info_box_text.clone(),
loading_icon: data.1.get_loading().to_string(),
selected_panel: data.1.get_selected_panel(),
sorted_by: data.0.get_sorted(),
}
}
}
/// Draw the main ui to a frame of the terminal
/// TODO add a single line area for debug message - if not in release mode?
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
value_capture!(height, app_data.lock().get_container_len());
value_capture!(column_widths, app_data.lock().get_width());
value_capture!(has_containers, app_data.lock().get_container_len() > 0);
value_capture!(sorted_by, app_data.lock().get_sorted());
value_capture!(delete_confirm, gui_state.lock().get_delete_container());
value_capture!(has_error, app_data.lock().get_error());
value_capture!(info_text, gui_state.lock().info_box_text.clone());
value_capture!(loading_icon, gui_state.lock().get_loading().to_string());
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
// set max height for container section, needs +5 to deal with docker commands list and borders
let height = if height < 12 { height + 5 } else { 12 };
let whole_layout = get_wholelayout(f);
#[cfg(debug_assertions)]
draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string());
let whole_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
.split(f.size());
#[cfg(debug_assertions)]
let whole_layout_split = (1, 2);
#[cfg(not(debug_assertions))]
let whole_layout_split = (0, 1);
// Split into 3, containers+controls, logs, then graphs
let upper_main = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Max(height.try_into().unwrap_or_default()),
Constraint::Percentage(50),
]
.as_ref(),
)
.split(whole_layout[1]);
.constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
.split(whole_layout[whole_layout_split.1]);
let top_split = if has_containers {
let top_split = if fd.has_containers {
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
} else {
vec![Constraint::Percentage(100)]
@@ -289,7 +318,7 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
.constraints(top_split)
.split(upper_main[0]);
let lower_split = if has_containers {
let lower_split = if fd.has_containers {
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
} else {
vec![Constraint::Percentage(100)]
@@ -301,22 +330,14 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
.constraints(lower_split)
.split(upper_main[1]);
draw_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths);
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state, &fd.columns);
draw_blocks::logs(app_data, lower_main[0], f, gui_state, &loading_icon);
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
draw_blocks::heading_bar(
whole_layout[0],
&column_widths,
f,
has_containers,
&loading_icon,
sorted_by,
gui_state,
);
draw_blocks::heading_bar(whole_layout[whole_layout_split.0], f, &fd, gui_state);
if let Some(id) = delete_confirm {
app_data.lock().get_container_name_by_id(&id).map_or_else(
if let Some(id) = fd.delete_confirm.as_ref() {
app_data.lock().get_container_name_by_id(id).map_or_else(
|| {
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
// so if in that unique situation, just clear the delete_container id
@@ -329,21 +350,21 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
}
// only draw commands + charts if there are containers
if has_containers {
draw_blocks::commands(app_data, top_panel[1], f, gui_state);
if fd.has_containers {
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
draw_blocks::chart(f, lower_main[1], app_data);
}
if let Some(info) = info_text {
draw_blocks::info(f, info);
if let Some(info) = fd.info_text {
draw_blocks::info(f, &info);
}
// Check if error, and show popup if so
if gui_state.lock().status_contains(&[Status::Help]) {
if fd.help_visible {
draw_blocks::help_box(f);
}
if let Some(error) = has_error {
if let Some(error) = fd.has_error {
draw_blocks::error(f, error, None);
}
}