chore: merge release-v0.3.2 into main
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -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]
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[default.extend-words]
|
||||
ratatui = "ratatui"
|
||||
+3
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user