From 7c92ffef7da20143a31706a310b5e6f2c3e0554f Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 29 Mar 2023 18:00:31 +0000 Subject: [PATCH 1/2] refactor: button_item() include brackets --- src/ui/draw_blocks.rs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 56286c5..da6cf7a 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -515,12 +515,12 @@ impl HelpInfo { Span::styled(input.to_owned(), Style::default().fg(color)) } - /// Span to black text span + /// &str to black text span fn black_span<'a>(input: &str) -> Span<'a> { Self::span(input, Color::Black) } - /// Span to white text span + /// &str to white text span fn white_span<'a>(input: &str) -> Span<'a> { Self::span(input, Color::White) } @@ -559,7 +559,7 @@ impl HelpInfo { /// Generate the button information span + metadata fn gen_button() -> Self { - let button_item = |x: &str| Self::white_span(&format!(" {x} ")); + let button_item = |x: &str| Self::white_span(&format!(" ( {x} ) ")); let button_desc = |x: &str| Self::black_span(x); let or = || button_desc("or"); let space = || button_desc(" "); @@ -567,52 +567,52 @@ impl HelpInfo { let spans = [ Spans::from(vec![ space(), - button_item("( tab )"), + button_item("tab"), or(), - button_item("( shift+tab )"), + button_item("shift+tab"), button_desc("to change panels"), ]), Spans::from(vec![ space(), - button_item("( ↑ ↓ )"), + button_item("↑ ↓"), or(), - button_item("( j k )"), + button_item("j k"), or(), - button_item("( PgUp PgDown )"), + button_item("PgUp PgDown"), or(), - button_item("( Home End )"), + button_item("Home End"), button_desc("to change selected line"), ]), Spans::from(vec![ space(), - button_item("( enter )"), + button_item("enter"), button_desc("to send docker container command"), ]), Spans::from(vec![ space(), - button_item("( h )"), + button_item("h"), button_desc("to toggle this help information"), ]), Spans::from(vec![ space(), - button_item("( 0 )"), + button_item("0"), button_desc("to stop sort"), ]), Spans::from(vec![ space(), - button_item("( 1 - 9 )"), + button_item("1 - 9"), button_desc("sort by header - or click header"), ]), Spans::from(vec![ space(), - button_item("( m )"), + button_item("m"), button_desc( "to toggle mouse capture - if disabled, text on screen can be selected & copied", ), ]), Spans::from(vec![ space(), - button_item("( q )"), + button_item("q"), button_desc("to quit at any time"), ]), ]; @@ -635,7 +635,9 @@ impl HelpInfo { )]), Spans::from(vec![Span::styled( REPO.to_owned(), - Style::default().fg(Color::White).add_modifier(Modifier::UNDERLINED), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::UNDERLINED), )]), ]; let height = spans.len(); From 937202fe34d1692693c62dd1a7ad19db37651233 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 30 Mar 2023 02:12:03 +0000 Subject: [PATCH 2/2] feat: delete container, closes #27 Enable a user to delete a container. A dialog will pop up to ask the user to confirm the deletion. A user can then click on either button, or press N/Y to make a selection --- src/app_data/container_state.rs | 13 ++-- src/app_data/mod.rs | 10 +++ src/docker_data/message.rs | 12 +-- src/docker_data/mod.rs | 33 +++++++- src/input_handler/mod.rs | 91 +++++++++++++++++----- src/main.rs | 2 +- src/ui/draw_blocks.rs | 132 ++++++++++++++++++++++++++++---- src/ui/gui_state.rs | 49 +++++++++++- src/ui/mod.rs | 23 +++++- 9 files changed, 310 insertions(+), 55 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 8e3fa92..974356b 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -207,6 +207,7 @@ pub enum DockerControls { Start, Stop, Unpause, + Delete, } impl DockerControls { @@ -216,6 +217,7 @@ impl DockerControls { Self::Restart => Color::Magenta, Self::Start => Color::Green, Self::Stop => Color::Red, + Self::Delete => Color::Gray, Self::Unpause => Color::Blue, } } @@ -223,11 +225,11 @@ impl DockerControls { /// Docker commands available depending on the containers state pub fn gen_vec(state: State) -> Vec { match state { - State::Dead | State::Exited => vec![Self::Start, Self::Restart], - State::Paused => vec![Self::Unpause, Self::Stop], - State::Restarting => vec![Self::Stop], - State::Running => vec![Self::Pause, Self::Restart, Self::Stop], - _ => vec![], + State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete], + State::Paused => vec![Self::Unpause, Self::Stop, Self::Delete], + State::Restarting => vec![Self::Stop, Self::Delete], + State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete], + _ => vec![Self::Delete], } } } @@ -236,6 +238,7 @@ impl fmt::Display for DockerControls { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let disp = match self { Self::Pause => "pause", + Self::Delete => "delete", Self::Restart => "restart", Self::Start => "start", Self::Stop => "stop", diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index dc80aeb..fda6cbb 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -456,6 +456,15 @@ impl AppData { self.containers.items.iter_mut().find(|i| &i.id == id) } + /// return a mutable container by given id + pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { + self.containers + .items + .iter_mut() + .find(|i| &i.id == id) + .map(|i| i.name.clone()) + } + /// Find the id of the currently selected container. /// If any containers on system, will always return a ContainerId /// Only returns None when no containers found. @@ -532,6 +541,7 @@ impl AppData { } } } + // Trim a &String and return String let trim_owned = |x: &String| x.trim().to_owned(); diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs index f7de6f5..0a6b67e 100644 --- a/src/docker_data/message.rs +++ b/src/docker_data/message.rs @@ -2,11 +2,13 @@ use crate::app_data::ContainerId; #[derive(Debug, Clone)] pub enum DockerMessage { - Update, - Start(ContainerId), - Restart(ContainerId), + Delete(ContainerId), + ConfirmDelete(ContainerId), Pause(ContainerId), - Unpause(ContainerId), - Stop(ContainerId), Quit, + Restart(ContainerId), + Start(ContainerId), + Stop(ContainerId), + Unpause(ContainerId), + Update, } diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 8104ccc..391b3fc 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -1,5 +1,8 @@ use bollard::{ - container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions}, + container::{ + ListContainersOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions, Stats, + StatsOptions, + }, service::ContainerSummary, Docker, }; @@ -335,13 +338,14 @@ impl DockerData { } /// Handle incoming messages, container controls & all container information update - /// Spawn dowcker commands off into own thread + /// Spawn Docker commands off into own thread async fn message_handler(&mut self) { while let Some(message) = self.receiver.recv().await { let docker = Arc::clone(&self.docker); let gui_state = Arc::clone(&self.gui_state); let app_data = Arc::clone(&self.app_data); let uuid = Uuid::new_v4(); + // TODO need to refactor these match message { DockerMessage::Pause(id) => { tokio::spawn(async move { @@ -397,6 +401,31 @@ impl DockerData { }); self.update_everything().await; } + DockerMessage::Delete(id) => { + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + if docker + .remove_container( + id.get(), + Some(RemoveContainerOptions { + v: false, + force: true, + link: false, + }), + ) + .await + .is_err() + { + Self::set_error(&app_data, DockerControls::Stop, &gui_state); + } + Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + }); + self.update_everything().await; + self.gui_state.lock().set_delete_container(None); + } + DockerMessage::ConfirmDelete(id) => { + self.gui_state.lock().set_delete_container(Some(id)) + } DockerMessage::Update => self.update_everything().await, DockerMessage::Quit => { self.spawns diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 696e3c9..421f86a 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -19,7 +19,7 @@ use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{GuiState, SelectablePanel, Status, Ui}, + ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui}, }; pub use message::InputMessages; @@ -62,13 +62,21 @@ impl InputHandler { match message { InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await, InputMessages::MouseEvent(mouse_event) => { - let error_or_help = self - .gui_state - .lock() - .status_contains(&[Status::Error, Status::Help]); + let error_or_help = self.gui_state.lock().status_contains(&[ + Status::Error, + Status::Help, + Status::DeleteConfirm, + ]); if !error_or_help { self.mouse_press(mouse_event); } + let delete_confirm = self + .gui_state + .lock() + .status_contains(&[Status::DeleteConfirm]); + if delete_confirm { + self.button_intersect(mouse_event).await; + } } } if !self.is_running.load(Ordering::SeqCst) { @@ -133,41 +141,59 @@ impl InputHandler { } } + /// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container + async fn confirm_delete(&self) { + let id = self.gui_state.lock().get_delete_container(); + if let Some(id) = id { + self.docker_sender + .send(DockerMessage::Delete(id)) + .await + .ok(); + } + } + + /// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel) + fn clear_delete(&self) { + self.gui_state.lock().set_delete_container(None); + } + /// Handle any keyboard button events #[allow(clippy::too_many_lines)] async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { // TODO - refactor this to a single call, maybe return Error, Help or Normal let contains_error = self.gui_state.lock().status_contains(&[Status::Error]); let contains_help = self.gui_state.lock().status_contains(&[Status::Help]); + let contains_delete = self + .gui_state + .lock() + .status_contains(&[Status::DeleteConfirm]); - // Quit on Ctrl + c/C + // Always just quit on Ctrl + c/C or q/Q let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); - if key_modififer == KeyModifiers::CONTROL && is_c() { + let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); + if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() { self.quit().await; } if contains_error { - match key_code { - KeyCode::Char('q' | 'Q') => self.quit().await, - KeyCode::Char('c' | 'C') => { - self.app_data.lock().remove_error(); - self.gui_state.lock().status_del(Status::Error); - } - _ => (), + if let KeyCode::Char('c' | 'C') = key_code { + self.app_data.lock().remove_error(); + self.gui_state.lock().status_del(Status::Error); } } else if contains_help { match key_code { - KeyCode::Char('q' | 'Q') => self.quit().await, KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help), KeyCode::Char('m' | 'M') => self.m_key(), _ => (), } - } else { - // let abc = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::Ctrl); + } else if contains_delete { + match key_code { + KeyCode::Char('y' | 'Y') => self.confirm_delete().await, + KeyCode::Char('n' | 'N') => self.clear_delete(), + _ => (), + } + } else { match key_code { - // KeyCode::Ctrl('c') => { - // self.quit().await; - // } KeyCode::Char('0') => self.app_data.lock().reset_sorted(), KeyCode::Char('1') => self.sort(Header::State), KeyCode::Char('2') => self.sort(Header::Status), @@ -178,7 +204,6 @@ impl InputHandler { KeyCode::Char('7') => self.sort(Header::Image), KeyCode::Char('8') => self.sort(Header::Rx), KeyCode::Char('9') => self.sort(Header::Tx), - KeyCode::Char('q' | 'Q') => self.quit().await, KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Tab => { @@ -251,6 +276,11 @@ impl InputHandler { }; if let Some(id) = option_id { match command { + DockerControls::Delete => self + .docker_sender + .send(DockerMessage::ConfirmDelete(id)) + .await + .ok(), DockerControls::Pause => { self.docker_sender.send(DockerMessage::Pause(id)).await.ok() } @@ -280,6 +310,25 @@ impl InputHandler { } } + /// Check if a button press interacts with either the yes or no buttons in the delete container confirm window + async fn button_intersect(&mut self, mouse_event: MouseEvent) { + if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) { + let intersect = self.gui_state.lock().button_intersect(Rect::new( + mouse_event.column, + mouse_event.row, + 1, + 1, + )); + + if let Some(button) = intersect { + match button { + DeleteButton::Yes => self.confirm_delete().await, + DeleteButton::No => self.clear_delete(), + } + } + } + } + /// Handle mouse button events fn mouse_press(&mut self, mouse_event: MouseEvent) { match mouse_event.kind { diff --git a/src/main.rs b/src/main.rs index fd99297..29db59b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ use ui::{GuiState, Status, Ui}; use crate::docker_data::DockerMessage; -// this is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container +/// This is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container const ENTRY_POINT: &str = "/app/oxker"; const ENV_KEY: &str = "OXKER_RUNTIME"; const ENV_VALUE: &str = "container"; diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index da6cf7a..c6834d1 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -21,7 +21,7 @@ use crate::{ app_error::AppError, }; -use super::gui_state::{BoxLocation, Region}; +use super::gui_state::{BoxLocation, DeleteButton, Region}; use super::{GuiState, SelectablePanel}; const NAME_TEXT: &str = r#" @@ -43,6 +43,14 @@ const MARGIN: &str = " "; const ARROW: &str = "▶ "; const CIRCLE: &str = "⚪ "; +/// From a given &str, return the maximum number of chars on a single line +fn max_line_width(text: &str) -> usize { + text.lines() + .map(|i| i.chars().count()) + .max() + .unwrap_or_default() +} + /// Generate block, add a border if is the selected panel, /// add custom title based on state of each panel fn generate_block<'a>( @@ -53,7 +61,7 @@ fn generate_block<'a>( ) -> Block<'a> { gui_state .lock() - .update_heading_map(Region::Panel(panel), area); + .update_region_map(Region::Panel(panel), area); let current_selected_panel = gui_state.lock().selected_panel; let mut title = match panel { SelectablePanel::Containers => { @@ -459,7 +467,7 @@ pub fn heading_bar( let rect = headers_section[index]; gui_state .lock() - .update_heading_map(Region::Header(header), rect); + .update_region_map(Region::Header(header), rect); f.render_widget(paragraph, rect); } } @@ -479,14 +487,6 @@ pub fn heading_bar( f.render_widget(help_paragraph, split_bar[help_index]); } -/// From a given &str, return the maximum number of chars on a single line -fn max_line_width(text: &str) -> usize { - text.lines() - .map(|i| i.chars().count()) - .max() - .unwrap_or_default() -} - /// Help popup box needs these three pieces of information struct HelpInfo { spans: Vec>, @@ -593,11 +593,7 @@ impl HelpInfo { button_item("h"), button_desc("to toggle this help information"), ]), - Spans::from(vec![ - space(), - button_item("0"), - button_desc("to stop sort"), - ]), + Spans::from(vec![space(), button_item("0"), button_desc("to stop sort")]), Spans::from(vec![ space(), button_item("1 - 9"), @@ -728,6 +724,110 @@ pub fn help_box(f: &mut Frame<'_, B>) { f.render_widget(block, area); } +/// Draw the delete confirm box in the centre of the screen +/// take in container id and container name here? +pub fn delete_confirm( + f: &mut Frame<'_, B>, + gui_state: &Arc>, + name: &str, +) { + let block = Block::default() + .title(" Confirm Delete ") + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .title_alignment(Alignment::Center) + .borders(Borders::ALL); + + let confirm = Spans::from(vec![ + Span::from("Are you sure you want to delete container: "), + Span::styled( + name, + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ), + ]); + + let yes_text = " (Y)es "; + let no_text = " (N)o "; + + // Find the maximum line width & height, and add some padding + let max_line_width = u16::try_from(confirm.width()).unwrap_or(64) + 12; + let lines = 8; + + let confirm_para = Paragraph::new(confirm).alignment(Alignment::Center); + + let button_block = || { + Block::default() + .border_type(BorderType::Rounded) + .borders(Borders::ALL) + }; + + let yes_para = Paragraph::new(yes_text) + .alignment(Alignment::Center) + .block(button_block()); + // Need to add some padding for the borders + let yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9); + + let no_para = Paragraph::new(no_text) + .alignment(Alignment::Center) + .block(button_block()); + // Need to add some padding for the borders + let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8); + + let area = popup( + lines, + max_line_width.into(), + f.size(), + BoxLocation::MiddleCentre, + ); + + let split_popup = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Min(2), + Constraint::Max(1), + Constraint::Max(1), + Constraint::Max(3), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(area); + + let button_spacing = (max_line_width - no_chars - yes_chars) / 3; + let split_buttons = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Min(button_spacing), + Constraint::Max(no_chars), + Constraint::Min(button_spacing), + Constraint::Max(yes_chars), + Constraint::Min(button_spacing), + ] + .as_ref(), + ) + .split(split_popup[3]); + + let no_area = split_buttons[1]; + let yes_area = split_buttons[3]; + + // Insert button areas into region map, so can interact with them on click + gui_state + .lock() + .update_region_map(Region::Delete(DeleteButton::No), no_area); + + gui_state + .lock() + .update_region_map(Region::Delete(DeleteButton::Yes), yes_area); + + 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); +} + /// Draw an error popup over whole screen pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option) { let block = Block::default() diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index cb8571b..baa575a 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -5,7 +5,7 @@ use std::{ }; use uuid::Uuid; -use crate::app_data::Header; +use crate::app_data::{ContainerId, Header}; #[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] pub enum SelectablePanel { @@ -43,6 +43,13 @@ impl SelectablePanel { pub enum Region { Panel(SelectablePanel), Header(Header), + Delete(DeleteButton), +} + +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] +pub enum DeleteButton { + Yes, + No, } #[allow(unused)] @@ -191,11 +198,13 @@ impl fmt::Display for Loading { /// The application gui state can be in multiple of these four states at the same time /// Various functions (e.g input handler), operate differently depending upon current Status +// Copy #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Status { Init, Help, DockerConnect, + DeleteConfirm, Error, } @@ -206,7 +215,9 @@ pub struct GuiState { is_loading: HashSet, loading_icon: Loading, panel_map: HashMap, + delete_map: HashMap, status: HashSet, + delete_container: Option, pub info_box_text: Option, pub selected_panel: SelectablePanel, } @@ -229,6 +240,16 @@ impl GuiState { } } + /// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button + pub fn button_intersect(&mut self, rect: Rect) -> Option { + self.delete_map + .iter() + .filter(|i| i.1.intersects(rect)) + .collect::>() + .get(0) + .map(|data| *data.0) + } + /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels pub fn header_intersect(&mut self, rect: Rect) -> Option
{ self.heading_map @@ -240,7 +261,7 @@ impl GuiState { } /// Insert, or updates header area panel into heading_map - pub fn update_heading_map(&mut self, region: Region, area: Rect) { + pub fn update_region_map(&mut self, region: Region, area: Rect) { match region { Region::Header(header) => self .heading_map @@ -252,9 +273,30 @@ impl GuiState { .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), }; } + /// Check if an ContainerId is set in the delete_container field + pub fn get_delete_container(&self) -> Option { + self.delete_container.clone() + } + + /// Set either a ContainerId, or None, to the delete_container field + /// If Some, will also insert the DeleteConfirm status into self.status + pub fn set_delete_container(&mut self, id: Option) { + if id.is_some() { + self.status.insert(Status::DeleteConfirm); + } else { + self.status.remove(&Status::DeleteConfirm); + } + self.delete_container = id; + } + /// Check if the current gui_status contains any of the given status' /// Don't really like this methodology for gui state, needs a re-think pub fn status_contains(&self, status: &[Status]) -> bool { @@ -264,6 +306,9 @@ impl GuiState { /// Remove a gui_status into the current gui_status HashSet pub fn status_del(&mut self, status: Status) { self.status.remove(&status); + if status == Status::DeleteConfirm { + self.status.remove(&Status::DeleteConfirm); + } } /// Insert a gui_status into the current gui_status HashSet diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ddee0c2..53ec6a1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -24,7 +24,7 @@ mod draw_blocks; mod gui_state; pub use self::color_match::*; -pub use self::gui_state::{GuiState, SelectablePanel, Status}; +pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::AppData, app_error::AppError, docker_data::DockerMessage, input_handler::InputMessages, @@ -198,20 +198,23 @@ impl Ui { } /// Draw the main ui to a frame of the terminal +/// TODO add a single line area for debug message - if not in release mode, maybe with #[cfg(debug_assertions)] ? fn draw_frame( f: &mut Frame<'_, B>, app_data: &Arc>, gui_state: &Arc>, ) { - // set max height for container section, needs +4 to deal with docker commands list and borders + // set max height for container section, needs +5 to deal with docker commands list and borders let height = app_data.lock().get_container_len(); - let height = if height < 12 { height + 4 } else { 12 }; + let height = if height < 12 { height + 5 } else { 12 }; let column_widths = app_data.lock().get_width(); let has_containers = app_data.lock().get_container_len() > 0; let has_error = app_data.lock().get_error(); let sorted_by = app_data.lock().get_sorted(); + let delete_confirm = gui_state.lock().get_delete_container(); + let show_help = gui_state.lock().status_contains(&[Status::Help]); let info_text = gui_state.lock().info_box_text.clone(); let loading_icon = gui_state.lock().get_loading(); @@ -274,6 +277,20 @@ fn draw_frame( gui_state, ); + if let Some(id) = delete_confirm { + let name = app_data.lock().get_container_name_by_id(&id); + name.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 + gui_state.lock().set_delete_container(None); + }, + |name| { + draw_blocks::delete_confirm(f, gui_state, &name); + }, + ); + } + // only draw charts if there are containers if has_containers { draw_blocks::chart(f, lower_main[1], app_data);