chore: merge release-v0.7.0 into main
This commit is contained in:
@@ -25,11 +25,11 @@
|
|||||||
"extensions": [
|
"extensions": [
|
||||||
"bmuskalla.vscode-tldr",
|
"bmuskalla.vscode-tldr",
|
||||||
"christian-kohler.path-intellisense",
|
"christian-kohler.path-intellisense",
|
||||||
|
"fill-labs.dependi",
|
||||||
"foxundermoon.shell-format",
|
"foxundermoon.shell-format",
|
||||||
"mutantdino.resourcemonitor",
|
"mutantdino.resourcemonitor",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"rust-lang.rust-analyzer",
|
"rust-lang.rust-analyzer",
|
||||||
"serayuzgur.crates",
|
|
||||||
"tamasfe.even-better-toml",
|
"tamasfe.even-better-toml",
|
||||||
"timonwong.shellcheck",
|
"timonwong.shellcheck",
|
||||||
"vadimcn.vscode-lldb"
|
"vadimcn.vscode-lldb"
|
||||||
|
|||||||
+21
-4
@@ -1,12 +1,29 @@
|
|||||||
### 2024-05-25
|
### 2024-08-01
|
||||||
|
|
||||||
### Chores
|
### Chores
|
||||||
+ Dependencies updated, [51fdd26be5b3166bcff5c26ece6d6ec0d893381e], [c1be658b8cc4786a9a7f2e0a88568019b3995c14]
|
+ .devcontainer extensions updated, [0288cbc8146cde1dd40ceaec9550198b635bb8f5]
|
||||||
|
+ dependencies updated, [1df4f78dc41013c33d901925933b1ccb29ad4bc8], [5ae253b8734ba0495e4e8149b17d5228b3d86f8d], [7a517db9f7c14c35e56ff70cf76ffb608fd30e17], [9c291cd9c81b6d9a02085878588ed3b845fd0046], [0e90f4eb55ac5fb5d45e7d212c3686027dd3913e], [fe71cbfb00f166b7c02a6e28e64650ed1b47d15d]
|
||||||
|
+ docker-compose alpine version bump, [51ceab3ebdb09356cd401d2f268840239255126f]
|
||||||
|
+ Rust 1.80 linting, [93e1279b1fc77019442a385e2e36be2fe438e828]
|
||||||
|
+ create_release v0.5.6, [f408acfe9a9f5a976735b8a8a51500fd7b865daf]
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
+ exec mode "not available on Windows", in both README.md and help panel, [df449a85376bbeec87215952d6a9196721f7132e]
|
+ screenshot updated, [6975ebe70f7058229c232e4a56b090f55247d2a2]
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ left align all text, [e0d421c4918a17c9e0e21fd214edb99d71281c9d]
|
||||||
|
+ place image name in logs panel title, [12f24357a68abe871f44d871d95b6e2ef062181e]
|
||||||
|
+ distinguish between unhealthy & healthy running containers, closes #43, [de8768181631c6d961ce0e4dacb50c2ed02abc36]
|
||||||
|
+ filter containers, use `F1` or `/` to enter filter mode, closes #37, thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e], [[7ee1f06f804683e3395953a02138d4e9da115ea9]]
|
||||||
|
+ place image name in logs panel title, [ef19b9cf89a881d0a7ac818885317ce2bd683dfc]
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
+ closes #36 Double key strokes on Windows, [9b7d575a76398cbe19e17f6494baf802dbb512b9]
|
+ log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc8113869b81be9d697ca77418c919e4bf]
|
||||||
|
+ Dockerfile command use uppercase, [068e4025a5d6049a9a6951a0480a6bdef7379f88]
|
||||||
|
+ heading section help margin, [0e927aae178c1d8f60561b93607a26d45a1d9331]
|
||||||
|
+ install.sh use curl, [197a031b8cf356f49f08e04472d0d1c489699415]
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
+ fix layout tests with new left alignment, [dfced564278eafdbb8a5b95badbae3a7c4bf87b3]
|
||||||
|
|
||||||
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 41 KiB |
@@ -1,3 +1,32 @@
|
|||||||
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.7.0'>v0.7.0</a>
|
||||||
|
### 2024-08-01
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
+ .devcontainer extensions updated, [0288cbc8](https://github.com/mrjackwills/oxker/commit/0288cbc8146cde1dd40ceaec9550198b635bb8f5)
|
||||||
|
+ dependencies updated, [1df4f78d](https://github.com/mrjackwills/oxker/commit/1df4f78dc41013c33d901925933b1ccb29ad4bc8), [5ae253b8](https://github.com/mrjackwills/oxker/commit/5ae253b8734ba0495e4e8149b17d5228b3d86f8d), [7a517db9](https://github.com/mrjackwills/oxker/commit/7a517db9f7c14c35e56ff70cf76ffb608fd30e17), [9c291cd9](https://github.com/mrjackwills/oxker/commit/9c291cd9c81b6d9a02085878588ed3b845fd0046), [0e90f4eb](https://github.com/mrjackwills/oxker/commit/0e90f4eb55ac5fb5d45e7d212c3686027dd3913e), [fe71cbfb](https://github.com/mrjackwills/oxker/commit/fe71cbfb00f166b7c02a6e28e64650ed1b47d15d)
|
||||||
|
+ docker-compose alpine version bump, [51ceab3e](https://github.com/mrjackwills/oxker/commit/51ceab3ebdb09356cd401d2f268840239255126f)
|
||||||
|
+ Rust 1.80 linting, [93e1279b](https://github.com/mrjackwills/oxker/commit/93e1279b1fc77019442a385e2e36be2fe438e828)
|
||||||
|
+ create_release v0.5.6, [f408acfe](https://github.com/mrjackwills/oxker/commit/f408acfe9a9f5a976735b8a8a51500fd7b865daf)
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
+ screenshot updated, [6975ebe7](https://github.com/mrjackwills/oxker/commit/6975ebe70f7058229c232e4a56b090f55247d2a2)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ left align all text, [e0d421c4](https://github.com/mrjackwills/oxker/commit/e0d421c4918a17c9e0e21fd214edb99d71281c9d)
|
||||||
|
+ place image name in logs panel title, [12f24357](https://github.com/mrjackwills/oxker/commit/12f24357a68abe871f44d871d95b6e2ef062181e)
|
||||||
|
+ distinguish between unhealthy & healthy running containers, closes [#43](https://github.com/mrjackwills/oxker/issues/43), [de876818](https://github.com/mrjackwills/oxker/commit/de8768181631c6d961ce0e4dacb50c2ed02abc36)
|
||||||
|
+ filter containers, use `F1` or `/` to enter filter mode, closes [#37](https://github.com/mrjackwills/oxker/issues/37), thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0db](https://github.com/mrjackwills/oxker/commit/d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e), [[7ee1f06f804683e3395953a02138d4e9da115ea9]]
|
||||||
|
+ place image name in logs panel title, [ef19b9cf](https://github.com/mrjackwills/oxker/commit/ef19b9cf89a881d0a7ac818885317ce2bd683dfc)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
+ log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc](https://github.com/mrjackwills/oxker/commit/0dc98dfc8113869b81be9d697ca77418c919e4bf)
|
||||||
|
+ Dockerfile command use uppercase, [068e4025](https://github.com/mrjackwills/oxker/commit/068e4025a5d6049a9a6951a0480a6bdef7379f88)
|
||||||
|
+ heading section help margin, [0e927aae](https://github.com/mrjackwills/oxker/commit/0e927aae178c1d8f60561b93607a26d45a1d9331)
|
||||||
|
+ install.sh use curl, [197a031b](https://github.com/mrjackwills/oxker/commit/197a031b8cf356f49f08e04472d0d1c489699415)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
+ fix layout tests with new left alignment, [dfced564](https://github.com/mrjackwills/oxker/commit/dfced564278eafdbb8a5b95badbae3a7c4bf87b3)
|
||||||
|
|
||||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.4'>v0.6.4</a>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.4'>v0.6.4</a>
|
||||||
### 2024-05-25
|
### 2024-05-25
|
||||||
|
|
||||||
|
|||||||
Generated
+266
-184
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.6.4"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Jack Wills <email@mrjackwills.com>"]
|
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||||
description = "A simple tui to view & control docker containers"
|
description = "A simple tui to view & control docker containers"
|
||||||
@@ -27,19 +27,19 @@ similar_names = "allow"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bollard = "0.16"
|
bollard = "0.17"
|
||||||
cansi = "2.2"
|
cansi = "2.2"
|
||||||
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
||||||
crossterm = "0.27"
|
crossterm = "0.28"
|
||||||
directories = "5.0"
|
directories = "5.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
parking_lot = { version = "0.12" }
|
parking_lot = { version = "0.12" }
|
||||||
ratatui = "0.26"
|
ratatui = "0.27"
|
||||||
tokio = { version = "1.37", features = ["full"] }
|
tokio = { version = "1.39", features = ["full"] }
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
uuid = { version = "1.8", features = ["fast-rng", "v4"] }
|
uuid = { version = "1.10", features = ["fast-rng", "v4"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ In application controls
|
|||||||
| ```( enter )```| Run selected docker command.|
|
| ```( enter )```| Run selected docker command.|
|
||||||
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
||||||
| ```( 0 )``` | Stop sorting.|
|
| ```( 0 )``` | Stop sorting.|
|
||||||
|
| ```( F1 )``` or ```( / )``` | Enter filter mode. |
|
||||||
| ```( e )``` | Exec into the selected container - not available on Windows.|
|
| ```( e )``` | Exec into the selected container - not available on Windows.|
|
||||||
| ```( h )``` | Toggle help menu.|
|
| ```( h )``` | Toggle help menu.|
|
||||||
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
||||||
@@ -167,10 +168,10 @@ cargo test
|
|||||||
|
|
||||||
Run some example docker images
|
Run some example docker images
|
||||||
|
|
||||||
using docker-compose.yml;
|
using docker/docker-compose.yml;
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose -f docker-compose.yml up -d
|
docker compose -f ./docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
or individually
|
or individually
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
## Builder ##
|
## Builder ##
|
||||||
#############
|
#############
|
||||||
|
|
||||||
FROM --platform=linux/amd64 rust:slim as BUILDER
|
FROM --platform=linux/amd64 rust:slim AS builder
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
@@ -49,12 +49,12 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
|
|||||||
## Runtime ##
|
## Runtime ##
|
||||||
#############
|
#############
|
||||||
|
|
||||||
FROM scratch as RUNTIME
|
FROM scratch
|
||||||
|
|
||||||
# Set an ENV to indicate that we're running in a container
|
# Set an ENV to indicate that we're running in a container
|
||||||
ENV OXKER_RUNTIME=container
|
ENV OXKER_RUNTIME=container
|
||||||
|
|
||||||
COPY --from=BUILDER /oxker /app/
|
COPY --from=builder /oxker /app/
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
# this is used in the application itself so DO NOT EDIT
|
# this is used in the application itself so DO NOT EDIT
|
||||||
|
|||||||
+17
-16
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# rust create_release v0.5.5
|
# rust create_release v0.5.6
|
||||||
|
# 2024-07-27
|
||||||
|
|
||||||
STAR_LINE='****************************************'
|
STAR_LINE='****************************************'
|
||||||
CWD=$(pwd)
|
CWD=$(pwd)
|
||||||
@@ -191,25 +192,25 @@ check_cross() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
cargo_build_x86_linux() {
|
cross_build_x86_linux() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
|
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
|
||||||
cross build --target x86_64-unknown-linux-musl --release
|
cross build --target x86_64-unknown-linux-musl --release
|
||||||
}
|
}
|
||||||
|
|
||||||
cargo_build_aarch64_linux() {
|
cross_build_aarch64_linux() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
|
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
|
||||||
cross build --target aarch64-unknown-linux-musl --release
|
cross build --target aarch64-unknown-linux-musl --release
|
||||||
}
|
}
|
||||||
|
|
||||||
cargo_build_armv6_linux() {
|
cross_build_armv6_linux() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
|
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
|
||||||
cross build --target arm-unknown-linux-musleabihf --release
|
cross build --target arm-unknown-linux-musleabihf --release
|
||||||
}
|
}
|
||||||
|
|
||||||
cargo_build_x86_windows() {
|
cross_build_x86_windows() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
|
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
|
||||||
cross build --target x86_64-pc-windows-gnu --release
|
cross build --target x86_64-pc-windows-gnu --release
|
||||||
@@ -217,15 +218,15 @@ cargo_build_x86_windows() {
|
|||||||
|
|
||||||
# Build all releases that GitHub workflow would
|
# Build all releases that GitHub workflow would
|
||||||
# This will download GB's of docker images
|
# This will download GB's of docker images
|
||||||
cargo_build_all() {
|
cross_build_all() {
|
||||||
cargo clean
|
cargo clean
|
||||||
cargo_build_armv6_linux
|
cross_build_armv6_linux
|
||||||
ask_continue
|
ask_continue
|
||||||
cargo_build_aarch64_linux
|
cross_build_aarch64_linux
|
||||||
ask_continue
|
ask_continue
|
||||||
cargo_build_x86_linux
|
cross_build_x86_linux
|
||||||
ask_continue
|
ask_continue
|
||||||
cargo_build_x86_windows
|
cross_build_x86_windows
|
||||||
ask_continue
|
ask_continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +265,7 @@ release_flow() {
|
|||||||
get_git_remote_url
|
get_git_remote_url
|
||||||
|
|
||||||
cargo_test
|
cargo_test
|
||||||
cargo_build_all
|
cross_build_all
|
||||||
cargo_publish
|
cargo_publish
|
||||||
|
|
||||||
cd "${CWD}" || error_close "Can't find ${CWD}"
|
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||||
@@ -347,23 +348,23 @@ build_choice() {
|
|||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
cargo_build_x86_linux
|
cross_build_x86_linux
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
cargo_build_aarch64_linux
|
cross_build_aarch64_linux
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
cargo_build_armv6_linux
|
cross_build_armv6_linux
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
cargo_build_x86_windows
|
cross_build_x86_windows
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
5)
|
5)
|
||||||
cargo_build_all
|
cross_build_all
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Use an official lightweight image as a base
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install a simple utility (e.g., curl) to run as a health check
|
||||||
|
RUN apk --no-cache add curl
|
||||||
|
|
||||||
|
# Create a dummy file that we will use in our health check
|
||||||
|
RUN touch /tmp/healthy
|
||||||
|
|
||||||
|
# Define a simple health check
|
||||||
|
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
|
||||||
|
CMD [ ! -f /tmp/healthy ] || exit 1
|
||||||
|
|
||||||
|
# Start a basic loop that keeps the container running
|
||||||
|
CMD ["sh", "-c", "while :; do echo 'Container is running but will be unhealthy'; sleep 30; done"]
|
||||||
|
|
||||||
|
# docker build -t unhealthy-container . -f Dockerfile.unhealthy; docker run -d --name unhealthy unhealthy-container
|
||||||
@@ -4,7 +4,7 @@ networks:
|
|||||||
name: oxker-examaple-net
|
name: oxker-examaple-net
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:alpine3.19
|
image: postgres:alpine3.20
|
||||||
container_name: postgres
|
container_name: postgres
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
||||||
@@ -18,7 +18,7 @@ services:
|
|||||||
limits:
|
limits:
|
||||||
memory: 1024M
|
memory: 1024M
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine3.19
|
image: redis:alpine3.20
|
||||||
container_name: redis
|
container_name: redis
|
||||||
ipc: private
|
ipc: private
|
||||||
restart: always
|
restart: always
|
||||||
@@ -39,5 +39,20 @@ services:
|
|||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M
|
memory: 512M
|
||||||
|
some_container:
|
||||||
|
container_name: some_container
|
||||||
|
image: some_container
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.unhealthy
|
||||||
|
ipc: private
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- oxker-example-net
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+1
-1
@@ -9,7 +9,7 @@ esac
|
|||||||
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
if [ -n "$SUFFIX" ]; then
|
||||||
OXKER_GZ="oxker_linux_${SUFFIX}.tar.gz"
|
OXKER_GZ="oxker_linux_${SUFFIX}.tar.gz"
|
||||||
wget "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}"
|
curl -L -O "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}"
|
||||||
tar xzvf "${OXKER_GZ}" oxker
|
tar xzvf "${OXKER_GZ}" oxker
|
||||||
install -Dm 755 oxker -t "${HOME}/.local/bin"
|
install -Dm 755 oxker -t "${HOME}/.local/bin"
|
||||||
rm "${OXKER_GZ}" oxker
|
rm "${OXKER_GZ}" oxker
|
||||||
|
|||||||
+140
-39
@@ -10,6 +10,8 @@ use ratatui::{
|
|||||||
widgets::{ListItem, ListState},
|
widgets::{ListItem, ListState},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::ui::ORANGE;
|
||||||
|
|
||||||
use super::Header;
|
use super::Header;
|
||||||
|
|
||||||
const ONE_KB: f64 = 1000.0;
|
const ONE_KB: f64 = 1000.0;
|
||||||
@@ -48,6 +50,9 @@ impl PartialOrd for ContainerId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait Contains {
|
||||||
|
fn contains(&self, input: &str) -> bool;
|
||||||
|
}
|
||||||
/// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them
|
/// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them
|
||||||
macro_rules! unit_struct {
|
macro_rules! unit_struct {
|
||||||
($name:ident) => {
|
($name:ident) => {
|
||||||
@@ -67,7 +72,7 @@ macro_rules! unit_struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl$name {
|
impl $name {
|
||||||
pub fn get(&self) -> &str {
|
pub fn get(&self) -> &str {
|
||||||
self.0.as_str()
|
self.0.as_str()
|
||||||
}
|
}
|
||||||
@@ -77,20 +82,18 @@ macro_rules! unit_struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Contains for $name {
|
||||||
|
fn contains(&self, input: &str) -> bool {
|
||||||
|
self.0.to_lowercase().contains(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for $name {
|
impl std::fmt::Display for $name {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
if self.0.chars().count() >= 30 {
|
if self.0.chars().count() >= 30 {
|
||||||
write!(
|
write!(f, "{}…", self.0.chars().take(29).collect::<String>())
|
||||||
f,
|
|
||||||
"{}…",
|
|
||||||
self.0.chars().take(29).collect::<String>()
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(f, "{}", self.0)
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
self.0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,62 +210,108 @@ impl<T> StatefulList<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// States of the container
|
/// Store the containers status in a struct, so can then check for healthy/unhealthy status
|
||||||
|
/// It's usually something like "Up 1 hour", "Exited (0) 10 hours ago", "Up 10 minutes (unhealthy)"
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||||
|
pub struct ContainerStatus(String);
|
||||||
|
|
||||||
|
impl From<String> for ContainerStatus {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerStatus {
|
||||||
|
/// Check if a container is unhealthy
|
||||||
|
pub fn unhealthy(&self) -> bool {
|
||||||
|
self.contains("(unhealthy)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the source string
|
||||||
|
pub const fn get(&self) -> &String {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Contains for ContainerStatus {
|
||||||
|
/// Check if the state contains a specific string
|
||||||
|
fn contains(&self, item: &str) -> bool {
|
||||||
|
self.0.to_lowercase().contains(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default a container's running status will be healthy
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd)]
|
||||||
|
pub enum RunningState {
|
||||||
|
Healthy,
|
||||||
|
Unhealthy,
|
||||||
|
}
|
||||||
|
/// States of the container
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Dead,
|
Dead,
|
||||||
Exited,
|
Exited,
|
||||||
Paused,
|
Paused,
|
||||||
Removing,
|
Removing,
|
||||||
Restarting,
|
Restarting,
|
||||||
Running,
|
Running(RunningState),
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub const fn is_alive(self) -> bool {
|
pub const fn is_alive(self) -> bool {
|
||||||
matches!(self, Self::Running)
|
matches!(self, Self::Running(_))
|
||||||
}
|
}
|
||||||
pub const fn get_color(self) -> Color {
|
pub const fn get_color(self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Self::Paused => Color::Yellow,
|
Self::Paused => Color::Yellow,
|
||||||
Self::Removing => Color::LightRed,
|
Self::Removing => Color::LightRed,
|
||||||
Self::Restarting => Color::LightGreen,
|
Self::Restarting => Color::LightGreen,
|
||||||
Self::Running => Color::Green,
|
Self::Running(RunningState::Healthy) => Color::Green,
|
||||||
|
Self::Running(RunningState::Unhealthy) => ORANGE,
|
||||||
_ => Color::Red,
|
_ => Color::Red,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Dirty way to create order for the state, rather than impl Ord
|
/// Dirty way to create order for the state, rather than impl Ord
|
||||||
pub const fn order(self) -> u8 {
|
pub const fn order(self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Self::Running => 0,
|
Self::Running(RunningState::Healthy) => 0,
|
||||||
Self::Paused => 1,
|
Self::Running(RunningState::Unhealthy) => 1,
|
||||||
Self::Restarting => 2,
|
Self::Paused => 2,
|
||||||
Self::Removing => 3,
|
Self::Restarting => 3,
|
||||||
Self::Exited => 4,
|
Self::Removing => 4,
|
||||||
Self::Dead => 5,
|
Self::Exited => 5,
|
||||||
Self::Unknown => 6,
|
Self::Dead => 6,
|
||||||
|
Self::Unknown => 7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for State {
|
/// Need status, to check if container is unhealthy or not
|
||||||
fn from(input: &str) -> Self {
|
impl From<(&str, &ContainerStatus)> for State {
|
||||||
|
fn from((input, status): (&str, &ContainerStatus)) -> Self {
|
||||||
match input {
|
match input {
|
||||||
"dead" => Self::Dead,
|
"dead" => Self::Dead,
|
||||||
"exited" => Self::Exited,
|
"exited" => Self::Exited,
|
||||||
"paused" => Self::Paused,
|
"paused" => Self::Paused,
|
||||||
"removing" => Self::Removing,
|
"removing" => Self::Removing,
|
||||||
"restarting" => Self::Restarting,
|
"restarting" => Self::Restarting,
|
||||||
"running" => Self::Running,
|
"running" => {
|
||||||
|
if status.unhealthy() {
|
||||||
|
Self::Running(RunningState::Unhealthy)
|
||||||
|
} else {
|
||||||
|
Self::Running(RunningState::Healthy)
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => Self::Unknown,
|
_ => Self::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<String>> for State {
|
/// Again, need status, to check if container is unhealthy or not
|
||||||
fn from(input: Option<String>) -> Self {
|
impl From<(Option<String>, &ContainerStatus)> for State {
|
||||||
input.map_or(Self::Unknown, |input| Self::from(input.as_str()))
|
fn from((input, status): (Option<String>, &ContainerStatus)) -> Self {
|
||||||
|
input.map_or(Self::Unknown, |input| Self::from((input.as_str(), status)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +323,8 @@ impl fmt::Display for State {
|
|||||||
Self::Paused => "॥ paused",
|
Self::Paused => "॥ paused",
|
||||||
Self::Removing => "removing",
|
Self::Removing => "removing",
|
||||||
Self::Restarting => "↻ restarting",
|
Self::Restarting => "↻ restarting",
|
||||||
Self::Running => "✓ running",
|
Self::Running(RunningState::Healthy) => "✓ running",
|
||||||
|
Self::Running(RunningState::Unhealthy) => "! running",
|
||||||
Self::Unknown => "? unknown",
|
Self::Unknown => "? unknown",
|
||||||
};
|
};
|
||||||
write!(f, "{disp}")
|
write!(f, "{disp}")
|
||||||
@@ -310,7 +360,7 @@ impl DockerControls {
|
|||||||
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
||||||
State::Paused => vec![Self::Resume, Self::Stop, Self::Delete],
|
State::Paused => vec![Self::Resume, Self::Stop, Self::Delete],
|
||||||
State::Restarting => vec![Self::Stop, Self::Delete],
|
State::Restarting => vec![Self::Stop, Self::Delete],
|
||||||
State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete],
|
State::Running(_) => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete],
|
||||||
_ => vec![Self::Delete],
|
_ => vec![Self::Delete],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,7 +589,7 @@ pub struct ContainerItem {
|
|||||||
pub ports: Vec<ContainerPorts>,
|
pub ports: Vec<ContainerPorts>,
|
||||||
pub rx: ByteStats,
|
pub rx: ByteStats,
|
||||||
pub state: State,
|
pub state: State,
|
||||||
pub status: String,
|
pub status: ContainerStatus,
|
||||||
pub tx: ByteStats,
|
pub tx: ByteStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +618,7 @@ impl ContainerItem {
|
|||||||
name: String,
|
name: String,
|
||||||
ports: Vec<ContainerPorts>,
|
ports: Vec<ContainerPorts>,
|
||||||
state: State,
|
state: State,
|
||||||
status: String,
|
status: ContainerStatus,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
||||||
docker_controls.start();
|
docker_controls.start();
|
||||||
@@ -665,14 +715,14 @@ impl Columns {
|
|||||||
pub const fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: (Header::Name, 4),
|
name: (Header::Name, 4),
|
||||||
state: (Header::State, 11),
|
state: (Header::State, 5),
|
||||||
status: (Header::Status, 16),
|
status: (Header::Status, 6),
|
||||||
cpu: (Header::Cpu, 7),
|
cpu: (Header::Cpu, 3),
|
||||||
mem: (Header::Memory, 7, 7),
|
mem: (Header::Memory, 7, 7),
|
||||||
id: (Header::Id, 8),
|
id: (Header::Id, 8),
|
||||||
image: (Header::Image, 5),
|
image: (Header::Image, 5),
|
||||||
net_rx: (Header::Rx, 7),
|
net_rx: (Header::Rx, 4),
|
||||||
net_tx: (Header::Tx, 7),
|
net_tx: (Header::Tx, 4),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,11 +732,11 @@ mod tests {
|
|||||||
use ratatui::widgets::ListItem;
|
use ratatui::widgets::ListItem;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{ContainerImage, Logs},
|
app_data::{ContainerImage, Logs, RunningState},
|
||||||
ui::log_sanitizer,
|
ui::log_sanitizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ByteStats, ContainerName, CpuStats, LogsTz};
|
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Display CpuStats as a string
|
/// Display CpuStats as a string
|
||||||
@@ -770,4 +820,55 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(logs.logs.items.len(), 2);
|
assert_eq!(logs.logs.items.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// check ContainerStatus unhealthy state
|
||||||
|
fn test_container_state_unhealthy() {
|
||||||
|
let input = ContainerStatus::from("Up 1 hour".to_owned());
|
||||||
|
|
||||||
|
assert!(!input.unhealthy());
|
||||||
|
|
||||||
|
let input = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned());
|
||||||
|
|
||||||
|
assert!(input.unhealthy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Generate container State from a &str and &ContainerStatus
|
||||||
|
fn test_container_status_unhealthy() {
|
||||||
|
let healthy = ContainerStatus::from("Up 1 hour".to_owned());
|
||||||
|
let unhealthy = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned());
|
||||||
|
|
||||||
|
// Running and healthy
|
||||||
|
let input = State::from(("running", &healthy));
|
||||||
|
assert_eq!(input, State::Running(RunningState::Healthy));
|
||||||
|
|
||||||
|
// Running and unhealthy
|
||||||
|
let input = State::from(("running", &unhealthy));
|
||||||
|
assert_eq!(input, State::Running(RunningState::Unhealthy));
|
||||||
|
|
||||||
|
// Dead
|
||||||
|
let input = State::from(("dead", &healthy));
|
||||||
|
assert_eq!(input, State::Dead);
|
||||||
|
|
||||||
|
// Exited
|
||||||
|
let input = State::from(("exited", &healthy));
|
||||||
|
assert_eq!(input, State::Exited);
|
||||||
|
|
||||||
|
// Paused
|
||||||
|
let input = State::from(("paused", &healthy));
|
||||||
|
assert_eq!(input, State::Paused);
|
||||||
|
|
||||||
|
// Removing
|
||||||
|
let input = State::from(("removing", &healthy));
|
||||||
|
assert_eq!(input, State::Removing);
|
||||||
|
|
||||||
|
// Restarting
|
||||||
|
let input = State::from(("restarting", &healthy));
|
||||||
|
assert_eq!(input, State::Restarting);
|
||||||
|
|
||||||
|
// Unknown
|
||||||
|
let input = State::from(("oxker", &healthy));
|
||||||
|
assert_eq!(input, State::Unknown);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+513
-111
File diff suppressed because it is too large
Load Diff
+27
-20
@@ -22,7 +22,7 @@ use tokio::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, DockerControls, State},
|
app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
parse_args::CliArgs,
|
parse_args::CliArgs,
|
||||||
ui::{GuiState, Status},
|
ui::{GuiState, Status},
|
||||||
@@ -201,7 +201,7 @@ impl DockerData {
|
|||||||
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
|
/// 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
|
/// Just make sure that items sent are guaranteed to have an id
|
||||||
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
|
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
|
||||||
pub async fn update_all_containers(&mut self) -> Vec<(State, ContainerId)> {
|
pub async fn update_all_containers(&self) -> Vec<(State, ContainerId)> {
|
||||||
let containers = self
|
let containers = self
|
||||||
.docker
|
.docker
|
||||||
.list_containers(Some(ListContainersOptions::<String> {
|
.list_containers(Some(ListContainersOptions::<String> {
|
||||||
@@ -236,7 +236,15 @@ impl DockerData {
|
|||||||
output
|
output
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|i| {
|
.filter_map(|i| {
|
||||||
i.id.map(|id| (State::from(i.state), ContainerId::from(id.as_str())))
|
i.id.map(|id| {
|
||||||
|
(
|
||||||
|
State::from((
|
||||||
|
i.state,
|
||||||
|
&ContainerStatus::from(i.status.map_or_else(String::new, |i| i)),
|
||||||
|
)),
|
||||||
|
ContainerId::from(id.as_str()),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
@@ -271,7 +279,7 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update all logs, spawn each container into own tokio::spawn thread
|
/// Update all logs, spawn each container into own tokio::spawn thread
|
||||||
fn init_all_logs(&mut self, all_ids: &[(State, ContainerId)]) {
|
fn init_all_logs(&self, all_ids: &[(State, ContainerId)]) {
|
||||||
for (_, id) in all_ids {
|
for (_, id) in all_ids {
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
@@ -303,13 +311,14 @@ impl DockerData {
|
|||||||
};
|
};
|
||||||
self.update_all_container_stats(&all_ids);
|
self.update_all_container_stats(&all_ids);
|
||||||
self.app_data.lock().sort_containers();
|
self.app_data.lock().sort_containers();
|
||||||
|
self.gui_state.lock().stop_loading_animation(Uuid::nil());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) {
|
async fn initialise_container_data(&mut self) {
|
||||||
self.gui_state.lock().status_push(Status::Init);
|
self.gui_state.lock().status_push(Status::Init);
|
||||||
let loading_uuid = Uuid::new_v4();
|
let loading_uuid = Uuid::new_v4();
|
||||||
let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid);
|
GuiState::start_loading_animation(&self.gui_state, loading_uuid);
|
||||||
let all_ids = self.update_all_containers().await;
|
let all_ids = self.update_all_containers().await;
|
||||||
|
|
||||||
self.update_all_container_stats(&all_ids);
|
self.update_all_container_stats(&all_ids);
|
||||||
@@ -323,9 +332,7 @@ impl DockerData {
|
|||||||
self.init = None;
|
self.init = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.gui_state
|
self.gui_state.lock().stop_loading_animation(loading_uuid);
|
||||||
.lock()
|
|
||||||
.stop_loading_animation(&loading_handle, loading_uuid);
|
|
||||||
self.gui_state.lock().status_del(Status::Init);
|
self.gui_state.lock().status_del(Status::Init);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,27 +363,27 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
DockerMessage::Pause(id) => {
|
DockerMessage::Pause(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.pause_container(id.get()).await.is_err() {
|
if docker.pause_container(id.get()).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
|
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Restart(id) => {
|
DockerMessage::Restart(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.restart_container(id.get(), None).await.is_err() {
|
if docker.restart_container(id.get(), None).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
|
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Start(id) => {
|
DockerMessage::Start(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker
|
if docker
|
||||||
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
||||||
.await
|
.await
|
||||||
@@ -384,33 +391,33 @@ impl DockerData {
|
|||||||
{
|
{
|
||||||
Self::set_error(&app_data, DockerControls::Start, &gui_state);
|
Self::set_error(&app_data, DockerControls::Start, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Stop(id) => {
|
DockerMessage::Stop(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.stop_container(id.get(), None).await.is_err() {
|
if docker.stop_container(id.get(), None).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Resume(id) => {
|
DockerMessage::Resume(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.unpause_container(id.get()).await.is_err() {
|
if docker.unpause_container(id.get()).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Resume, &gui_state);
|
Self::set_error(&app_data, DockerControls::Resume, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Delete(id) => {
|
DockerMessage::Delete(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker
|
if docker
|
||||||
.remove_container(
|
.remove_container(
|
||||||
id.get(),
|
id.get(),
|
||||||
@@ -425,7 +432,7 @@ impl DockerData {
|
|||||||
{
|
{
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
self.gui_state.lock().set_delete_container(None);
|
self.gui_state.lock().set_delete_container(None);
|
||||||
|
|||||||
+7
-2
@@ -18,7 +18,7 @@ use tokio::{
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, State},
|
app_data::{AppData, ContainerId, RunningState, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,7 +162,12 @@ impl ExecMode {
|
|||||||
let container = app_data.lock().get_selected_container_id_state_name();
|
let container = app_data.lock().get_selected_container_id_state_name();
|
||||||
|
|
||||||
if let Some((id, state, _)) = container {
|
if let Some((id, state, _)) = container {
|
||||||
if state == State::Running {
|
if [
|
||||||
|
State::Running(RunningState::Healthy),
|
||||||
|
State::Running(RunningState::Unhealthy),
|
||||||
|
]
|
||||||
|
.contains(&state)
|
||||||
|
{
|
||||||
if tty_readable() && !use_cli {
|
if tty_readable() && !use_cli {
|
||||||
if let Ok(exec) = docker
|
if let Ok(exec) = docker
|
||||||
.create_exec(
|
.create_exec(
|
||||||
|
|||||||
+122
-69
@@ -71,6 +71,7 @@ impl InputHandler {
|
|||||||
Status::Error,
|
Status::Error,
|
||||||
Status::Help,
|
Status::Help,
|
||||||
Status::DeleteConfirm,
|
Status::DeleteConfirm,
|
||||||
|
Status::Filter,
|
||||||
]) {
|
]) {
|
||||||
self.mouse_press(mouse_event);
|
self.mouse_press(mouse_event);
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ impl InputHandler {
|
|||||||
let is_oxker = self.app_data.lock().is_oxker();
|
let is_oxker = self.app_data.lock().is_oxker();
|
||||||
if !is_oxker && tty_readable() {
|
if !is_oxker && tty_readable() {
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
|
GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||||
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
|
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
|
||||||
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ impl InputHandler {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.gui_state.lock().stop_loading_animation(&handle, uuid);
|
self.gui_state.lock().stop_loading_animation(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +178,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
|
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
|
||||||
async fn s_key(&mut self) {
|
async fn s_key(&self) {
|
||||||
/// This is the inner workings, *inlined* here to return a Result
|
/// This is the inner workings, *inlined* here to return a Result
|
||||||
async fn save_logs(
|
async fn save_logs(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
@@ -248,7 +249,7 @@ impl InputHandler {
|
|||||||
self.gui_state.lock().status_push(log_status);
|
self.gui_state.lock().status_push(log_status);
|
||||||
|
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
|
GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||||
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
|
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
@@ -260,12 +261,12 @@ impl InputHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.gui_state.lock().status_del(log_status);
|
self.gui_state.lock().status_del(log_status);
|
||||||
self.gui_state.lock().stop_loading_animation(&handle, uuid);
|
self.gui_state.lock().stop_loading_animation(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send docker command, if the Commands panel is selected
|
/// Send docker command, if the Commands panel is selected
|
||||||
async fn enter_key(&mut self) {
|
async fn enter_key(&self) {
|
||||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||||
let panel = self.gui_state.lock().get_selected_panel();
|
let panel = self.gui_state.lock().get_selected_panel();
|
||||||
if panel == SelectablePanel::Commands {
|
if panel == SelectablePanel::Commands {
|
||||||
@@ -306,7 +307,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change the the "next" selectable panel
|
/// Change the the "next" selectable panel
|
||||||
fn tab_key(&mut self) {
|
fn tab_key(&self) {
|
||||||
let is_containers =
|
let is_containers =
|
||||||
self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
|
self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
|
||||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||||
@@ -320,7 +321,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change to previously selected panel
|
/// Change to previously selected panel
|
||||||
fn back_tab_key(&mut self) {
|
fn back_tab_key(&self) {
|
||||||
let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
|
let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
|
||||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||||
2
|
2
|
||||||
@@ -332,7 +333,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn home_key(&mut self) {
|
fn home_key(&self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
@@ -343,7 +344,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Go to end of the list of the currently selected panel
|
/// Go to end of the list of the currently selected panel
|
||||||
fn end_key(&mut self) {
|
fn end_key(&self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
@@ -353,6 +354,104 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Actions to take when in Help status active
|
||||||
|
fn handle_help(&mut self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Esc | KeyCode::Char('h' | 'H') => {
|
||||||
|
self.gui_state.lock().status_del(Status::Help);
|
||||||
|
}
|
||||||
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions to take when Error status active
|
||||||
|
fn handle_error(&self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
|
||||||
|
self.app_data.lock().remove_error();
|
||||||
|
self.gui_state.lock().status_del(Status::Error);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions to take when Delete status active
|
||||||
|
async fn handle_delete(&self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||||
|
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions to take when Filter status active
|
||||||
|
fn handle_filter(&self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.app_data.lock().filter_term_clear();
|
||||||
|
self.gui_state.lock().status_del(Status::Filter);
|
||||||
|
}
|
||||||
|
KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('/') => {
|
||||||
|
self.gui_state.lock().status_del(Status::Filter);
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.app_data.lock().filter_term_pop();
|
||||||
|
}
|
||||||
|
KeyCode::Char(x) => {
|
||||||
|
self.app_data.lock().filter_term_push(x);
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
self.app_data.lock().filter_by_next();
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
self.app_data.lock().filter_by_prev();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle button presses in all other scenarios
|
||||||
|
async fn handle_others(&mut self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
||||||
|
KeyCode::Char('1') => self.sort(Header::Name),
|
||||||
|
KeyCode::Char('2') => self.sort(Header::State),
|
||||||
|
KeyCode::Char('3') => self.sort(Header::Status),
|
||||||
|
KeyCode::Char('4') => self.sort(Header::Cpu),
|
||||||
|
KeyCode::Char('5') => self.sort(Header::Memory),
|
||||||
|
KeyCode::Char('6') => self.sort(Header::Id),
|
||||||
|
KeyCode::Char('7') => self.sort(Header::Image),
|
||||||
|
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||||
|
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||||
|
KeyCode::Char('e' | 'E') => self.e_key().await,
|
||||||
|
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||||
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
|
KeyCode::Char('s' | 'S') => self.s_key().await,
|
||||||
|
KeyCode::Tab => self.tab_key(),
|
||||||
|
KeyCode::BackTab => self.back_tab_key(),
|
||||||
|
KeyCode::Home => self.home_key(),
|
||||||
|
KeyCode::End => self.end_key(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
||||||
|
KeyCode::PageUp => {
|
||||||
|
for _ in 0..=6 {
|
||||||
|
self.previous();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::F(1) | KeyCode::Char('/') => {
|
||||||
|
self.gui_state.lock().status_push(Status::Filter);
|
||||||
|
self.docker_tx.send(DockerMessage::Update).await.ok();
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
||||||
|
KeyCode::PageDown => {
|
||||||
|
for _ in 0..=6 {
|
||||||
|
self.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => self.enter_key().await,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Handle keyboard button events
|
/// Handle keyboard button events
|
||||||
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
|
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
|
||||||
let contains_delete = self
|
let contains_delete = self
|
||||||
@@ -365,78 +464,32 @@ impl InputHandler {
|
|||||||
let contains_error = contains(Status::Error);
|
let contains_error = contains(Status::Error);
|
||||||
let contains_help = contains(Status::Help);
|
let contains_help = contains(Status::Help);
|
||||||
let contains_exec = contains(Status::Exec);
|
let contains_exec = contains(Status::Exec);
|
||||||
|
let contains_filter: bool = contains(Status::Filter);
|
||||||
|
|
||||||
if !contains_exec {
|
if !contains_exec {
|
||||||
// Always just quit on Ctrl + c/C or q/Q
|
|
||||||
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||||
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||||
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() {
|
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() && !contains_filter {
|
||||||
|
// Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active
|
||||||
self.quit().await;
|
self.quit().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if contains_error {
|
if contains_error {
|
||||||
match key_code {
|
self.handle_error(key_code);
|
||||||
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
|
|
||||||
self.app_data.lock().remove_error();
|
|
||||||
self.gui_state.lock().status_del(Status::Error);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
} else if contains_help {
|
} else if contains_help {
|
||||||
match key_code {
|
self.handle_help(key_code);
|
||||||
KeyCode::Esc | KeyCode::Char('h' | 'H') => {
|
} else if contains_filter {
|
||||||
self.gui_state.lock().status_del(Status::Help);
|
self.handle_filter(key_code);
|
||||||
}
|
|
||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
} else if contains_delete {
|
} else if contains_delete {
|
||||||
match key_code {
|
self.handle_delete(key_code).await;
|
||||||
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
|
||||||
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
match key_code {
|
self.handle_others(key_code).await;
|
||||||
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
|
||||||
KeyCode::Char('1') => self.sort(Header::Name),
|
|
||||||
KeyCode::Char('2') => self.sort(Header::State),
|
|
||||||
KeyCode::Char('3') => self.sort(Header::Status),
|
|
||||||
KeyCode::Char('4') => self.sort(Header::Cpu),
|
|
||||||
KeyCode::Char('5') => self.sort(Header::Memory),
|
|
||||||
KeyCode::Char('6') => self.sort(Header::Id),
|
|
||||||
KeyCode::Char('7') => self.sort(Header::Image),
|
|
||||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
|
||||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
|
||||||
KeyCode::Char('e' | 'E') => self.e_key().await,
|
|
||||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
|
||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
|
||||||
KeyCode::Char('s' | 'S') => self.s_key().await,
|
|
||||||
KeyCode::Tab => self.tab_key(),
|
|
||||||
KeyCode::BackTab => self.back_tab_key(),
|
|
||||||
KeyCode::Home => self.home_key(),
|
|
||||||
KeyCode::End => self.end_key(),
|
|
||||||
KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
|
||||||
KeyCode::PageUp => {
|
|
||||||
for _ in 0..=6 {
|
|
||||||
self.previous();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
|
||||||
KeyCode::PageDown => {
|
|
||||||
for _ in 0..=6 {
|
|
||||||
self.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Enter => self.enter_key().await,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
|
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
|
||||||
async fn button_intersect(&mut self, mouse_event: MouseEvent) {
|
async fn button_intersect(&self, mouse_event: MouseEvent) {
|
||||||
if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
|
if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
|
||||||
let intersect = self.gui_state.lock().button_intersect(Rect::new(
|
let intersect = self.gui_state.lock().button_intersect(Rect::new(
|
||||||
mouse_event.column,
|
mouse_event.column,
|
||||||
@@ -455,7 +508,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle mouse button events
|
/// Handle mouse button events
|
||||||
fn mouse_press(&mut self, mouse_event: MouseEvent) {
|
fn mouse_press(&self, mouse_event: MouseEvent) {
|
||||||
match mouse_event.kind {
|
match mouse_event.kind {
|
||||||
MouseEventKind::ScrollUp => self.previous(),
|
MouseEventKind::ScrollUp => self.previous(),
|
||||||
MouseEventKind::ScrollDown => self.next(),
|
MouseEventKind::ScrollDown => self.next(),
|
||||||
@@ -481,7 +534,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change state to next, depending which panel is currently in focus
|
/// Change state to next, depending which panel is currently in focus
|
||||||
fn next(&mut self) {
|
fn next(&self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
@@ -492,7 +545,7 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change state to previous, depending which panel is currently in focus
|
/// Change state to previous, depending which panel is currently in focus
|
||||||
fn previous(&mut self) {
|
fn previous(&self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
let mut locked_data = self.app_data.lock();
|
||||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||||
match selected_panel {
|
match selected_panel {
|
||||||
|
|||||||
+13
-3
@@ -167,10 +167,18 @@ async fn main() {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
|
#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
collections::{HashSet, VecDeque},
|
||||||
|
vec,
|
||||||
|
};
|
||||||
|
|
||||||
use bollard::service::{ContainerSummary, Port};
|
use bollard::service::{ContainerSummary, Port};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList},
|
app_data::{
|
||||||
|
AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter,
|
||||||
|
RunningState, State, StatefulList,
|
||||||
|
},
|
||||||
parse_args::CliArgs,
|
parse_args::CliArgs,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -201,16 +209,18 @@ mod tests {
|
|||||||
private: u16::try_from(index).unwrap_or(1) + 8000,
|
private: u16::try_from(index).unwrap_or(1) + 8000,
|
||||||
public: None,
|
public: None,
|
||||||
}],
|
}],
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
format!("Up {index} hour"),
|
ContainerStatus::from(format!("Up {index} hour")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
||||||
AppData {
|
AppData {
|
||||||
containers: StatefulList::new(containers.to_vec()),
|
containers: StatefulList::new(containers.to_vec()),
|
||||||
|
hidden_containers: vec![],
|
||||||
error: None,
|
error: None,
|
||||||
sorted_by: None,
|
sorted_by: None,
|
||||||
|
filter: Filter::new(),
|
||||||
args: gen_args(),
|
args: gen_args(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -41,15 +41,19 @@ pub mod log_sanitizer {
|
|||||||
|
|
||||||
/// Remove all ansi formatting from a given string and create ratatui Lines
|
/// Remove all ansi formatting from a given string and create ratatui Lines
|
||||||
pub fn remove_ansi<'a>(input: &str) -> Vec<Line<'a>> {
|
pub fn remove_ansi<'a>(input: &str) -> Vec<Line<'a>> {
|
||||||
raw(&categorise_text(input)
|
vec![Line::from(
|
||||||
.into_iter()
|
categorise_text(input)
|
||||||
.map(|i| i.text)
|
.into_iter()
|
||||||
.collect::<String>())
|
.map(|i| i.text)
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_owned(),
|
||||||
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// create ratatui Lines that exactly match the given strings
|
/// create ratatui Lines that exactly match the given strings
|
||||||
pub fn raw<'a>(input: &str) -> Vec<Line<'a>> {
|
pub fn raw<'a>(input: &str) -> Vec<Line<'a>> {
|
||||||
vec![Line::from(Span::raw(input.to_owned()))]
|
vec![Line::from(input.escape_debug().collect::<String>())]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change from ansi to tui colors
|
/// Change from ansi to tui colors
|
||||||
@@ -62,7 +66,7 @@ pub mod log_sanitizer {
|
|||||||
CansiColor::Blue => Color::Blue,
|
CansiColor::Blue => Color::Blue,
|
||||||
CansiColor::Magenta => Color::Magenta,
|
CansiColor::Magenta => Color::Magenta,
|
||||||
CansiColor::Cyan => Color::Cyan,
|
CansiColor::Cyan => Color::Cyan,
|
||||||
CansiColor::White | CansiColor::BrightWhite => Color::White,
|
CansiColor::White | CansiColor::BrightWhite => Color::Gray,
|
||||||
CansiColor::BrightRed => Color::LightRed,
|
CansiColor::BrightRed => Color::LightRed,
|
||||||
CansiColor::BrightGreen => Color::LightGreen,
|
CansiColor::BrightGreen => Color::LightGreen,
|
||||||
CansiColor::BrightYellow => Color::LightYellow,
|
CansiColor::BrightYellow => Color::LightYellow,
|
||||||
@@ -92,7 +96,7 @@ mod tests {
|
|||||||
let expected = vec![Line {
|
let expected = vec![Line {
|
||||||
spans: [Span {
|
spans: [Span {
|
||||||
content: std::borrow::Cow::Borrowed(
|
content: std::borrow::Cow::Borrowed(
|
||||||
"\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m",
|
"\\u{1b}[31;47mo\\u{1b}[32;40mx\\u{1b}[33;41mk\\u{1b}[34;42me\\u{1b}[35;43mr\\u{1b}[0m",
|
||||||
),
|
),
|
||||||
style: Style::default(),
|
style: Style::default(),
|
||||||
}]
|
}]
|
||||||
@@ -111,7 +115,7 @@ mod tests {
|
|||||||
spans: vec![
|
spans: vec![
|
||||||
Span {
|
Span {
|
||||||
content: std::borrow::Cow::Borrowed("o"),
|
content: std::borrow::Cow::Borrowed("o"),
|
||||||
style: Style::default().fg(Color::Red).bg(Color::White),
|
style: Style::default().fg(Color::Red).bg(Color::Gray),
|
||||||
},
|
},
|
||||||
Span {
|
Span {
|
||||||
content: std::borrow::Cow::Borrowed("x"),
|
content: std::borrow::Cow::Borrowed("x"),
|
||||||
|
|||||||
+1263
-926
File diff suppressed because it is too large
Load Diff
+34
-31
@@ -163,19 +163,21 @@ pub enum Status {
|
|||||||
DockerConnect,
|
DockerConnect,
|
||||||
Error,
|
Error,
|
||||||
Exec,
|
Exec,
|
||||||
|
Filter,
|
||||||
Help,
|
Help,
|
||||||
Init,
|
Init,
|
||||||
Logs,
|
Logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default)]
|
||||||
pub struct GuiState {
|
pub struct GuiState {
|
||||||
delete_container: Option<ContainerId>,
|
delete_container: Option<ContainerId>,
|
||||||
delete_map: HashMap<DeleteButton, Rect>,
|
delete_map: HashMap<DeleteButton, Rect>,
|
||||||
heading_map: HashMap<Header, Rect>,
|
heading_map: HashMap<Header, Rect>,
|
||||||
is_loading: HashSet<Uuid>,
|
loading_handle: Option<JoinHandle<()>>,
|
||||||
loading_index: u8,
|
loading_index: u8,
|
||||||
|
loading_set: HashSet<Uuid>,
|
||||||
panel_map: HashMap<SelectablePanel, Rect>,
|
panel_map: HashMap<SelectablePanel, Rect>,
|
||||||
selected_panel: SelectablePanel,
|
selected_panel: SelectablePanel,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
@@ -207,7 +209,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
|
||||||
pub fn button_intersect(&mut self, rect: Rect) -> Option<DeleteButton> {
|
pub fn button_intersect(&self, rect: Rect) -> Option<DeleteButton> {
|
||||||
self.delete_map
|
self.delete_map
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.1.intersects(rect))
|
.filter(|i| i.1.intersects(rect))
|
||||||
@@ -217,7 +219,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
|
pub fn header_intersect(&self, rect: Rect) -> Option<Header> {
|
||||||
self.heading_map
|
self.heading_map
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| i.1.intersects(rect))
|
.filter(|i| i.1.intersects(rect))
|
||||||
@@ -293,7 +295,7 @@ impl GuiState {
|
|||||||
self.status.insert(Status::Exec);
|
self.status.insert(Status::Exec);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_exec_mode(&mut self) -> Option<ExecMode> {
|
pub fn get_exec_mode(&self) -> Option<ExecMode> {
|
||||||
self.exec_mode.clone()
|
self.exec_mode.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,45 +327,46 @@ impl GuiState {
|
|||||||
} else {
|
} else {
|
||||||
self.loading_index += 1;
|
self.loading_index += 1;
|
||||||
}
|
}
|
||||||
self.is_loading.insert(uuid);
|
self.loading_set.insert(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_loading(&self) -> bool {
|
||||||
|
!self.loading_set.is_empty()
|
||||||
|
}
|
||||||
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
||||||
pub fn get_loading(&self) -> char {
|
pub fn get_loading(&self) -> char {
|
||||||
if self.is_loading.is_empty() {
|
if self.is_loading() {
|
||||||
' '
|
|
||||||
} else {
|
|
||||||
FRAMES[usize::from(self.loading_index)]
|
FRAMES[usize::from(self.loading_index)]
|
||||||
}
|
} else {
|
||||||
}
|
' '
|
||||||
|
|
||||||
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
|
|
||||||
fn remove_loading(&mut self, uuid: Uuid) {
|
|
||||||
self.is_loading.remove(&uuid);
|
|
||||||
if self.is_loading.is_empty() {
|
|
||||||
self.loading_index = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Animate the loading icon in its own Tokio thread
|
/// Animate the loading icon in its own Tokio thread
|
||||||
pub fn start_loading_animation(
|
/// This should only be able to executed once, rather than multiple spawns
|
||||||
gui_state: &Arc<Mutex<Self>>,
|
pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
|
||||||
loading_uuid: Uuid,
|
if !gui_state.lock().is_loading() {
|
||||||
) -> JoinHandle<()> {
|
let inner_state = Arc::clone(gui_state);
|
||||||
|
gui_state.lock().loading_handle = Some(tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
inner_state.lock().next_loading(loading_uuid);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
gui_state.lock().next_loading(loading_uuid);
|
gui_state.lock().next_loading(loading_uuid);
|
||||||
let gui_state = Arc::clone(gui_state);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
||||||
gui_state.lock().next_loading(loading_uuid);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop the loading_spin function, and reset gui loading status
|
/// Stop the loading_spin function, and reset gui loading status
|
||||||
pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) {
|
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
|
||||||
handle.abort();
|
self.loading_set.remove(&loading_uuid);
|
||||||
self.remove_loading(loading_uuid);
|
if self.loading_set.is_empty() {
|
||||||
|
self.loading_index = 0;
|
||||||
|
if let Some(h) = &self.loading_handle {
|
||||||
|
h.abort();
|
||||||
|
}
|
||||||
|
self.loading_handle = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set info box content
|
/// Set info box content
|
||||||
|
|||||||
+17
-5
@@ -32,6 +32,8 @@ use crate::{
|
|||||||
input_handler::InputMessages,
|
input_handler::InputMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
||||||
|
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
@@ -64,7 +66,6 @@ impl Ui {
|
|||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
if let Ok(mut terminal) = Self::setup_terminal() {
|
if let Ok(mut terminal) = Self::setup_terminal() {
|
||||||
// let args = app_data.lock().args.clone();
|
|
||||||
let cursor_position = terminal.get_cursor().unwrap_or_default();
|
let cursor_position = terminal.get_cursor().unwrap_or_default();
|
||||||
let mut ui = Self {
|
let mut ui = Self {
|
||||||
app_data,
|
app_data,
|
||||||
@@ -264,14 +265,20 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
|||||||
/// Draw the main ui to a frame of the terminal
|
/// Draw the main ui to a frame of the terminal
|
||||||
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
||||||
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
||||||
|
let contains_filter = gui_state.lock().status_contains(&[Status::Filter]);
|
||||||
|
|
||||||
|
let whole_constraints = if contains_filter {
|
||||||
|
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Max(1), Constraint::Min(1)]
|
||||||
|
};
|
||||||
|
|
||||||
let whole_layout = Layout::default()
|
let whole_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Max(1), Constraint::Min(1)].as_ref())
|
.constraints(whole_constraints)
|
||||||
.split(f.size());
|
.split(f.size());
|
||||||
|
|
||||||
// Split into 3, containers+controls, logs, then graphs
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
// This one is the issue!
|
|
||||||
let upper_main = Layout::default()
|
let upper_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref())
|
.constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref())
|
||||||
@@ -306,6 +313,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
|
|
||||||
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
|
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
|
||||||
|
|
||||||
|
// Draw filter bar
|
||||||
|
if let Some(rect) = whole_layout.get(2) {
|
||||||
|
draw_blocks::filter_bar(*rect, f, app_data);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(id) = fd.delete_confirm.as_ref() {
|
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||||
app_data.lock().get_container_name_by_id(id).map_or_else(
|
app_data.lock().get_container_name_by_id(id).map_or_else(
|
||||||
|| {
|
|| {
|
||||||
@@ -320,8 +332,8 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only draw commands + charts if there are containers
|
// only draw commands + charts if there are containers
|
||||||
if fd.has_containers {
|
if let Some(rect) = top_panel.get(1) {
|
||||||
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
|
draw_blocks::commands(app_data, *rect, f, &fd, gui_state);
|
||||||
|
|
||||||
// Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border)
|
// Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border)
|
||||||
let max_lens = app_data.lock().get_longest_port();
|
let max_lens = app_data.lock().get_longest_port();
|
||||||
|
|||||||
Reference in New Issue
Block a user