chore: merge release-v0.1.6 into main

This commit is contained in:
Jack Wills
2022-10-16 02:15:16 +00:00
14 changed files with 297 additions and 200 deletions
+10 -11
View File
@@ -1,21 +1,20 @@
### 2022-10-07
### 2022-10-16
### Chores
+ Update clap to v4, [15597dbe6942ec053541398ce0e9dedc10a4d3ea]
+ Cargo update, [c3e72ae7369a25d903f39e55a4349cb005671dd4]
+ create_release.sh v0.1.0, [3c8d59c666bd4cda9ca54989b2f1b48bba17bc57]
+ uuid updated to version 1.2, [438ad770f4a5ecb5f4bbc308066ad9e808f66514]
### Docs
+ readme.md updated, [a05bf561cc6d96237f683ab0b3c782d6841974d9]
### Fixes
+ loading icon shifting error fix, also make icon white, closes #15, [59797685dffa29752a48c98e6cf465884d6d9df6]
### Features
+ use newtype construct for container id, [41cbb84f2896f8be2c37eba87e390d998aff7382]
+ Show container name in log panel title, closes #16, [9cb0c414afc284947fc2b8494504387e4e7edd87]
+ use gui_state HashSet to keep track of application gui state, [9e9d51559a13944622abf4fcbd3bd63766d11467]
+ terminal.clear() after run_app finished, [67c49575682cb271fac0998ff377a6504cd0bc86]
### Refactors
+ Impl Copy where able to, [e76878f424d72b943713ef84e95e25fada77d79e]
+ replace async fn with just fn, [17dc604befac75cb9dc0311a0e43f9927fe0ca30]
+ remove pointless clone()'s & variable declarations, [6731002ee42c9460042c2c38aff5101b1bcebbe6]
+ replace String::from("") with String::new(), [62fb22478697cc9a7ab9fb562a724965b437233a]
+ replace map_or_else with map_or, [3e26f292c7dc5e13af4580952767ebe821aa5183], [5660b34d5149dce27706ff6daa90b854e6f84e14]
+ CpuStats & MemStats use tuple struct, [a060d032586a0707ac91cb13d922aae0850449c5]
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
+23 -5
View File
@@ -1,7 +1,25 @@
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.6'>v0.1.6</a>
### 2022-10-16
### Chores
+ Cargo update, [c3e72ae7](https://github.com/mrjackwills/oxker/commit/c3e72ae7369a25d903f39e55a4349cb005671dd4),
+ create_release.sh v0.1.0, [3c8d59c6](https://github.com/mrjackwills/oxker/commit/3c8d59c666bd4cda9ca54989b2f1b48bba17bc57),
+ uuid updated to version 1.2, [438ad770](https://github.com/mrjackwills/oxker/commit/438ad770f4a5ecb5f4bbc308066ad9e808f66514),
### Fixes
+ loading icon shifting error fix, also make icon white, closes [#15](https://github.com/mrjackwills/oxker/issues/15), [59797685](https://github.com/mrjackwills/oxker/commit/59797685dffa29752a48c98e6cf465884d6d9df6),
### Features
+ Show container name in log panel title, closes [#16](https://github.com/mrjackwills/oxker/issues/16), [9cb0c414](https://github.com/mrjackwills/oxker/commit/9cb0c414afc284947fc2b8494504387e4e7edd87),
+ use gui_state HashSet to keep track of application gui state, [9e9d5155](https://github.com/mrjackwills/oxker/commit/9e9d51559a13944622abf4fcbd3bd63766d11467),
+ terminal.clear() after run_app finished, [67c49575](https://github.com/mrjackwills/oxker/commit/67c49575682cb271fac0998ff377a6504cd0bc86),
### Refactors
+ CpuStats & MemStats use tuple struct, [a060d032](https://github.com/mrjackwills/oxker/commit/a060d032586a0707ac91cb13d922aae0850449c5),
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.5'>v0.1.5</a>
### 2022-10-07
### Chores
+ Update clap to v4, [15597dbe](https://github.com/mrjackwills/oxker/commit/15597dbe6942ec053541398ce0e9dedc10a4d3ea),
@@ -16,13 +34,13 @@
+ replace async fn with just fn, [17dc604b](https://github.com/mrjackwills/oxker/commit/17dc604befac75cb9dc0311a0e43f9927fe0ca30),
+ remove pointless clone()'s & variable declarations, [6731002e](https://github.com/mrjackwills/oxker/commit/6731002ee42c9460042c2c38aff5101b1bcebbe6),
+ replace String::from("") with String::new(), [62fb2247](https://github.com/mrjackwills/oxker/commit/62fb22478697cc9a7ab9fb562a724965b437233a),
+ replace map_or_else with map_or, [3e26f292](https://github.com/mrjackwills/oxker/commit/3e26f292c7dc5e13af4580952767ebe821aa5183),, [5660b34d](https://github.com/mrjackwills/oxker/commit/5660b34d5149dce27706ff6daa90b854e6f84e14),
+ replace map_or_else with map_or, [3e26f292](https://github.com/mrjackwills/oxker/commit/3e26f292c7dc5e13af4580952767ebe821aa5183), [5660b34d](https://github.com/mrjackwills/oxker/commit/5660b34d5149dce27706ff6daa90b854e6f84e14),
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.1.4'>v0.1.4</a>
### 2022-09-07
### Chores
+ dependencies updated, [a3168daa](https://github.com/mrjackwills/oxker/commit/a3168daa3f769a6747dfbe61103073a7e80a1485),, [78e59160](https://github.com/mrjackwills/oxker/commit/78e59160bb6a978ee80e3a99eb72f051fb64e737),
+ dependencies updated, [a3168daa](https://github.com/mrjackwills/oxker/commit/a3168daa3f769a6747dfbe61103073a7e80a1485),[78e59160](https://github.com/mrjackwills/oxker/commit/78e59160bb6a978ee80e3a99eb72f051fb64e737),
### Features
+ containerize self, github action to build and push to [Docker Hub](https://hub.docker.com/r/mrjackwills/oxker), [07f97202](https://github.com/mrjackwills/oxker/commit/07f972022a69f22bac57925e6ad84234381f7890),
@@ -138,9 +156,9 @@
### 2022-04-29
### Features
+ allow toggling of mouse caputre, to select & copy text with mouse, closes #2, [aec184ea](https://github.com/mrjackwills/oxker/commit/aec184ea22b289e91942a4c3e6a415685884bc47),
+ allow toggling of mouse caputre, to select & copy text with mouse, closes [#2](https://github.com/mrjackwills/oxker/issues/2), [aec184ea](https://github.com/mrjackwills/oxker/commit/aec184ea22b289e91942a4c3e6a415685884bc47),
+ show id column, [b10f9274](https://github.com/mrjackwills/oxker/commit/b10f927481c9e38a48c1d4b94e744ec48e8b6ba6),
+ draw_popup, using enum to draw in one of 9 areas, closes #6, [1017850a](https://github.com/mrjackwills/oxker/commit/1017850a6cc91328abc1127bdb117495f5e909d8),
+ draw_popup, using enum to draw in one of 9 areas, closes [#6](https://github.com/mrjackwills/oxker/issues/6), [1017850a](https://github.com/mrjackwills/oxker/commit/1017850a6cc91328abc1127bdb117495f5e909d8),
+ use a message rx/sx for all docker commands, remove update loop, wait for update message from gui instead, [9b70fdfa](https://github.com/mrjackwills/oxker/commit/9b70fdfad7b38361ebee301bdc2545d3f0dfcf9e),
### Fixes
Generated
+15 -15
View File
@@ -101,9 +101,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.0.10"
version = "4.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b1a0a4208c6c483b952ad35c6eed505fc13b46f08f631b81e828084a9318d74"
checksum = "6bf8832993da70a4c6d13c581f4463c2bdda27b9bf1c5498dc4365543abe6d6f"
dependencies = [
"atty",
"bitflags",
@@ -118,9 +118,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.0.10"
version = "4.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db342ce9fda24fb191e2ed4e102055a4d381c1086a06630174cd8da8d5d917ce"
checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad"
dependencies = [
"heck",
"proc-macro-error",
@@ -433,9 +433,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.134"
version = "0.2.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
[[package]]
name = "lock_api"
@@ -514,7 +514,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "oxker"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"anyhow",
"bollard",
@@ -623,9 +623,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.46"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
@@ -712,9 +712,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.85"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
"itoa",
"ryu",
@@ -1034,9 +1034,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-normalization"
@@ -1072,9 +1072,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.1.2"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83"
dependencies = [
"getrandom",
"rand",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "oxker"
version = "0.1.5"
version = "0.1.6"
edition = "2021"
authors = ["Jack Wills <email@mrjackwills.com>"]
description = "A simple tui to view & control docker containers"
@@ -23,7 +23,7 @@ tokio = {version = "1.21", features=["full"]}
tracing = "0.1"
tracing-subscriber = "0.3"
tui = "0.19"
uuid = {version = "1.1", features = ["v4", "fast-rng"]}
uuid = {version = "1.2", features = ["v4", "fast-rng"]}
[dev-dependencies]
+42 -16
View File
@@ -1,9 +1,8 @@
#!/bin/bash
# rust create_release
# v0.0.15
# v0.1.2
PACKAGE_NAME='oxker'
STAR_LINE='****************************************'
CWD=$(pwd)
@@ -20,11 +19,6 @@ error_close() {
exit 1
}
if [ -z "$PACKAGE_NAME" ]
then
error_close "No package name"
fi
# $1 string - question to ask
ask_yn () {
printf "%b%s? [y/N]:%b " "${GREEN}" "$1" "${RESET}"
@@ -114,8 +108,8 @@ update_release_body_and_changelog () {
sed -i -E "s=(\s)\[([0-9a-f]{8})([0-9a-f]{32})\]= [\2](${GIT_REPO_URL}/commit/\2\3),=g" ./CHANGELOG.md
# Update changelog to add links to closed issues - comma included!
# "closes [#1]," -> "closes [#1](https:/www.../issues/1),""
sed -i -r -E "s=closes \[#([0-9]+)\],=closes [#\1](${GIT_REPO_URL}/issues/\1),=g" ./CHANGELOG.md
# "closes #1," -> "closes [#1](https:/www.../issues/1),""
sed -i -r -E "s=closes \#([0-9]+)\,=closes [#\1](${GIT_REPO_URL}/issues/\1),=g" ./CHANGELOG.md
}
# update version in cargo.toml, to match selected current version
@@ -193,34 +187,66 @@ cargo_build () {
ask_continue
}
# $1 text to colourise
release_continue () {
echo -e "\n${PURPLE}$1${RESET}"
ask_continue
}
# Full flow to create a new release
release_flow() {
check_git
get_git_remote_url
cargo_test
cargo_build
cd "${CWD}" || error_close "Can't find ${CWD}"
check_tag
NEW_TAG_WITH_V="v${MAJOR}.${MINOR}.${PATCH}"
printf "\nnew tag chosen: %s\n\n" "${NEW_TAG_WITH_V}"
RELEASE_BRANCH=release-$NEW_TAG_WITH_V
echo -e
ask_changelog_update
git checkout -b "$RELEASE_BRANCH"
update_version_number_in_files
cargo fmt
git add .
git commit -m "chore: release $NEW_TAG_WITH_V"
release_continue "checkout ${RELEASE_BRANCH}"
git checkout -b "$RELEASE_BRANCH"
release_continue "update_version_number_in_files"
update_version_number_in_files
echo -e "\ncargo fmt"
cargo fmt
release_continue "git add ."
git add .
release_continue "git commit -m \"chore: release \"${NEW_TAG_WITH_V}\""
git commit -m "chore: release ${NEW_TAG_WITH_V}"
release_continue "git checkout main"
git checkout main
release_continue "git merge --no-ff \"${RELEASE_BRANCH}\" -m \"chore: merge ${RELEASE_BRANCH} into main\""
git merge --no-ff "$RELEASE_BRANCH" -m "chore: merge ${RELEASE_BRANCH} into main"
release_continue "git tag -am \"${RELEASE_BRANCH}\" \"$NEW_TAG_WITH_V\""
git tag -am "${RELEASE_BRANCH}" "$NEW_TAG_WITH_V"
echo "git tag -am \"${RELEASE_BRANCH}\" \"$NEW_TAG_WITH_V\""
release_continue "git push --atomic origin main \"$NEW_TAG_WITH_V\""
git push --atomic origin main "$NEW_TAG_WITH_V"
release_continue "git checkout dev"
git checkout dev
git merge --no-ff main -m 'chore: merge main into dev'
release_continue "git merge --no-ff main -m \"chore: merge main into dev\""
git merge --no-ff main -m "chore: merge main into dev"
release_continue "git push origin dev"
git push origin dev
release_continue "git branch -d \"$RELEASE_BRANCH\""
git branch -d "$RELEASE_BRANCH"
}
+19 -22
View File
@@ -107,6 +107,7 @@ impl<T> StatefulList<T> {
}
}
/// Return the current status of the select list, e.g. 2/5,
pub fn get_state_title(&self) -> String {
if self.items.is_empty() {
String::new()
@@ -254,13 +255,11 @@ pub trait Stats {
/// So can use custom display formatter
/// Use trait Stats for use as generic in draw_chart function
#[derive(Debug, Default, Clone, Copy)]
pub struct CpuStats {
value: f64,
}
pub struct CpuStats(f64);
impl CpuStats {
pub const fn new(value: f64) -> Self {
Self { value }
Self(value)
}
}
@@ -268,21 +267,21 @@ impl Eq for CpuStats {}
impl PartialEq for CpuStats {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
self.0 == other.0
}
}
impl PartialOrd for CpuStats {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value.partial_cmp(&other.value)
self.0.partial_cmp(&other.0)
}
}
impl Ord for CpuStats {
fn cmp(&self, other: &Self) -> Ordering {
if self.value > other.value {
if self.0 > other.0 {
Ordering::Greater
} else if (self.value - other.value).abs() < 0.01 {
} else if (self.0 - other.0).abs() < 0.01 {
Ordering::Equal
} else {
Ordering::Less
@@ -292,13 +291,13 @@ impl Ord for CpuStats {
impl Stats for CpuStats {
fn get_value(&self) -> f64 {
self.value
self.0
}
}
impl fmt::Display for CpuStats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = format!("{:05.2}%", self.value);
let disp = format!("{:05.2}%", self.0);
write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1))
}
}
@@ -307,41 +306,39 @@ impl fmt::Display for CpuStats {
/// So can use custom display formatter
/// Use trait Stats for use as generic in draw_chart function
#[derive(Debug, Default, Clone, Copy, Eq)]
pub struct ByteStats {
value: u64,
}
pub struct ByteStats(u64);
impl PartialEq for ByteStats {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
self.0 == other.0
}
}
impl PartialOrd for ByteStats {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.value.partial_cmp(&other.value)
self.0.partial_cmp(&other.0)
}
}
impl Ord for ByteStats {
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
self.0.cmp(&other.0)
}
}
impl ByteStats {
pub const fn new(value: u64) -> Self {
Self { value }
Self(value)
}
pub fn update(&mut self, value: u64) {
self.value = value;
self.0 = value;
}
}
#[allow(clippy::cast_precision_loss)]
impl Stats for ByteStats {
fn get_value(&self) -> f64 {
self.value as f64
self.0 as f64
}
}
@@ -353,7 +350,7 @@ impl fmt::Display for ByteStats {
x if x >= ONE_GB => format!("{y:.2} GB", y = as_f64 / ONE_GB),
x if x >= ONE_MB => format!("{y:.2} MB", y = as_f64 / ONE_MB),
x if x >= ONE_KB => format!("{y:.2} kB", y = as_f64 / ONE_KB),
_ => format!("{} B", self.value),
_ => format!("{} B", self.0),
};
write!(f, "{:>x$}", p, x = f.width().unwrap_or(1))
}
@@ -426,7 +423,7 @@ impl ContainerItem {
self.cpu_stats
.iter()
.enumerate()
.map(|i| (i.0 as f64, i.1.value as f64))
.map(|i| (i.0 as f64, i.1 .0 as f64))
.collect::<Vec<_>>()
}
@@ -436,7 +433,7 @@ impl ContainerItem {
self.mem_stats
.iter()
.enumerate()
.map(|i| (i.0 as f64, i.1.value as f64))
.map(|i| (i.0 as f64, i.1 .0 as f64))
.collect::<Vec<_>>()
}
+12 -7
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,
}
}
@@ -257,11 +253,20 @@ impl AppData {
}
/// Get the title for log panel for selected container
/// will be "logs x/x"
/// will be either
/// 1) "logs x/x - container_name" where container_name is 32 chars max
/// 2) "logs - container_name" when no logs found, again 32 chars max
pub fn get_log_title(&self) -> String {
self.get_selected_log_index()
.map_or("".to_owned(), |index| {
self.containers.items[index].logs.get_state_title()
let logs_len = self.containers.items[index].logs.get_state_title();
let mut name = self.containers.items[index].name.clone();
name.truncate(32);
if logs_len.is_empty() {
format!("- {} ", name)
} else {
format!("{} - {}", logs_len, name)
}
})
}
+43 -45
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;
@@ -316,8 +316,9 @@ impl DockerData {
self.gui_state.lock().remove_loading(loading_uuid);
}
// Initialize docker container data, before any messages are received
/// 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,62 @@ 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 docker error, and set gui_state to error
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 a docker command, will start and stop the loading spinner, and set correct error
async fn exec_docker(
&mut self,
docker_fn: impl Future<Output = Result<(), bollard::errors::Error>> + Send,
control: DockerControls,
) {
let uuid = Uuid::new_v4();
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();
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()), 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),
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>>),
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), 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()), DockerControls::Unpause)
.await;
self.update_everything().await;
}
DockerMessage::Update => self.update_everything().await,
+29 -19
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,9 +64,11 @@ 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 {
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
Err(_) => {
self.app_data
.lock()
.set_error(AppError::MouseCapture(false)),
.set_error(AppError::MouseCapture(false));
}
}
} else {
match execute!(std::io::stdout(), EnableMouseCapture) {
@@ -96,11 +99,13 @@ 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));
}
}
};
// If the info box sleep handle is currently being executed, as in m is pressed twice within a 4000ms window
// If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window
// then cancel the first handle, as a new handle will be invoked
if let Some(info_sleep_timer) = self.info_sleep.as_ref() {
info_sleep_timer.abort();
@@ -129,32 +134,37 @@ impl InputHandler {
}
/// Send a quit message to docker, to abort all spawns, if an error is return, set is_running to false here instead
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
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.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;
// 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]);
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 -7
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();
@@ -45,9 +45,8 @@ async fn main() {
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
match Docker::connect_with_socket_defaults() {
Ok(docker) => match docker.ping().await {
Ok(_) => {
if let Ok(docker) = Docker::connect_with_socket_defaults() {
if docker.ping().await.is_ok() {
let docker = Arc::new(docker);
let is_running = Arc::clone(&is_running);
tokio::spawn(DockerData::init(
@@ -58,10 +57,13 @@ async fn main() {
docker_rx,
is_running,
));
} else {
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),
} else {
app_data.lock().set_error(AppError::DockerConnect);
docker_gui_state.lock().status_push(Status::DockerConnect);
}
let input_app_data = Arc::clone(&app_data);
+38 -20
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,
@@ -50,7 +51,9 @@ fn generate_block<'a>(
gui_state: &Arc<Mutex<GuiState>>,
panel: SelectablePanel,
) -> Block<'a> {
gui_state.lock().update_map(Region::Panel(panel), area);
gui_state
.lock()
.update_heading_map(Region::Panel(panel), area);
let current_selected_panel = gui_state.lock().selected_panel;
let title = match panel {
SelectablePanel::Containers => {
@@ -215,9 +218,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)
@@ -337,6 +339,7 @@ fn make_chart<'a, T: Stats + Display>(
}
/// Draw heading bar at top of program, always visible
/// TODO Should seperate into loading icon/headers/help functions
#[allow(clippy::too_many_lines)]
pub fn heading_bar<B: Backend>(
area: Rect,
@@ -347,10 +350,10 @@ pub fn heading_bar<B: Backend>(
sorted_by: Option<(Header, SortedOrder)>,
gui_state: &Arc<Mutex<GuiState>>,
) {
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
let info_visible = gui_state.lock().show_help;
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
let help_visible = gui_state.lock().status_contains(&[Status::Help]);
f.render_widget(block(), area);
f.render_widget(block(Color::Black), area);
// Generate a bloack for the header, if the header is currently being used to sort a column, then highlight it white
let header_block = |x: &Header| {
@@ -380,8 +383,7 @@ pub fn heading_bar<B: Backend>(
let block = header_block(header);
let text = match header {
Header::State => format!(
" {}{:>width$}{ic}",
loading_icon,
"{:>width$}{ic}",
header,
ic = block.1,
width = width - block.2,
@@ -393,7 +395,6 @@ pub fn heading_bar<B: Backend>(
ic = block.1,
width = width - block.2
),
_ => format!(
"{}{:>width$}{ic}",
MARGIN,
@@ -430,14 +431,15 @@ 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;
let column_width = if column_width > 0 { column_width } else { 1 };
let splits = if has_containers {
vec![
Constraint::Min(2),
Constraint::Min(column_width.try_into().unwrap_or_default()),
Constraint::Min(info_width.try_into().unwrap_or_default()),
]
@@ -450,28 +452,44 @@ pub fn heading_bar<B: Backend>(
.constraints(splits.as_ref())
.split(area);
if has_containers {
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
// Draw loading icon, or not, and a prefix with a single space
let loading_icon = format!("{:>2}", loading_icon);
let loading_paragraph = Paragraph::new(loading_icon)
.block(block(Color::White))
.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)
.constraints(container_splits.as_ref())
.split(split_bar[0]);
.split(split_bar[1]);
// draw the actual header blocks
for (index, (paragraph, header, _)) in header_data.into_iter().enumerate() {
let rect = headers_section[index];
gui_state.lock().update_map(Region::Header(header), rect);
gui_state
.lock()
.update_heading_map(Region::Header(header), rect);
f.render_widget(paragraph, rect);
}
}
let paragraph = Paragraph::new(info_text)
.block(block())
// show/hide help
let color = if help_visible {
Color::Black
} else {
Color::White
};
let help_paragraph = Paragraph::new(info_text)
.block(block(color))
.alignment(Alignment::Right);
// If no containers, don't display the headers, could maybe do this first?
let index = if has_containers { 1 } else { 0 };
f.render_widget(paragraph, split_bar[index]);
let help_index = if has_containers { 2 } else { 0 };
// render help info
f.render_widget(help_paragraph, split_bar[help_index]);
}
/// From a given &str, return the maximum number of chars on a single line
@@ -487,7 +505,7 @@ fn max_line_width(text: &str) -> usize {
}
/// Draw the help box in the centre of the screen
/// TODO this is message, should make every line it's own renderable span
/// TODO should make every line it's own renderable span
pub fn help_box<B: Backend>(f: &mut Frame<'_, B>) {
let title = format!(" {} ", VERSION);
+30 -4
View File
@@ -59,6 +59,7 @@ pub enum BoxLocation {
}
impl BoxLocation {
/// Screen is divided into 3x3 sections
pub const fn get_indexes(self) -> (usize, usize) {
match self {
Self::TopLeft => (0, 0),
@@ -172,6 +173,16 @@ impl fmt::Display for Loading {
}
}
/// 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
#[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 {
@@ -179,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 {
@@ -191,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(),
}
}
@@ -226,7 +237,7 @@ impl GuiState {
}
/// Insert, or updates header area panel into heading_map
pub fn update_map(&mut self, region: Region, area: Rect) {
pub fn update_heading_map(&mut self, region: Region, area: Rect) {
match region {
Region::Header(header) => self
.heading_map
@@ -241,6 +252,21 @@ impl GuiState {
};
}
/// Check if the current gui_status contains any of the given status'
pub fn status_contains(&self, status: &[Status]) -> bool {
status.iter().any(|i| self.status.contains(i))
}
/// Remove a gui_status into the current gui_status hashset
pub fn status_del(&mut self, status: Status) {
self.status.remove(&status);
}
/// Insert a gui_status into the current gui_status hashset
pub fn status_push(&mut self, status: Status) {
self.status.insert(status);
}
/// Change to next selectable panel
pub fn next_panel(&mut self) {
self.selected_panel = self.selected_panel.next();
@@ -260,7 +286,7 @@ impl GuiState {
/// If is_loading has any entries, return the current loading_icon, else an emtpy string
pub fn get_loading(&mut self) -> String {
if self.is_loading.is_empty() {
String::new()
String::from(" ")
} else {
self.loading_icon.to_string()
}
+5 -7
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,6 +56,7 @@ pub async fn create_ui(
update_duration,
)
.await;
terminal.clear()?;
disable_raw_mode()?;
execute!(
@@ -82,10 +83,8 @@ async fn run_app<B: Backend + Send>(
update_duration: Duration,
) -> Result<(), AppError> {
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 {
@@ -156,7 +155,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();
@@ -240,7 +239,6 @@ fn ui<B: Backend>(
}
if let Some(error) = has_error {
app_data.lock().show_error = true;
draw_blocks::error(f, error, None);
}
}