Merge branch 'feat/application_state' into dev

This commit is contained in:
Jack Wills
2022-10-15 00:23:41 +00:00
7 changed files with 137 additions and 91 deletions
+1 -5
View File
@@ -14,10 +14,8 @@ pub struct AppData {
args: CliArgs,
error: Option<AppError>,
logs_parsed: bool,
pub containers: StatefulList<ContainerItem>,
pub init: bool,
pub show_error: bool,
sorted_by: Option<(Header, SortedOrder)>,
pub containers: StatefulList<ContainerItem>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
@@ -79,9 +77,7 @@ impl AppData {
args,
containers: StatefulList::new(vec![]),
error: None,
init: false,
logs_parsed: false,
show_error: false,
sorted_by: None,
}
}
+57 -44
View File
@@ -3,7 +3,7 @@ use bollard::{
service::ContainerSummary,
Docker,
};
use futures_util::StreamExt;
use futures_util::{Future, StreamExt};
use parking_lot::Mutex;
use std::{
collections::HashMap,
@@ -19,7 +19,7 @@ use crate::{
app_data::{AppData, ContainerId, DockerControls},
app_error::AppError,
parse_args::CliArgs,
ui::GuiState,
ui::{GuiState, Status},
};
mod message;
pub use message::DockerMessage;
@@ -318,6 +318,7 @@ impl DockerData {
// Initialize docker container data, before any messages are received
async fn initialise_container_data(&mut self) {
self.gui_state.lock().status_push(Status::Init);
let loading_uuid = Uuid::new_v4();
let loading_spin = self.loading_spin(loading_uuid).await;
@@ -336,65 +337,77 @@ impl DockerData {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
self.initialised = self.app_data.lock().initialised(&all_ids);
}
self.app_data.lock().init = true;
self.gui_state.lock().status_del(Status::Init);
self.stop_loading_spin(&loading_spin, loading_uuid);
}
/// Set the global error as the dockererror, and set gui_state to errro
fn set_error(&mut self, error: DockerControls) {
self.app_data
.lock()
.set_error(AppError::DockerCommand(error));
self.gui_state.lock().status_push(Status::Error);
}
/// Execute docker commands, will start and stop the loading spinner
async fn exec_docker(
&mut self,
docker_fn: impl Future<Output = Result<(), bollard::errors::Error>>,
uuid: Uuid,
control: DockerControls,
) {
let loading_spin = self.loading_spin(uuid).await;
if docker_fn.await.is_err() {
self.set_error(control);
};
self.stop_loading_spin(&loading_spin, uuid);
}
/// 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 loading_uuid = Uuid::new_v4();
let docker = Arc::clone(&self.docker);
match message {
DockerMessage::Pause(id) => {
let loading_spin = self.loading_spin(loading_uuid).await;
if docker.pause_container(id.get()).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Pause));
};
self.stop_loading_spin(&loading_spin, loading_uuid);
self.exec_docker(
docker.pause_container(id.get()),
loading_uuid,
DockerControls::Pause,
)
.await;
}
DockerMessage::Restart(id) => {
let loading_spin = self.loading_spin(loading_uuid).await;
if docker.restart_container(id.get(), None).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Restart));
};
self.stop_loading_spin(&loading_spin, loading_uuid);
self.exec_docker(
docker.restart_container(id.get(), None),
loading_uuid,
DockerControls::Restart,
)
.await;
}
DockerMessage::Start(id) => {
let loading_spin = self.loading_spin(loading_uuid).await;
if docker
.start_container(id.get(), None::<StartContainerOptions<String>>)
.await
.is_err()
{
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Start));
};
self.stop_loading_spin(&loading_spin, loading_uuid);
self.exec_docker(
docker.start_container(id.get(), None::<StartContainerOptions<String>>),
loading_uuid,
DockerControls::Start,
)
.await;
}
DockerMessage::Stop(id) => {
let loading_spin = self.loading_spin(loading_uuid).await;
if docker.stop_container(id.get(), None).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Stop));
};
self.stop_loading_spin(&loading_spin, loading_uuid);
self.exec_docker(
docker.stop_container(id.get(), None),
loading_uuid,
DockerControls::Stop,
)
.await;
}
DockerMessage::Unpause(id) => {
let loading_spin = self.loading_spin(loading_uuid).await;
if docker.unpause_container(id.get()).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Unpause));
};
self.stop_loading_spin(&loading_spin, loading_uuid);
self.exec_docker(
docker.unpause_container(id.get()),
loading_uuid,
DockerControls::Unpause,
)
.await;
self.update_everything().await;
}
DockerMessage::Update => self.update_everything().await,
+30 -20
View File
@@ -21,7 +21,7 @@ use crate::{
app_data::{AppData, DockerControls, Header, SortedOrder},
app_error::AppError,
docker_data::DockerMessage,
ui::{GuiState, SelectablePanel},
ui::{GuiState, SelectablePanel, Status},
};
pub use message::InputMessages;
@@ -64,10 +64,12 @@ impl InputHandler {
match message {
InputMessages::ButtonPress(key_code) => self.button_press(key_code).await,
InputMessages::MouseEvent(mouse_event) => {
let show_error = self.app_data.lock().show_error;
let show_info = self.gui_state.lock().show_help;
if !show_error && !show_info {
self.mouse_press(mouse_event);
let error_or_help = self
.gui_state
.lock()
.status_contains(&[Status::Error, Status::Help]);
if !error_or_help {
self.mouse_press(mouse_event)
}
}
}
@@ -85,10 +87,11 @@ impl InputHandler {
.gui_state
.lock()
.set_info_box("✖ mouse capture disabled".to_owned()),
Err(_) => self
.app_data
.lock()
.set_error(AppError::MouseCapture(false)),
Err(_) => {
self.app_data
.lock()
.set_error(AppError::MouseCapture(false));
}
}
} else {
match execute!(std::io::stdout(), EnableMouseCapture) {
@@ -96,7 +99,9 @@ impl InputHandler {
.gui_state
.lock()
.set_info_box("✓ mouse capture enabled".to_owned()),
Err(_) => self.app_data.lock().set_error(AppError::MouseCapture(true)),
Err(_) => {
self.app_data.lock().set_error(AppError::MouseCapture(true));
}
}
};
@@ -130,31 +135,36 @@ impl InputHandler {
/// Send a quit message to docker, to abort all spawns, if an error is return, set is_running to false here instead
async fn quit(&self) {
match self.docker_sender.send(DockerMessage::Quit).await {
Ok(_) => (),
Err(_) => self.is_running.store(false, Ordering::SeqCst),
let error_init = self
.gui_state
.lock()
.status_contains(&[Status::Error, Status::Init]);
if error_init {
self.is_running.store(false, Ordering::SeqCst);
} else if self.docker_sender.send(DockerMessage::Quit).await.is_err() {
self.is_running.store(false, Ordering::SeqCst)
}
}
/// Handle any keyboard button events
#[allow(clippy::too_many_lines)]
async fn button_press(&mut self, key_code: KeyCode) {
let show_error = self.app_data.lock().show_error;
let show_info = self.gui_state.lock().show_help;
let contains_error = self.gui_state.lock().status_contains(&[Status::Error]);
let contains_help = self.gui_state.lock().status_contains(&[Status::Help]);
if show_error {
if contains_error {
match key_code {
KeyCode::Char('q' | 'Q') => self.quit().await,
KeyCode::Char('c' | 'C') => {
self.app_data.lock().show_error = false;
self.app_data.lock().remove_error();
self.gui_state.lock().status_del(Status::Error);
}
_ => (),
}
} else if show_info {
} else if contains_help {
match key_code {
KeyCode::Char('q' | 'Q') => self.quit().await,
KeyCode::Char('h' | 'H') => self.gui_state.lock().show_help = false,
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
KeyCode::Char('m' | 'M') => self.m_key(),
_ => (),
}
@@ -171,7 +181,7 @@ impl InputHandler {
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().show_help = true,
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
KeyCode::Char('m' | 'M') => self.m_key(),
KeyCode::Tab => {
// Skip control panel if no containers, could be refactored
+9 -3
View File
@@ -24,7 +24,7 @@ mod input_handler;
mod parse_args;
mod ui;
use ui::{create_ui, GuiState};
use ui::{create_ui, GuiState, Status};
fn setup_tracing() {
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
@@ -59,9 +59,15 @@ async fn main() {
is_running,
));
}
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
Err(_) => {
app_data.lock().set_error(AppError::DockerConnect);
docker_gui_state.lock().status_push(Status::DockerConnect)
}
},
Err(_) => app_data.lock().set_error(AppError::DockerConnect),
Err(_) => {
app_data.lock().set_error(AppError::DockerConnect);
docker_gui_state.lock().status_push(Status::DockerConnect)
}
}
let input_app_data = Arc::clone(&app_data);
+9 -11
View File
@@ -15,6 +15,7 @@ use tui::{
};
use crate::app_data::{Header, SortedOrder};
use crate::ui::Status;
use crate::{
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
app_error::AppError,
@@ -215,9 +216,8 @@ pub fn logs<B: Backend>(
loading_icon: &str,
) {
let block = generate_block(app_data, area, gui_state, SelectablePanel::Logs);
let init = app_data.lock().init;
if !init {
let contains_init = gui_state.lock().status_contains(&[Status::Init]);
if contains_init {
let paragraph = Paragraph::new(format!("parsing logs {}", loading_icon))
.style(Style::default())
.block(block)
@@ -349,7 +349,7 @@ pub fn heading_bar<B: Backend>(
gui_state: &Arc<Mutex<GuiState>>,
) {
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
let info_visible = gui_state.lock().show_help;
let help_visible = gui_state.lock().status_contains(&[Status::Help]);
f.render_widget(block(Color::Black), area);
@@ -429,8 +429,8 @@ pub fn heading_bar<B: Backend>(
})
.collect::<Vec<_>>();
let suffix = if info_visible { "exit" } else { "show" };
let info_text = format!("( h ) {} help {}", suffix, MARGIN);
let suffix = if help_visible { "exit" } else { "show" };
let info_text = format!("( h ) {} help {}", suffix, MARGIN,);
let info_width = info_text.chars().count();
let column_width = usize::from(area.width) - info_width;
@@ -457,8 +457,6 @@ pub fn heading_bar<B: Backend>(
.alignment(Alignment::Center);
f.render_widget(loading_paragraph, split_bar[0]);
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
let headers_section = Layout::default()
.direction(Direction::Horizontal)
@@ -474,10 +472,10 @@ pub fn heading_bar<B: Backend>(
}
// show/hide help
let color = if info_visible {
Color::White
} else {
let color = if help_visible {
Color::Black
} else {
Color::White
};
let help_paragraph = Paragraph::new(info_text)
.block(block(color))
+24 -2
View File
@@ -173,6 +173,16 @@ impl fmt::Display for Loading {
}
}
/// The application can be in these four states
/// Various functions (e.g input handler), operate differently depending upon current Status
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Status {
Init,
Help,
DockerConnect,
Error,
}
/// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug, Clone)]
pub struct GuiState {
@@ -180,8 +190,8 @@ pub struct GuiState {
heading_map: HashMap<Header, Rect>,
loading_icon: Loading,
is_loading: HashSet<Uuid>,
status: HashSet<Status>,
pub selected_panel: SelectablePanel,
pub show_help: bool,
pub info_box_text: Option<String>,
}
impl GuiState {
@@ -192,9 +202,9 @@ impl GuiState {
heading_map: HashMap::new(),
loading_icon: Loading::One,
selected_panel: SelectablePanel::Containers,
show_help: false,
is_loading: HashSet::new(),
info_box_text: None,
status: HashSet::new(),
}
}
@@ -242,6 +252,18 @@ impl GuiState {
};
}
pub fn status_push(&mut self, status: Status) {
self.status.insert(status);
}
pub fn status_del(&mut self, status: Status) {
self.status.remove(&status);
}
pub fn status_contains(&self, status: &[Status]) -> bool {
status.iter().any(|i| self.status.contains(i))
}
/// Change to next selectable panel
pub fn next_panel(&mut self) {
self.selected_panel = self.selected_panel.next();
+7 -6
View File
@@ -25,7 +25,7 @@ mod draw_blocks;
mod gui_state;
pub use self::color_match::*;
pub use self::gui_state::{GuiState, SelectablePanel};
pub use self::gui_state::{GuiState, SelectablePanel, Status};
use crate::{
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
input_handler::InputMessages,
@@ -56,7 +56,7 @@ pub async fn create_ui(
update_duration,
)
.await;
terminal.clear()?;
terminal.clear()?;
disable_raw_mode()?;
execute!(
@@ -85,8 +85,9 @@ async fn run_app<B: Backend + Send>(
let input_poll_rate = std::time::Duration::from_millis(75);
// Check for docker connect errors before attempting to draw the gui
let e = app_data.lock().get_error();
if let Some(AppError::DockerConnect) = e {
let status_dockerconnect = gui_state.lock().status_contains(&[Status::DockerConnect]);
if status_dockerconnect {
let mut seconds = 5;
loop {
if seconds < 1 {
@@ -138,6 +139,7 @@ async fn run_app<B: Backend + Send>(
break;
}
}
// }
}
Ok(())
}
@@ -157,7 +159,7 @@ fn ui<B: Backend>(
let log_index = app_data.lock().get_selected_log_index();
let sorted_by = app_data.lock().get_sorted();
let show_help = gui_state.lock().show_help;
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();
@@ -241,7 +243,6 @@ fn ui<B: Backend>(
}
if let Some(error) = has_error {
app_data.lock().show_error = true;
draw_blocks::error(f, error, None);
}
}