chore: merge release-v0.7.0 into main
This commit is contained in:
@@ -25,11 +25,11 @@
|
||||
"extensions": [
|
||||
"bmuskalla.vscode-tldr",
|
||||
"christian-kohler.path-intellisense",
|
||||
"fill-labs.dependi",
|
||||
"foxundermoon.shell-format",
|
||||
"mutantdino.resourcemonitor",
|
||||
"redhat.vscode-yaml",
|
||||
"rust-lang.rust-analyzer",
|
||||
"serayuzgur.crates",
|
||||
"tamasfe.even-better-toml",
|
||||
"timonwong.shellcheck",
|
||||
"vadimcn.vscode-lldb"
|
||||
|
||||
+21
-4
@@ -1,12 +1,29 @@
|
||||
### 2024-05-25
|
||||
### 2024-08-01
|
||||
|
||||
### 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
|
||||
+ 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
|
||||
+ 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
|
||||
|
||||
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>
|
||||
### 2024-05-25
|
||||
|
||||
|
||||
Generated
+266
-184
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "oxker"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||
description = "A simple tui to view & control docker containers"
|
||||
@@ -27,19 +27,19 @@ similar_names = "allow"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bollard = "0.16"
|
||||
bollard = "0.17"
|
||||
cansi = "2.2"
|
||||
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
||||
crossterm = "0.27"
|
||||
crossterm = "0.28"
|
||||
directories = "5.0"
|
||||
futures-util = "0.3"
|
||||
parking_lot = { version = "0.12" }
|
||||
ratatui = "0.26"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
ratatui = "0.27"
|
||||
tokio = { version = "1.39", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
uuid = { version = "1.8", features = ["fast-rng", "v4"] }
|
||||
uuid = { version = "1.10", features = ["fast-rng", "v4"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -101,6 +101,7 @@ In application controls
|
||||
| ```( enter )```| Run selected docker command.|
|
||||
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
||||
| ```( 0 )``` | Stop sorting.|
|
||||
| ```( F1 )``` or ```( / )``` | Enter filter mode. |
|
||||
| ```( e )``` | Exec into the selected container - not available on Windows.|
|
||||
| ```( h )``` | Toggle help menu.|
|
||||
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
||||
@@ -167,10 +168,10 @@ cargo test
|
||||
|
||||
Run some example docker images
|
||||
|
||||
using docker-compose.yml;
|
||||
using docker/docker-compose.yml;
|
||||
|
||||
```shell
|
||||
docker compose -f docker-compose.yml up -d
|
||||
docker compose -f ./docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
or individually
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Builder ##
|
||||
#############
|
||||
|
||||
FROM --platform=linux/amd64 rust:slim as BUILDER
|
||||
FROM --platform=linux/amd64 rust:slim AS builder
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -49,12 +49,12 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
|
||||
## Runtime ##
|
||||
#############
|
||||
|
||||
FROM scratch as RUNTIME
|
||||
FROM scratch
|
||||
|
||||
# Set an ENV to indicate that we're running in a container
|
||||
ENV OXKER_RUNTIME=container
|
||||
|
||||
COPY --from=BUILDER /oxker /app/
|
||||
COPY --from=builder /oxker /app/
|
||||
|
||||
# Run the application
|
||||
# this is used in the application itself so DO NOT EDIT
|
||||
|
||||
+17
-16
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# rust create_release v0.5.5
|
||||
# rust create_release v0.5.6
|
||||
# 2024-07-27
|
||||
|
||||
STAR_LINE='****************************************'
|
||||
CWD=$(pwd)
|
||||
@@ -191,25 +192,25 @@ check_cross() {
|
||||
fi
|
||||
}
|
||||
|
||||
cargo_build_x86_linux() {
|
||||
cross_build_x86_linux() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
|
||||
cross build --target x86_64-unknown-linux-musl --release
|
||||
}
|
||||
|
||||
cargo_build_aarch64_linux() {
|
||||
cross_build_aarch64_linux() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
|
||||
cross build --target aarch64-unknown-linux-musl --release
|
||||
}
|
||||
|
||||
cargo_build_armv6_linux() {
|
||||
cross_build_armv6_linux() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
|
||||
cross build --target arm-unknown-linux-musleabihf --release
|
||||
}
|
||||
|
||||
cargo_build_x86_windows() {
|
||||
cross_build_x86_windows() {
|
||||
check_cross
|
||||
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
|
||||
cross build --target x86_64-pc-windows-gnu --release
|
||||
@@ -217,15 +218,15 @@ cargo_build_x86_windows() {
|
||||
|
||||
# Build all releases that GitHub workflow would
|
||||
# This will download GB's of docker images
|
||||
cargo_build_all() {
|
||||
cross_build_all() {
|
||||
cargo clean
|
||||
cargo_build_armv6_linux
|
||||
cross_build_armv6_linux
|
||||
ask_continue
|
||||
cargo_build_aarch64_linux
|
||||
cross_build_aarch64_linux
|
||||
ask_continue
|
||||
cargo_build_x86_linux
|
||||
cross_build_x86_linux
|
||||
ask_continue
|
||||
cargo_build_x86_windows
|
||||
cross_build_x86_windows
|
||||
ask_continue
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ release_flow() {
|
||||
get_git_remote_url
|
||||
|
||||
cargo_test
|
||||
cargo_build_all
|
||||
cross_build_all
|
||||
cargo_publish
|
||||
|
||||
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||
@@ -347,23 +348,23 @@ build_choice() {
|
||||
exit
|
||||
;;
|
||||
1)
|
||||
cargo_build_x86_linux
|
||||
cross_build_x86_linux
|
||||
exit
|
||||
;;
|
||||
2)
|
||||
cargo_build_aarch64_linux
|
||||
cross_build_aarch64_linux
|
||||
exit
|
||||
;;
|
||||
3)
|
||||
cargo_build_armv6_linux
|
||||
cross_build_armv6_linux
|
||||
exit
|
||||
;;
|
||||
4)
|
||||
cargo_build_x86_windows
|
||||
cross_build_x86_windows
|
||||
exit
|
||||
;;
|
||||
5)
|
||||
cargo_build_all
|
||||
cross_build_all
|
||||
exit
|
||||
;;
|
||||
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
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:alpine3.19
|
||||
image: postgres:alpine3.20
|
||||
container_name: postgres
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
limits:
|
||||
memory: 1024M
|
||||
redis:
|
||||
image: redis:alpine3.19
|
||||
image: redis:alpine3.20
|
||||
container_name: redis
|
||||
ipc: private
|
||||
restart: always
|
||||
@@ -39,5 +39,20 @@ services:
|
||||
resources:
|
||||
limits:
|
||||
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
|
||||
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
|
||||
install -Dm 755 oxker -t "${HOME}/.local/bin"
|
||||
rm "${OXKER_GZ}" oxker
|
||||
|
||||
+140
-39
@@ -10,6 +10,8 @@ use ratatui::{
|
||||
widgets::{ListItem, ListState},
|
||||
};
|
||||
|
||||
use crate::ui::ORANGE;
|
||||
|
||||
use super::Header;
|
||||
|
||||
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
|
||||
macro_rules! unit_struct {
|
||||
($name:ident) => {
|
||||
@@ -67,7 +72,7 @@ macro_rules! unit_struct {
|
||||
}
|
||||
}
|
||||
|
||||
impl$name {
|
||||
impl $name {
|
||||
pub fn get(&self) -> &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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if self.0.chars().count() >= 30 {
|
||||
write!(
|
||||
f,
|
||||
"{}…",
|
||||
self.0.chars().take(29).collect::<String>()
|
||||
)
|
||||
write!(f, "{}…", self.0.chars().take(29).collect::<String>())
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.0
|
||||
)
|
||||
write!(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)]
|
||||
pub enum RunningState {
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
/// States of the container
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum State {
|
||||
Dead,
|
||||
Exited,
|
||||
Paused,
|
||||
Removing,
|
||||
Restarting,
|
||||
Running,
|
||||
Running(RunningState),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub const fn is_alive(self) -> bool {
|
||||
matches!(self, Self::Running)
|
||||
matches!(self, Self::Running(_))
|
||||
}
|
||||
pub const fn get_color(self) -> Color {
|
||||
match self {
|
||||
Self::Paused => Color::Yellow,
|
||||
Self::Removing => Color::LightRed,
|
||||
Self::Restarting => Color::LightGreen,
|
||||
Self::Running => Color::Green,
|
||||
Self::Running(RunningState::Healthy) => Color::Green,
|
||||
Self::Running(RunningState::Unhealthy) => ORANGE,
|
||||
_ => Color::Red,
|
||||
}
|
||||
}
|
||||
/// Dirty way to create order for the state, rather than impl Ord
|
||||
pub const fn order(self) -> u8 {
|
||||
match self {
|
||||
Self::Running => 0,
|
||||
Self::Paused => 1,
|
||||
Self::Restarting => 2,
|
||||
Self::Removing => 3,
|
||||
Self::Exited => 4,
|
||||
Self::Dead => 5,
|
||||
Self::Unknown => 6,
|
||||
Self::Running(RunningState::Healthy) => 0,
|
||||
Self::Running(RunningState::Unhealthy) => 1,
|
||||
Self::Paused => 2,
|
||||
Self::Restarting => 3,
|
||||
Self::Removing => 4,
|
||||
Self::Exited => 5,
|
||||
Self::Dead => 6,
|
||||
Self::Unknown => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for State {
|
||||
fn from(input: &str) -> Self {
|
||||
/// Need status, to check if container is unhealthy or not
|
||||
impl From<(&str, &ContainerStatus)> for State {
|
||||
fn from((input, status): (&str, &ContainerStatus)) -> Self {
|
||||
match input {
|
||||
"dead" => Self::Dead,
|
||||
"exited" => Self::Exited,
|
||||
"paused" => Self::Paused,
|
||||
"removing" => Self::Removing,
|
||||
"restarting" => Self::Restarting,
|
||||
"running" => Self::Running,
|
||||
"running" => {
|
||||
if status.unhealthy() {
|
||||
Self::Running(RunningState::Unhealthy)
|
||||
} else {
|
||||
Self::Running(RunningState::Healthy)
|
||||
}
|
||||
}
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<String>> for State {
|
||||
fn from(input: Option<String>) -> Self {
|
||||
input.map_or(Self::Unknown, |input| Self::from(input.as_str()))
|
||||
/// Again, need status, to check if container is unhealthy or not
|
||||
impl From<(Option<String>, &ContainerStatus)> for State {
|
||||
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::Removing => "removing",
|
||||
Self::Restarting => "↻ restarting",
|
||||
Self::Running => "✓ running",
|
||||
Self::Running(RunningState::Healthy) => "✓ running",
|
||||
Self::Running(RunningState::Unhealthy) => "! running",
|
||||
Self::Unknown => "? unknown",
|
||||
};
|
||||
write!(f, "{disp}")
|
||||
@@ -310,7 +360,7 @@ impl DockerControls {
|
||||
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
||||
State::Paused => vec![Self::Resume, 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],
|
||||
}
|
||||
}
|
||||
@@ -539,7 +589,7 @@ pub struct ContainerItem {
|
||||
pub ports: Vec<ContainerPorts>,
|
||||
pub rx: ByteStats,
|
||||
pub state: State,
|
||||
pub status: String,
|
||||
pub status: ContainerStatus,
|
||||
pub tx: ByteStats,
|
||||
}
|
||||
|
||||
@@ -568,7 +618,7 @@ impl ContainerItem {
|
||||
name: String,
|
||||
ports: Vec<ContainerPorts>,
|
||||
state: State,
|
||||
status: String,
|
||||
status: ContainerStatus,
|
||||
) -> Self {
|
||||
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
||||
docker_controls.start();
|
||||
@@ -665,14 +715,14 @@ impl Columns {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
name: (Header::Name, 4),
|
||||
state: (Header::State, 11),
|
||||
status: (Header::Status, 16),
|
||||
cpu: (Header::Cpu, 7),
|
||||
state: (Header::State, 5),
|
||||
status: (Header::Status, 6),
|
||||
cpu: (Header::Cpu, 3),
|
||||
mem: (Header::Memory, 7, 7),
|
||||
id: (Header::Id, 8),
|
||||
image: (Header::Image, 5),
|
||||
net_rx: (Header::Rx, 7),
|
||||
net_tx: (Header::Tx, 7),
|
||||
net_rx: (Header::Rx, 4),
|
||||
net_tx: (Header::Tx, 4),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,11 +732,11 @@ mod tests {
|
||||
use ratatui::widgets::ListItem;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerImage, Logs},
|
||||
app_data::{ContainerImage, Logs, RunningState},
|
||||
ui::log_sanitizer,
|
||||
};
|
||||
|
||||
use super::{ByteStats, ContainerName, CpuStats, LogsTz};
|
||||
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
|
||||
|
||||
#[test]
|
||||
/// Display CpuStats as a string
|
||||
@@ -770,4 +820,55 @@ mod tests {
|
||||
|
||||
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 crate::{
|
||||
app_data::{AppData, ContainerId, DockerControls, State},
|
||||
app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State},
|
||||
app_error::AppError,
|
||||
parse_args::CliArgs,
|
||||
ui::{GuiState, Status},
|
||||
@@ -201,7 +201,7 @@ impl DockerData {
|
||||
/// 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
|
||||
/// 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
|
||||
.docker
|
||||
.list_containers(Some(ListContainersOptions::<String> {
|
||||
@@ -236,7 +236,15 @@ impl DockerData {
|
||||
output
|
||||
.into_iter()
|
||||
.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<_>>()
|
||||
}
|
||||
@@ -271,7 +279,7 @@ impl DockerData {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let docker = Arc::clone(&self.docker);
|
||||
let app_data = Arc::clone(&self.app_data);
|
||||
@@ -303,13 +311,14 @@ impl DockerData {
|
||||
};
|
||||
self.update_all_container_stats(&all_ids);
|
||||
self.app_data.lock().sort_containers();
|
||||
self.gui_state.lock().stop_loading_animation(Uuid::nil());
|
||||
}
|
||||
|
||||
/// Initialize docker container data, before any messages are received
|
||||
async fn initialise_container_data(&mut self) {
|
||||
self.gui_state.lock().status_push(Status::Init);
|
||||
let loading_uuid = Uuid::new_v4();
|
||||
let loading_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;
|
||||
|
||||
self.update_all_container_stats(&all_ids);
|
||||
@@ -323,9 +332,7 @@ impl DockerData {
|
||||
self.init = None;
|
||||
}
|
||||
}
|
||||
self.gui_state
|
||||
.lock()
|
||||
.stop_loading_animation(&loading_handle, loading_uuid);
|
||||
self.gui_state.lock().stop_loading_animation(loading_uuid);
|
||||
self.gui_state.lock().status_del(Status::Init);
|
||||
}
|
||||
|
||||
@@ -356,27 +363,27 @@ impl DockerData {
|
||||
}
|
||||
DockerMessage::Pause(id) => {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
DockerMessage::Restart(id) => {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
DockerMessage::Start(id) => {
|
||||
tokio::spawn(async move {
|
||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
||||
GuiState::start_loading_animation(&gui_state, uuid);
|
||||
if docker
|
||||
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
||||
.await
|
||||
@@ -384,33 +391,33 @@ impl DockerData {
|
||||
{
|
||||
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;
|
||||
}
|
||||
DockerMessage::Stop(id) => {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
DockerMessage::Resume(id) => {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
DockerMessage::Delete(id) => {
|
||||
tokio::spawn(async move {
|
||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
||||
GuiState::start_loading_animation(&gui_state, uuid);
|
||||
if docker
|
||||
.remove_container(
|
||||
id.get(),
|
||||
@@ -425,7 +432,7 @@ impl DockerData {
|
||||
{
|
||||
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.gui_state.lock().set_delete_container(None);
|
||||
|
||||
+7
-2
@@ -18,7 +18,7 @@ use tokio::{
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, State},
|
||||
app_data::{AppData, ContainerId, RunningState, State},
|
||||
app_error::AppError,
|
||||
};
|
||||
|
||||
@@ -162,7 +162,12 @@ impl ExecMode {
|
||||
let container = app_data.lock().get_selected_container_id_state_name();
|
||||
|
||||
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 let Ok(exec) = docker
|
||||
.create_exec(
|
||||
|
||||
+122
-69
@@ -71,6 +71,7 @@ impl InputHandler {
|
||||
Status::Error,
|
||||
Status::Help,
|
||||
Status::DeleteConfirm,
|
||||
Status::Filter,
|
||||
]) {
|
||||
self.mouse_press(mouse_event);
|
||||
}
|
||||
@@ -125,7 +126,7 @@ impl InputHandler {
|
||||
let is_oxker = self.app_data.lock().is_oxker();
|
||||
if !is_oxker && tty_readable() {
|
||||
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>>();
|
||||
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
|
||||
async fn s_key(&mut self) {
|
||||
async fn s_key(&self) {
|
||||
/// This is the inner workings, *inlined* here to return a Result
|
||||
async fn save_logs(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
@@ -248,7 +249,7 @@ impl InputHandler {
|
||||
self.gui_state.lock().status_push(log_status);
|
||||
|
||||
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)
|
||||
.await
|
||||
.is_err()
|
||||
@@ -260,12 +261,12 @@ impl InputHandler {
|
||||
);
|
||||
}
|
||||
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
|
||||
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
|
||||
let panel = self.gui_state.lock().get_selected_panel();
|
||||
if panel == SelectablePanel::Commands {
|
||||
@@ -306,7 +307,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Change the the "next" selectable panel
|
||||
fn tab_key(&mut self) {
|
||||
fn tab_key(&self) {
|
||||
let is_containers =
|
||||
self.gui_state.lock().get_selected_panel() == SelectablePanel::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
|
||||
fn back_tab_key(&mut self) {
|
||||
fn back_tab_key(&self) {
|
||||
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 {
|
||||
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 selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
@@ -343,7 +344,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// 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 selected_panel = self.gui_state.lock().get_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
|
||||
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
|
||||
let contains_delete = self
|
||||
@@ -365,78 +464,32 @@ impl InputHandler {
|
||||
let contains_error = contains(Status::Error);
|
||||
let contains_help = contains(Status::Help);
|
||||
let contains_exec = contains(Status::Exec);
|
||||
let contains_filter: bool = contains(Status::Filter);
|
||||
|
||||
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_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;
|
||||
}
|
||||
|
||||
if contains_error {
|
||||
match key_code {
|
||||
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
|
||||
self.app_data.lock().remove_error();
|
||||
self.gui_state.lock().status_del(Status::Error);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
self.handle_error(key_code);
|
||||
} else if contains_help {
|
||||
match key_code {
|
||||
KeyCode::Esc | KeyCode::Char('h' | 'H') => {
|
||||
self.gui_state.lock().status_del(Status::Help);
|
||||
}
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
_ => (),
|
||||
}
|
||||
self.handle_help(key_code);
|
||||
} else if contains_filter {
|
||||
self.handle_filter(key_code);
|
||||
} else if contains_delete {
|
||||
match key_code {
|
||||
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||
_ => (),
|
||||
}
|
||||
self.handle_delete(key_code).await;
|
||||
} else {
|
||||
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::Down | KeyCode::Char('j' | 'J') => self.next(),
|
||||
KeyCode::PageDown => {
|
||||
for _ in 0..=6 {
|
||||
self.next();
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => self.enter_key().await,
|
||||
_ => (),
|
||||
}
|
||||
self.handle_others(key_code).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let intersect = self.gui_state.lock().button_intersect(Rect::new(
|
||||
mouse_event.column,
|
||||
@@ -455,7 +508,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Handle mouse button events
|
||||
fn mouse_press(&mut self, mouse_event: MouseEvent) {
|
||||
fn mouse_press(&self, mouse_event: MouseEvent) {
|
||||
match mouse_event.kind {
|
||||
MouseEventKind::ScrollUp => self.previous(),
|
||||
MouseEventKind::ScrollDown => self.next(),
|
||||
@@ -481,7 +534,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// 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 selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
@@ -492,7 +545,7 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// 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 selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
|
||||
+13
-3
@@ -167,10 +167,18 @@ async fn main() {
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
|
||||
mod tests {
|
||||
use std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
vec,
|
||||
};
|
||||
|
||||
use bollard::service::{ContainerSummary, Port};
|
||||
|
||||
use crate::{
|
||||
app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList},
|
||||
app_data::{
|
||||
AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter,
|
||||
RunningState, State, StatefulList,
|
||||
},
|
||||
parse_args::CliArgs,
|
||||
};
|
||||
|
||||
@@ -201,16 +209,18 @@ mod tests {
|
||||
private: u16::try_from(index).unwrap_or(1) + 8000,
|
||||
public: None,
|
||||
}],
|
||||
State::Running,
|
||||
format!("Up {index} hour"),
|
||||
State::Running(RunningState::Healthy),
|
||||
ContainerStatus::from(format!("Up {index} hour")),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
||||
AppData {
|
||||
containers: StatefulList::new(containers.to_vec()),
|
||||
hidden_containers: vec![],
|
||||
error: None,
|
||||
sorted_by: None,
|
||||
filter: Filter::new(),
|
||||
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
|
||||
pub fn remove_ansi<'a>(input: &str) -> Vec<Line<'a>> {
|
||||
raw(&categorise_text(input)
|
||||
.into_iter()
|
||||
.map(|i| i.text)
|
||||
.collect::<String>())
|
||||
vec![Line::from(
|
||||
categorise_text(input)
|
||||
.into_iter()
|
||||
.map(|i| i.text)
|
||||
.collect::<String>()
|
||||
.trim()
|
||||
.to_owned(),
|
||||
)]
|
||||
}
|
||||
|
||||
/// create ratatui Lines that exactly match the given strings
|
||||
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
|
||||
@@ -62,7 +66,7 @@ pub mod log_sanitizer {
|
||||
CansiColor::Blue => Color::Blue,
|
||||
CansiColor::Magenta => Color::Magenta,
|
||||
CansiColor::Cyan => Color::Cyan,
|
||||
CansiColor::White | CansiColor::BrightWhite => Color::White,
|
||||
CansiColor::White | CansiColor::BrightWhite => Color::Gray,
|
||||
CansiColor::BrightRed => Color::LightRed,
|
||||
CansiColor::BrightGreen => Color::LightGreen,
|
||||
CansiColor::BrightYellow => Color::LightYellow,
|
||||
@@ -92,7 +96,7 @@ mod tests {
|
||||
let expected = vec![Line {
|
||||
spans: [Span {
|
||||
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(),
|
||||
}]
|
||||
@@ -111,7 +115,7 @@ mod tests {
|
||||
spans: vec![
|
||||
Span {
|
||||
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 {
|
||||
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,
|
||||
Error,
|
||||
Exec,
|
||||
Filter,
|
||||
Help,
|
||||
Init,
|
||||
Logs,
|
||||
}
|
||||
|
||||
/// Global gui_state, stored in an Arc<Mutex>
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiState {
|
||||
delete_container: Option<ContainerId>,
|
||||
delete_map: HashMap<DeleteButton, Rect>,
|
||||
heading_map: HashMap<Header, Rect>,
|
||||
is_loading: HashSet<Uuid>,
|
||||
loading_handle: Option<JoinHandle<()>>,
|
||||
loading_index: u8,
|
||||
loading_set: HashSet<Uuid>,
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
selected_panel: SelectablePanel,
|
||||
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
|
||||
pub fn button_intersect(&mut self, rect: Rect) -> Option<DeleteButton> {
|
||||
pub fn button_intersect(&self, rect: Rect) -> Option<DeleteButton> {
|
||||
self.delete_map
|
||||
.iter()
|
||||
.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
|
||||
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
|
||||
pub fn header_intersect(&self, rect: Rect) -> Option<Header> {
|
||||
self.heading_map
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
@@ -293,7 +295,7 @@ impl GuiState {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -325,45 +327,46 @@ impl GuiState {
|
||||
} else {
|
||||
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 ' '
|
||||
pub fn get_loading(&self) -> char {
|
||||
if self.is_loading.is_empty() {
|
||||
' '
|
||||
} else {
|
||||
if self.is_loading() {
|
||||
FRAMES[usize::from(self.loading_index)]
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the loading icon in its own Tokio thread
|
||||
pub fn start_loading_animation(
|
||||
gui_state: &Arc<Mutex<Self>>,
|
||||
loading_uuid: Uuid,
|
||||
) -> JoinHandle<()> {
|
||||
/// This should only be able to executed once, rather than multiple spawns
|
||||
pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
|
||||
if !gui_state.lock().is_loading() {
|
||||
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);
|
||||
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
|
||||
pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) {
|
||||
handle.abort();
|
||||
self.remove_loading(loading_uuid);
|
||||
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
|
||||
self.loading_set.remove(&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
|
||||
|
||||
+17
-5
@@ -32,6 +32,8 @@ use crate::{
|
||||
input_handler::InputMessages,
|
||||
};
|
||||
|
||||
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
||||
|
||||
pub struct Ui {
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
@@ -64,7 +66,6 @@ impl Ui {
|
||||
is_running: Arc<AtomicBool>,
|
||||
) {
|
||||
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 mut ui = Self {
|
||||
app_data,
|
||||
@@ -264,14 +265,20 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
||||
/// 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>>) {
|
||||
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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Max(1), Constraint::Min(1)].as_ref())
|
||||
.constraints(whole_constraints)
|
||||
.split(f.size());
|
||||
|
||||
// Split into 3, containers+controls, logs, then graphs
|
||||
// This one is the issue!
|
||||
let upper_main = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.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 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() {
|
||||
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
|
||||
if fd.has_containers {
|
||||
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
|
||||
if let Some(rect) = top_panel.get(1) {
|
||||
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)
|
||||
let max_lens = app_data.lock().get_longest_port();
|
||||
|
||||
Reference in New Issue
Block a user