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