diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs new file mode 100644 index 0000000..3d008f2 --- /dev/null +++ b/src/docker_data/message.rs @@ -0,0 +1,9 @@ +#[derive(Debug, Clone)] +pub enum DockerMessage { + Update, + Start(String), + Restart(String), + Pause(String), + Unpause(String), + Stop(String), +} diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index e6364c3..bf68656 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -1,22 +1,27 @@ use bollard::{ - container::{ListContainersOptions, LogsOptions, Stats, StatsOptions}, + container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions}, Docker, }; use futures_util::{future::join_all, StreamExt}; use parking_lot::Mutex; -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; +use std::sync::Arc; +use tokio::{sync::mpsc::Receiver, task::JoinHandle}; -use crate::{app_data::AppData, parse_args::CliArgs, ui::GuiState}; +use crate::{ + app_data::{AppData, DockerControls}, + app_error::AppError, + parse_args::CliArgs, + ui::GuiState, +}; +mod message; +pub use message::DockerMessage; pub struct DockerData { app_data: Arc>, docker: Arc, gui_state: Arc>, initialised: bool, - sleep_duration: Duration, + receiver: Receiver, timestamps: bool, } @@ -207,37 +212,23 @@ impl DockerData { self.update_all_container_stats(&all_ids).await; } - /// Initialise self, and start the updated loop - pub async fn init( - args: CliArgs, - app_data: Arc>, - docker: Arc, - gui_state: Arc>, - ) { - if app_data.lock().get_error().is_none() { - let mut inner = Self { - app_data, - docker, - gui_state, - initialised: false, - sleep_duration: Duration::from_millis(args.docker_interval as u64), - timestamps: args.timestamp, - }; - inner.initialise_container_data().await; - inner.update_loop().await; - } - } - - async fn initialise_container_data(&mut self) { - let gui_state = Arc::clone(&self.gui_state); - // could also just loop while init is false, would need to move an arc mutex into here - // so instead just abort at end of function - let loading_spin = tokio::spawn(async move { + async fn loading_spin(gui_state: Arc>) -> JoinHandle<()> { + tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_millis(100)).await; gui_state.lock().next_loading(); } - }); + }) + } + + fn stop_loading_spin(handle: JoinHandle<()>, gui_state: &Arc>) { + handle.abort(); + gui_state.lock().reset_loading(); + } + + async fn initialise_container_data(&mut self) { + let gui_state = Arc::clone(&self.gui_state); + let loading_spin = Self::loading_spin(gui_state).await; let all_ids = self.update_all_containers().await; self.update_all_container_stats(&all_ids).await; @@ -255,23 +246,111 @@ impl DockerData { self.initialised = self.app_data.lock().initialised(&all_ids); } self.app_data.lock().init = true; - loading_spin.abort(); - self.gui_state.lock().reset_loading(); + Self::stop_loading_spin(loading_spin, &self.gui_state); } - /// Update all items, wait until all complete - /// sleep for CliArgs.docker ms before updating next - async fn update_loop(&mut self) { - loop { - let start = Instant::now(); - self.update_everything().await; - - let elapsed = start.elapsed(); - if elapsed < self.sleep_duration { - tokio::time::sleep(self.sleep_duration - elapsed).await; + /// Handle incoming messages, container controls & all container information update + async fn message_handler(&mut self) { + while let Some(message) = self.receiver.recv().await { + let docker = Arc::clone(&self.docker); + let app_data = Arc::clone(&self.app_data); + let gui_state = Arc::clone(&self.gui_state); + match message { + DockerMessage::Pause(id) => { + let spin_gui = Arc::clone(&gui_state); + let loading_spin = Self::loading_spin(gui_state).await; + tokio::spawn(async move { + docker.pause_container(&id).await.unwrap_or_else(|_| { + app_data + .lock() + .set_error(AppError::DockerCommand(DockerControls::Pause)) + }); + Self::stop_loading_spin(loading_spin, &spin_gui); + }); + } + DockerMessage::Restart(id) => { + let spin_gui = Arc::clone(&gui_state); + let loading_spin = Self::loading_spin(gui_state).await; + tokio::spawn(async move { + docker + .restart_container(&id, None) + .await + .unwrap_or_else(|_| { + app_data + .lock() + .set_error(AppError::DockerCommand(DockerControls::Restart)) + }); + Self::stop_loading_spin(loading_spin, &spin_gui); + }); + } + DockerMessage::Start(id) => { + let spin_gui = Arc::clone(&gui_state); + let loading_spin = Self::loading_spin(gui_state).await; + tokio::spawn(async move { + docker + .start_container(&id, None::>) + .await + .unwrap_or_else(|_| { + app_data + .lock() + .set_error(AppError::DockerCommand(DockerControls::Start)) + }); + Self::stop_loading_spin(loading_spin, &spin_gui); + }); + } + DockerMessage::Stop(id) => { + let spin_gui = Arc::clone(&gui_state); + let loading_spin = Self::loading_spin(gui_state).await; + tokio::spawn(async move { + docker.stop_container(&id, None).await.unwrap_or_else(|_| { + app_data + .lock() + .set_error(AppError::DockerCommand(DockerControls::Stop)) + }); + Self::stop_loading_spin(loading_spin, &spin_gui); + }); + } + DockerMessage::Unpause(id) => { + let spin_gui = Arc::clone(&gui_state); + let loading_spin = Self::loading_spin(gui_state).await; + tokio::spawn(async move { + docker.unpause_container(&id).await.unwrap_or_else(|_| { + app_data + .lock() + .set_error(AppError::DockerCommand(DockerControls::Unpause)) + }); + Self::stop_loading_spin(loading_spin, &spin_gui); + }); + } + DockerMessage::Update => self.update_everything().await, } } } + + /// Initialise self, and start the updated loop + pub async fn init( + args: CliArgs, + app_data: Arc>, + docker: Arc, + gui_state: Arc>, + receiver: Receiver, + ) { + if app_data.lock().get_error().is_none() { + let mut inner = Self { + app_data, + docker, + gui_state, + initialised: false, + receiver, + timestamps: args.timestamp, + }; + inner.initialise_container_data().await; + + // todo!(" change this to recv.next()"); + // inner.update_loop().await; + inner.message_handler().await; + } + } } // tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 9baadc7..0dfead7 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -3,7 +3,6 @@ use std::sync::{ Arc, }; -use bollard::{container::StartContainerOptions, Docker}; use crossterm::{ event::{ DisableMouseCapture, EnableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind, @@ -11,13 +10,17 @@ use crossterm::{ execute, }; use parking_lot::Mutex; -use tokio::{sync::broadcast::Receiver, task::JoinHandle}; +use tokio::{ + sync::mpsc::{Receiver, Sender}, + task::JoinHandle, +}; use tui::layout::Rect; mod message; use crate::{ app_data::{AppData, DockerControls}, app_error::AppError, + docker_data::DockerMessage, ui::{GuiState, SelectablePanel}, }; pub use message::InputMessages; @@ -26,12 +29,12 @@ pub use message::InputMessages; #[derive(Debug)] pub struct InputHandler { app_data: Arc>, - docker: Arc, + docker_sender: Sender, gui_state: Arc>, - is_running: Arc, - rec: Receiver, - mouse_capture: bool, info_sleep: Option>, + is_running: Arc, + mouse_capture: bool, + rec: Receiver, } impl InputHandler { @@ -39,13 +42,13 @@ impl InputHandler { pub async fn init( app_data: Arc>, rec: Receiver, - docker: Arc, + docker_sender: Sender, gui_state: Arc>, is_running: Arc, ) { let mut inner = Self { app_data, - docker, + docker_sender, gui_state, is_running, rec, @@ -57,7 +60,7 @@ impl InputHandler { /// check for incoming messages async fn start(&mut self) { - while let Ok(message) = self.rec.recv().await { + while let Some(message) = self.rec.recv().await { match message { InputMessages::ButtonPress(key_code) => self.button_press(key_code).await, InputMessages::MouseEvent(mouse_event) => { @@ -168,90 +171,44 @@ impl InputHandler { } } KeyCode::Enter => { - // Does is matter though? // This isn't great, just means you can't send docker commands before full initialization of the program // could change to to if loading = true, although at the moment don't have a loading bool + // Does is matter though? let panel = self.gui_state.lock().selected_panel; if panel == SelectablePanel::Commands { let command = self.app_data.lock().get_docker_command(); if command.is_some() { let id = self.app_data.lock().get_selected_container_id(); - let app_data = Arc::clone(&self.app_data); - let docker = Arc::clone(&self.docker); if id.is_some() { let id = id.unwrap(); match command.unwrap() { - DockerControls::Pause => { - tokio::spawn(async move { - docker.pause_container(&id).await.unwrap_or_else( - |_| { - app_data.lock().set_error( - AppError::DockerCommand( - DockerControls::Pause, - ), - ) - }, - ); - }); - } - DockerControls::Unpause => { - tokio::spawn(async move { - docker.unpause_container(&id).await.unwrap_or_else( - |_| { - app_data.lock().set_error( - AppError::DockerCommand( - DockerControls::Unpause, - ), - ) - }, - ); - }); - } - DockerControls::Start => { - tokio::spawn(async move { - docker - .start_container( - &id, - None::>, - ) - .await - .unwrap_or_else(|_| { - app_data.lock().set_error( - AppError::DockerCommand( - DockerControls::Start, - ), - ) - }); - }); - } - DockerControls::Stop => { - tokio::spawn(async move { - docker.stop_container(&id, None).await.unwrap_or_else( - |_| { - app_data.lock().set_error( - AppError::DockerCommand( - DockerControls::Stop, - ), - ) - }, - ); - }); - } - DockerControls::Restart => { - tokio::spawn(async move { - docker - .restart_container(&id, None) - .await - .unwrap_or_else(|_| { - app_data.lock().set_error( - AppError::DockerCommand( - DockerControls::Restart, - ), - ) - }); - }); - } + // TODO handle theses errors? + DockerControls::Pause => self + .docker_sender + .send(DockerMessage::Pause(id)) + .await + .unwrap(), + DockerControls::Unpause => self + .docker_sender + .send(DockerMessage::Unpause(id)) + .await + .unwrap(), + DockerControls::Start => self + .docker_sender + .send(DockerMessage::Start(id)) + .await + .unwrap(), + DockerControls::Stop => self + .docker_sender + .send(DockerMessage::Stop(id)) + .await + .unwrap(), + DockerControls::Restart => self + .docker_sender + .send(DockerMessage::Restart(id)) + .await + .unwrap(), } } } diff --git a/src/main.rs b/src/main.rs index 112118a..2b38183 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -#![allow(unused)] - use app_data::AppData; use app_error::AppError; use bollard::Docker; @@ -33,13 +31,21 @@ async fn main() { let docker_app_data = Arc::clone(&app_data); let docker_gui_state = Arc::clone(&gui_state); + let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(16); // Create docker daemon handler, and only spawn up the docker data handler if ping returns non-error let docker = Arc::new(Docker::connect_with_socket_defaults().unwrap()); match docker.ping().await { Ok(_) => { let docker = Arc::clone(&docker); tokio::spawn(async move { - DockerData::init(docker_args, docker_app_data, docker, docker_gui_state).await; + DockerData::init( + docker_args, + docker_app_data, + docker, + docker_gui_state, + docker_rx, + ) + .await; }); } Err(_) => app_data.lock().set_error(AppError::DockerConnect), @@ -47,19 +53,20 @@ async fn main() { let input_app_data = Arc::clone(&app_data); - let (s, r) = tokio::sync::broadcast::channel(16); + let (input_sx, input_rx) = tokio::sync::mpsc::channel(16); - let input_docker = Arc::clone(&docker); + // let input_docker = Arc::clone(&docker); let is_running = Arc::new(AtomicBool::new(true)); let input_is_running = Arc::clone(&is_running); let input_gui_state = Arc::clone(&gui_state); + let input_docker_sender = docker_sx.clone(); // Spawn input handling into own tokio thread tokio::spawn(async { input_handler::InputHandler::init( input_app_data, - r, - input_docker, + input_rx, + input_docker_sender, input_gui_state, input_is_running, ) @@ -73,6 +80,16 @@ async fn main() { tokio::time::sleep(std::time::Duration::from_millis(5000)).await; } } else { - create_ui(app_data, s, is_running, gui_state).await.unwrap(); + let update_duration = std::time::Duration::from_millis(args.docker_interval as u64); + create_ui( + app_data, + input_sx, + is_running, + gui_state, + docker_sx, + update_duration, + ) + .await + .unwrap(); } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 4d71af0..2aeb768 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -223,6 +223,7 @@ pub fn draw_logs( f: &mut Frame<'_, B>, gui_state: &Arc>, index: Option, + loading_icon: String, selected_panel: &SelectablePanel, ) { let panel = SelectablePanel::Logs; @@ -233,8 +234,8 @@ pub fn draw_logs( let init = app_data.lock().init; if !init { - let icon = gui_state.lock().get_loading(); - let parsing_logs = format!("parsing logs {}", icon); + // let icon = gui_state.lock().get_loading(); + let parsing_logs = format!("parsing logs {}", loading_icon); let paragraph = Paragraph::new(parsing_logs) .style(Style::default()) .block(block) @@ -367,16 +368,22 @@ pub fn draw_heading_bar( columns: &Columns, f: &mut Frame<'_, B>, has_containers: bool, + loading_icon: String, info_visible: bool, ) { let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black)); f.render_widget(block(), area); - let mut column_headings = format!(" {:>width$}", columns.state.0, width = columns.state.1); + let mut column_headings = format!( + " {}{:>width$}", + loading_icon, + columns.state.0, + width = columns.state.1 + ); column_headings.push_str( format!( - "{} {:>width$}", + "{} {:>width$}", MARGIN, columns.status.0, width = columns.status.1 @@ -471,7 +478,9 @@ pub fn draw_help_box(f: &mut Frame<'_, B>) { help_text.push_str("\n ( ↑ ↓ ← → ) to change selected line"); help_text.push_str("\n ( enter ) to send docker container commands"); help_text.push_str("\n ( h ) to toggle this help information"); - help_text.push_str("\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied"); + help_text.push_str( + "\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied", + ); help_text.push_str("\n ( q ) to quit at any time"); help_text.push_str("\n mouse scrolling & clicking also available"); help_text.push_str("\n\n currenty an early work in progress, all and any input appreciated"); @@ -602,7 +611,6 @@ pub fn draw_info(f: &mut Frame<'_, B>, text: String) { .title_alignment(Alignment::Center) .borders(Borders::NONE); - let mut max_line_width = 0; text.lines().into_iter().for_each(|line| { let width = line.chars().count(); diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 72ef238..5a99a51 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -8,6 +8,7 @@ pub enum SelectablePanel { Logs, } +#[allow(unused)] #[derive(Debug, Clone, Copy)] pub enum BoxLocation { TopLeft, @@ -165,10 +166,12 @@ pub struct GuiState { // Is an issue if two panels are in the same space, sush as a smaller panel embedded, yet infront of, a larger panel // If a BMapTree think it would mean have to implement ordering for SelectablePanel area_map: HashMap, - loading: Loading, + loading_icon: Loading, + // Should be a vec, each time loading add a new to the vec, and reset remove from vec + // for for if is_loading just check if vec is empty or not + is_loading: bool, pub selected_panel: SelectablePanel, pub show_help: bool, - // show_info_panel: bool, pub info_box_text: Option, } @@ -177,10 +180,10 @@ impl GuiState { pub fn default() -> Self { Self { area_map: HashMap::new(), - loading: Loading::One, + loading_icon: Loading::One, selected_panel: SelectablePanel::Containers, show_help: false, - // show_info_panel: false, + is_loading: false, info_box_text: None, } } @@ -218,29 +221,33 @@ impl GuiState { self.selected_panel = self.selected_panel.prev(); } + /// Advance loading animation pub fn next_loading(&mut self) { - self.loading = self.loading.next() + self.loading_icon = self.loading_icon.next(); + self.is_loading = true; } + /// if is_loading, return loading animation frame, else single space pub fn get_loading(&mut self) -> String { - self.loading.to_string() + if self.is_loading { + self.loading_icon.to_string() + } else { + String::from(" ") + } } + /// set is_loading to false, but keep animation frame at same state pub fn reset_loading(&mut self) { - self.loading = Loading::One; + self.is_loading = false; } + /// Set info box content pub fn set_info_box(&mut self, text: String) { self.info_box_text = Some(text); - // self.show_info_panel = true; - - // Should spawn and after 10 seconds close? - // Need to copy whatever we're doing with parsing logs icon } + /// Remove info box content pub fn reset_info_box(&mut self) { - // self.loading = Loading::One; self.info_box_text = None; - // self.show_info_panel = false; } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7027a5a..33dafc6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,12 +5,15 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use parking_lot::Mutex; -use std::sync::atomic::AtomicBool; use std::{ io, sync::{atomic::Ordering, Arc}, }; -use tokio::sync::broadcast::Sender; +use std::{ + sync::atomic::AtomicBool, + time::{Duration, Instant}, +}; +use tokio::sync::mpsc::Sender; use tracing::error; use tui::{ backend::{Backend, CrosstermBackend}, @@ -24,7 +27,10 @@ mod gui_state; pub use self::color_match::*; pub use self::gui_state::{GuiState, SelectablePanel}; -use crate::{app_data::AppData, app_error::AppError, input_handler::InputMessages}; +use crate::{ + app_data::AppData, app_error::AppError, docker_data::DockerMessage, + input_handler::InputMessages, +}; use draw_blocks::*; /// Take control of the terminal in order to draw gui @@ -33,6 +39,8 @@ pub async fn create_ui( sender: Sender, is_running: Arc, gui_state: Arc>, + docker_sx: Sender, + update_duration: Duration, ) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); @@ -40,7 +48,16 @@ pub async fn create_ui( let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let res = run_app(&mut terminal, app_data, sender, is_running, gui_state).await; + let res = run_app( + &mut terminal, + app_data, + sender, + is_running, + gui_state, + docker_sx, + update_duration, + ) + .await; disable_raw_mode().unwrap(); execute!( @@ -63,6 +80,8 @@ async fn run_app( sender: Sender, is_running: Arc, gui_state: Arc>, + docker_sx: Sender, + update_duration: Duration, ) -> Result<(), AppError> { let input_poll_rate = std::time::Duration::from_millis(75); @@ -84,6 +103,7 @@ async fn run_app( } } } else { + let mut now = Instant::now(); loop { terminal.draw(|f| ui(f, &app_data, &gui_state)).unwrap(); if crossterm::event::poll(input_poll_rate).unwrap() { @@ -91,15 +111,24 @@ async fn run_app( if let Event::Key(key) = event { sender .send(InputMessages::ButtonPress(key.code)) - .unwrap_or(0); + .await + .unwrap_or(()); } else if let Event::Mouse(m) = event { - sender.send(InputMessages::MouseEvent(m)).unwrap_or(0); + sender + .send(InputMessages::MouseEvent(m)) + .await + .unwrap_or(()); } else if let Event::Resize(_, _) = event { gui_state.lock().clear_area_map(); terminal.autoresize().unwrap_or(()); } } + if now.elapsed() >= update_duration { + docker_sx.send(DockerMessage::Update).await.unwrap(); + now = Instant::now(); + } + if !is_running.load(Ordering::SeqCst) { break; } @@ -128,6 +157,7 @@ fn ui( let selected_panel = gui_state.lock().selected_panel; let show_help = gui_state.lock().show_help; let info_text = gui_state.lock().info_box_text.clone(); + let loading_icon = gui_state.lock().get_loading(); let whole_layout = Layout::default() .direction(Direction::Vertical) @@ -189,6 +219,7 @@ fn ui( f, gui_state, log_index, + loading_icon.to_owned(), &selected_panel, ); @@ -197,6 +228,7 @@ fn ui( &column_widths, f, has_containers, + loading_icon, show_help, );