chore: merge release-v0.3.2 into main

This commit is contained in:
Jack Wills
2023-08-28 13:32:35 +00:00
15 changed files with 511 additions and 459 deletions
+9 -7
View File
@@ -1,13 +1,15 @@
### 2023-06-04
### 2023-08-28
### Chores
+ github workflow ubuntu latest, build for x86 musl, [4fa841e6e74e3e10e3d3e82eac1a1ca1338814cf]
+ dependencies updated, [0caa92f6a4728d50d8b2d8f15d96a21112732ec5], [1fd1dfc75d6fa4e84451ebc845b9e1c730381f41]
+ `Spans` -> `Line`, ratatui 0.21 update, [4679ddc885a9b35c901f3600b63fd9e86118264c], [0d37ac55018038363e5f92dc4215996f8cff7b2e]
+ `create_release.sh` updated, [7dec5f14a381d237c5e72fbf9551bcf398f93f3e]
+ dependencies updated, [8ce5a1877a8c56d9bbab560c97e2596ea87cc4c0], [94a20584e6ef0701c9f36838b0dfbcd911698dbe], [29e02e0d1faae4a836c7e5cfd0d791338ff586e3], [8e4c2e686761df56920df2267b765ab1297c9972]
+ `_typos.toml` added, [84ba1020939606abf4a287cbd1de1f3a10d3f0c0]
### Fixes
+ workflow additional image fix, closes #29, [47cda44b8213cfb8c3807df6c43e3f5dc2452b57]
### Features
+ Custom hostname. `oxker` will use `$DOCKER_HOST` env if set, or one can use the cli argument `--host`, which takes priority over the `$DOCKER_HOST`, closes #30, [10950787649d2b66fc1e8cd8b85526df51479857]
### Refactors
+ `set_error()` takes `gui_state` and error enum, to make sure app_data & gui_state is in sync [62c78dfaa50a8d8c084f7fbf7e203b50aaa731ae]
+ `fn loading_spin` doesn't need to be async, [2e27462d1b3f0bdb27d7646511e36d0c9af07f3e]
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
+14
View File
@@ -1,3 +1,17 @@
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.2'>v0.3.2</a>
### 2023-08-28
### Chores
+ dependencies updated, [8ce5a187](https://github.com/mrjackwills/oxker/commit/8ce5a1877a8c56d9bbab560c97e2596ea87cc4c0), [94a20584](https://github.com/mrjackwills/oxker/commit/94a20584e6ef0701c9f36838b0dfbcd911698dbe), [29e02e0d](https://github.com/mrjackwills/oxker/commit/29e02e0d1faae4a836c7e5cfd0d791338ff586e3), [8e4c2e68](https://github.com/mrjackwills/oxker/commit/8e4c2e686761df56920df2267b765ab1297c9972)
+ `_typos.toml` added, [84ba1020](https://github.com/mrjackwills/oxker/commit/84ba1020939606abf4a287cbd1de1f3a10d3f0c0)
### Features
+ Custom hostname. `oxker` will use `$DOCKER_HOST` env if set, or one can use the cli argument `--host`, which takes priority over the `$DOCKER_HOST`, closes [#30](https://github.com/mrjackwills/oxker/issues/30), [10950787](https://github.com/mrjackwills/oxker/commit/10950787649d2b66fc1e8cd8b85526df51479857)
### Refactors
+ `set_error()` takes `gui_state` and error enum, to make sure app_data & gui_state is in sync [62c78dfa](https://github.com/mrjackwills/oxker/commit/62c78dfaa50a8d8c084f7fbf7e203b50aaa731ae)
+ `fn loading_spin` doesn't need to be async, [2e27462d](https://github.com/mrjackwills/oxker/commit/2e27462d1b3f0bdb27d7646511e36d0c9af07f3e)
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.1'>v0.3.1</a>
### 2023-06-04
Generated
+306 -294
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "oxker"
version = "0.3.1"
version = "0.3.2"
edition = "2021"
authors = ["Jack Wills <email@mrjackwills.com>"]
description = "A simple tui to view & control docker containers"
@@ -16,14 +16,14 @@ anyhow = "1.0"
bollard = "0.14"
cansi = "2.2"
clap = { version = "4.3", features = ["derive", "unicode", "color"] }
crossterm = "0.26"
crossterm = "0.27"
futures-util = "0.3"
parking_lot = { version= "0.12" }
tokio = {version = "1.28", features=["full"]}
tokio = { version = "1.32", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
ratatui = "0.21"
uuid = {version = "1.3", features = ["v4", "fast-rng"]}
ratatui = "0.23"
uuid = { version = "1.4", features = ["v4", "fast-rng"] }
[dev-dependencies]
+1
View File
@@ -106,6 +106,7 @@ Available command line arguments
| argument|result|
|--|--|
|```-d [number > 0]```| set the minimum update interval for docker information, in ms, defaults to 1000 (1 second) |
|```--host [hostname]```| connect to Docker with a custom hostname, defaults to `/var/run/docker.sock`, will use `$DOCKER_HOST` env if set |
|```-r```| show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
|```-c```| attempt to color the logs (conflicts with -r) |
|```-t```| remove timestamps from each log entry |
+2
View File
@@ -0,0 +1,2 @@
[default.extend-words]
ratatui = "ratatui"
+3 -5
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# rust create_release
# v0.2.2
# v0.3.0
STAR_LINE='****************************************'
CWD=$(pwd)
@@ -50,9 +50,7 @@ update_patch () {
# Get the url of the github repo, strip .git from the end of it
get_git_remote_url() {
REMOTE_ORIGIN=$(git config --get remote.origin.url)
TO_REMOVE=".git"
GIT_REPO_URL="${REMOTE_ORIGIN//$TO_REMOVE}"
GIT_REPO_URL="$(git config --get remote.origin.url | sed 's/\.git$//')"
}
# Check that git status is clean
@@ -121,7 +119,7 @@ update_version_number_in_files () {
# create new semver version based on user input
# Set MAJOR MINOR PATCH
check_tag () {
LATEST_TAG=$(git describe --tags --abbrev=0 --always)
LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)")
echo -e "\nCurrent tag: ${PURPLE}${LATEST_TAG}${RESET}\n"
echo -e "${YELLOW}Choose new tag version:${RESET}\n"
if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]
+13 -3
View File
@@ -1,11 +1,20 @@
use bollard::models::ContainerSummary;
use core::fmt;
use parking_lot::Mutex;
use ratatui::widgets::{ListItem, ListState};
use std::time::{SystemTime, UNIX_EPOCH};
use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
mod container_state;
use crate::{app_error::AppError, parse_args::CliArgs, ui::log_sanitizer, ENTRY_POINT};
use crate::{
app_error::AppError,
parse_args::CliArgs,
ui::{log_sanitizer, GuiState, Status},
ENTRY_POINT,
};
pub use container_state::*;
/// Global app_state, stored in an Arc<Mutex>
@@ -389,7 +398,8 @@ impl AppData {
}
/// insert single app_state error
pub fn set_error(&mut self, error: AppError) {
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
gui_state.lock().status_push(status);
self.error = Some(error);
}
+13 -12
View File
@@ -287,7 +287,7 @@ impl DockerData {
}
/// Animate the loading icon
async fn loading_spin(loading_uuid: Uuid, gui_state: &Arc<Mutex<GuiState>>) -> JoinHandle<()> {
fn loading_spin(loading_uuid: Uuid, gui_state: &Arc<Mutex<GuiState>>) -> JoinHandle<()> {
let gui_state = Arc::clone(gui_state);
tokio::spawn(async move {
loop {
@@ -311,7 +311,7 @@ impl DockerData {
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, &Arc::clone(&self.gui_state)).await;
let loading_spin = Self::loading_spin(loading_uuid, &Arc::clone(&self.gui_state));
let all_ids = self.update_all_containers().await;
@@ -323,8 +323,8 @@ impl DockerData {
while !self.app_data.lock().initialised(&all_ids) {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
self.gui_state.lock().status_del(Status::Init);
Self::stop_loading_spin(&self.gui_state, &loading_spin, loading_uuid);
self.gui_state.lock().status_del(Status::Init);
}
/// Set the global error as the docker error, and set gui_state to error
@@ -333,8 +333,9 @@ impl DockerData {
error: DockerControls,
gui_state: &Arc<Mutex<GuiState>>,
) {
app_data.lock().set_error(AppError::DockerCommand(error));
gui_state.lock().status_push(Status::Error);
app_data
.lock()
.set_error(AppError::DockerCommand(error), gui_state, Status::Error);
}
/// Handle incoming messages, container controls & all container information update
@@ -349,7 +350,7 @@ impl DockerData {
match message {
DockerMessage::Pause(id) => {
tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
let loading_spin = Self::loading_spin(uuid, &gui_state);
if docker.pause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
}
@@ -359,7 +360,7 @@ impl DockerData {
}
DockerMessage::Restart(id) => {
tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
let loading_spin = Self::loading_spin(uuid, &gui_state);
if docker.restart_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
}
@@ -369,7 +370,7 @@ impl DockerData {
}
DockerMessage::Start(id) => {
tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
let loading_spin = Self::loading_spin(uuid, &gui_state);
if docker
.start_container(id.get(), None::<StartContainerOptions<String>>)
.await
@@ -383,7 +384,7 @@ impl DockerData {
}
DockerMessage::Stop(id) => {
tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
let loading_spin = Self::loading_spin(uuid, &gui_state);
if docker.stop_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
}
@@ -393,7 +394,7 @@ impl DockerData {
}
DockerMessage::Unpause(id) => {
tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
let loading_spin = Self::loading_spin(uuid, &gui_state);
if docker.unpause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Unpause, &gui_state);
}
@@ -403,7 +404,7 @@ impl DockerData {
}
DockerMessage::Delete(id) => {
tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
let loading_spin = Self::loading_spin(uuid, &gui_state);
if docker
.remove_container(
id.get(),
@@ -448,7 +449,7 @@ impl DockerData {
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
) {
let args = app_data.lock().args;
let args = app_data.lock().args.clone();
if app_data.lock().get_error().is_none() {
let mut inner = Self {
app_data,
+37 -23
View File
@@ -20,6 +20,7 @@ use crate::{
app_error::AppError,
docker_data::DockerMessage,
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
value_capture,
};
pub use message::InputMessages;
@@ -62,12 +63,11 @@ 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(&[
if !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
@@ -93,18 +93,22 @@ impl InputHandler {
.lock()
.set_info_box("✖ mouse capture disabled".to_owned());
} else {
self.app_data
.lock()
.set_error(AppError::MouseCapture(false));
self.gui_state.lock().status_push(Status::Error);
self.app_data.lock().set_error(
AppError::MouseCapture(false),
&self.gui_state,
Status::Error,
);
}
} else if Ui::enable_mouse_capture().is_ok() {
self.gui_state
.lock()
.set_info_box("✓ mouse capture enabled".to_owned());
} else {
self.app_data.lock().set_error(AppError::MouseCapture(true));
self.gui_state.lock().status_push(Status::Error);
self.app_data.lock().set_error(
AppError::MouseCapture(true),
&self.gui_state,
Status::Error,
);
};
// If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window
@@ -160,13 +164,21 @@ impl InputHandler {
/// 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
value_capture!(
contains_delete,
self.gui_state
.lock()
.status_contains(&[Status::DeleteConfirm]);
.status_contains(&[Status::DeleteConfirm])
);
value_capture!(
contains_error,
self.gui_state.lock().status_contains(&[Status::Error])
);
value_capture!(
contains_help,
self.gui_state.lock().status_contains(&[Status::Help])
);
// Always just quit on Ctrl + c/C or q/Q
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
@@ -208,10 +220,9 @@ impl InputHandler {
KeyCode::Char('m' | 'M') => self.m_key(),
KeyCode::Tab => {
// Skip control panel if no containers, could be refactored
let has_containers = self.app_data.lock().get_container_len() == 0;
let is_containers =
self.gui_state.lock().selected_panel == SelectablePanel::Containers;
let count = if has_containers && is_containers {
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
2
} else {
1
@@ -222,10 +233,9 @@ impl InputHandler {
}
KeyCode::BackTab => {
// Skip control panel if no containers, could be refactored
let has_containers = self.app_data.lock().get_container_len() == 0;
let is_containers =
self.gui_state.lock().selected_panel == SelectablePanel::Logs;
let count = if has_containers && is_containers {
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
2
} else {
1
@@ -236,7 +246,8 @@ impl InputHandler {
}
KeyCode::Home => {
let mut locked_data = self.app_data.lock();
match self.gui_state.lock().selected_panel {
let selected_panel = self.gui_state.lock().selected_panel;
match selected_panel {
SelectablePanel::Containers => locked_data.containers_start(),
SelectablePanel::Logs => locked_data.log_start(),
SelectablePanel::Commands => locked_data.docker_command_start(),
@@ -244,7 +255,8 @@ impl InputHandler {
}
KeyCode::End => {
let mut locked_data = self.app_data.lock();
match self.gui_state.lock().selected_panel {
let selected_panel = self.gui_state.lock().selected_panel;
match selected_panel {
SelectablePanel::Containers => locked_data.containers_end(),
SelectablePanel::Logs => locked_data.log_end(),
SelectablePanel::Commands => locked_data.docker_command_end(),
@@ -358,7 +370,8 @@ 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();
match self.gui_state.lock().selected_panel {
let selected_panel = self.gui_state.lock().selected_panel;
match selected_panel {
SelectablePanel::Containers => locked_data.containers_next(),
SelectablePanel::Logs => locked_data.log_next(),
SelectablePanel::Commands => locked_data.docker_command_next(),
@@ -368,7 +381,8 @@ 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();
match self.gui_state.lock().selected_panel {
let selected_panel = self.gui_state.lock().selected_panel;
match selected_panel {
SelectablePanel::Containers => locked_data.containers_previous(),
SelectablePanel::Logs => locked_data.log_previous(),
SelectablePanel::Commands => locked_data.docker_command_previous(),
+50 -14
View File
@@ -17,17 +17,20 @@
use app_data::AppData;
use app_error::AppError;
use bollard::Docker;
use bollard::{Docker, API_DEFAULT_VERSION};
use docker_data::DockerData;
use input_handler::InputMessages;
use parking_lot::Mutex;
use parse_args::CliArgs;
use std::sync::{
use std::{
process,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{info, Level};
use tracing::{error, info, Level};
mod app_data;
mod app_error;
@@ -44,6 +47,7 @@ use crate::docker_data::DockerMessage;
const ENTRY_POINT: &str = "/app/oxker";
const ENV_KEY: &str = "OXKER_RUNTIME";
const ENV_VALUE: &str = "container";
const DOCKER_HOST: &str = "DOCKER_HOST";
/// Enable tracing, only really used in debug mode, for now
/// write to file if `-g` is set?
@@ -62,6 +66,18 @@ fn check_if_containerised() -> bool {
}
}
/// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env
fn read_docker_host(args: &CliArgs) -> Option<String> {
args.host.as_ref().map_or_else(
|| {
std::env::vars()
.find(|x| x.0 == DOCKER_HOST)
.map(|(_, val)| val)
},
|x| Some(x.to_string()),
)
}
/// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error
async fn docker_init(
app_data: &Arc<Mutex<AppData>>,
@@ -69,8 +85,13 @@ async fn docker_init(
docker_rx: Receiver<DockerMessage>,
gui_state: &Arc<Mutex<GuiState>>,
is_running: &Arc<AtomicBool>,
host: Option<String>,
) {
if let Ok(docker) = Docker::connect_with_socket_defaults() {
let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| {
Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION)
});
if let Ok(docker) = connection {
if docker.ping().await.is_ok() {
let app_data = Arc::clone(app_data);
let gui_state = Arc::clone(gui_state);
@@ -84,12 +105,14 @@ async fn docker_init(
is_running,
));
} else {
app_data.lock().set_error(AppError::DockerConnect);
gui_state.lock().status_push(Status::DockerConnect);
app_data
.lock()
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
}
} else {
app_data.lock().set_error(AppError::DockerConnect);
gui_state.lock().status_push(Status::DockerConnect);
app_data
.lock()
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
}
}
@@ -120,23 +143,36 @@ async fn main() {
setup_tracing();
let args = CliArgs::new();
let app_data = Arc::new(Mutex::new(AppData::default(args)));
let host = read_docker_host(&args);
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 (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
docker_init(&app_data, containerised, docker_rx, &gui_state, &is_running).await;
handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running);
docker_init(
&app_data,
containerised,
docker_rx,
&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;
} else {
info!("in debug mode");
while is_running.load(Ordering::SeqCst) {
// Debug mode for testing, mostly pointless, doesn't take terminal
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,
+6 -1
View File
@@ -3,7 +3,7 @@ use std::process;
use clap::Parser;
use tracing::error;
#[derive(Parser, Debug, Clone, Copy)]
#[derive(Parser, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
#[command(version, about)]
pub struct CliArgs {
@@ -19,6 +19,10 @@ pub struct CliArgs {
#[clap(short = 'c', conflicts_with = "raw")]
pub color: bool,
/// Docker host, defaults to `/var/run/docker.sock`
#[clap(long, short = None)]
pub host: Option<String>,
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
#[clap(short = 'r', conflicts_with = "color")]
pub raw: bool,
@@ -46,6 +50,7 @@ impl CliArgs {
Self {
color: args.color,
docker_interval: args.docker_interval,
host: args.host,
gui: !args.gui,
show_self: !args.show_self,
raw: args.raw,
+6 -8
View File
@@ -62,7 +62,6 @@ fn generate_block<'a>(
gui_state
.lock()
.update_region_map(Region::Panel(panel), area);
let current_selected_panel = gui_state.lock().selected_panel;
let mut title = match panel {
SelectablePanel::Containers => {
format!("{} {}", panel.title(), app_data.lock().container_title())
@@ -79,7 +78,7 @@ fn generate_block<'a>(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title);
if current_selected_panel == panel {
if gui_state.lock().selected_panel == panel {
block = block.border_style(Style::default().fg(Color::LightCyan));
}
block
@@ -819,6 +818,11 @@ pub fn delete_confirm<B: Backend>(
let no_area = split_buttons[1];
let yes_area = split_buttons[3];
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);
// Insert button areas into region map, so can interact with them on click
gui_state
.lock()
@@ -827,12 +831,6 @@ pub fn delete_confirm<B: Backend>(
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
+19 -63
View File
@@ -1,8 +1,5 @@
use ratatui::layout::{Constraint, Rect};
use std::{
collections::{HashMap, HashSet},
fmt,
};
use std::collections::{HashMap, HashSet};
use uuid::Uuid;
use crate::app_data::{ContainerId, Header};
@@ -145,60 +142,12 @@ impl BoxLocation {
}
}
/// State for the loading animation
#[derive(Debug, Default, Clone, Copy)]
pub enum Loading {
#[default]
One,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
}
impl Loading {
pub const fn next(self) -> Self {
match self {
Self::One => Self::Two,
Self::Two => Self::Three,
Self::Three => Self::Four,
Self::Four => Self::Five,
Self::Five => Self::Six,
Self::Six => Self::Seven,
Self::Seven => Self::Eight,
Self::Eight => Self::Nine,
Self::Nine => Self::Ten,
Self::Ten => Self::One,
}
}
}
impl fmt::Display for Loading {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
Self::One => '⠋',
Self::Two => '⠙',
Self::Three => '⠹',
Self::Four => '⠸',
Self::Five => '⠼',
Self::Six => '⠴',
Self::Seven => '⠦',
Self::Eight => '⠧',
Self::Nine => '⠇',
Self::Ten => '⠏',
};
write!(f, "{disp}")
}
}
// loading animation frames
const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
const FRAMES_LEN: u8 = 9;
/// 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,
@@ -213,7 +162,7 @@ pub enum Status {
pub struct GuiState {
heading_map: HashMap<Header, Rect>,
is_loading: HashSet<Uuid>,
loading_icon: Loading,
loading_index: u8,
panel_map: HashMap<SelectablePanel, Rect>,
delete_map: HashMap<DeleteButton, Rect>,
status: HashSet<Status>,
@@ -327,24 +276,31 @@ impl GuiState {
self.selected_panel = self.selected_panel.prev();
}
/// Insert a new loading_uuid into HashSet, and advance the animation by one frame
/// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array
pub fn next_loading(&mut self, uuid: Uuid) {
self.loading_icon = self.loading_icon.next();
if self.loading_index == FRAMES_LEN {
self.loading_index = 0;
} else {
self.loading_index += 1;
}
self.is_loading.insert(uuid);
}
/// If is_loading has any entries, return the current loading_icon, else an empty string, which needs to take up the same space, hence ' '
pub fn get_loading(&mut self) -> String {
/// 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 {
if self.is_loading.is_empty() {
String::from(" ")
' '
} else {
self.loading_icon.to_string()
FRAMES[usize::from(self.loading_index)]
}
}
/// Remove a loading_uuid from the is_loading HashSet
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
pub fn remove_loading(&mut self, uuid: Uuid) {
self.is_loading.remove(&uuid);
if self.is_loading.is_empty() {
self.loading_index = 0;
}
}
/// Set info box content
+22 -19
View File
@@ -197,28 +197,34 @@ 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();
};
}
/// 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)] ?
/// TODO add a single line area for debug message - if not in release mode?
fn draw_frame<B: Backend>(
f: &mut Frame<'_, B>,
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());
// 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 + 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();
let whole_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
@@ -261,10 +267,6 @@ fn draw_frame<B: Backend>(
draw_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths);
if has_containers {
draw_blocks::commands(app_data, top_panel[1], f, gui_state);
}
draw_blocks::logs(app_data, lower_main[0], f, gui_state, &loading_icon);
draw_blocks::heading_bar(
@@ -290,8 +292,9 @@ fn draw_frame<B: Backend>(
);
}
// only draw charts if there are containers
// only draw commands + charts if there are containers
if has_containers {
draw_blocks::commands(app_data, top_panel[1], f, gui_state);
draw_blocks::chart(f, lower_main[1], app_data);
}
@@ -300,7 +303,7 @@ fn draw_frame<B: Backend>(
}
// Check if error, and show popup if so
if show_help {
if gui_state.lock().status_contains(&[Status::Help]) {
draw_blocks::help_box(f);
}