diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 4ca9b72..8c4973f 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -28,6 +28,11 @@ impl ContainerId { pub fn get(&self) -> &str { self.0.as_str() } + + /// Only return first 8 chars of id, is usually more than enough for uniqueness + pub fn get_short(&self) -> String { + self.0.chars().take(8).collect::() + } } impl Ord for ContainerId { @@ -443,6 +448,20 @@ pub struct ContainerItem { pub is_oxker: bool, } +/// Basic display information, for when running in debug mode +impl fmt::Display for ContainerItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}, {}, {}, {}", + self.id.get_short(), + self.name, + self.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)), + self.mem_stats.back().unwrap_or(&ByteStats::new(0)) + ) + } +} + impl ContainerItem { /// Create a new container item pub fn new( diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 30a44e7..3ad7a44 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -17,6 +17,7 @@ use crate::{ }; pub use container_state::*; +#[cfg(not(debug_assertions))] /// Global app_state, stored in an Arc #[derive(Debug, Clone)] pub struct AppData { @@ -26,6 +27,17 @@ pub struct AppData { pub args: CliArgs, } +#[cfg(debug_assertions)] +/// Global app_state, stored in an Arc +#[derive(Debug, Clone)] +pub struct AppData { + containers: StatefulList, + error: Option, + sorted_by: Option<(Header, SortedOrder)>, + debug_string: String, + pub args: CliArgs, +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum SortedOrder { Asc, @@ -64,6 +76,17 @@ impl fmt::Display for Header { } impl AppData { + #[cfg(debug_assertions)] + pub fn get_debug_string(&self) -> &str { + &self.debug_string + } + + #[cfg(debug_assertions)] + #[allow(unused)] + pub fn set_debug_string(&mut self, x: &str) { + self.debug_string.push_str(x); + } + /// Change the sorted order, also set the selected container state to match new order fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { self.sorted_by = x; @@ -86,6 +109,7 @@ impl AppData { } /// Generate a default app_state + #[cfg(not(debug_assertions))] pub fn default(args: CliArgs) -> Self { Self { args, @@ -95,6 +119,18 @@ impl AppData { } } + /// Generate a default app_state + #[cfg(debug_assertions)] + pub fn default(args: CliArgs) -> Self { + Self { + args, + containers: StatefulList::new(vec![]), + error: None, + sorted_by: None, + debug_string: String::new(), + } + } + /// Container sort related methods /// Remove the sorted header & order, and sort by default - created datetime @@ -410,18 +446,6 @@ impl AppData { self.get_selected_container().map_or(false, |i| i.is_oxker) } - /// Check if the initial parsing has been completed, by making sure that all ids given (which are running) have a non empty cpu_stats vecdec - pub fn initialised(&mut self, all_ids: &[(bool, ContainerId)]) -> bool { - let count_is_running = all_ids.iter().filter(|i| i.0).count(); - let number_with_cpu_status = self - .containers - .items - .iter() - .filter(|i| !i.cpu_stats.is_empty()) - .count(); - count_is_running == number_with_cpu_status - } - /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly pub fn get_width(&self) -> Columns { diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index b88d915..41de793 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -56,11 +56,6 @@ impl Binate { } } -// struct Init { -// done: , -// len: -// } - pub struct DockerData { app_data: Arc>, args: CliArgs, @@ -113,19 +108,7 @@ impl DockerData { spawn_id: SpawnId, spawns: Arc>>>, ) { - // if dead and !init then inspect! - if state.is_alive() || init.is_some() { - // // if state == State::Paused && init.is_some() { - // // app_data.lock().debug_string.push_str("is paused"); - - // // if let Ok(result) = docker.inspect_container(id.get(), Some(InspectContainerOptions{size:false})).await { - // // let mem_limit = format!("{}", result.host_config.map_or(0, |i|i.memory.unwrap_or_default())); - - // // app_data.lock().debug_string.push_str(&mem_limit); - // // } - - // // }else if state.is_alive() || init.is_some() { let mut stream = docker .stats( id.get(), @@ -136,14 +119,6 @@ impl DockerData { ) .take(1); - // let a = stream.next().await; - // app_data.lock().debug_string.push_str(&format!("{:?}", a.is_some())); - // let bb = a.unwrap().unwrap(); - - // // } - - // app_data.lock().debug_string.push_str("jkl"); - while let Some(Ok(stats)) = stream.next().await { let mem_stat = if state.is_alive() { Some(stats.memory_stats.usage.unwrap_or_default()) @@ -186,7 +161,6 @@ impl DockerData { /// Update all stats, spawn each container into own tokio::spawn thread fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { - // let thing =all_ids.len(); for (state, id) in all_ids { // let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len()))); let docker = Arc::clone(&self.docker); @@ -308,7 +282,7 @@ impl DockerData { .lock() .entry(SpawnId::Log(container.id.clone())) .or_insert_with(|| { - // TODO make a struct that can create this data + // MAYBE make a struct that can create this data? let app_data = Arc::clone(&self.app_data); let docker = Arc::clone(&self.docker); let id = container.id.clone(); diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 8e9ba2b..0f8cb91 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -21,7 +21,6 @@ use crate::{ app_error::AppError, docker_data::DockerMessage, ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND}, - value_capture, }; pub use message::InputMessages; @@ -201,14 +200,13 @@ impl InputHandler { } /// Handle any keyboard button events + // TODO refactor this #[allow(clippy::too_many_lines)] async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { - value_capture!( - contains_delete, - self.gui_state - .lock() - .status_contains(&[Status::DeleteConfirm]) - ); + let contains_delete = self + .gui_state + .lock() + .status_contains(&[Status::DeleteConfirm]); let contains = |s: Status| self.gui_state.lock().status_contains(&[s]); @@ -258,8 +256,8 @@ impl InputHandler { KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Tab => { // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Containers; + let is_containers = self.gui_state.lock().get_selected_panel() + == SelectablePanel::Containers; let count = if self.app_data.lock().get_container_len() == 0 && is_containers { 2 @@ -273,7 +271,7 @@ impl InputHandler { KeyCode::BackTab => { // Skip control panel if no containers, could be refactored let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Logs; + self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs; let count = if self.app_data.lock().get_container_len() == 0 && is_containers { 2 @@ -286,7 +284,7 @@ impl InputHandler { } KeyCode::Home => { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_start(), SelectablePanel::Logs => locked_data.log_start(), @@ -295,7 +293,7 @@ impl InputHandler { } KeyCode::End => { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_end(), SelectablePanel::Logs => locked_data.log_end(), @@ -316,7 +314,7 @@ impl InputHandler { } KeyCode::Enter => { // This isn't great, just means you can't send docker commands before full initialization of the program - let panel = self.gui_state.lock().selected_panel; + let panel = self.gui_state.lock().get_selected_panel(); if panel == SelectablePanel::Commands { let option_command = self.app_data.lock().selected_docker_command(); @@ -417,7 +415,7 @@ impl InputHandler { /// Change state to next, depending which panel is currently in focus fn next(&mut self) { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_next(), SelectablePanel::Logs => locked_data.log_next(), @@ -428,7 +426,7 @@ impl InputHandler { /// Change state to previous, depending which panel is currently in focus fn previous(&mut self) { let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; + let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { SelectablePanel::Containers => locked_data.containers_previous(), SelectablePanel::Logs => locked_data.log_previous(), diff --git a/src/main.rs b/src/main.rs index bad3e04..b2387cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,7 @@ fn read_docker_host(args: &CliArgs) -> Option { async fn docker_init( app_data: &Arc>, docker_rx: Receiver, + docker_tx: Sender, gui_state: &Arc>, is_running: &Arc, host: Option, @@ -79,8 +80,9 @@ async fn docker_init( let app_data = Arc::clone(app_data); let gui_state = Arc::clone(gui_state); let is_running = Arc::clone(is_running); + tokio::spawn(DockerData::init( - app_data, docker, docker_rx, gui_state, is_running, + app_data, docker, docker_rx, docker_tx, gui_state, is_running, )); } else { app_data @@ -102,15 +104,15 @@ fn handler_init( input_rx: Receiver, is_running: &Arc, ) { - let input_app_data = Arc::clone(app_data); - let input_gui_state = Arc::clone(gui_state); - let input_is_running = Arc::clone(is_running); + let app_data = Arc::clone(app_data); + let gui_state = Arc::clone(gui_state); + let is_running = Arc::clone(is_running); tokio::spawn(input_handler::InputHandler::init( - input_app_data, + app_data, input_rx, docker_sx.clone(), - input_gui_state, - input_is_running, + gui_state, + is_running, )); } @@ -124,28 +126,49 @@ async fn main() { let app_data = Arc::new(Mutex::new(AppData::default(args.clone()))); let gui_state = Arc::new(Mutex::new(GuiState::default())); let is_running = Arc::new(AtomicBool::new(true)); - let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32); + let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32); - docker_init(&app_data, docker_rx, &gui_state, &is_running, host).await; + docker_init( + &app_data, + docker_rx, + docker_tx.clone(), + &gui_state, + &is_running, + host, + ) + .await; if args.gui { let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); - handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running); - Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await; + handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); + Ui::create(app_data, gui_state, is_running, input_sx).await; } else { - info!("in debug mode"); - // Debug mode for testing, mostly pointless, doesn't take terminal + info!("in debug mode\n"); + // Debug mode for testing, less pointless now, will diplay some basic information while is_running.load(Ordering::SeqCst) { loop { if let Some(err) = app_data.lock().get_error() { error!("{}", err); process::exit(1); } - docker_sx.send(DockerMessage::Update).await.ok(); tokio::time::sleep(std::time::Duration::from_millis(u64::from( args.docker_interval, ))) .await; + let containers = app_data + .lock() + .get_container_items() + .clone() + .iter() + .map(|i| format!("{i}")) + .collect::>(); + + if !containers.is_empty() { + for item in containers { + info!("{item}"); + } + println!(); + } } } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 8d227de..9fe3cf0 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -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>, area: Rect, + fd: &FrameData, gui_state: &Arc>, 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>, area: Rect, f: &mut Frame, + fd: &FrameData, gui_state: &Arc>, ) { - 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.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>, area: Rect, f: &mut Frame, + fd: &FrameData, gui_state: &Arc>, 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.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::(), - 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::>(); 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>, area: Rect, f: &mut Frame, + fd: &FrameData, gui_state: &Arc>, - 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>, ) { 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::>(); - 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::>(); 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) { } /// 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() diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 967aff9..1a6bb26 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -166,15 +166,15 @@ pub enum Status { /// Global gui_state, stored in an Arc #[derive(Debug, Default, Clone)] pub struct GuiState { + delete_container: Option, + delete_map: HashMap, heading_map: HashMap, is_loading: HashSet, loading_index: u8, panel_map: HashMap, - delete_map: HashMap, + selected_panel: SelectablePanel, status: HashSet, - delete_container: Option, pub info_box_text: Option, - 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>, 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); } }) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7d81228..3cc8248 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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>, - docker_sx: Sender, gui_state: Arc>, input_poll_rate: Duration, is_running: Arc, @@ -60,17 +60,14 @@ impl Ui { /// Create a new Ui struct, and execute the drawing loop pub async fn create( app_data: Arc>, - docker_sx: Sender, gui_state: Arc>, is_running: Arc, sender: Sender, ) { 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, + has_containers: bool, + has_error: Option, + height: u16, + help_visible: bool, + init: bool, + info_text: Option, + 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>, gui_state: &Arc>) { - 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>, gui_state: &Arc>, gui_state: &Arc>, gui_state: &Arc