diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index dec7caa..4893600 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,9 +1,14 @@
-# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/rust/.devcontainer/base.Dockerfile
-
-# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye
-ARG VARIANT="buster"
-FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT}
+ARG VARIANT="bullseye"
+FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
RUN printf "alias cls='clear'\nalias ll='ls -l --human-readable --color=auto --group-directories-first --classify --time-style=long-iso -all'" >> /etc/bash.bashrc
-# RUN apt-get update && apt-get -y install upx-ucl
+ENV PATH="/home/vscode/.cargo/bin:${PATH}"
+
+RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
+ && apt-get -y install --no-install-recommends build-essential pkg-config libssl-dev
+
+USER vscode
+RUN curl --proto '=https' --tlsv1.2 -sSf curl https://sh.rustup.rs | sh -s -- -y
+# RUN rustup target add x86_64-unknown-linux-musl
+
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 41a7ebe..d4f7835 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -34,7 +34,7 @@
"extensions": [
"vadimcn.vscode-lldb",
"mutantdino.resourcemonitor",
- "matklad.rust-analyzer",
+ "rust-lang.rust-analyzer",
"tamasfe.even-better-toml",
"serayuzgur.crates",
"christian-kohler.path-intellisense",
diff --git a/.github/release-body.md b/.github/release-body.md
index 31b0f6c..895c033 100644
--- a/.github/release-body.md
+++ b/.github/release-body.md
@@ -1,14 +1,22 @@
-### 2022-08-04
+### 2022-09-07
### Chores
-+ dependencies updated, [d9801cdf372521fe5624a8d68fac83ed39ef81f4]
-+ linting: nursery, pedantic, unused_unwraps, [1bd61d4ce8b369d6d078201add3eea0f59fe0dea], [1263662bd9412afacddbc10721bf216ae3a843f1], [ca3315a69f593ad705eb637f227f195edd7781b2]
++ dependencies updated, [a3168daa3f769a6747dfbe61103073a7e80a1485], [78e59160bb6a978ee80e3a99eb72f051fb64e737]
### Features
-+ build all production targets on release, [44f8140eaec330abe5a94f3ddae9e8b223688aa8]
++ containerize self, github action to build and push to [Docker Hub](https://hub.docker.com/r/mrjackwills/oxker), [07f972022a69f22bac57925e6ad84234381f7890]
++ gui_state is_loading use a HashSet to enable multiple things be loading at the same time, [66583e1b037b7e2f3e47948d70d8a4c6f6a2f2d5]
++ github action publish to crates.io, [90b2e3f6db0d5f63840cd80888a30da6ecc22f20]
++ derive Eq where appropriate, [d7c2601f959bc12a64cd25cef59c837e1e8c2b2a]
++ ignore containers 'oxker' containers, [1be9f52ad4a68f93142784e9df630c59cdec0a79]
++ update container info if container is either running OR restarting, [5f12362db7cb61ca68f75b99ecfc9725380d87d2]
### Fixes
-+ toml keywords, [dd2d82d114537e09dbeb12f360157f0e68e7846e]
++ devcontainer updated, [3bde4f5629539cab3dbb57556663ab81685f9d7a]
++ Use Binate enum to enable two cycles of cpu/mem update to be executed (for each container) at the same time, refactor hashmap spawn insertions, [7ec58e79a1316ad1f7e50a2781dea0fe8422c588]
+
+### Refactors
++ improved way to remove leading '/' of container name, [832e9782d7765872cbb84df6b3703fc08cb353c9]
see CHANGELOG.md for more details
diff --git a/.github/screenshot_01.jpg b/.github/screenshot_01.jpg
deleted file mode 100644
index 7d11678..0000000
Binary files a/.github/screenshot_01.jpg and /dev/null differ
diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png
new file mode 100644
index 0000000..cad8a02
Binary files /dev/null and b/.github/screenshot_01.png differ
diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml
index 0d629ae..988be16 100644
--- a/.github/workflows/create_release_and_build.yml
+++ b/.github/workflows/create_release_and_build.yml
@@ -7,16 +7,8 @@ jobs:
deploy:
runs-on: ubuntu-18.04
steps:
- - uses: actions/checkout@master
-
- # cache some rust data?
- - uses: actions/cache@v2
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- target
- key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ - name: Checkout
+ uses: actions/checkout@v3
# Build for linux x86_64
- name: build release linux_x86_64
@@ -72,6 +64,30 @@ jobs:
- name: compress windows_x86_64 binary
run: zip -j ./oxker_windows_x86_64.zip target/x86_64-pc-windows-gnu/release/oxker.exe
+ # Build images for Dockerhub
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - uses: docker/setup-buildx-action@v2
+ id: buildx
+ with:
+ install: true
+ - name: Build for Docker Hub
+ run: |
+ docker build --platform linux/arm/v6,linux/arm64,linux/amd64 \
+ -t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:latest \
+ --push \
+ -f containerised/Dockerfile .
+
+ # Publish to crates.io
+ - name: publish to crates.io
+ uses: katyo/publish-crates@v1
+ with:
+ registry-token: ${{ secrets.CRATES_IO_TOKEN }}
+
- name: Release
uses: softprops/action-gh-release@v1
with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 602b578..9dfa2ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,30 @@
+# v0.1.4
+### 2022-09-07
+
+### Chores
++ 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),
++ gui_state is_loading use a HashSet to enable multiple things be loading at the same time, [66583e1b](https://github.com/mrjackwills/oxker/commit/66583e1b037b7e2f3e47948d70d8a4c6f6a2f2d5),
++ github action publish to crates.io, [90b2e3f6](https://github.com/mrjackwills/oxker/commit/90b2e3f6db0d5f63840cd80888a30da6ecc22f20),
++ derive Eq where appropriate, [d7c2601f](https://github.com/mrjackwills/oxker/commit/d7c2601f959bc12a64cd25cef59c837e1e8c2b2a),
++ ignore containers 'oxker' containers, [1be9f52a](https://github.com/mrjackwills/oxker/commit/1be9f52ad4a68f93142784e9df630c59cdec0a79),
++ update container info if container is either running OR restarting, [5f12362d](https://github.com/mrjackwills/oxker/commit/5f12362db7cb61ca68f75b99ecfc9725380d87d2),
+
+### Fixes
++ devcontainer updated, [3bde4f56](https://github.com/mrjackwills/oxker/commit/3bde4f5629539cab3dbb57556663ab81685f9d7a),
++ Use Binate enum to enable two cycles of cpu/mem update to be executed (for each container) at the same time, refactor hashmap spawn insertions, [7ec58e79](https://github.com/mrjackwills/oxker/commit/7ec58e79a1316ad1f7e50a2781dea0fe8422c588),
+
+### Refactors
++ improved way to remove leading '/' of container name, [832e9782](https://github.com/mrjackwills/oxker/commit/832e9782d7765872cbb84df6b3703fc08cb353c9),
+
# v0.1.3
### 2022-08-04
### Chores
+ dependencies updated, [d9801cdf](https://github.com/mrjackwills/oxker/commit/d9801cdf372521fe5624a8d68fac83ed39ef81f4),
-+ linting: nursery, pedantic, unused_unwraps, [1bd61d4c](https://github.com/mrjackwills/oxker/commit/1bd61d4ce8b369d6d078201add3eea0f59fe0dea),, [1263662b](https://github.com/mrjackwills/oxker/commit/1263662bd9412afacddbc10721bf216ae3a843f1),, [ca3315a6](https://github.com/mrjackwills/oxker/commit/ca3315a69f593ad705eb637f227f195edd7781b2),
++ linting: nursery, pedantic, unused_unwraps, [1bd61d4c](https://github.com/mrjackwills/oxker/commit/1bd61d4ce8b369d6d078201add3eea0f59fe0dea), [1263662b](https://github.com/mrjackwills/oxker/commit/1263662bd9412afacddbc10721bf216ae3a843f1), [ca3315a6](https://github.com/mrjackwills/oxker/commit/ca3315a69f593ad705eb637f227f195edd7781b2),
### Features
+ build all production targets on release, [44f8140e](https://github.com/mrjackwills/oxker/commit/44f8140eaec330abe5a94f3ddae9e8b223688aa8),
diff --git a/Cargo.toml b/Cargo.toml
index 3d7654b..12c456f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "oxker"
-version = "0.1.3"
+version = "0.1.4"
edition = "2021"
authors = ["Jack Wills "]
description = "a simple tui to view & control docker containers"
@@ -8,21 +8,22 @@ repository = "https://github.com/mrjackwills/oxker"
homepage = "https://github.com/mrjackwills/oxker"
license = "MIT"
readme = "README.md"
-keywords = ["docker", "tui", "tui-rs", "tokio", "terminal", "podman", "container"]
+keywords = ["docker", "tui", "tokio", "terminal", "podman"]
categories = ["command-line-utilities"]
[dependencies]
anyhow = "1.0"
bollard = "0.13"
-cansi = "2.1"
+cansi = "2.2"
clap={version="3.2", features = ["derive", "unicode"] }
-crossterm = "0.24"
+crossterm = "0.25"
futures-util = "0.3"
parking_lot = {version= "0.12"}
-tokio = {version = "1.20", features=["full"]}
+tokio = {version = "1.21", features=["full"]}
tracing = "0.1"
tracing-subscriber = "0.3"
-tui = "0.18"
+tui = "0.19"
+uuid = {version = "1.1", features = ["v4", "fast-rng"]}
[dev-dependencies]
diff --git a/README.md b/README.md
index 5ec8231..a6a9ac1 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,37 @@
-
+
-
oxker
+ oxker
- A simple tui to view & control docker containers
+ A simple tui to view & control docker containers
- Built in Rust, making heavy use of tui-rs & Bollard
+ Built in Rust, making heavy use of tui-rs & Bollard
-
-
+
+
+## Run via Docker
+
+Published on Docker Hub, with images built for `linux/amd64`, `linux/arm64v8`, and `linux/armv6`
+
+`docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro mrjackwills/oxker:latest`
+
## Download & install
-Now published on crates.io, so if you have cargo installed, simply run
-
-``` cargo install oxker```
+Published on crates.io, so if you have cargo installed, simply run
+```cargo install oxker```
else see the pre-built binaries
@@ -107,11 +112,8 @@ using docker-compose.yml;
or individually
-
```docker run --name redis -d redis:alpine3.16```
```docker run --name postgres -e POSTGRES_PASSWORD=never_use_this_password_in_production -d postgres:alpine3.16```
-```docker run -d --hostname my-rabbit --name rabbitmq rabbitmq:3```
-
-
+```docker run -d --hostname my-rabbit --name rabbitmq rabbitmq:3```
\ No newline at end of file
diff --git a/containerised/DOCKERHUB_README.md b/containerised/DOCKERHUB_README.md
new file mode 100644
index 0000000..91da76d
--- /dev/null
+++ b/containerised/DOCKERHUB_README.md
@@ -0,0 +1,26 @@
+
+
+
+
+
+
oxker
+
+ A simple tui to view & control docker containers
+
+
+
+
+
+
+
+
+
+## Run
+
+Images built for `linux/amd64`, `linux/arm64v8`, and `linux/armv6`
+
+`docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro mrjackwills/oxker:latest`
+
+## Help
+
+visit the Github repo
\ No newline at end of file
diff --git a/containerised/Dockerfile b/containerised/Dockerfile
new file mode 100644
index 0000000..18e9953
--- /dev/null
+++ b/containerised/Dockerfile
@@ -0,0 +1,59 @@
+#############
+## Builder ##
+#############
+
+FROM --platform=linux/amd64 rust:slim as builder
+
+ARG TARGETARCH
+
+# These are build platform depandant, but will be ignored if not needed
+ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="aarch64-linux-gnu-gcc"
+ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-lgcc"
+ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER="arm-linux-gnueabihf-ld"
+
+COPY ./containerised/platform.sh .
+
+RUN chmod +x ./platform.sh && ./platform.sh
+
+RUN apt-get update && apt-get install $(cat /.compiler) -y
+
+WORKDIR /usr/src
+
+# Create blank project
+RUN cargo new oxker
+
+# We want dependencies cached, so copy those first
+COPY Cargo.* /usr/src/oxker/
+
+# Set the working directory
+WORKDIR /usr/src/oxker
+
+# Install target platform (Cross-Compilation)
+RUN rustup target add $(cat /.platform)
+
+# This is a dummy build to get the dependencies cached
+RUN cargo build --target $(cat /.platform) --release
+
+# Now copy in the rest of the sources
+COPY src /usr/src/oxker/src/
+
+## Touch main.rs to prevent cached release build
+RUN touch /usr/src/oxker/src/main.rs
+
+# This is the actual application build
+RUN cargo build --release --target $(cat /.platform)
+
+RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
+
+#############
+## Runtime ##
+#############
+
+FROM alpine:latest AS runtime
+
+# Copy application binary from builder image
+COPY --from=builder /oxker /usr/local/bin
+COPY ./containerised/start_oxker.sh ./
+
+# Run the application
+ENTRYPOINT [ "./start_oxker.sh"]
diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev
new file mode 100644
index 0000000..f6f5148
--- /dev/null
+++ b/containerised/Dockerfile_dev
@@ -0,0 +1,20 @@
+#############
+## Runtime ##
+#############
+
+FROM alpine:latest AS runtime
+
+# Copy application binary from builder image
+COPY ./target/x86_64-unknown-linux-musl/release/oxker /usr/local/bin
+COPY ./containerised/start_oxker.sh ./
+
+## Run the application
+ENTRYPOINT [ "./start_oxker.sh"]
+
+## One liner to build musl program, build docker image, then execute the image
+# cargo build --release --target x86_64-unknown-linux-musl && docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
+
+
+## Buildx command to build musl version for all three platforms, should probably be executed in create_release
+# docker buildx create --use
+# docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile .
\ No newline at end of file
diff --git a/containerised/platform.sh b/containerised/platform.sh
new file mode 100644
index 0000000..393769a
--- /dev/null
+++ b/containerised/platform.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+# Used in Docker build to set platform dependent variables
+
+case $TARGETARCH in
+
+ "amd64")
+ echo "x86_64-unknown-linux-musl" > /.platform
+ echo "" > /.compiler
+ ;;
+ "arm64")
+ echo "aarch64-unknown-linux-musl" > /.platform
+ echo "gcc-aarch64-linux-gnu" > /.compiler
+ ;;
+ "arm")
+ echo "arm-unknown-linux-musleabihf" > /.platform
+ echo "gcc-arm-linux-gnueabihf" > /.compiler
+ ;;
+esac
\ No newline at end of file
diff --git a/containerised/start_oxker.sh b/containerised/start_oxker.sh
new file mode 100755
index 0000000..7ac5fa3
--- /dev/null
+++ b/containerised/start_oxker.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -e
+
+# Without this sleep, the docker image will instantly close
+# No idea why this is solving my issue, or even where the issue is originally coming from
+sleep .1
+
+exec /usr/local/bin/oxker "$@"
\ No newline at end of file
diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs
index 183186c..6da9756 100644
--- a/src/app_data/container_state.rs
+++ b/src/app_data/container_state.rs
@@ -78,7 +78,7 @@ impl StatefulList {
}
/// States of the container
-#[derive(Clone, Debug, PartialEq, PartialOrd)]
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd)]
pub enum State {
Dead,
Exited,
@@ -113,6 +113,20 @@ impl State {
}
}
+impl From for State {
+ fn from(input: String) -> Self {
+ match input.as_ref() {
+ "dead" => Self::Dead,
+ "exited" => Self::Exited,
+ "paused" => Self::Paused,
+ "removing" => Self::Removing,
+ "restarting" => Self::Restarting,
+ "running" => Self::Running,
+ _ => Self::Unknown,
+ }
+ }
+}
+
impl From<&str> for State {
fn from(input: &str) -> Self {
match input {
diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs
index 39bc5ee..a96b0e8 100644
--- a/src/app_data/mod.rs
+++ b/src/app_data/mod.rs
@@ -20,7 +20,7 @@ pub struct AppData {
sorted_by: Option<(Header, SortedOrder)>,
}
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SortedOrder {
Asc,
Desc,
@@ -410,7 +410,7 @@ impl AppData {
}
/// Update, or insert, containers
- pub fn update_containers(&mut self, containers: &[ContainerSummary]) {
+ pub fn update_containers(&mut self, containers: &mut [ContainerSummary]) {
let all_ids = self.get_all_ids();
if !containers.is_empty() && self.containers.state.selected().is_none() {
@@ -435,29 +435,33 @@ impl AppData {
}
}
- for i in containers.iter() {
+ for i in containers.iter_mut() {
if let Some(id) = i.id.as_ref() {
- let mut name = i
- .names
- .as_ref()
- .unwrap_or(&vec!["".to_owned()])
- .get(0)
- .unwrap_or(&String::from(""))
- .clone();
- if let Some(c) = name.chars().next() {
- if c == '/' {
- name.remove(0);
- }
- }
+ // maybe if no name then continue?
+ let name = i.names.as_mut().map_or("".to_owned(), |n| {
+ n.get_mut(0).map_or("".to_owned(), |f| {
+ if f.starts_with('/') {
+ f.remove(0);
+ }
+ f.clone()
+ })
+ });
- let state = State::from(i.state.as_ref().unwrap_or(&"dead".to_owned()).trim());
+ let state = State::from(
+ i.state
+ .as_ref()
+ .map_or("dead".to_owned(), |f| f.trim().to_owned()),
+ );
let status = i
.status
.as_ref()
- .unwrap_or(&"".to_owned())
- .trim()
- .to_owned();
- let image = i.image.as_ref().unwrap_or(&"".to_owned()).trim().to_owned();
+ .map_or("".to_owned(), |f| f.trim().to_owned());
+
+ let image = i
+ .image
+ .as_ref()
+ .map_or("".to_owned(), std::clone::Clone::clone);
+
if let Some(current_container) = self.get_container_by_id(id) {
if current_container.name != name {
current_container.name = name;
@@ -478,9 +482,13 @@ impl AppData {
current_container.state = state;
};
if current_container.image != image {
+ // limit image name to 64 chars?
+ // current_container.image = image.chars().into_iter().take(64).collect();
current_container.image = image;
};
} else {
+ // limit image name to 64 chars?
+ // let mut container = ContainerItem::new(id.clone(), status, image.chars().into_iter().take(64).collect(), state, name);
let mut container = ContainerItem::new(id.clone(), status, image, state, name);
container.logs.end();
self.containers.items.push(container);
@@ -511,7 +519,7 @@ impl AppData {
}
if container.logs.state.selected().is_none()
- || container.logs.state.selected().unwrap_or_default() + 1 == current_len
+ || container.logs.state.selected().map_or(1, |f| f + 1) == current_len
{
container.logs.end();
}
diff --git a/src/app_error.rs b/src/app_error.rs
index 1f2d889..b1bcd83 100644
--- a/src/app_error.rs
+++ b/src/app_error.rs
@@ -16,17 +16,16 @@ pub enum AppError {
/// Convert errors into strings to display
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let disp = match self {
- Self::DockerConnect => "Unable to access docker daemon".to_owned(),
- Self::DockerInterval => "Docker update interval needs to be greater than 0".to_owned(),
- Self::InputPoll => "Unable to poll user input".to_owned(),
- Self::Terminal => "Unable to draw to terminal".to_owned(),
- Self::DockerCommand(s) => format!("Unable to {} container", s),
+ match self {
+ Self::DockerConnect => write!(f, "Unable to access docker daemon"),
+ Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
+ Self::InputPoll => write!(f, "Unable to poll user input"),
+ Self::Terminal => write!(f, "Unable to draw to terminal"),
+ Self::DockerCommand(s) => write!(f, "Unable to {} container", s),
Self::MouseCapture(x) => {
let reason = if *x { "en" } else { "dis" };
- format!("Unable to {}able mouse capture", reason)
+ write!(f, "Unbale to {}able mouse capture", reason)
}
- };
- write!(f, "{}", disp)
+ }
}
}
diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs
index 19019ba..feb6314 100644
--- a/src/docker_data/mod.rs
+++ b/src/docker_data/mod.rs
@@ -1,18 +1,19 @@
use bollard::{
container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
+ service::ContainerSummary,
Docker,
};
use futures_util::StreamExt;
use parking_lot::Mutex;
use std::{
collections::HashMap,
- fmt,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
+use uuid::Uuid;
use crate::{
app_data::{AppData, DockerControls},
@@ -25,17 +26,25 @@ pub use message::DockerMessage;
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
enum SpawnId {
- Stats(String),
+ Stats((String, Binate)),
Log(String),
}
-impl fmt::Display for SpawnId {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let disp = match self {
- Self::Stats(id) => format!("stats::{id}"),
- Self::Log(id) => format!("logs::{id}"),
- };
- write!(f, "{}", disp)
+/// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time
+/// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap
+/// Binate value is toggled when all join handles have been spawned off
+#[derive(Debug, Hash, Clone, PartialEq, Eq, Copy)]
+enum Binate {
+ One,
+ Two,
+}
+
+impl Binate {
+ const fn toggle(self) -> Self {
+ match self {
+ Self::One => Self::Two,
+ Self::Two => Self::One,
+ }
}
}
@@ -48,6 +57,7 @@ pub struct DockerData {
receiver: Receiver,
spawns: Arc>>>,
timestamps: bool,
+ binate: Binate,
}
impl DockerData {
@@ -87,6 +97,7 @@ impl DockerData {
app_data: Arc>,
is_running: bool,
spawns: Arc>>>,
+ spawn_id: SpawnId,
) {
let mut stream = docker
.stats(
@@ -109,14 +120,15 @@ impl DockerData {
let cpu_stats = Self::calculate_usage(&stats);
- let no_bytes = (0, 0);
+ let no_bytes = || (0, 0);
+
let (rx, tx) = if let Some(key) = some_key {
match stats.networks.unwrap_or_default().get(&key) {
- Some(data) => (data.rx_bytes.to_owned(), data.tx_bytes.to_owned()),
- None => no_bytes,
+ Some(data) => (data.rx_bytes, data.tx_bytes),
+ None => no_bytes(),
}
} else {
- no_bytes
+ no_bytes()
};
if is_running {
@@ -133,7 +145,7 @@ impl DockerData {
.lock()
.update_stats(&id, None, None, mem_limit, rx, tx);
}
- spawns.lock().remove(&SpawnId::Stats(id.clone()));
+ spawns.lock().remove(&spawn_id);
}
}
@@ -143,26 +155,28 @@ impl DockerData {
let docker = Arc::clone(&self.docker);
let app_data = Arc::clone(&self.app_data);
let spawns = Arc::clone(&self.spawns);
- let is_running = *is_running;
let id = id.clone();
- let key = SpawnId::Stats(id.clone());
- let spawn_contains_id = spawns.lock().contains_key(&key);
- let s = tokio::spawn(Self::update_container_stat(
- docker,
- id.clone(),
- app_data,
- is_running,
- spawns,
- ));
- if !spawn_contains_id {
- self.spawns.lock().insert(key, s);
- }
+ let key = SpawnId::Stats((id.clone(), self.binate));
+
+ let spawn_key = key.clone();
+ self.spawns.lock().entry(key).or_insert_with(|| {
+ tokio::spawn(Self::update_container_stat(
+ docker,
+ id.clone(),
+ app_data,
+ *is_running,
+ spawns,
+ spawn_key,
+ ))
+ });
}
+ self.binate = self.binate.toggle();
}
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
/// Just make sure that items sent are guaranteed to have an id
+ /// Will ignore any container that contains `oxker` as an entry point
pub async fn update_all_containers(&mut self) -> Vec<(bool, String)> {
let containers = self
.docker
@@ -173,24 +187,33 @@ impl DockerData {
.await
.unwrap_or_default();
- let mut output = vec![];
- // iter over containers, to only send ones which have an id, as use id for identification throughout!
- containers
+ let mut output = containers
.iter()
- .filter(|i| i.id.is_some())
- .for_each(|c| output.push(c.clone()));
+ .filter_map(|f| match f.id {
+ Some(_) => {
+ if f.command.as_ref().map_or(false, |c| c.contains("oxker")) {
+ None
+ } else {
+ Some(f.clone())
+ }
+ }
+ None => None,
+ })
+ .collect::>();
- self.app_data.lock().update_containers(&output);
+ self.app_data.lock().update_containers(&mut output);
let current_sort = self.app_data.lock().get_sorted();
self.app_data.lock().set_sorted(current_sort);
+ // Just get the containers that are currently running, or being restarted, no point updating info on paused or dead containers
output
.iter()
.filter_map(|i| {
i.id.as_ref().map(|id| {
(
- i.state.as_ref().unwrap_or(&String::new()) == "running",
+ i.state == Some("running".to_owned())
+ || i.state == Some("restarting".to_owned()),
id.clone(),
)
})
@@ -241,60 +264,59 @@ impl DockerData {
let app_data = Arc::clone(&self.app_data);
let spawns = Arc::clone(&self.spawns);
let key = SpawnId::Log(id.clone());
- let s = tokio::spawn(Self::update_log(
- docker, id, timestamps, 0, app_data, spawns,
- ));
-
- self.spawns.lock().insert(key, s);
+ self.spawns.lock().insert(
+ key,
+ tokio::spawn(Self::update_log(
+ docker, id, timestamps, 0, app_data, spawns,
+ )),
+ );
}
}
+ /// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed)
async fn update_everything(&mut self) {
let all_ids = self.update_all_containers().await;
let optional_index = self.app_data.lock().get_selected_log_index();
if let Some(index) = optional_index {
+ // this could be neater
let id = self.app_data.lock().containers.items[index].id.clone();
-
let key = SpawnId::Log(id.clone());
- let running = self.spawns.lock().contains_key(&key);
- if !running {
+ self.spawns.lock().entry(key).or_insert_with(|| {
let since = self.app_data.lock().containers.items[index].last_updated as i64;
let docker = Arc::clone(&self.docker);
let timestamps = self.timestamps;
-
let app_data = Arc::clone(&self.app_data);
let spawns = Arc::clone(&self.spawns);
- let s = tokio::spawn(Self::update_log(
+ tokio::spawn(Self::update_log(
docker, id, timestamps, since, app_data, spawns,
- ));
- self.spawns.lock().insert(key, s);
- }
+ ))
+ });
};
-
self.update_all_container_stats(&all_ids).await;
}
/// Animate the loading icon
- async fn loading_spin(&mut self) -> JoinHandle<()> {
+ async fn loading_spin(&mut self, loading_uuid: Uuid) -> JoinHandle<()> {
let gui_state = Arc::clone(&self.gui_state);
tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
- gui_state.lock().next_loading();
+ gui_state.lock().next_loading(loading_uuid);
}
})
}
/// Stop the loading_spin function, and reset gui loading status
- fn stop_loading_spin(&mut self, handle: &JoinHandle<()>) {
+ fn stop_loading_spin(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) {
handle.abort();
- self.gui_state.lock().reset_loading();
+ self.gui_state.lock().remove_loading(loading_uuid);
}
// Initialize docker container data, before any messages are received
async fn initialise_container_data(&mut self) {
- let loading_spin = self.loading_spin().await;
+ let loading_uuid = Uuid::new_v4();
+ let loading_spin = self.loading_spin(loading_uuid).await;
let all_ids = self.update_all_containers().await;
self.update_all_container_stats(&all_ids).await;
@@ -312,7 +334,7 @@ impl DockerData {
self.initialised = self.app_data.lock().initialised(&all_ids);
}
self.app_data.lock().init = true;
- self.stop_loading_spin(&loading_spin);
+ self.stop_loading_spin(&loading_spin, loading_uuid);
}
/// Handle incoming messages, container controls & all container information update
@@ -320,57 +342,58 @@ impl DockerData {
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().await;
- docker.pause_container(&id).await.unwrap_or_else(|_| {
+ let loading_spin = self.loading_spin(loading_uuid).await;
+ if docker.pause_container(&id).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Pause));
- });
- self.stop_loading_spin(&loading_spin);
+ };
+ self.stop_loading_spin(&loading_spin, loading_uuid);
}
DockerMessage::Restart(id) => {
- let loading_spin = self.loading_spin().await;
- docker
- .restart_container(&id, None)
- .await
- .unwrap_or_else(|_| {
- app_data
- .lock()
- .set_error(AppError::DockerCommand(DockerControls::Restart));
- });
- self.stop_loading_spin(&loading_spin);
+ let loading_spin = self.loading_spin(loading_uuid).await;
+ if docker.restart_container(&id, None).await.is_err() {
+ app_data
+ .lock()
+ .set_error(AppError::DockerCommand(DockerControls::Restart));
+ };
+ self.stop_loading_spin(&loading_spin, loading_uuid);
}
DockerMessage::Start(id) => {
- let loading_spin = self.loading_spin().await;
- docker
+ let loading_spin = self.loading_spin(loading_uuid).await;
+ if docker
.start_container(&id, None::>)
.await
- .unwrap_or_else(|_| {
- app_data
- .lock()
- .set_error(AppError::DockerCommand(DockerControls::Start));
- });
- self.stop_loading_spin(&loading_spin);
+ .is_err()
+ {
+ app_data
+ .lock()
+ .set_error(AppError::DockerCommand(DockerControls::Start));
+ };
+ self.stop_loading_spin(&loading_spin, loading_uuid);
}
DockerMessage::Stop(id) => {
- let loading_spin = self.loading_spin().await;
- docker.stop_container(&id, None).await.unwrap_or_else(|_| {
+ let loading_spin = self.loading_spin(loading_uuid).await;
+ if docker.stop_container(&id, None).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Stop));
- });
- self.stop_loading_spin(&loading_spin);
+ };
+ self.stop_loading_spin(&loading_spin, loading_uuid);
}
DockerMessage::Unpause(id) => {
- let loading_spin = self.loading_spin().await;
- docker.unpause_container(&id).await.unwrap_or_else(|_| {
+ let loading_spin = self.loading_spin(loading_uuid).await;
+ if docker.unpause_container(&id).await.is_err() {
app_data
.lock()
.set_error(AppError::DockerCommand(DockerControls::Unpause));
- });
- self.stop_loading_spin(&loading_spin);
+ };
+ // loading sping take uuid to remove
+ // stop_loading_sping(uuid)
+ self.stop_loading_spin(&loading_spin, loading_uuid);
self.update_everything().await;
}
DockerMessage::Update => self.update_everything().await,
@@ -405,6 +428,7 @@ impl DockerData {
spawns: Arc::new(Mutex::new(HashMap::new())),
timestamps: args.timestamp,
is_running,
+ binate: Binate::One,
};
inner.initialise_container_data().await;
diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs
index 97d49ee..731b005 100644
--- a/src/input_handler/mod.rs
+++ b/src/input_handler/mod.rs
@@ -143,8 +143,8 @@ impl InputHandler {
if show_error {
match key_code {
- KeyCode::Char('q') => self.quit().await,
- KeyCode::Char('c') => {
+ KeyCode::Char('q' | 'Q') => self.quit().await,
+ KeyCode::Char('c' | 'C') => {
self.app_data.lock().show_error = false;
self.app_data.lock().remove_error();
}
@@ -152,9 +152,9 @@ impl InputHandler {
}
} else if show_info {
match key_code {
- KeyCode::Char('q') => self.quit().await,
- KeyCode::Char('h') => self.gui_state.lock().show_help = false,
- KeyCode::Char('m') => self.m_button(),
+ KeyCode::Char('q' | 'Q') => self.quit().await,
+ KeyCode::Char('h' | 'H') => self.gui_state.lock().show_help = false,
+ KeyCode::Char('m' | 'M') => self.m_button(),
_ => (),
}
} else {
@@ -169,9 +169,9 @@ impl InputHandler {
KeyCode::Char('7') => self.sort(Header::Image),
KeyCode::Char('8') => self.sort(Header::Rx),
KeyCode::Char('9') => self.sort(Header::Tx),
- KeyCode::Char('q') => self.quit().await,
- KeyCode::Char('h') => self.gui_state.lock().show_help = true,
- KeyCode::Char('m') => self.m_button(),
+ KeyCode::Char('q' | 'Q') => self.quit().await,
+ KeyCode::Char('h' | 'H') => self.gui_state.lock().show_help = true,
+ KeyCode::Char('m' | 'M') => self.m_button(),
KeyCode::Tab => {
// Skip control panel if no containers, could be refactored
let has_containers = self.app_data.lock().get_container_len() == 0;
@@ -216,13 +216,13 @@ impl InputHandler {
SelectablePanel::Commands => locked_data.docker_command_end(),
}
}
- KeyCode::Up | KeyCode::Char('k') => self.previous(),
+ KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
KeyCode::PageUp => {
for _ in 0..=6 {
self.previous();
}
}
- KeyCode::Down | KeyCode::Char('j') => self.next(),
+ KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
KeyCode::PageDown => {
for _ in 0..=6 {
self.next();
diff --git a/src/main.rs b/src/main.rs
index b7fda0e..f4b67b0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -28,6 +28,7 @@ use ui::{create_ui, GuiState};
fn setup_tracing() {
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
+ // TODO write to file?
}
#[tokio::main]
@@ -99,6 +100,7 @@ async fn main() {
.unwrap_or(());
} else {
loop {
+ // TODO this needs to be improved to display something useful
info!("in debug mode");
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
}
diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs
index 149d74a..a95dc88 100644
--- a/src/ui/color_match.rs
+++ b/src/ui/color_match.rs
@@ -6,7 +6,7 @@ pub mod log_sanitizer {
text::{Span, Spans},
};
- /// Attempt to colorize the given string to tui-rs standars
+ /// Attempt to colorize the given string to tui-rs standards
pub fn colorize_logs(input: &str) -> Vec> {
vec![Spans::from(
categorise_text(input)
diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs
index 30d594a..1573a37 100644
--- a/src/ui/draw_blocks.rs
+++ b/src/ui/draw_blocks.rs
@@ -279,8 +279,8 @@ pub fn chart(
.style(Style::default().fg(Color::Cyan))
.graph_type(GraphType::Line)
.data(&mem.0)];
- let cpu_stats = CpuStats::new(cpu.0.last().unwrap_or(&(0.00, 0.00)).1);
- let mem_stats = ByteStats::new(mem.0.last().unwrap_or(&(0.0, 0.0)).1 as u64);
+ let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
+ let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64));
let cpu_chart = make_chart(&cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1);
let mem_chart = make_chart(&mem.2, "memory", mem_dataset, &mem_stats, &mem.1);
diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs
index 5711148..ec17be3 100644
--- a/src/ui/gui_state.rs
+++ b/src/ui/gui_state.rs
@@ -1,5 +1,9 @@
-use std::{collections::HashMap, fmt};
+use std::{
+ collections::{HashMap, HashSet},
+ fmt,
+};
use tui::layout::{Constraint, Rect};
+use uuid::Uuid;
use crate::app_data::Header;
@@ -10,6 +14,30 @@ pub enum SelectablePanel {
Logs,
}
+impl SelectablePanel {
+ pub const fn title(self) -> &'static str {
+ match self {
+ Self::Containers => "Containers",
+ Self::Logs => "Logs",
+ Self::Commands => "",
+ }
+ }
+ pub fn next(self) -> Self {
+ match self {
+ Self::Containers => Self::Commands,
+ Self::Commands => Self::Logs,
+ Self::Logs => Self::Containers,
+ }
+ }
+ pub fn prev(self) -> Self {
+ match self {
+ Self::Containers => Self::Logs,
+ Self::Commands => Self::Containers,
+ Self::Logs => Self::Commands,
+ }
+ }
+}
+
pub enum Region {
Panel(SelectablePanel),
Header(Header),
@@ -93,7 +121,8 @@ impl BoxLocation {
}
}
-#[derive(Debug, Clone)]
+/// State for the loading animation
+#[derive(Debug, Clone, Copy)]
pub enum Loading {
One,
Two,
@@ -108,7 +137,7 @@ pub enum Loading {
}
impl Loading {
- pub const fn next(&self) -> Self {
+ pub const fn next(self) -> Self {
match self {
Self::One => Self::Two,
Self::Two => Self::Three,
@@ -127,57 +156,28 @@ impl Loading {
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 => "⠏",
+ Self::One => '⠋',
+ Self::Two => '⠙',
+ Self::Three => '⠹',
+ Self::Four => '⠸',
+ Self::Five => '⠼',
+ Self::Six => '⠴',
+ Self::Seven => '⠦',
+ Self::Eight => '⠧',
+ Self::Nine => '⠇',
+ Self::Ten => '⠏',
};
write!(f, "{}", disp)
}
}
-impl SelectablePanel {
- pub const fn title(self) -> &'static str {
- match self {
- Self::Containers => "Containers",
- Self::Logs => "Logs",
- Self::Commands => "",
- }
- }
- pub const fn next(self) -> Self {
- match self {
- Self::Containers => Self::Commands,
- Self::Commands => Self::Logs,
- Self::Logs => Self::Containers,
- }
- }
- pub const fn prev(self) -> Self {
- match self {
- Self::Containers => Self::Logs,
- Self::Commands => Self::Containers,
- Self::Logs => Self::Commands,
- }
- }
-}
-
/// Global gui_state, stored in an Arc
#[derive(Debug, Clone)]
pub struct GuiState {
- // Think this should be a BMapTree, so can define order when iterating over potential intersects
- // Is an issue if two panels are in the same space, sush as a smaller panel embedded, yet infront of, a larger panel
- // If a BMapTree think it would mean have to implement ordering for SelectablePanel
panel_map: HashMap,
heading_map: HashMap,
loading_icon: Loading,
- // Should be a vec, each time loading add a new to the vec, and reset remove from vec
- // for for if is_loading just check if vec is empty or not
- is_loading: bool,
+ is_loading: HashSet,
pub selected_panel: SelectablePanel,
pub show_help: bool,
pub info_box_text: Option,
@@ -191,7 +191,7 @@ impl GuiState {
loading_icon: Loading::One,
selected_panel: SelectablePanel::Containers,
show_help: false,
- is_loading: false,
+ is_loading: HashSet::new(),
info_box_text: None,
}
}
@@ -251,14 +251,14 @@ impl GuiState {
}
/// Advance loading animation
- pub fn next_loading(&mut self) {
+ pub fn next_loading(&mut self, uuid: Uuid) {
self.loading_icon = self.loading_icon.next();
- self.is_loading = true;
+ self.is_loading.insert(uuid);
}
/// if is_loading, return loading animation frame, else single space
pub fn get_loading(&mut self) -> String {
- if self.is_loading {
+ if !self.is_loading.is_empty() {
self.loading_icon.to_string()
} else {
String::from(" ")
@@ -266,8 +266,8 @@ impl GuiState {
}
/// set is_loading to false, but keep animation frame at same state
- pub fn reset_loading(&mut self) {
- self.is_loading = false;
+ pub fn remove_loading(&mut self, uuid: Uuid) {
+ self.is_loading.remove(&uuid);
}
/// Set info box content
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 9a529fd..3f5fc07 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -57,13 +57,13 @@ pub async fn create_ui(
)
.await;
- disable_raw_mode().unwrap_or(());
+ disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
- terminal.show_cursor().unwrap_or(());
+ terminal.show_cursor()?;
if let Err(err) = res {
println!("{}", err);
@@ -85,23 +85,21 @@ async fn run_app(
// Check for docker connect errors before attempting to draw the gui
let e = app_data.lock().get_error();
- if let Some(error) = e {
- if let AppError::DockerConnect = error {
- let mut seconds = 5;
- loop {
- if seconds < 1 {
- is_running.store(false, Ordering::SeqCst);
- break;
- }
- if terminal
- .draw(|f| draw_blocks::error(f, &AppError::DockerConnect, Some(seconds)))
- .is_err()
- {
- return Err(AppError::Terminal);
- }
- tokio::time::sleep(std::time::Duration::from_secs(1)).await;
- seconds -= 1;
+ if let Some(AppError::DockerConnect) = e {
+ let mut seconds = 5;
+ loop {
+ if seconds < 1 {
+ is_running.store(false, Ordering::SeqCst);
+ break;
}
+ if terminal
+ .draw(|f| draw_blocks::error(f, &AppError::DockerConnect, Some(seconds)))
+ .is_err()
+ {
+ return Err(AppError::Terminal);
+ }
+ tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+ seconds -= 1;
}
} else {
let mut now = Instant::now();
@@ -109,7 +107,7 @@ async fn run_app(
if terminal.draw(|f| ui(f, &app_data, &gui_state)).is_err() {
return Err(AppError::Terminal);
}
- if crossterm::event::poll(input_poll_rate).unwrap_or_default() {
+ if crossterm::event::poll(input_poll_rate).unwrap_or(false) {
if let Ok(event) = event::read() {
if let Event::Key(key) = event {
sender