chore: merge release-v0.9.0 into main
This commit is contained in:
+34
-6
@@ -1,14 +1,42 @@
|
|||||||
### 2024-10-22
|
### 2024-12-05
|
||||||
|
|
||||||
### Chores
|
### Chores
|
||||||
+ dependencies updated, [ea877d23711b98ffd1108a74206d93d43482d44d], [af609c0dbf0caab4a073f822166de34999afb41b]
|
+ dependencies updated, [b78713579c4706d605e5b35fcd832610a0152294], [c6200e8f77f8bb1f0152cb9374029d15cc45df9d]
|
||||||
+ .devcontainer updated, [a9844436d003b84a3e9d8b600ea029b232566f3a]
|
+ Rust 1.83 linting, [751d997a3dac823e144ae62e6c1455676e50ddb8]
|
||||||
+ create_release.sh updated, [c4943370f4a67f6c01c75a8a7f825912427666a2], [1389d8adbba75fef480eb1de09337eb7beb10ba3]
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
+ Add Stderr output to logs, thanks [vincentmasse](https://github.com/vincentmasse), closes #48, merges #49, [b95c9311416cd0dbcfa5de90c23f3065bc2d6b17], [9936ad45e186ee431aade920674a2dc283937355], [289ede3f2531feeec56094a76bf34f4c69431bbe]
|
+ `--no-stderr` cli arg, removes Standard error output from logs, closes #52, [c739637b91c8fa742a69f4d888678d7b3964678c]
|
||||||
|
+ ContainerPorts use ipaddr, [1b26997d25f748e0d452f41fe41791533046ecdf]
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
+ update containerised Dockerfile, [0c6f53228f01196e352c2069383ba1e7a10950a8]
|
||||||
|
+ calculate_usage overflow, [5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e]
|
||||||
|
+ DockerData spawns insertion error, [d4906d33c26b75d92e7d80040c488faa90a257c6]
|
||||||
|
|
||||||
### Refactors
|
### Refactors
|
||||||
+ Rust 1.82 linting, [c058c5a301cfd4e8d7a0079c4c3f8fdeae2803e5]
|
+ speed up docker logs init process, [8b9fe4246865441704ae12dff0938868a4fe6f81]
|
||||||
|
+ remove docker sleep, [f1562d1084336fe5be39894c93cb49107f0a4a6d]
|
||||||
|
+ dead code removed, [5ee48d5708fa6de0206c021db0bb611196e66fba], [ba6a95241389f99d504ee4bf3e87e19006f12e49], [f0b1145651625ad4e577d79baaf902d4d3bc0579]
|
||||||
|
+ input_handler, [7f4238349525c01ae9fb8b1f6c0946e5364dd55e]
|
||||||
|
+ statefulList get_state_title, [2d540b0e2210cc04d73035ec59211ffc739174f6]
|
||||||
|
+ statefulList next/previous, [7bb2bef28d90ebc58da86a0365a1904a0c32dffe]
|
||||||
|
+ help_box closure fn, [2860426d57a4458fcee49a2fd20e8e7bb9e71fb5]
|
||||||
|
+ use check_sub for sleep calculations, [fe3696e5576739d8b033d9e748b5ea696c4b4e4f]
|
||||||
|
+ rename scheduler to heartbeat, [68a6551ed038a36330b2f098112829465a1c3c7a]
|
||||||
|
+ remove unnecessary is_running load, [76ccf7c00691f815c3ab0bede838c99252ba84f0]
|
||||||
|
+ execute_command(), [2a834d6c2fa4a15124d24ddbd12f667829e148ad]
|
||||||
|
+ Remove numerous clones(), [e5927f781a7e9517b9fa00a2d1a835d2774a9d26]
|
||||||
|
+ remove app_data param from generate_lock(), [1a8dab654a1fdbf351a72dc54fe3d1943355bba6]
|
||||||
|
+ combine get_filter methods, [356ea5549bb4877e9893fe0e1053e73c5a62e806]
|
||||||
|
+ FrameData refactors, [57781701ff14c553dfbafb965ee8a33ab44dd36f], [6e2f82db81caaa98ce4781fa15928eb9e246ace6]
|
||||||
|
+ update_container_stat combine is_alive(), [55cc746736f6863aedc5ad838744a983796244d8]
|
||||||
|
+ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b700b9fde5578ae204992a67986d456ab]
|
||||||
|
+ pass `&FrameDate` into `draw_frame()`, [35aec5060fdbe606267be26656b4aeee43d50c02]
|
||||||
|
+ dead code removed, [caf23be4a7faff99aaca80b081a02e4e0a372009]
|
||||||
|
+ input_handler, [9c4f8910381b90b563da12eaba4b79cb60c40129]
|
||||||
|
+ draw_block, [de76bc22936b124dcb9646f302f6cc14691dbb63]
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
+ fix logs tests, [9b22f5da18e4bf92766a68a7f4cd61ad72724cfd]
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.9.0'>v0.9.0</a>
|
||||||
|
### 2024-12-05
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
+ dependencies updated, [b7871357](https://github.com/mrjackwills/oxker/commit/b78713579c4706d605e5b35fcd832610a0152294), [c6200e8f](https://github.com/mrjackwills/oxker/commit/c6200e8f77f8bb1f0152cb9374029d15cc45df9d)
|
||||||
|
+ Rust 1.83 linting, [751d997a](https://github.com/mrjackwills/oxker/commit/751d997a3dac823e144ae62e6c1455676e50ddb8)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ `--no-stderr` cli arg, removes Standard error output from logs, closes [#52](https://github.com/mrjackwills/oxker/issues/52), [c739637b](https://github.com/mrjackwills/oxker/commit/c739637b91c8fa742a69f4d888678d7b3964678c)
|
||||||
|
+ ContainerPorts use ipaddr, [1b26997d](https://github.com/mrjackwills/oxker/commit/1b26997d25f748e0d452f41fe41791533046ecdf)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
+ update containerised Dockerfile, [0c6f5322](https://github.com/mrjackwills/oxker/commit/0c6f53228f01196e352c2069383ba1e7a10950a8)
|
||||||
|
+ calculate_usage overflow, [5106a01f](https://github.com/mrjackwills/oxker/commit/5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e)
|
||||||
|
+ DockerData spawns insertion error, [d4906d33](https://github.com/mrjackwills/oxker/commit/d4906d33c26b75d92e7d80040c488faa90a257c6)
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
+ speed up docker logs init process, [8b9fe424](https://github.com/mrjackwills/oxker/commit/8b9fe4246865441704ae12dff0938868a4fe6f81)
|
||||||
|
+ remove docker sleep, [f1562d10](https://github.com/mrjackwills/oxker/commit/f1562d1084336fe5be39894c93cb49107f0a4a6d)
|
||||||
|
+ dead code removed, [5ee48d57](https://github.com/mrjackwills/oxker/commit/5ee48d5708fa6de0206c021db0bb611196e66fba), [ba6a9524](https://github.com/mrjackwills/oxker/commit/ba6a95241389f99d504ee4bf3e87e19006f12e49), [f0b11456](https://github.com/mrjackwills/oxker/commit/f0b1145651625ad4e577d79baaf902d4d3bc0579)
|
||||||
|
+ input_handler, [7f423834](https://github.com/mrjackwills/oxker/commit/7f4238349525c01ae9fb8b1f6c0946e5364dd55e)
|
||||||
|
+ statefulList get_state_title, [2d540b0e](https://github.com/mrjackwills/oxker/commit/2d540b0e2210cc04d73035ec59211ffc739174f6)
|
||||||
|
+ statefulList next/previous, [7bb2bef2](https://github.com/mrjackwills/oxker/commit/7bb2bef28d90ebc58da86a0365a1904a0c32dffe)
|
||||||
|
+ help_box closure fn, [2860426d](https://github.com/mrjackwills/oxker/commit/2860426d57a4458fcee49a2fd20e8e7bb9e71fb5)
|
||||||
|
+ use check_sub for sleep calculations, [fe3696e5](https://github.com/mrjackwills/oxker/commit/fe3696e5576739d8b033d9e748b5ea696c4b4e4f)
|
||||||
|
+ rename scheduler to heartbeat, [68a6551e](https://github.com/mrjackwills/oxker/commit/68a6551ed038a36330b2f098112829465a1c3c7a)
|
||||||
|
+ remove unnecessary is_running load, [76ccf7c0](https://github.com/mrjackwills/oxker/commit/76ccf7c00691f815c3ab0bede838c99252ba84f0)
|
||||||
|
+ execute_command(), [2a834d6c](https://github.com/mrjackwills/oxker/commit/2a834d6c2fa4a15124d24ddbd12f667829e148ad)
|
||||||
|
+ Remove numerous clones(), [e5927f78](https://github.com/mrjackwills/oxker/commit/e5927f781a7e9517b9fa00a2d1a835d2774a9d26)
|
||||||
|
+ remove app_data param from generate_lock(), [1a8dab65](https://github.com/mrjackwills/oxker/commit/1a8dab654a1fdbf351a72dc54fe3d1943355bba6)
|
||||||
|
+ combine get_filter methods, [356ea554](https://github.com/mrjackwills/oxker/commit/356ea5549bb4877e9893fe0e1053e73c5a62e806)
|
||||||
|
+ FrameData refactors, [57781701](https://github.com/mrjackwills/oxker/commit/57781701ff14c553dfbafb965ee8a33ab44dd36f), [6e2f82db](https://github.com/mrjackwills/oxker/commit/6e2f82db81caaa98ce4781fa15928eb9e246ace6)
|
||||||
|
+ update_container_stat combine is_alive(), [55cc7467](https://github.com/mrjackwills/oxker/commit/55cc746736f6863aedc5ad838744a983796244d8)
|
||||||
|
+ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b](https://github.com/mrjackwills/oxker/commit/69f6c96b700b9fde5578ae204992a67986d456ab)
|
||||||
|
+ pass `&FrameDate` into `draw_frame()`, [35aec506](https://github.com/mrjackwills/oxker/commit/35aec5060fdbe606267be26656b4aeee43d50c02)
|
||||||
|
+ dead code removed, [caf23be4](https://github.com/mrjackwills/oxker/commit/caf23be4a7faff99aaca80b081a02e4e0a372009)
|
||||||
|
+ input_handler, [9c4f8910](https://github.com/mrjackwills/oxker/commit/9c4f8910381b90b563da12eaba4b79cb60c40129)
|
||||||
|
+ draw_block, [de76bc22](https://github.com/mrjackwills/oxker/commit/de76bc22936b124dcb9646f302f6cc14691dbb63)
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
+ fix logs tests, [9b22f5da](https://github.com/mrjackwills/oxker/commit/9b22f5da18e4bf92766a68a7f4cd61ad72724cfd)
|
||||||
|
|
||||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.8.0'>v0.8.0</a>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.8.0'>v0.8.0</a>
|
||||||
### 2024-10-22
|
### 2024-10-22
|
||||||
|
|
||||||
|
|||||||
Generated
+476
-152
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.8.0"
|
version = "0.9.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,7 +27,7 @@ similar_names = "allow"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bollard = "0.17"
|
bollard = "0.18"
|
||||||
cansi = "2.2"
|
cansi = "2.2"
|
||||||
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
@@ -35,7 +35,7 @@ directories = "5.0"
|
|||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
parking_lot = { version = "0.12" }
|
parking_lot = { version = "0.12" }
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
tokio = { version = "1.41", features = ["full"] }
|
tokio = { version = "1.42", features = ["full"] }
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ Available command line arguments
|
|||||||
|```-s```| If running via Docker, will display the oxker container.|
|
|```-s```| If running via Docker, will display the oxker container.|
|
||||||
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
||||||
|```--host [string]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
|
|```--host [string]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
|
||||||
|
|```--no-stderr```| Do not include stderr output in logs.|
|
||||||
|```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
|
|```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
|
||||||
|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
|
|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
## Builder ##
|
## Builder ##
|
||||||
#############
|
#############
|
||||||
|
|
||||||
FROM --platform=linux/amd64 rust:slim AS builder
|
FROM --platform=$BUILDPLATFORM rust:slim AS builder
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
@@ -11,9 +11,9 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="aarch64-linux-gnu-gcc"
|
|||||||
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-lgcc"
|
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-lgcc"
|
||||||
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER="arm-linux-gnueabihf-ld"
|
ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER="arm-linux-gnueabihf-ld"
|
||||||
|
|
||||||
COPY ./containerised/platform.sh .
|
COPY ./containerised/target.sh .
|
||||||
|
|
||||||
RUN chmod +x ./platform.sh && ./platform.sh
|
RUN chmod +x ./target.sh && ./target.sh
|
||||||
|
|
||||||
RUN apt-get update && apt-get install $(cat /.compiler) -y
|
RUN apt-get update && apt-get install $(cat /.compiler) -y
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ COPY Cargo.* /usr/src/oxker/
|
|||||||
WORKDIR /usr/src/oxker
|
WORKDIR /usr/src/oxker
|
||||||
|
|
||||||
# Install target platform (Cross-Compilation)
|
# Install target platform (Cross-Compilation)
|
||||||
RUN rustup target add $(cat /.platform)
|
RUN rustup target add $(cat /.target)
|
||||||
|
|
||||||
# This is a dummy build to get the dependencies cached - probably not needed - as run via a github action
|
# This is a dummy build to get the dependencies cached - probably not needed - as run via a github action
|
||||||
RUN cargo build --target $(cat /.platform) --release
|
RUN cargo build --target $(cat /.target) --release
|
||||||
|
|
||||||
# Now copy in the rest of the sources
|
# Now copy in the rest of the sources
|
||||||
COPY src /usr/src/oxker/src/
|
COPY src /usr/src/oxker/src/
|
||||||
@@ -41,9 +41,9 @@ COPY src /usr/src/oxker/src/
|
|||||||
RUN touch /usr/src/oxker/src/main.rs
|
RUN touch /usr/src/oxker/src/main.rs
|
||||||
|
|
||||||
# This is the actual application build
|
# This is the actual application build
|
||||||
RUN cargo build --release --target $(cat /.platform)
|
RUN cargo build --release --target $(cat /.target)
|
||||||
|
|
||||||
RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
|
RUN cp /usr/src/oxker/target/$(cat /.target)/release/oxker /
|
||||||
|
|
||||||
#############
|
#############
|
||||||
## Runtime ##
|
## Runtime ##
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
case $TARGETARCH in
|
case $TARGETARCH in
|
||||||
|
|
||||||
"amd64")
|
"amd64")
|
||||||
echo "x86_64-unknown-linux-musl" >/.platform
|
echo "x86_64-unknown-linux-musl" >/.target
|
||||||
echo "" >/.compiler
|
echo "" >/.compiler
|
||||||
;;
|
;;
|
||||||
"arm64")
|
"arm64")
|
||||||
echo "aarch64-unknown-linux-musl" >/.platform
|
echo "aarch64-unknown-linux-musl" >/.target
|
||||||
echo "gcc-aarch64-linux-gnu" >/.compiler
|
echo "gcc-aarch64-linux-gnu" >/.compiler
|
||||||
;;
|
;;
|
||||||
"arm")
|
"arm")
|
||||||
echo "arm-unknown-linux-musleabihf" >/.platform
|
echo "arm-unknown-linux-musleabihf" >/.target
|
||||||
echo "gcc-arm-linux-gnueabihf" >/.compiler
|
echo "gcc-arm-linux-gnueabihf" >/.compiler
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -2,6 +2,7 @@ use std::{
|
|||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
collections::{HashSet, VecDeque},
|
collections::{HashSet, VecDeque},
|
||||||
fmt,
|
fmt,
|
||||||
|
net::IpAddr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bollard::service::Port;
|
use bollard::service::Port;
|
||||||
@@ -103,17 +104,17 @@ macro_rules! unit_struct {
|
|||||||
unit_struct!(ContainerName);
|
unit_struct!(ContainerName);
|
||||||
unit_struct!(ContainerImage);
|
unit_struct!(ContainerImage);
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ContainerPorts {
|
pub struct ContainerPorts {
|
||||||
pub ip: Option<String>,
|
pub ip: Option<IpAddr>,
|
||||||
pub private: u16,
|
pub private: u16,
|
||||||
pub public: Option<u16>,
|
pub public: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Port> for ContainerPorts {
|
impl From<Port> for ContainerPorts {
|
||||||
fn from(value: &Port) -> Self {
|
fn from(value: Port) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ip: value.ip.clone(),
|
ip: value.ip.and_then(|i| i.parse::<IpAddr>().ok()),
|
||||||
private: value.private_port,
|
private: value.private_port,
|
||||||
public: value.public_port,
|
public: value.public_port,
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,9 @@ impl From<&Port> for ContainerPorts {
|
|||||||
|
|
||||||
impl ContainerPorts {
|
impl ContainerPorts {
|
||||||
pub fn len_ip(&self) -> usize {
|
pub fn len_ip(&self) -> usize {
|
||||||
self.ip.as_ref().unwrap_or(&String::new()).chars().count()
|
self.ip
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |i| i.to_string().chars().count())
|
||||||
}
|
}
|
||||||
pub fn len_private(&self) -> usize {
|
pub fn len_private(&self) -> usize {
|
||||||
format!("{}", self.private).chars().count()
|
format!("{}", self.private).chars().count()
|
||||||
@@ -133,11 +136,12 @@ impl ContainerPorts {
|
|||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print(&self) -> (String, String, String) {
|
/// Return as tuple of Strings, ip address, private port, and public port
|
||||||
|
pub fn get_all(&self) -> (String, String, String) {
|
||||||
(
|
(
|
||||||
self.ip
|
self.ip
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(String::new(), std::borrow::ToOwned::to_owned),
|
.map_or(String::new(), std::string::ToString::to_string),
|
||||||
format!("{}", self.private),
|
format!("{}", self.private),
|
||||||
self.public.map_or(String::new(), |s| s.to_string()),
|
self.public.map_or(String::new(), |s| s.to_string()),
|
||||||
)
|
)
|
||||||
@@ -171,27 +175,25 @@ impl<T> StatefulList<T> {
|
|||||||
|
|
||||||
pub fn next(&mut self) {
|
pub fn next(&mut self) {
|
||||||
if !self.items.is_empty() {
|
if !self.items.is_empty() {
|
||||||
let i = match self.state.selected() {
|
self.state.select(Some(self.state.selected().map_or(0, |i| {
|
||||||
Some(i) => {
|
if i < self.items.len() - 1 {
|
||||||
if i < self.items.len() - 1 {
|
i + 1
|
||||||
i + 1
|
} else {
|
||||||
} else {
|
i
|
||||||
i
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => 0,
|
})));
|
||||||
};
|
|
||||||
self.state.select(Some(i));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous(&mut self) {
|
pub fn previous(&mut self) {
|
||||||
if !self.items.is_empty() {
|
if !self.items.is_empty() {
|
||||||
let i = self
|
self.state.select(Some(self.state.selected().map_or(0, |i| {
|
||||||
.state
|
if i == 0 {
|
||||||
.selected()
|
0
|
||||||
.map_or(0, |i| if i == 0 { 0 } else { i - 1 });
|
} else {
|
||||||
self.state.select(Some(i));
|
i - 1
|
||||||
|
}
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,11 +203,11 @@ impl<T> StatefulList<T> {
|
|||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
let len = self.items.len();
|
let len = self.items.len();
|
||||||
let c = self
|
let count = self
|
||||||
.state
|
.state
|
||||||
.selected()
|
.selected()
|
||||||
.map_or(0, |value| if len > 0 { value + 1 } else { value });
|
.map_or(0, |value| if len > 0 { value + 1 } else { value });
|
||||||
format!(" {c}/{}", self.items.len())
|
format!(" {count}/{len}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,9 +261,12 @@ pub enum State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
/// The container is alive if the start is Running, either healthy or unhealthy
|
||||||
pub const fn is_alive(self) -> bool {
|
pub const fn is_alive(self) -> bool {
|
||||||
matches!(self, Self::Running(_))
|
matches!(self, Self::Running(_))
|
||||||
}
|
}
|
||||||
|
/// Color of the state for the containers section
|
||||||
|
/// TODO allow usable editable colours
|
||||||
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,
|
||||||
@@ -333,7 +338,7 @@ impl fmt::Display for State {
|
|||||||
|
|
||||||
/// Items for the container control list
|
/// Items for the container control list
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum DockerControls {
|
pub enum DockerCommand {
|
||||||
Pause,
|
Pause,
|
||||||
Restart,
|
Restart,
|
||||||
Start,
|
Start,
|
||||||
@@ -342,7 +347,7 @@ pub enum DockerControls {
|
|||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DockerControls {
|
impl DockerCommand {
|
||||||
pub const fn get_color(self) -> Color {
|
pub const fn get_color(self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Self::Pause => Color::Yellow,
|
Self::Pause => Color::Yellow,
|
||||||
@@ -366,7 +371,7 @@ impl DockerControls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DockerControls {
|
impl fmt::Display for DockerCommand {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let disp = match self {
|
let disp = match self {
|
||||||
Self::Pause => "pause",
|
Self::Pause => "pause",
|
||||||
@@ -577,7 +582,7 @@ impl Logs {
|
|||||||
pub struct ContainerItem {
|
pub struct ContainerItem {
|
||||||
pub cpu_stats: VecDeque<CpuStats>,
|
pub cpu_stats: VecDeque<CpuStats>,
|
||||||
pub created: u64,
|
pub created: u64,
|
||||||
pub docker_controls: StatefulList<DockerControls>,
|
pub docker_controls: StatefulList<DockerCommand>,
|
||||||
pub id: ContainerId,
|
pub id: ContainerId,
|
||||||
pub image: ContainerImage,
|
pub image: ContainerImage,
|
||||||
pub is_oxker: bool,
|
pub is_oxker: bool,
|
||||||
@@ -620,7 +625,7 @@ impl ContainerItem {
|
|||||||
state: State,
|
state: State,
|
||||||
status: ContainerStatus,
|
status: ContainerStatus,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
let mut docker_controls = StatefulList::new(DockerCommand::gen_vec(state));
|
||||||
docker_controls.start();
|
docker_controls.start();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
+95
-97
@@ -143,10 +143,10 @@ impl AppData {
|
|||||||
Self {
|
Self {
|
||||||
args,
|
args,
|
||||||
containers: StatefulList::new(vec![]),
|
containers: StatefulList::new(vec![]),
|
||||||
hidden_containers: vec![],
|
|
||||||
error: None,
|
error: None,
|
||||||
sorted_by: None,
|
|
||||||
filter: Filter::new(),
|
filter: Filter::new(),
|
||||||
|
hidden_containers: vec![],
|
||||||
|
sorted_by: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,15 +160,9 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Filter related methods
|
/// Filter related methods
|
||||||
|
/// Get the filterby and filter_term
|
||||||
/// Get the current filter term
|
pub const fn get_filter(&self) -> (FilterBy, Option<&String>) {
|
||||||
pub const fn get_filter_term(&self) -> Option<&String> {
|
(self.filter.by, self.filter.term.as_ref())
|
||||||
self.filter.term.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current filter by choice
|
|
||||||
pub const fn get_filter_by(&self) -> FilterBy {
|
|
||||||
self.filter.by
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by
|
/// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by
|
||||||
@@ -252,7 +246,7 @@ impl AppData {
|
|||||||
self.filter_containers();
|
self.filter_containers();
|
||||||
}
|
}
|
||||||
|
|
||||||
// change the filter_by option
|
/// change the filter_by option
|
||||||
pub fn filter_by_next(&mut self) {
|
pub fn filter_by_next(&mut self) {
|
||||||
if let Some(by) = self.filter.by.next() {
|
if let Some(by) = self.filter.by.next() {
|
||||||
self.filter.by = by;
|
self.filter.by = by;
|
||||||
@@ -260,7 +254,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// change the filter_by option
|
/// change the filter_by option
|
||||||
pub fn filter_by_prev(&mut self) {
|
pub fn filter_by_prev(&mut self) {
|
||||||
if let Some(by) = self.filter.by.prev() {
|
if let Some(by) = self.filter.by.prev() {
|
||||||
self.filter.by = by;
|
self.filter.by = by;
|
||||||
@@ -280,7 +274,6 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Container sort related methods
|
/// Container sort related methods
|
||||||
|
|
||||||
/// Change the sorted order, also set the selected container state to match new order
|
/// Change the sorted order, also set the selected container state to match new order
|
||||||
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
||||||
self.sorted_by = x;
|
self.sorted_by = x;
|
||||||
@@ -350,7 +343,6 @@ impl AppData {
|
|||||||
.back()
|
.back()
|
||||||
.cmp(&item_ord.1.mem_stats.back())
|
.cmp(&item_ord.1.mem_stats.back())
|
||||||
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
||||||
|
|
||||||
Header::Id => item_ord
|
Header::Id => item_ord
|
||||||
.0
|
.0
|
||||||
.id
|
.id
|
||||||
@@ -372,7 +364,6 @@ impl AppData {
|
|||||||
.tx
|
.tx
|
||||||
.cmp(&item_ord.1.tx)
|
.cmp(&item_ord.1.tx)
|
||||||
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
||||||
|
|
||||||
Header::Name => item_ord
|
Header::Name => item_ord
|
||||||
.0
|
.0
|
||||||
.name
|
.name
|
||||||
@@ -392,19 +383,26 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Container state methods
|
/// Container state methods
|
||||||
|
|
||||||
/// Get the total number of none "hidden" containers
|
/// Get the total number of none "hidden" containers
|
||||||
pub fn get_container_len(&self) -> usize {
|
pub fn get_container_len(&self) -> usize {
|
||||||
self.containers.items.len()
|
self.containers.items.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_all_id_state(&self) -> Vec<(State, ContainerId)> {
|
||||||
|
self.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| (i.state, i.id.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all the ContainerItems
|
/// Get all the ContainerItems
|
||||||
pub fn get_container_items(&self) -> &[ContainerItem] {
|
pub fn get_container_items(&self) -> &[ContainerItem] {
|
||||||
&self.containers.items
|
&self.containers.items
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get title for containers section, add a suffix indicating if the containers are currently under filter
|
/// Get title for containers section, add a suffix indicating if the containers are currently under filter
|
||||||
pub fn container_title(&self) -> String {
|
pub fn get_container_title(&self) -> String {
|
||||||
let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() {
|
let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() {
|
||||||
" - filtered"
|
" - filtered"
|
||||||
} else {
|
} else {
|
||||||
@@ -447,43 +445,41 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public)
|
/// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public)
|
||||||
|
///display like this: "│ ip, private, public│", so (5,10,9) are the minimum lengths required
|
||||||
pub fn get_longest_port(&self) -> (usize, usize, usize) {
|
pub fn get_longest_port(&self) -> (usize, usize, usize) {
|
||||||
let mut longest_ip = 5;
|
let mut output = (5, 10, 9);
|
||||||
let mut longest_private = 10;
|
|
||||||
let mut longest_public = 9;
|
|
||||||
|
|
||||||
for item in [&self.containers.items, &self.hidden_containers] {
|
for item in [&self.containers.items, &self.hidden_containers] {
|
||||||
for item in item {
|
for item in item {
|
||||||
longest_ip = longest_ip.max(
|
output.0 = output.0.max(
|
||||||
item.ports
|
item.ports
|
||||||
.iter()
|
.iter()
|
||||||
.map(ContainerPorts::len_ip)
|
.map(ContainerPorts::len_ip)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(3),
|
.unwrap_or(output.0),
|
||||||
);
|
);
|
||||||
longest_private = longest_private.max(
|
output.1 = output.1.max(
|
||||||
item.ports
|
item.ports
|
||||||
.iter()
|
.iter()
|
||||||
.map(ContainerPorts::len_private)
|
.map(ContainerPorts::len_private)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(8),
|
.unwrap_or(output.1),
|
||||||
);
|
);
|
||||||
longest_public = longest_public.max(
|
output.2 = output.2.max(
|
||||||
item.ports
|
item.ports
|
||||||
.iter()
|
.iter()
|
||||||
.map(ContainerPorts::len_public)
|
.map(ContainerPorts::len_public)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(6),
|
.unwrap_or(output.2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
output
|
||||||
(longest_ip, longest_private, longest_public)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Option of the current selected container's ports, sorted by private port
|
/// Get Option of the current selected container's ports, sorted by private port
|
||||||
pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> {
|
pub fn get_selected_ports(&self) -> Option<(Vec<ContainerPorts>, State)> {
|
||||||
if let Some(item) = self.get_mut_selected_container() {
|
if let Some(item) = self.get_selected_container() {
|
||||||
let mut ports = item.ports.clone();
|
let mut ports = item.ports.clone();
|
||||||
ports.sort_by(|a, b| a.private.cmp(&b.private));
|
ports.sort_by(|a, b| a.private.cmp(&b.private));
|
||||||
return Some((ports, item.state));
|
return Some((ports, item.state));
|
||||||
@@ -510,12 +506,12 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the ContainerName of by ID
|
/// Get the ContainerName of by ID
|
||||||
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<ContainerName> {
|
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<&ContainerName> {
|
||||||
self.containers
|
self.containers
|
||||||
.items
|
.items
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|i| &i.id == id)
|
.find(|i| &i.id == id)
|
||||||
.map(|i| i.name.clone())
|
.map(|i| &i.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the id of the currently selected container.
|
/// Find the id of the currently selected container.
|
||||||
@@ -532,10 +528,9 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Selected DockerCommand methods
|
/// Selected DockerCommand methods
|
||||||
|
|
||||||
/// Get the current selected docker command
|
/// Get the current selected docker command
|
||||||
/// So know which command to execute
|
/// So know which command to execute
|
||||||
pub fn selected_docker_controls(&self) -> Option<DockerControls> {
|
pub fn selected_docker_controls(&self) -> Option<DockerCommand> {
|
||||||
self.get_selected_container().and_then(|i| {
|
self.get_selected_container().and_then(|i| {
|
||||||
i.docker_controls.state.selected().and_then(|x| {
|
i.docker_controls.state.selected().and_then(|x| {
|
||||||
i.docker_controls
|
i.docker_controls
|
||||||
@@ -574,21 +569,19 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mutable Option of the currently selected container DockerControls state
|
/// Get mutable Option of the currently selected container DockerCommand state
|
||||||
pub fn get_control_state(&mut self) -> Option<&mut ListState> {
|
pub fn get_control_state(&mut self) -> Option<&mut ListState> {
|
||||||
self.get_mut_selected_container()
|
self.get_mut_selected_container()
|
||||||
.map(|i| &mut i.docker_controls.state)
|
.map(|i| &mut i.docker_controls.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mutable Option of the currently selected container DockerControls items
|
/// Get mutable Option of the currently selected container DockerConmand items
|
||||||
/// TODO command or control, need a uniform name across the application
|
pub fn get_control_items(&mut self) -> Option<&mut Vec<DockerCommand>> {
|
||||||
pub fn get_control_items(&mut self) -> Option<&mut Vec<DockerControls>> {
|
|
||||||
self.get_mut_selected_container()
|
self.get_mut_selected_container()
|
||||||
.map(|i| &mut i.docker_controls.items)
|
.map(|i| &mut i.docker_controls.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logs related methods
|
/// Logs related methods
|
||||||
|
|
||||||
/// Get the title for log panel for selected container, will be either
|
/// Get the title for log panel for selected container, will be either
|
||||||
/// 1) "logs x/x - container_name - container_image"
|
/// 1) "logs x/x - container_name - container_image"
|
||||||
/// 2) "logs - container_name - container_image" when no logs found
|
/// 2) "logs - container_name - container_image" when no logs found
|
||||||
@@ -635,11 +628,11 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get mutable Vec of current containers logs
|
/// Get mutable Vec of current containers logs
|
||||||
pub fn get_logs(&mut self) -> Vec<ListItem<'static>> {
|
pub fn get_logs(&self) -> Vec<ListItem<'static>> {
|
||||||
self.containers
|
self.containers
|
||||||
.state
|
.state
|
||||||
.selected()
|
.selected()
|
||||||
.and_then(|i| self.containers.items.get_mut(i))
|
.and_then(|i| self.containers.items.get(i))
|
||||||
.map_or(vec![], |i| i.logs.to_vec())
|
.map_or(vec![], |i| i.logs.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,18 +646,16 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Chart data related methods
|
/// Chart data related methods
|
||||||
|
|
||||||
/// Get mutable Option of the currently selected container chart data
|
/// Get mutable Option of the currently selected container chart data
|
||||||
pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> {
|
pub fn get_chart_data(&self) -> Option<(CpuTuple, MemTuple)> {
|
||||||
self.containers
|
self.containers
|
||||||
.state
|
.state
|
||||||
.selected()
|
.selected()
|
||||||
.and_then(|i| self.containers.items.get_mut(i))
|
.and_then(|i| self.containers.items.get(i))
|
||||||
.map(|i| i.get_chart_data())
|
.map(container_state::ContainerItem::get_chart_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error related methods
|
/// Error related methods
|
||||||
|
|
||||||
/// Get single app_state error
|
/// Get single app_state error
|
||||||
pub const fn get_error(&self) -> Option<AppError> {
|
pub const fn get_error(&self) -> Option<AppError> {
|
||||||
self.error
|
self.error
|
||||||
@@ -701,7 +692,6 @@ impl AppData {
|
|||||||
let mut columns = Columns::new();
|
let mut columns = Columns::new();
|
||||||
let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12);
|
let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12);
|
||||||
|
|
||||||
// Should probably find a refactor here somewhere
|
|
||||||
for container in [&self.containers.items, &self.hidden_containers] {
|
for container in [&self.containers.items, &self.hidden_containers] {
|
||||||
for container in container {
|
for container in container {
|
||||||
let cpu_count = container.cpu_stats.back().map_or_else(
|
let cpu_count = container.cpu_stats.back().map_or_else(
|
||||||
@@ -729,7 +719,6 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update related methods
|
/// Update related methods
|
||||||
|
|
||||||
/// Get mutable reference to a container in the containers vec & the hidden_containers vec
|
/// Get mutable reference to a container in the containers vec & the hidden_containers vec
|
||||||
fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||||
if self.get_hidden_container_by_id(id).is_some() {
|
if self.get_hidden_container_by_id(id).is_some() {
|
||||||
@@ -769,12 +758,11 @@ impl AppData {
|
|||||||
container.tx.update(tx);
|
container.tx.update(tx);
|
||||||
container.mem_limit.update(mem_limit);
|
container.mem_limit.update(mem_limit);
|
||||||
}
|
}
|
||||||
// need to benchmark this?
|
|
||||||
self.sort_containers();
|
self.sort_containers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update, or insert, containers
|
/// Update, or insert, containers
|
||||||
pub fn update_containers(&mut self, all_containers: &mut [ContainerSummary]) {
|
pub fn update_containers(&mut self, mut all_containers: Vec<ContainerSummary>) {
|
||||||
let all_ids = self
|
let all_ids = self
|
||||||
.containers
|
.containers
|
||||||
.items
|
.items
|
||||||
@@ -809,7 +797,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in all_containers {
|
for mut i in all_containers {
|
||||||
if let Some(id) = i.id.as_ref() {
|
if let Some(id) = i.id.as_ref() {
|
||||||
let name = i.names.as_mut().map_or(String::new(), |names| {
|
let name = i.names.as_mut().map_or(String::new(), |names| {
|
||||||
names.first_mut().map_or(String::new(), |f| {
|
names.first_mut().map_or(String::new(), |f| {
|
||||||
@@ -820,8 +808,8 @@ impl AppData {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let ports = i.ports.as_ref().map_or(vec![], |i| {
|
let ports = i.ports.map_or(vec![], |i| {
|
||||||
i.iter().map(ContainerPorts::from).collect::<Vec<_>>()
|
i.into_iter().map(ContainerPorts::from).collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
let id = ContainerId::from(id.as_str());
|
let id = ContainerId::from(id.as_str());
|
||||||
@@ -855,7 +843,7 @@ impl AppData {
|
|||||||
item.status = status;
|
item.status = status;
|
||||||
};
|
};
|
||||||
if item.state != state {
|
if item.state != state {
|
||||||
item.docker_controls.items = DockerControls::gen_vec(state);
|
item.docker_controls.items = DockerCommand::gen_vec(state);
|
||||||
// Update the list state, needs to be None if the gen_vec returns an empty vec
|
// Update the list state, needs to be None if the gen_vec returns an empty vec
|
||||||
match state {
|
match state {
|
||||||
State::Removing | State::Restarting | State::Unknown => {
|
State::Removing | State::Restarting | State::Unknown => {
|
||||||
@@ -1476,7 +1464,7 @@ mod tests {
|
|||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
let result = app_data.get_container_name_by_id(&ContainerId::from("2"));
|
let result = app_data.get_container_name_by_id(&ContainerId::from("2"));
|
||||||
assert_eq!(result, Some(ContainerName::from("container_2")));
|
assert_eq!(result, Some(&ContainerName::from("container_2")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1526,7 +1514,7 @@ mod tests {
|
|||||||
app_data.docker_controls_start();
|
app_data.docker_controls_start();
|
||||||
|
|
||||||
let result = app_data.selected_docker_controls();
|
let result = app_data.selected_docker_controls();
|
||||||
assert_eq!(result, Some(DockerControls::Pause));
|
assert_eq!(result, Some(DockerCommand::Pause));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1539,7 +1527,7 @@ mod tests {
|
|||||||
app_data.docker_controls_next();
|
app_data.docker_controls_next();
|
||||||
|
|
||||||
let result = app_data.selected_docker_controls();
|
let result = app_data.selected_docker_controls();
|
||||||
assert_eq!(result, Some(DockerControls::Restart));
|
assert_eq!(result, Some(DockerCommand::Restart));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1551,12 +1539,12 @@ mod tests {
|
|||||||
app_data.docker_controls_end();
|
app_data.docker_controls_end();
|
||||||
|
|
||||||
let result = app_data.selected_docker_controls();
|
let result = app_data.selected_docker_controls();
|
||||||
assert_eq!(result, Some(DockerControls::Delete));
|
assert_eq!(result, Some(DockerCommand::Delete));
|
||||||
|
|
||||||
// Next has no effect when at end
|
// Next has no effect when at end
|
||||||
app_data.docker_controls_next();
|
app_data.docker_controls_next();
|
||||||
let result = app_data.selected_docker_controls();
|
let result = app_data.selected_docker_controls();
|
||||||
assert_eq!(result, Some(DockerControls::Delete));
|
assert_eq!(result, Some(DockerCommand::Delete));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1569,19 +1557,19 @@ mod tests {
|
|||||||
app_data.docker_controls_previous();
|
app_data.docker_controls_previous();
|
||||||
|
|
||||||
let result = app_data.selected_docker_controls();
|
let result = app_data.selected_docker_controls();
|
||||||
assert_eq!(result, Some(DockerControls::Stop));
|
assert_eq!(result, Some(DockerCommand::Stop));
|
||||||
|
|
||||||
// previous has no effect when at start
|
// previous has no effect when at start
|
||||||
app_data.docker_controls_start();
|
app_data.docker_controls_start();
|
||||||
app_data.docker_controls_previous();
|
app_data.docker_controls_previous();
|
||||||
let result = app_data.selected_docker_controls();
|
let result = app_data.selected_docker_controls();
|
||||||
assert_eq!(result, Some(DockerControls::Pause));
|
assert_eq!(result, Some(DockerCommand::Pause));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// DockerCommands get correct controls dependant on container state
|
/// DockerCommands get correct controls dependant on container state
|
||||||
fn test_app_data_get_control_items() {
|
fn test_app_data_get_control_items() {
|
||||||
let test_state = |state: State, expected: &mut Vec<DockerControls>| {
|
let test_state = |state: State, expected: &mut Vec<DockerCommand>| {
|
||||||
let gen_item_state = |state: State| {
|
let gen_item_state = |state: State| {
|
||||||
ContainerItem::new(
|
ContainerItem::new(
|
||||||
1,
|
1,
|
||||||
@@ -1605,42 +1593,42 @@ mod tests {
|
|||||||
test_state(
|
test_state(
|
||||||
State::Dead,
|
State::Dead,
|
||||||
&mut vec![
|
&mut vec![
|
||||||
DockerControls::Start,
|
DockerCommand::Start,
|
||||||
DockerControls::Restart,
|
DockerCommand::Restart,
|
||||||
DockerControls::Delete,
|
DockerCommand::Delete,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
test_state(
|
test_state(
|
||||||
State::Exited,
|
State::Exited,
|
||||||
&mut vec![
|
&mut vec![
|
||||||
DockerControls::Start,
|
DockerCommand::Start,
|
||||||
DockerControls::Restart,
|
DockerCommand::Restart,
|
||||||
DockerControls::Delete,
|
DockerCommand::Delete,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
test_state(
|
test_state(
|
||||||
State::Paused,
|
State::Paused,
|
||||||
&mut vec![
|
&mut vec![
|
||||||
DockerControls::Resume,
|
DockerCommand::Resume,
|
||||||
DockerControls::Stop,
|
DockerCommand::Stop,
|
||||||
DockerControls::Delete,
|
DockerCommand::Delete,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
test_state(State::Removing, &mut vec![DockerControls::Delete]);
|
test_state(State::Removing, &mut vec![DockerCommand::Delete]);
|
||||||
test_state(
|
test_state(
|
||||||
State::Restarting,
|
State::Restarting,
|
||||||
&mut vec![DockerControls::Stop, DockerControls::Delete],
|
&mut vec![DockerCommand::Stop, DockerCommand::Delete],
|
||||||
);
|
);
|
||||||
test_state(
|
test_state(
|
||||||
State::Running(RunningState::Healthy),
|
State::Running(RunningState::Healthy),
|
||||||
&mut vec![
|
&mut vec![
|
||||||
DockerControls::Pause,
|
DockerCommand::Pause,
|
||||||
DockerControls::Restart,
|
DockerCommand::Restart,
|
||||||
DockerControls::Stop,
|
DockerCommand::Stop,
|
||||||
DockerControls::Delete,
|
DockerCommand::Delete,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
test_state(State::Unknown, &mut vec![DockerControls::Delete]);
|
test_state(State::Unknown, &mut vec![DockerCommand::Delete]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ****** //
|
// ****** //
|
||||||
@@ -1654,13 +1642,13 @@ mod tests {
|
|||||||
|
|
||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter().1.is_none());
|
||||||
|
|
||||||
let pre_len = app_data.containers.items.len();
|
let pre_len = app_data.containers.items.len();
|
||||||
app_data.filter_term_push('_');
|
app_data.filter_term_push('_');
|
||||||
app_data.filter_term_push('2');
|
app_data.filter_term_push('2');
|
||||||
|
|
||||||
assert_eq!(app_data.get_filter_term(), Some(&"_2".to_string()));
|
assert_eq!(app_data.get_filter().1, Some(&"_2".to_string()));
|
||||||
|
|
||||||
app_data.filter_containers();
|
app_data.filter_containers();
|
||||||
let post_len = app_data.containers.items.len();
|
let post_len = app_data.containers.items.len();
|
||||||
@@ -1680,7 +1668,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter().1.is_none());
|
||||||
|
|
||||||
let pre_len = app_data.containers.items.len();
|
let pre_len = app_data.containers.items.len();
|
||||||
for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] {
|
for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] {
|
||||||
@@ -1689,8 +1677,10 @@ mod tests {
|
|||||||
// app_data.filter_term_push('2');
|
// app_data.filter_term_push('2');
|
||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
|
|
||||||
assert_eq!(app_data.get_filter_by(), FilterBy::Image);
|
assert_eq!(
|
||||||
assert_eq!(app_data.get_filter_term(), Some(&"image_2".to_string()));
|
app_data.get_filter(),
|
||||||
|
(FilterBy::Image, Some(&"image_2".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
app_data.filter_containers();
|
app_data.filter_containers();
|
||||||
let post_len = app_data.containers.items.len();
|
let post_len = app_data.containers.items.len();
|
||||||
@@ -1709,7 +1699,7 @@ mod tests {
|
|||||||
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter().1.is_none());
|
||||||
|
|
||||||
let pre_len = app_data.containers.items.len();
|
let pre_len = app_data.containers.items.len();
|
||||||
app_data.filter_term_push('x');
|
app_data.filter_term_push('x');
|
||||||
@@ -1717,8 +1707,10 @@ mod tests {
|
|||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
|
|
||||||
assert_eq!(app_data.get_filter_by(), FilterBy::Status);
|
assert_eq!(
|
||||||
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
|
app_data.get_filter(),
|
||||||
|
(FilterBy::Status, Some(&"x".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
app_data.filter_containers();
|
app_data.filter_containers();
|
||||||
let post_len = app_data.containers.items.len();
|
let post_len = app_data.containers.items.len();
|
||||||
@@ -1737,7 +1729,7 @@ mod tests {
|
|||||||
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter().1.is_none());
|
||||||
|
|
||||||
let pre_len = app_data.containers.items.len();
|
let pre_len = app_data.containers.items.len();
|
||||||
app_data.filter_term_push('x');
|
app_data.filter_term_push('x');
|
||||||
@@ -1746,8 +1738,10 @@ mod tests {
|
|||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
|
|
||||||
assert_eq!(app_data.get_filter_by(), FilterBy::All);
|
assert_eq!(
|
||||||
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
|
app_data.get_filter(),
|
||||||
|
(FilterBy::All, Some(&"x".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
app_data.filter_containers();
|
app_data.filter_containers();
|
||||||
let post_len = app_data.containers.items.len();
|
let post_len = app_data.containers.items.len();
|
||||||
@@ -1766,7 +1760,7 @@ mod tests {
|
|||||||
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter().1.is_none());
|
||||||
|
|
||||||
let pre_len = app_data.containers.items.len();
|
let pre_len = app_data.containers.items.len();
|
||||||
app_data.filter_term_push('x');
|
app_data.filter_term_push('x');
|
||||||
@@ -1774,8 +1768,10 @@ mod tests {
|
|||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
app_data.filter_by_next();
|
app_data.filter_by_next();
|
||||||
|
|
||||||
assert_eq!(app_data.get_filter_by(), FilterBy::Status);
|
assert_eq!(
|
||||||
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
|
app_data.get_filter(),
|
||||||
|
(FilterBy::Status, Some(&"x".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
app_data.filter_containers();
|
app_data.filter_containers();
|
||||||
let post_len = app_data.containers.items.len();
|
let post_len = app_data.containers.items.len();
|
||||||
@@ -1787,8 +1783,10 @@ mod tests {
|
|||||||
assert!(!app_data.can_insert(&containers[2]));
|
assert!(!app_data.can_insert(&containers[2]));
|
||||||
|
|
||||||
app_data.filter_by_prev();
|
app_data.filter_by_prev();
|
||||||
assert_eq!(app_data.get_filter_by(), FilterBy::Image);
|
assert_eq!(
|
||||||
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
|
app_data.get_filter(),
|
||||||
|
(FilterBy::Image, Some(&"x".to_string()))
|
||||||
|
);
|
||||||
|
|
||||||
app_data.filter_containers();
|
app_data.filter_containers();
|
||||||
let post_len = app_data.containers.items.len();
|
let post_len = app_data.containers.items.len();
|
||||||
@@ -2230,12 +2228,12 @@ mod tests {
|
|||||||
let (_ids, containers) = gen_containers();
|
let (_ids, containers) = gen_containers();
|
||||||
let mut app_data = gen_appdata(&containers);
|
let mut app_data = gen_appdata(&containers);
|
||||||
let result_pre = app_data.get_container_items().to_owned();
|
let result_pre = app_data.get_container_items().to_owned();
|
||||||
let mut input = [
|
let input = vec![
|
||||||
gen_container_summary(1, "paused"),
|
gen_container_summary(1, "paused"),
|
||||||
gen_container_summary(2, "dead"),
|
gen_container_summary(2, "dead"),
|
||||||
];
|
];
|
||||||
|
|
||||||
app_data.update_containers(&mut input);
|
app_data.update_containers(input);
|
||||||
let result_post = app_data.get_container_items().to_owned();
|
let result_post = app_data.get_container_items().to_owned();
|
||||||
assert_ne!(result_pre, result_post);
|
assert_ne!(result_pre, result_post);
|
||||||
assert_eq!(result_post[0].state, State::Paused);
|
assert_eq!(result_post[0].state, State::Paused);
|
||||||
|
|||||||
+2
-7
@@ -1,16 +1,13 @@
|
|||||||
use crate::app_data::DockerControls;
|
use crate::app_data::DockerCommand;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
/// app errors to set in global state
|
/// app errors to set in global state
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
DockerCommand(DockerControls),
|
DockerCommand(DockerCommand),
|
||||||
DockerExec,
|
DockerExec,
|
||||||
DockerLogs,
|
DockerLogs,
|
||||||
DockerConnect,
|
DockerConnect,
|
||||||
DockerInterval,
|
|
||||||
InputPoll,
|
|
||||||
MouseCapture(bool),
|
MouseCapture(bool),
|
||||||
Terminal,
|
Terminal,
|
||||||
}
|
}
|
||||||
@@ -23,8 +20,6 @@ impl fmt::Display for AppError {
|
|||||||
Self::DockerExec => write!(f, "Unable to exec into container"),
|
Self::DockerExec => write!(f, "Unable to exec into container"),
|
||||||
Self::DockerLogs => write!(f, "Unable to save logs"),
|
Self::DockerLogs => write!(f, "Unable to save logs"),
|
||||||
Self::DockerConnect => write!(f, "Unable to access docker daemon"),
|
Self::DockerConnect => write!(f, "Unable to access docker daemon"),
|
||||||
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
|
|
||||||
Self::InputPoll => write!(f, "Unable to poll user input"),
|
|
||||||
Self::MouseCapture(x) => {
|
Self::MouseCapture(x) => {
|
||||||
let reason = if *x { "en" } else { "dis" };
|
let reason = if *x { "en" } else { "dis" };
|
||||||
write!(f, "Unable to {reason}able mouse capture")
|
write!(f, "Unable to {reason}able mouse capture")
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::app_data::ContainerId;
|
use crate::app_data::{ContainerId, DockerCommand};
|
||||||
use bollard::Docker;
|
use bollard::Docker;
|
||||||
use tokio::sync::oneshot::Sender;
|
use tokio::sync::oneshot::Sender;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DockerMessage {
|
pub enum DockerMessage {
|
||||||
ConfirmDelete(ContainerId),
|
ConfirmDelete(ContainerId),
|
||||||
Delete(ContainerId),
|
Control((DockerCommand, ContainerId)),
|
||||||
Exec(Sender<Arc<Docker>>),
|
Exec(Sender<Arc<Docker>>),
|
||||||
Pause(ContainerId),
|
|
||||||
Quit,
|
|
||||||
Restart(ContainerId),
|
|
||||||
Start(ContainerId),
|
|
||||||
Stop(ContainerId),
|
|
||||||
Resume(ContainerId),
|
|
||||||
Update,
|
Update,
|
||||||
}
|
}
|
||||||
|
|||||||
+390
-317
@@ -10,10 +10,7 @@ use futures_util::StreamExt;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
atomic::{AtomicBool, AtomicUsize},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::{Receiver, Sender},
|
sync::mpsc::{Receiver, Sender},
|
||||||
@@ -22,7 +19,7 @@ use tokio::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State},
|
app_data::{AppData, ContainerId, DockerCommand, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
parse_args::CliArgs,
|
parse_args::CliArgs,
|
||||||
ui::{GuiState, Status},
|
ui::{GuiState, Status},
|
||||||
@@ -37,6 +34,15 @@ enum SpawnId {
|
|||||||
Log(ContainerId),
|
Log(ContainerId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SpawnId {
|
||||||
|
/// Extract the &ContainerId out of self
|
||||||
|
const fn get_id(&self) -> &ContainerId {
|
||||||
|
match self {
|
||||||
|
Self::Log(id) | Self::Stats((id, _)) => id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time
|
/// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time
|
||||||
/// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap
|
/// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap
|
||||||
/// Binate value is toggled when all handles have been spawned off
|
/// Binate value is toggled when all handles have been spawned off
|
||||||
@@ -62,8 +68,6 @@ pub struct DockerData {
|
|||||||
binate: Binate,
|
binate: Binate,
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
|
||||||
init: Option<Arc<AtomicUsize>>,
|
|
||||||
receiver: Receiver<DockerMessage>,
|
receiver: Receiver<DockerMessage>,
|
||||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||||
}
|
}
|
||||||
@@ -71,24 +75,30 @@ pub struct DockerData {
|
|||||||
impl DockerData {
|
impl DockerData {
|
||||||
/// Use docker stats to calculate current cpu usage
|
/// Use docker stats to calculate current cpu usage
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
// TODO FIX: this can overflow
|
|
||||||
fn calculate_usage(stats: &Stats) -> f64 {
|
fn calculate_usage(stats: &Stats) -> f64 {
|
||||||
let mut cpu_percentage = 0.0;
|
let mut cpu_percentage = 0.0;
|
||||||
let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
|
let cpu_delta = stats
|
||||||
let cpu_delta = stats.cpu_stats.cpu_usage.total_usage as f64 - previous_cpu as f64;
|
.cpu_stats
|
||||||
|
.cpu_usage
|
||||||
|
.total_usage
|
||||||
|
.saturating_sub(stats.precpu_stats.cpu_usage.total_usage)
|
||||||
|
as f64;
|
||||||
|
|
||||||
if let (Some(cpu_stats_usage), Some(precpu_stats_usage)) = (
|
if let (Some(cpu_stats_usage), Some(precpu_stats_usage)) = (
|
||||||
stats.cpu_stats.system_cpu_usage,
|
stats.cpu_stats.system_cpu_usage,
|
||||||
stats.precpu_stats.system_cpu_usage,
|
stats.precpu_stats.system_cpu_usage,
|
||||||
) {
|
) {
|
||||||
let system_delta = (cpu_stats_usage - precpu_stats_usage) as f64;
|
let system_delta = cpu_stats_usage.saturating_sub(precpu_stats_usage) as f64;
|
||||||
let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| {
|
let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| {
|
||||||
stats
|
u64::try_from(
|
||||||
.cpu_stats
|
stats
|
||||||
.cpu_usage
|
.cpu_stats
|
||||||
.percpu_usage
|
.cpu_usage
|
||||||
.as_ref()
|
.percpu_usage
|
||||||
.map_or(0, std::vec::Vec::len) as u64
|
.as_ref()
|
||||||
|
.map_or(0, std::vec::Vec::len),
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
}) as f64;
|
}) as f64;
|
||||||
if system_delta > 0.0 && cpu_delta > 0.0 {
|
if system_delta > 0.0 && cpu_delta > 0.0 {
|
||||||
cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0;
|
cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0;
|
||||||
@@ -103,97 +113,86 @@ impl DockerData {
|
|||||||
async fn update_container_stat(
|
async fn update_container_stat(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
id: ContainerId,
|
|
||||||
init: Option<(Arc<AtomicUsize>, usize)>,
|
|
||||||
state: State,
|
state: State,
|
||||||
spawn_id: SpawnId,
|
spawn_id: SpawnId,
|
||||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||||
) {
|
) {
|
||||||
if state.is_alive() || init.is_some() {
|
let id = spawn_id.get_id();
|
||||||
let mut stream = docker
|
let mut stream = docker
|
||||||
.stats(
|
.stats(
|
||||||
id.get(),
|
id.get(),
|
||||||
Some(StatsOptions {
|
Some(StatsOptions {
|
||||||
stream: false,
|
stream: false,
|
||||||
one_shot: false,
|
one_shot: false,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.take(1);
|
.take(1);
|
||||||
|
|
||||||
while let Some(Ok(stats)) = stream.next().await {
|
while let Some(Ok(stats)) = stream.next().await {
|
||||||
// Memory stats are only collected if the container is alive - is this the behaviour we want?
|
// Memory stats are only collected if the container is alive - is this the behaviour we want?
|
||||||
let mem_stat = if state.is_alive() {
|
let (mem_stat, cpu_stats) = if state.is_alive() {
|
||||||
let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i {
|
let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i {
|
||||||
MemoryStatsStats::V1(x) => x.inactive_file,
|
MemoryStatsStats::V1(x) => x.inactive_file,
|
||||||
MemoryStatsStats::V2(x) => x.inactive_file,
|
MemoryStatsStats::V2(x) => x.inactive_file,
|
||||||
});
|
});
|
||||||
|
(
|
||||||
Some(
|
Some(
|
||||||
stats
|
stats
|
||||||
.memory_stats
|
.memory_stats
|
||||||
.usage
|
.usage
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.saturating_sub(mem_cache),
|
.saturating_sub(mem_cache),
|
||||||
)
|
),
|
||||||
} else {
|
Some(Self::calculate_usage(&stats)),
|
||||||
None
|
)
|
||||||
};
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
|
let op_key = stats
|
||||||
|
.networks
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|networks| networks.keys().next().cloned());
|
||||||
|
|
||||||
let op_key = stats
|
let (rx, tx) = if let Some(key) = op_key {
|
||||||
|
stats
|
||||||
.networks
|
.networks
|
||||||
.as_ref()
|
.unwrap_or_default()
|
||||||
.and_then(|networks| networks.keys().next().cloned());
|
.get(&key)
|
||||||
|
.map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
let cpu_stats = if state.is_alive() {
|
app_data.lock().update_stats_by_id(
|
||||||
Some(Self::calculate_usage(&stats))
|
id,
|
||||||
} else {
|
cpu_stats,
|
||||||
None
|
mem_stat,
|
||||||
};
|
stats.memory_stats.limit.unwrap_or_default(),
|
||||||
let (rx, tx) = if let Some(key) = op_key {
|
rx,
|
||||||
stats
|
tx,
|
||||||
.networks
|
);
|
||||||
.unwrap_or_default()
|
|
||||||
.get(&key)
|
|
||||||
.map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
|
|
||||||
} else {
|
|
||||||
(0, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
app_data
|
|
||||||
.lock()
|
|
||||||
.update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
spawns.lock().remove(&spawn_id);
|
spawns.lock().remove(&spawn_id);
|
||||||
if let Some((target, _)) = init {
|
|
||||||
target.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all stats, spawn each container into own tokio::spawn thread
|
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||||
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
fn update_all_container_stats(&mut self) {
|
||||||
|
let all_ids = self.app_data.lock().get_all_id_state();
|
||||||
for (state, id) in all_ids {
|
for (state, id) in all_ids {
|
||||||
let docker = Arc::clone(&self.docker);
|
let spawn_id = SpawnId::Stats((id, self.binate));
|
||||||
let app_data = Arc::clone(&self.app_data);
|
|
||||||
let spawns = Arc::clone(&self.spawns);
|
|
||||||
let spawn_id = SpawnId::Stats((id.clone(), self.binate));
|
|
||||||
|
|
||||||
let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len()));
|
if let std::collections::hash_map::Entry::Vacant(spawns) =
|
||||||
self.spawns
|
self.spawns.lock().entry(spawn_id.clone())
|
||||||
.lock()
|
{
|
||||||
.entry(spawn_id.clone())
|
spawns.insert(tokio::spawn(Self::update_container_stat(
|
||||||
.or_insert_with(|| {
|
Arc::clone(&self.app_data),
|
||||||
tokio::spawn(Self::update_container_stat(
|
Arc::clone(&self.docker),
|
||||||
app_data,
|
state,
|
||||||
docker,
|
spawn_id,
|
||||||
id.clone(),
|
Arc::clone(&self.spawns),
|
||||||
init,
|
)));
|
||||||
*state,
|
}
|
||||||
spawn_id,
|
|
||||||
spawns,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
self.binate = self.binate.toggle();
|
self.binate = self.binate.toggle();
|
||||||
}
|
}
|
||||||
@@ -201,7 +200,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(&self) -> Vec<(State, ContainerId)> {
|
async fn update_all_containers(&self) {
|
||||||
let containers = self
|
let containers = self
|
||||||
.docker
|
.docker
|
||||||
.list_containers(Some(ListContainersOptions::<String> {
|
.list_containers(Some(ListContainersOptions::<String> {
|
||||||
@@ -211,7 +210,7 @@ impl DockerData {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut output = containers
|
let output = containers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|f| match f.id {
|
.filter_map(|f| match f.id {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
@@ -230,23 +229,7 @@ impl DockerData {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<ContainerSummary>>();
|
.collect::<Vec<ContainerSummary>>();
|
||||||
|
|
||||||
self.app_data.lock().update_containers(&mut output);
|
self.app_data.lock().update_containers(output);
|
||||||
|
|
||||||
// Just get the containers that are currently running, or being restarted, no point updating info on paused or dead containers
|
|
||||||
output
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|i| {
|
|
||||||
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<_>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update single container logs
|
/// Update single container logs
|
||||||
@@ -257,10 +240,11 @@ impl DockerData {
|
|||||||
id: ContainerId,
|
id: ContainerId,
|
||||||
since: u64,
|
since: u64,
|
||||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||||
|
stderr: bool,
|
||||||
) {
|
) {
|
||||||
let options = Some(LogsOptions::<String> {
|
let options = Some(LogsOptions::<String> {
|
||||||
stdout: true,
|
stdout: true,
|
||||||
stderr: true,
|
stderr,
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
since: i64::try_from(since).unwrap_or_default(),
|
since: i64::try_from(since).unwrap_or_default(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -275,44 +259,29 @@ impl DockerData {
|
|||||||
output.push(data);
|
output.push(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spawns.lock().remove(&SpawnId::Log(id.clone()));
|
|
||||||
app_data.lock().update_log_by_id(output, &id);
|
app_data.lock().update_log_by_id(output, &id);
|
||||||
|
spawns.lock().remove(&SpawnId::Log(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(&self, all_ids: &[(State, ContainerId)]) {
|
fn init_all_logs(&self, all_ids: Vec<(State, ContainerId)>) -> Arc<AtomicUsize> {
|
||||||
|
let init = Arc::new(AtomicUsize::new(0));
|
||||||
for (_, id) in all_ids {
|
for (_, id) in all_ids {
|
||||||
|
let app_data: Arc<parking_lot::lock_api::Mutex<parking_lot::RawMutex, AppData>> =
|
||||||
|
Arc::clone(&self.app_data);
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
|
||||||
let spawns = Arc::clone(&self.spawns);
|
let spawns = Arc::clone(&self.spawns);
|
||||||
let key = SpawnId::Log(id.clone());
|
let std_err = self.args.std_err;
|
||||||
|
let init = Arc::clone(&init);
|
||||||
self.spawns.lock().insert(
|
self.spawns.lock().insert(
|
||||||
key,
|
SpawnId::Log(id.clone()),
|
||||||
tokio::spawn(Self::update_log(app_data, docker, id.clone(), 0, spawns)),
|
tokio::spawn(async move {
|
||||||
|
Self::update_log(app_data, docker, id, 0, spawns, std_err).await;
|
||||||
|
init.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
init
|
||||||
|
|
||||||
/// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed)
|
|
||||||
async fn update_everything(&mut self) {
|
|
||||||
let all_ids = self.update_all_containers().await;
|
|
||||||
if let Some(container) = self.app_data.lock().get_selected_container() {
|
|
||||||
let last_updated = container.last_updated;
|
|
||||||
self.spawns
|
|
||||||
.lock()
|
|
||||||
.entry(SpawnId::Log(container.id.clone()))
|
|
||||||
.or_insert_with(|| {
|
|
||||||
// MAYBE make a struct that can create this data?
|
|
||||||
let app_data = Arc::clone(&self.app_data);
|
|
||||||
let docker = Arc::clone(&self.docker);
|
|
||||||
let id = container.id.clone();
|
|
||||||
let spawns = Arc::clone(&self.spawns);
|
|
||||||
tokio::spawn(Self::update_log(app_data, docker, id, last_updated, spawns))
|
|
||||||
});
|
|
||||||
};
|
|
||||||
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
|
/// Initialize docker container data, before any messages are received
|
||||||
@@ -320,27 +289,48 @@ impl DockerData {
|
|||||||
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();
|
||||||
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_containers().await;
|
||||||
|
let all_ids = self.app_data.lock().get_all_id_state();
|
||||||
|
let all_ids_len = all_ids.len();
|
||||||
|
let init = self.init_all_logs(all_ids);
|
||||||
|
self.update_all_container_stats();
|
||||||
|
|
||||||
self.update_all_container_stats(&all_ids);
|
while init.load(std::sync::atomic::Ordering::SeqCst) != all_ids_len {
|
||||||
|
|
||||||
self.init_all_logs(&all_ids);
|
|
||||||
|
|
||||||
while let Some(x) = self.init.as_ref() {
|
|
||||||
self.app_data.lock().sort_containers();
|
self.app_data.lock().sort_containers();
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
if x.load(std::sync::atomic::Ordering::SeqCst) == all_ids.len() {
|
|
||||||
self.init = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.gui_state.lock().stop_loading_animation(loading_uuid);
|
self.gui_state.lock().stop_loading_animation(loading_uuid);
|
||||||
self.gui_state.lock().status_del(Status::Init);
|
self.gui_state.lock().status_del(Status::Init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed)
|
||||||
|
async fn update_everything(&mut self) {
|
||||||
|
self.update_all_containers().await;
|
||||||
|
if let Some(container) = self.app_data.lock().get_selected_container() {
|
||||||
|
let last_updated = container.last_updated;
|
||||||
|
let spawn_id = SpawnId::Log(container.id.clone());
|
||||||
|
// Only spawn if not already spawned with a given id/binate pair
|
||||||
|
if let std::collections::hash_map::Entry::Vacant(spawns) =
|
||||||
|
self.spawns.lock().entry(spawn_id)
|
||||||
|
{
|
||||||
|
spawns.insert(tokio::spawn(Self::update_log(
|
||||||
|
Arc::clone(&self.app_data),
|
||||||
|
Arc::clone(&self.docker),
|
||||||
|
container.id.clone(),
|
||||||
|
last_updated,
|
||||||
|
Arc::clone(&self.spawns),
|
||||||
|
self.args.std_err,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.update_all_container_stats();
|
||||||
|
self.app_data.lock().sort_containers();
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the global error as the docker error, and set gui_state to error
|
/// Set the global error as the docker error, and set gui_state to error
|
||||||
fn set_error(
|
fn set_error(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
error: DockerControls,
|
error: DockerCommand,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
) {
|
) {
|
||||||
app_data
|
app_data
|
||||||
@@ -348,150 +338,102 @@ impl DockerData {
|
|||||||
.set_error(AppError::DockerCommand(error), gui_state, Status::Error);
|
.set_error(AppError::DockerCommand(error), gui_state, Status::Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute docker commands (start, stop etc) on it's own tokio thread
|
||||||
|
async fn execute_command(&mut self, control: DockerCommand, id: ContainerId) {
|
||||||
|
let (app_data, docker, gui_state) = (
|
||||||
|
Arc::clone(&self.app_data),
|
||||||
|
Arc::clone(&self.docker),
|
||||||
|
Arc::clone(&self.gui_state),
|
||||||
|
);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
|
if match control {
|
||||||
|
DockerCommand::Delete => {
|
||||||
|
docker
|
||||||
|
.remove_container(
|
||||||
|
id.get(),
|
||||||
|
Some(RemoveContainerOptions {
|
||||||
|
v: false,
|
||||||
|
force: true,
|
||||||
|
link: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
DockerCommand::Pause => docker.pause_container(id.get()).await,
|
||||||
|
DockerCommand::Restart => docker.restart_container(id.get(), None).await,
|
||||||
|
DockerCommand::Resume => docker.unpause_container(id.get()).await,
|
||||||
|
DockerCommand::Start => {
|
||||||
|
docker
|
||||||
|
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
DockerCommand::Stop => docker.stop_container(id.get(), None).await,
|
||||||
|
}
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
Self::set_error(&app_data, control, &gui_state);
|
||||||
|
}
|
||||||
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.update_everything().await;
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle incoming messages, container controls & all container information update
|
/// Handle incoming messages, container controls & all container information update
|
||||||
/// Spawn Docker commands off into own thread
|
/// Spawn Docker commands off into own thread
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
async fn message_handler(&mut self) {
|
async fn message_handler(&mut self) {
|
||||||
while let Some(message) = self.receiver.recv().await {
|
while let Some(message) = self.receiver.recv().await {
|
||||||
let docker = Arc::clone(&self.docker);
|
|
||||||
let gui_state = Arc::clone(&self.gui_state);
|
|
||||||
let app_data = Arc::clone(&self.app_data);
|
|
||||||
let uuid = Uuid::new_v4();
|
|
||||||
// TODO need to refactor these
|
|
||||||
match message {
|
match message {
|
||||||
DockerMessage::Exec(docker_tx) => {
|
|
||||||
docker_tx.send(Arc::clone(&self.docker)).ok();
|
|
||||||
}
|
|
||||||
DockerMessage::Pause(id) => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
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(uuid);
|
|
||||||
});
|
|
||||||
self.update_everything().await;
|
|
||||||
}
|
|
||||||
DockerMessage::Restart(id) => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
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(uuid);
|
|
||||||
});
|
|
||||||
self.update_everything().await;
|
|
||||||
}
|
|
||||||
DockerMessage::Start(id) => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
GuiState::start_loading_animation(&gui_state, uuid);
|
|
||||||
if docker
|
|
||||||
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
Self::set_error(&app_data, DockerControls::Start, &gui_state);
|
|
||||||
}
|
|
||||||
gui_state.lock().stop_loading_animation(uuid);
|
|
||||||
});
|
|
||||||
self.update_everything().await;
|
|
||||||
}
|
|
||||||
DockerMessage::Stop(id) => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
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(uuid);
|
|
||||||
});
|
|
||||||
self.update_everything().await;
|
|
||||||
}
|
|
||||||
DockerMessage::Resume(id) => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
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(uuid);
|
|
||||||
});
|
|
||||||
self.update_everything().await;
|
|
||||||
}
|
|
||||||
DockerMessage::Delete(id) => {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
GuiState::start_loading_animation(&gui_state, uuid);
|
|
||||||
if docker
|
|
||||||
.remove_container(
|
|
||||||
id.get(),
|
|
||||||
Some(RemoveContainerOptions {
|
|
||||||
v: false,
|
|
||||||
force: true,
|
|
||||||
link: false,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
|
||||||
}
|
|
||||||
gui_state.lock().stop_loading_animation(uuid);
|
|
||||||
});
|
|
||||||
self.update_everything().await;
|
|
||||||
self.gui_state.lock().set_delete_container(None);
|
|
||||||
}
|
|
||||||
DockerMessage::ConfirmDelete(id) => {
|
DockerMessage::ConfirmDelete(id) => {
|
||||||
self.gui_state.lock().set_delete_container(Some(id));
|
self.gui_state.lock().set_delete_container(Some(id));
|
||||||
}
|
}
|
||||||
DockerMessage::Update => self.update_everything().await,
|
DockerMessage::Control((command, id)) => self.execute_command(command, id).await,
|
||||||
DockerMessage::Quit => {
|
DockerMessage::Exec(docker_tx) => {
|
||||||
self.spawns
|
docker_tx.send(Arc::clone(&self.docker)).ok();
|
||||||
.lock()
|
|
||||||
.values()
|
|
||||||
.for_each(tokio::task::JoinHandle::abort);
|
|
||||||
self.is_running
|
|
||||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
}
|
}
|
||||||
|
DockerMessage::Update => self.update_everything().await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send an update message every x ms, where x is the args.docker_interval
|
/// Send an update message every x ms, where x is the args.docker_interval
|
||||||
fn scheduler(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
|
fn heartbeat(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
|
||||||
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
|
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
|
||||||
let mut now = std::time::Instant::now();
|
let mut now = std::time::Instant::now();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
let to_sleep = update_duration.saturating_sub(now.elapsed());
|
|
||||||
tokio::time::sleep(to_sleep).await;
|
|
||||||
docker_tx.send(DockerMessage::Update).await.ok();
|
docker_tx.send(DockerMessage::Update).await.ok();
|
||||||
|
if let Some(to_sleep) = update_duration.checked_sub(now.elapsed()) {
|
||||||
|
tokio::time::sleep(to_sleep).await;
|
||||||
|
}
|
||||||
now = std::time::Instant::now();
|
now = std::time::Instant::now();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialise self, and start the message receiving loop
|
/// Initialise self, and start the message receiving loop
|
||||||
pub async fn init(
|
pub async fn start(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker: Docker,
|
docker: Docker,
|
||||||
docker_rx: Receiver<DockerMessage>,
|
docker_rx: Receiver<DockerMessage>,
|
||||||
docker_tx: Sender<DockerMessage>,
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
|
||||||
) {
|
) {
|
||||||
let args = app_data.lock().args.clone();
|
let args = app_data.lock().args.clone();
|
||||||
if app_data.lock().get_error().is_none() {
|
if app_data.lock().get_error().is_none() {
|
||||||
let mut inner = Self {
|
let mut inner = Self {
|
||||||
app_data,
|
app_data,
|
||||||
args: args.clone(),
|
args,
|
||||||
binate: Binate::One,
|
binate: Binate::One,
|
||||||
docker: Arc::new(docker),
|
docker: Arc::new(docker),
|
||||||
gui_state,
|
gui_state,
|
||||||
init: Some(Arc::new(AtomicUsize::new(0))),
|
|
||||||
is_running,
|
|
||||||
receiver: docker_rx,
|
receiver: docker_rx,
|
||||||
spawns: Arc::new(Mutex::new(HashMap::new())),
|
spawns: Arc::new(Mutex::new(HashMap::new())),
|
||||||
};
|
};
|
||||||
inner.initialise_container_data().await;
|
inner.initialise_container_data().await;
|
||||||
Self::scheduler(&args, docker_tx);
|
Self::heartbeat(&inner.args, docker_tx);
|
||||||
inner.message_handler().await;
|
inner.message_handler().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,19 +441,19 @@ impl DockerData {
|
|||||||
|
|
||||||
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
|
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use bollard::container::{
|
use bollard::container::{
|
||||||
BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData,
|
BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, Stats, StorageStats, ThrottlingData,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
fn gen_stats() -> Stats {
|
||||||
fn gen_stats(x: u64, y: u64) -> Stats {
|
|
||||||
Stats {
|
Stats {
|
||||||
read: String::new(),
|
read: String::new(),
|
||||||
preread: String::new(),
|
preread: String::new(),
|
||||||
num_procs: 0,
|
num_procs: 1,
|
||||||
pids_stats: PidsStats {
|
pids_stats: PidsStats {
|
||||||
current: None,
|
current: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
@@ -542,33 +484,12 @@ mod tests {
|
|||||||
},
|
},
|
||||||
cpu_stats: CPUStats {
|
cpu_stats: CPUStats {
|
||||||
cpu_usage: CPUUsage {
|
cpu_usage: CPUUsage {
|
||||||
percpu_usage: Some(vec![
|
percpu_usage: Some(vec![50]),
|
||||||
291_593_800,
|
usage_in_usermode: 10,
|
||||||
182_192_900,
|
total_usage: 100,
|
||||||
195_048_700,
|
usage_in_kernelmode: 20,
|
||||||
23_032_300,
|
|
||||||
132_928_700,
|
|
||||||
235_555_600,
|
|
||||||
120_225_700,
|
|
||||||
175_752_000,
|
|
||||||
213_060_300,
|
|
||||||
95_321_600,
|
|
||||||
226_821_000,
|
|
||||||
0,
|
|
||||||
109_151_300,
|
|
||||||
0,
|
|
||||||
86_240_200,
|
|
||||||
1_884_400,
|
|
||||||
59_077_300,
|
|
||||||
23_224_900,
|
|
||||||
95_386_300,
|
|
||||||
144_987_400,
|
|
||||||
]),
|
|
||||||
total_usage: 250_000_000,
|
|
||||||
usage_in_usermode: 1_020_000_000,
|
|
||||||
usage_in_kernelmode: 1_030_000_000,
|
|
||||||
},
|
},
|
||||||
system_cpu_usage: Some(x),
|
system_cpu_usage: Some(400),
|
||||||
online_cpus: Some(1),
|
online_cpus: Some(1),
|
||||||
throttling_data: ThrottlingData {
|
throttling_data: ThrottlingData {
|
||||||
periods: 0,
|
periods: 0,
|
||||||
@@ -578,33 +499,12 @@ mod tests {
|
|||||||
},
|
},
|
||||||
precpu_stats: CPUStats {
|
precpu_stats: CPUStats {
|
||||||
cpu_usage: CPUUsage {
|
cpu_usage: CPUUsage {
|
||||||
percpu_usage: Some(vec![
|
percpu_usage: Some(vec![50]),
|
||||||
291_593_800,
|
usage_in_usermode: 10,
|
||||||
182_192_900,
|
total_usage: 100,
|
||||||
195_048_700,
|
usage_in_kernelmode: 20,
|
||||||
23_032_300,
|
|
||||||
132_928_700,
|
|
||||||
235_555_600,
|
|
||||||
120_225_700,
|
|
||||||
175_752_000,
|
|
||||||
213_060_300,
|
|
||||||
95_321_600,
|
|
||||||
226_821_000,
|
|
||||||
0,
|
|
||||||
109_151_300,
|
|
||||||
0,
|
|
||||||
86_240_200,
|
|
||||||
1_884_400,
|
|
||||||
59_077_300,
|
|
||||||
23_224_900,
|
|
||||||
93_831_100,
|
|
||||||
144_987_400,
|
|
||||||
]),
|
|
||||||
total_usage: 200_000_000,
|
|
||||||
usage_in_usermode: 1_020_000_000,
|
|
||||||
usage_in_kernelmode: 1_020_000_000,
|
|
||||||
},
|
},
|
||||||
system_cpu_usage: Some(y),
|
system_cpu_usage: Some(400),
|
||||||
online_cpus: Some(1),
|
online_cpus: Some(1),
|
||||||
throttling_data: ThrottlingData {
|
throttling_data: ThrottlingData {
|
||||||
periods: 0,
|
periods: 0,
|
||||||
@@ -618,25 +518,198 @@ mod tests {
|
|||||||
write_count_normalized: None,
|
write_count_normalized: None,
|
||||||
write_size_bytes: None,
|
write_size_bytes: None,
|
||||||
},
|
},
|
||||||
name: "/container_1".to_owned(),
|
name: String::new(),
|
||||||
id: "1".to_owned(),
|
id: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(clippy::float_cmp)]
|
fn test_calculate_usage_50() {
|
||||||
/// Test the stats calculator, had to cheat here to get round input/outputs
|
let mut stats = gen_stats();
|
||||||
fn test_calculate_usage_no_previous_cpu() {
|
stats.precpu_stats = CPUStats {
|
||||||
let stats = gen_stats(1_000_000_000, 900_000_000);
|
cpu_usage: CPUUsage {
|
||||||
let result = DockerData::calculate_usage(&stats);
|
percpu_usage: Some(vec![50]),
|
||||||
assert_eq!(result, 50.0);
|
usage_in_usermode: 10,
|
||||||
|
total_usage: 100,
|
||||||
|
usage_in_kernelmode: 20,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(400),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
stats.cpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![150]),
|
||||||
|
usage_in_usermode: 20,
|
||||||
|
total_usage: 150,
|
||||||
|
usage_in_kernelmode: 30,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(500),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(50.0, cpu_percentage);
|
||||||
|
}
|
||||||
|
|
||||||
let stats = gen_stats(1_000_000_000, 800_000_000);
|
#[test]
|
||||||
let result = DockerData::calculate_usage(&stats);
|
fn test_calculate_usage_25() {
|
||||||
assert_eq!(result, 25.0);
|
let mut stats = gen_stats();
|
||||||
|
stats.precpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![50]),
|
||||||
|
usage_in_usermode: 10,
|
||||||
|
total_usage: 100,
|
||||||
|
usage_in_kernelmode: 20,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(400),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
stats.cpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![75]),
|
||||||
|
usage_in_usermode: 20,
|
||||||
|
total_usage: 125,
|
||||||
|
usage_in_kernelmode: 30,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(500),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let stats = gen_stats(1_000_000_000, 750_000_000);
|
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||||
let result = DockerData::calculate_usage(&stats);
|
assert_eq!(25.0, cpu_percentage);
|
||||||
assert_eq!(result, 20.00);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_usage_75() {
|
||||||
|
let mut stats = gen_stats();
|
||||||
|
stats.precpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![50]),
|
||||||
|
usage_in_usermode: 10,
|
||||||
|
total_usage: 100,
|
||||||
|
usage_in_kernelmode: 20,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(400),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.cpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![175]),
|
||||||
|
usage_in_usermode: 20,
|
||||||
|
total_usage: 175,
|
||||||
|
usage_in_kernelmode: 30,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(500),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(75.0, cpu_percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_usage_100() {
|
||||||
|
let mut stats = gen_stats();
|
||||||
|
stats.precpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![50]),
|
||||||
|
usage_in_usermode: 10,
|
||||||
|
total_usage: 100,
|
||||||
|
usage_in_kernelmode: 20,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(400),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
stats.cpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![200]),
|
||||||
|
usage_in_usermode: 20,
|
||||||
|
total_usage: 200,
|
||||||
|
usage_in_kernelmode: 30,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(500),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(100.0, cpu_percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_usage_175() {
|
||||||
|
let mut stats = gen_stats();
|
||||||
|
stats.precpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![50]),
|
||||||
|
usage_in_usermode: 10,
|
||||||
|
total_usage: 100,
|
||||||
|
usage_in_kernelmode: 20,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(400),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
stats.cpu_stats = CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![275]),
|
||||||
|
usage_in_usermode: 20,
|
||||||
|
total_usage: 275,
|
||||||
|
usage_in_kernelmode: 30,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(500),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let cpu_percentage = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(175.0, cpu_percentage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-7
@@ -144,9 +144,9 @@ impl TerminalSize {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ExecMode {
|
pub enum ExecMode {
|
||||||
// use Bollard Rust library
|
// use Bollard Rust library
|
||||||
Internal((ContainerId, Arc<Docker>)),
|
Internal((Arc<ContainerId>, Arc<Docker>)),
|
||||||
// use the external `docker-cli`
|
// use the external `docker-cli`
|
||||||
External(ContainerId),
|
External(Arc<ContainerId>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecMode {
|
impl ExecMode {
|
||||||
@@ -186,7 +186,10 @@ impl ExecMode {
|
|||||||
{
|
{
|
||||||
if let Some(Ok(msg)) = output.next().await {
|
if let Some(Ok(msg)) = output.next().await {
|
||||||
if !msg.to_string().starts_with(OCI_ERROR) {
|
if !msg.to_string().starts_with(OCI_ERROR) {
|
||||||
return Some(Self::Internal((id.clone(), Arc::clone(docker))));
|
return Some(Self::Internal((
|
||||||
|
Arc::new(id),
|
||||||
|
Arc::clone(docker),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +202,7 @@ impl ExecMode {
|
|||||||
{
|
{
|
||||||
if let Ok(output) = String::from_utf8(output.stdout) {
|
if let Ok(output) = String::from_utf8(output.stdout) {
|
||||||
if !output.starts_with(OCI_ERROR) {
|
if !output.starts_with(OCI_ERROR) {
|
||||||
return Some(Self::External(id.clone()));
|
return Some(Self::External(Arc::new(id)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,9 +305,9 @@ impl ExecMode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the fix for key pressed not being handled correctly on quit
|
/// This is the fix for key pressed not being handled correctly on quit
|
||||||
// It writes a special message to the stdout, and then listens out for a valid response
|
/// It writes a special message to the stdout, and then listens out for a valid response
|
||||||
// afterwhich it's assumes that we're completely done with TTY
|
/// afterwhich it's assumes that we're completely done with TTY
|
||||||
fn internal_cleanup(&self) -> Result<(), AppError> {
|
fn internal_cleanup(&self) -> Result<(), AppError> {
|
||||||
match self {
|
match self {
|
||||||
Self::External(_) => Ok(()),
|
Self::External(_) => Ok(()),
|
||||||
|
|||||||
+128
-174
@@ -1,14 +1,11 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{BufWriter, Write},
|
io::{BufWriter, Write},
|
||||||
sync::{
|
sync::{atomic::AtomicBool, Arc},
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use bollard::{container::LogsOptions, Docker};
|
use bollard::container::LogsOptions;
|
||||||
use cansi::v3::categorise_text;
|
use cansi::v3::categorise_text;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
||||||
@@ -22,7 +19,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, DockerControls, Header},
|
app_data::{AppData, DockerCommand, Header},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
docker_data::DockerMessage,
|
docker_data::DockerMessage,
|
||||||
exec::{tty_readable, ExecMode},
|
exec::{tty_readable, ExecMode},
|
||||||
@@ -43,7 +40,7 @@ pub struct InputHandler {
|
|||||||
|
|
||||||
impl InputHandler {
|
impl InputHandler {
|
||||||
/// Initialize self, and running the message handling loop
|
/// Initialize self, and running the message handling loop
|
||||||
pub async fn init(
|
pub async fn start(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
rec: Receiver<InputMessages>,
|
rec: Receiver<InputMessages>,
|
||||||
docker_tx: Sender<DockerMessage>,
|
docker_tx: Sender<DockerMessage>,
|
||||||
@@ -58,35 +55,30 @@ impl InputHandler {
|
|||||||
rec,
|
rec,
|
||||||
mouse_capture: true,
|
mouse_capture: true,
|
||||||
};
|
};
|
||||||
inner.start().await;
|
inner.message_handler().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// check for incoming messages
|
/// check for incoming messages
|
||||||
async fn start(&mut self) {
|
async fn message_handler(&mut self) {
|
||||||
while let Some(message) = self.rec.recv().await {
|
while let Some(message) = self.rec.recv().await {
|
||||||
match message {
|
match message {
|
||||||
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
||||||
InputMessages::MouseEvent(mouse_event) => {
|
InputMessages::MouseEvent(mouse_event) => {
|
||||||
if !self.gui_state.lock().status_contains(&[
|
let status = self.gui_state.lock().get_status();
|
||||||
Status::Error,
|
let contains = |s: Status| status.contains(&s);
|
||||||
Status::Help,
|
|
||||||
Status::DeleteConfirm,
|
if !contains(Status::Error)
|
||||||
Status::Filter,
|
| !contains(Status::Help)
|
||||||
]) {
|
| !contains(Status::DeleteConfirm)
|
||||||
|
| !contains(Status::Filter)
|
||||||
|
{
|
||||||
self.mouse_press(mouse_event);
|
self.mouse_press(mouse_event);
|
||||||
}
|
}
|
||||||
let delete_confirm = self
|
if contains(Status::DeleteConfirm) {
|
||||||
.gui_state
|
|
||||||
.lock()
|
|
||||||
.status_contains(&[Status::DeleteConfirm]);
|
|
||||||
if delete_confirm {
|
|
||||||
self.button_intersect(mouse_event).await;
|
self.button_intersect(mouse_event).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self.is_running.load(Ordering::SeqCst) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,12 +89,10 @@ impl InputHandler {
|
|||||||
|
|
||||||
/// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
|
/// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
|
||||||
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
|
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
|
||||||
async fn quit(&self) {
|
fn quit(&self) {
|
||||||
let error_init = self
|
let status = self.gui_state.lock().get_status();
|
||||||
.gui_state
|
let contains = |s: Status| status.contains(&s);
|
||||||
.lock()
|
if !contains(Status::Error) | !contains(Status::Init) {
|
||||||
.status_contains(&[Status::Error, Status::Init]);
|
|
||||||
if error_init || self.docker_tx.send(DockerMessage::Quit).await.is_err() {
|
|
||||||
self.is_running
|
self.is_running
|
||||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
@@ -112,7 +102,10 @@ impl InputHandler {
|
|||||||
async fn confirm_delete(&self) {
|
async fn confirm_delete(&self) {
|
||||||
let id = self.gui_state.lock().get_delete_container();
|
let id = self.gui_state.lock().get_delete_container();
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
self.docker_tx.send(DockerMessage::Delete(id)).await.ok();
|
self.docker_tx
|
||||||
|
.send(DockerMessage::Control((DockerCommand::Delete, id)))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +120,7 @@ impl InputHandler {
|
|||||||
if !is_oxker && tty_readable() {
|
if !is_oxker && tty_readable() {
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
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();
|
||||||
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
||||||
|
|
||||||
if let Ok(docker) = rx.await {
|
if let Ok(docker) = rx.await {
|
||||||
@@ -150,118 +143,109 @@ impl InputHandler {
|
|||||||
|
|
||||||
/// Toggle the mouse capture (via input of the 'm' key)
|
/// Toggle the mouse capture (via input of the 'm' key)
|
||||||
fn m_key(&mut self) {
|
fn m_key(&mut self) {
|
||||||
|
let err = || {
|
||||||
|
self.app_data.lock().set_error(
|
||||||
|
AppError::MouseCapture(!self.mouse_capture),
|
||||||
|
&self.gui_state,
|
||||||
|
Status::Error,
|
||||||
|
);
|
||||||
|
};
|
||||||
if self.mouse_capture {
|
if self.mouse_capture {
|
||||||
if execute!(std::io::stdout(), DisableMouseCapture).is_ok() {
|
if execute!(std::io::stdout(), DisableMouseCapture).is_ok() {
|
||||||
self.gui_state
|
self.gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.set_info_box("✖ mouse capture disabled");
|
.set_info_box("✖ mouse capture disabled");
|
||||||
} else {
|
} else {
|
||||||
self.app_data.lock().set_error(
|
err();
|
||||||
AppError::MouseCapture(false),
|
|
||||||
&self.gui_state,
|
|
||||||
Status::Error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if Ui::enable_mouse_capture().is_ok() {
|
} else if Ui::enable_mouse_capture().is_ok() {
|
||||||
self.gui_state
|
self.gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.set_info_box("✓ mouse capture enabled");
|
.set_info_box("✓ mouse capture enabled");
|
||||||
} else {
|
} else {
|
||||||
self.app_data.lock().set_error(
|
err();
|
||||||
AppError::MouseCapture(true),
|
|
||||||
&self.gui_state,
|
|
||||||
Status::Error,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.mouse_capture = !self.mouse_capture;
|
self.mouse_capture = !self.mouse_capture;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(&self) {
|
async fn save_logs(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// This is the inner workings, *inlined* here to return a Result
|
let args = self.app_data.lock().args.clone();
|
||||||
async fn save_logs(
|
let container = self.app_data.lock().get_selected_container_id_state_name();
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
if let Some((id, _, name)) = container {
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
if let Some(log_path) = args.save_dir {
|
||||||
docker_tx: &Sender<DockerMessage>,
|
let (sx, rx) = tokio::sync::oneshot::channel();
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
self.docker_tx.send(DockerMessage::Exec(sx)).await?;
|
||||||
let args = app_data.lock().args.clone();
|
|
||||||
let container = app_data.lock().get_selected_container_id_state_name();
|
|
||||||
if let Some((id, _, name)) = container {
|
|
||||||
if let Some(log_path) = args.save_dir {
|
|
||||||
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
|
|
||||||
docker_tx.send(DockerMessage::Exec(sx)).await?;
|
|
||||||
|
|
||||||
let now = SystemTime::now()
|
let now = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.map_or(0, |i| i.as_secs());
|
.map_or(0, |i| i.as_secs());
|
||||||
|
|
||||||
let path = log_path.join(format!("{name}_{now}.log"));
|
let path = log_path.join(format!("{name}_{now}.log"));
|
||||||
|
|
||||||
let docker = rx.await?;
|
let options = Some(LogsOptions::<String> {
|
||||||
let options = Some(LogsOptions::<String> {
|
stderr: true,
|
||||||
stderr: true,
|
stdout: true,
|
||||||
stdout: true,
|
timestamps: args.timestamp,
|
||||||
timestamps: args.timestamp,
|
since: 0,
|
||||||
since: 0,
|
..Default::default()
|
||||||
..Default::default()
|
});
|
||||||
});
|
let mut logs = rx.await?.logs(id.get(), options);
|
||||||
let mut logs = docker.logs(id.get(), options);
|
let mut output = vec![];
|
||||||
let mut output = vec![];
|
|
||||||
|
|
||||||
while let Some(Ok(value)) = logs.next().await {
|
while let Some(Ok(value)) = logs.next().await {
|
||||||
let data = value.to_string();
|
let data = value.to_string();
|
||||||
if !data.trim().is_empty() {
|
if !data.trim().is_empty() {
|
||||||
output.push(
|
output.push(
|
||||||
categorise_text(&data)
|
categorise_text(&data)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|i| i.text)
|
.map(|i| i.text)
|
||||||
.collect::<String>(),
|
.collect::<String>(),
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !output.is_empty() {
|
|
||||||
let mut stream = BufWriter::new(
|
|
||||||
OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(&path)?,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for line in &output {
|
|
||||||
stream.write_all(line.as_bytes())?;
|
|
||||||
}
|
|
||||||
stream.flush()?;
|
|
||||||
|
|
||||||
gui_state
|
|
||||||
.lock()
|
|
||||||
.set_info_box(&format!("saved to {}", path.display()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !output.is_empty() {
|
||||||
|
let mut stream = BufWriter::new(
|
||||||
|
OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&path)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
for line in &output {
|
||||||
|
stream.write_all(line.as_bytes())?;
|
||||||
|
}
|
||||||
|
stream.flush()?;
|
||||||
|
|
||||||
|
self.gui_state
|
||||||
|
.lock()
|
||||||
|
.set_info_box(&format!("saved to {}", path.display()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
let log_status = Status::Logs;
|
/// Attempt to save the currently selected container logs to a file
|
||||||
let status = self.gui_state.lock().status_contains(&[log_status]);
|
async fn s_key(&self) {
|
||||||
if !status {
|
let status = self.gui_state.lock().get_status();
|
||||||
self.gui_state.lock().status_push(log_status);
|
let contains = |s: Status| status.contains(&s);
|
||||||
|
|
||||||
|
if !contains(Status::Logs) {
|
||||||
|
self.gui_state.lock().status_push(Status::Logs);
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
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 self.save_logs().await.is_err() {
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
self.app_data.lock().set_error(
|
self.app_data.lock().set_error(
|
||||||
AppError::DockerLogs,
|
AppError::DockerLogs,
|
||||||
&self.gui_state,
|
&self.gui_state,
|
||||||
Status::Error,
|
Status::Error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.gui_state.lock().status_del(log_status);
|
self.gui_state.lock().status_del(Status::Logs);
|
||||||
self.gui_state.lock().stop_loading_animation(uuid);
|
self.gui_state.lock().stop_loading_animation(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,26 +265,17 @@ impl InputHandler {
|
|||||||
let option_id = self.app_data.lock().get_selected_container_id();
|
let option_id = self.app_data.lock().get_selected_container_id();
|
||||||
if let Some(id) = option_id {
|
if let Some(id) = option_id {
|
||||||
match command {
|
match command {
|
||||||
DockerControls::Delete => self
|
DockerCommand::Delete => self
|
||||||
.docker_tx
|
.docker_tx
|
||||||
.send(DockerMessage::ConfirmDelete(id))
|
.send(DockerMessage::ConfirmDelete(id))
|
||||||
.await
|
.await
|
||||||
.ok(),
|
.ok(),
|
||||||
DockerControls::Pause => {
|
|
||||||
self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
|
_ => self
|
||||||
}
|
.docker_tx
|
||||||
DockerControls::Resume => {
|
.send(DockerMessage::Control((command, id)))
|
||||||
self.docker_tx.send(DockerMessage::Resume(id)).await.ok()
|
.await
|
||||||
}
|
.ok(),
|
||||||
DockerControls::Start => {
|
|
||||||
self.docker_tx.send(DockerMessage::Start(id)).await.ok()
|
|
||||||
}
|
|
||||||
DockerControls::Stop => {
|
|
||||||
self.docker_tx.send(DockerMessage::Stop(id)).await.ok()
|
|
||||||
}
|
|
||||||
DockerControls::Restart => {
|
|
||||||
self.docker_tx.send(DockerMessage::Restart(id)).await.ok()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,50 +283,43 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Change the the "next" selectable panel
|
/// Change the the "next" selectable panel
|
||||||
|
/// If no containers, and on Commands panel, skip to next panel, as Commands panel isn't visible in this state
|
||||||
fn tab_key(&self) {
|
fn tab_key(&self) {
|
||||||
let is_containers =
|
self.gui_state.lock().next_panel();
|
||||||
self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
|
if self.app_data.lock().get_container_len() == 0
|
||||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
&& self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
|
||||||
2
|
{
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
for _ in 0..count {
|
|
||||||
self.gui_state.lock().next_panel();
|
self.gui_state.lock().next_panel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change to previously selected panel
|
/// Change to previously selected panel
|
||||||
|
/// Need to skip the commands planel if there no are current containers running
|
||||||
fn back_tab_key(&self) {
|
fn back_tab_key(&self) {
|
||||||
let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
|
self.gui_state.lock().previous_panel();
|
||||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
if self.app_data.lock().get_container_len() == 0
|
||||||
2
|
&& self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
|
||||||
} else {
|
{
|
||||||
1
|
|
||||||
};
|
|
||||||
for _ in 0..count {
|
|
||||||
self.gui_state.lock().previous_panel();
|
self.gui_state.lock().previous_panel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn home_key(&self) {
|
fn home_key(&self) {
|
||||||
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 {
|
||||||
SelectablePanel::Containers => locked_data.containers_start(),
|
SelectablePanel::Containers => self.app_data.lock().containers_start(),
|
||||||
SelectablePanel::Logs => locked_data.log_start(),
|
SelectablePanel::Logs => self.app_data.lock().log_start(),
|
||||||
SelectablePanel::Commands => locked_data.docker_controls_start(),
|
SelectablePanel::Commands => self.app_data.lock().docker_controls_start(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(&self) {
|
fn end_key(&self) {
|
||||||
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 {
|
||||||
SelectablePanel::Containers => locked_data.containers_end(),
|
SelectablePanel::Containers => self.app_data.lock().containers_end(),
|
||||||
SelectablePanel::Logs => locked_data.log_end(),
|
SelectablePanel::Logs => self.app_data.lock().log_end(),
|
||||||
SelectablePanel::Commands => locked_data.docker_controls_end(),
|
SelectablePanel::Commands => self.app_data.lock().docker_controls_end(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,24 +423,21 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
/// 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 status = self.gui_state.lock().get_status();
|
||||||
.gui_state
|
let contains = |s: Status| status.contains(&s);
|
||||||
.lock()
|
|
||||||
.status_contains(&[Status::DeleteConfirm]);
|
|
||||||
|
|
||||||
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
|
||||||
|
|
||||||
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);
|
let contains_filter = contains(Status::Filter);
|
||||||
|
let contains_delete = contains(Status::DeleteConfirm);
|
||||||
|
|
||||||
if !contains_exec {
|
if !contains_exec {
|
||||||
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() && !contains_filter {
|
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
|
// Always just quit on Ctrl + c/C or q/Q, unless in Filter status active
|
||||||
self.quit().await;
|
self.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if contains_error {
|
if contains_error {
|
||||||
@@ -514,22 +479,13 @@ impl InputHandler {
|
|||||||
MouseEventKind::ScrollUp => self.previous(),
|
MouseEventKind::ScrollUp => self.previous(),
|
||||||
MouseEventKind::ScrollDown => self.next(),
|
MouseEventKind::ScrollDown => self.next(),
|
||||||
MouseEventKind::Down(MouseButton::Left) => {
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
let header = self.gui_state.lock().header_intersect(Rect::new(
|
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
|
||||||
mouse_event.column,
|
let header = self.gui_state.lock().header_intersect(mouse_point);
|
||||||
mouse_event.row,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
));
|
|
||||||
if let Some(header) = header {
|
if let Some(header) = header {
|
||||||
self.sort(header);
|
self.sort(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gui_state.lock().panel_intersect(Rect::new(
|
self.gui_state.lock().panel_intersect(mouse_point);
|
||||||
mouse_event.column,
|
|
||||||
mouse_event.row,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
@@ -537,23 +493,21 @@ 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(&self) {
|
fn next(&self) {
|
||||||
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 {
|
||||||
SelectablePanel::Containers => locked_data.containers_next(),
|
SelectablePanel::Containers => self.app_data.lock().containers_next(),
|
||||||
SelectablePanel::Logs => locked_data.log_next(),
|
SelectablePanel::Logs => self.app_data.lock().log_next(),
|
||||||
SelectablePanel::Commands => locked_data.docker_controls_next(),
|
SelectablePanel::Commands => self.app_data.lock().docker_controls_next(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change state to previous, depending which panel is currently in focus
|
/// Change state to previous, depending which panel is currently in focus
|
||||||
fn previous(&self) {
|
fn previous(&self) {
|
||||||
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 {
|
||||||
SelectablePanel::Containers => locked_data.containers_previous(),
|
SelectablePanel::Containers => self.app_data.lock().containers_previous(),
|
||||||
SelectablePanel::Logs => locked_data.log_previous(),
|
SelectablePanel::Logs => self.app_data.lock().log_previous(),
|
||||||
SelectablePanel::Commands => locked_data.docker_controls_previous(),
|
SelectablePanel::Commands => self.app_data.lock().docker_controls_previous(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-44
@@ -52,32 +52,28 @@ async fn docker_init(
|
|||||||
docker_rx: Receiver<DockerMessage>,
|
docker_rx: Receiver<DockerMessage>,
|
||||||
docker_tx: Sender<DockerMessage>,
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
is_running: &Arc<AtomicBool>,
|
|
||||||
host: Option<String>,
|
|
||||||
) {
|
) {
|
||||||
|
let host = read_docker_host(&app_data.lock().args);
|
||||||
|
|
||||||
let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| {
|
let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| {
|
||||||
Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION)
|
Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(docker) = connection {
|
if let Ok(docker) = connection {
|
||||||
if docker.ping().await.is_ok() {
|
if docker.ping().await.is_ok() {
|
||||||
let app_data = Arc::clone(app_data);
|
tokio::spawn(DockerData::start(
|
||||||
let gui_state = Arc::clone(gui_state);
|
Arc::clone(app_data),
|
||||||
let is_running = Arc::clone(is_running);
|
docker,
|
||||||
|
docker_rx,
|
||||||
tokio::spawn(DockerData::init(
|
docker_tx,
|
||||||
app_data, docker, docker_rx, docker_tx, gui_state, is_running,
|
Arc::clone(gui_state),
|
||||||
));
|
));
|
||||||
} else {
|
return;
|
||||||
app_data
|
|
||||||
.lock()
|
|
||||||
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
app_data
|
|
||||||
.lock()
|
|
||||||
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
|
|
||||||
}
|
}
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create data for, and then spawn a tokio thread, for the input handler
|
/// Create data for, and then spawn a tokio thread, for the input handler
|
||||||
@@ -88,15 +84,12 @@ fn handler_init(
|
|||||||
input_rx: Receiver<InputMessages>,
|
input_rx: Receiver<InputMessages>,
|
||||||
is_running: &Arc<AtomicBool>,
|
is_running: &Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let app_data = Arc::clone(app_data);
|
tokio::spawn(input_handler::InputHandler::start(
|
||||||
let gui_state = Arc::clone(gui_state);
|
Arc::clone(app_data),
|
||||||
let is_running = Arc::clone(is_running);
|
|
||||||
tokio::spawn(input_handler::InputHandler::init(
|
|
||||||
app_data,
|
|
||||||
input_rx,
|
input_rx,
|
||||||
docker_sx.clone(),
|
docker_sx.clone(),
|
||||||
gui_state,
|
Arc::clone(gui_state),
|
||||||
is_running,
|
Arc::clone(is_running),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,34 +99,20 @@ async fn main() {
|
|||||||
|
|
||||||
let args = CliArgs::new();
|
let args = CliArgs::new();
|
||||||
|
|
||||||
// If running via Docker image, need to sleep else program will just quit straight away, no real idea why
|
|
||||||
// So just sleep for small while
|
|
||||||
if args.in_container {
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
|
||||||
}
|
|
||||||
let host = read_docker_host(&args);
|
|
||||||
|
|
||||||
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
||||||
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
||||||
let is_running = Arc::new(AtomicBool::new(true));
|
let is_running = Arc::new(AtomicBool::new(true));
|
||||||
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
|
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
|
||||||
|
|
||||||
docker_init(
|
docker_init(&app_data, docker_rx, docker_tx.clone(), &gui_state).await;
|
||||||
&app_data,
|
|
||||||
docker_rx,
|
|
||||||
docker_tx.clone(),
|
|
||||||
&gui_state,
|
|
||||||
&is_running,
|
|
||||||
host,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if args.gui {
|
if args.gui {
|
||||||
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
|
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
|
||||||
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
||||||
Ui::create(app_data, gui_state, input_tx, is_running).await;
|
Ui::start(app_data, gui_state, input_tx, is_running).await;
|
||||||
} else {
|
} else {
|
||||||
info!("in debug mode\n");
|
info!("in debug mode\n");
|
||||||
|
let mut now = std::time::Instant::now();
|
||||||
// Debug mode for testing, less pointless now, will display some basic information
|
// Debug mode for testing, less pointless now, will display some basic information
|
||||||
while is_running.load(Ordering::SeqCst) {
|
while is_running.load(Ordering::SeqCst) {
|
||||||
let err = app_data.lock().get_error();
|
let err = app_data.lock().get_error();
|
||||||
@@ -141,10 +120,12 @@ async fn main() {
|
|||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
if let Some(Ok(to_sleep)) = u128::from(args.docker_interval)
|
||||||
args.docker_interval,
|
.checked_sub(now.elapsed().as_millis())
|
||||||
)))
|
.map(u64::try_from)
|
||||||
.await;
|
{
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(to_sleep)).await;
|
||||||
|
}
|
||||||
let containers = app_data
|
let containers = app_data
|
||||||
.lock()
|
.lock()
|
||||||
.get_container_items()
|
.get_container_items()
|
||||||
@@ -158,6 +139,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
now = std::time::Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +164,7 @@ mod tests {
|
|||||||
docker_interval: 1000,
|
docker_interval: 1000,
|
||||||
gui: true,
|
gui: true,
|
||||||
host: None,
|
host: None,
|
||||||
|
std_err: false,
|
||||||
in_container: false,
|
in_container: false,
|
||||||
save_dir: None,
|
save_dir: None,
|
||||||
raw: false,
|
raw: false,
|
||||||
|
|||||||
+10
-9
@@ -37,13 +37,17 @@ pub struct Args {
|
|||||||
#[clap(long, short = None)]
|
#[clap(long, short = None)]
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
|
|
||||||
/// Force use of docker cli when execing into containers
|
/// Do not include stderr output in logs
|
||||||
#[clap(long="use-cli", short = None)]
|
#[clap(long = "no-stderr")]
|
||||||
pub use_cli: bool,
|
pub no_std_err: bool,
|
||||||
|
|
||||||
/// Directory for saving exported logs, defaults to `$HOME`
|
/// Directory for saving exported logs, defaults to `$HOME`
|
||||||
#[clap(long="save-dir", short = None)]
|
#[clap(long="save-dir", short = None)]
|
||||||
pub save_dir: Option<String>,
|
pub save_dir: Option<String>,
|
||||||
|
|
||||||
|
/// Force use of docker cli when execing into containers
|
||||||
|
#[clap(long="use-cli", short = None)]
|
||||||
|
pub use_cli: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -58,6 +62,7 @@ pub struct CliArgs {
|
|||||||
pub raw: bool,
|
pub raw: bool,
|
||||||
pub show_self: bool,
|
pub show_self: bool,
|
||||||
pub timestamp: bool,
|
pub timestamp: bool,
|
||||||
|
pub std_err: bool,
|
||||||
pub use_cli: bool,
|
pub use_cli: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,12 +70,7 @@ impl CliArgs {
|
|||||||
/// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately
|
/// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately
|
||||||
/// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container
|
/// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container
|
||||||
fn check_if_in_container() -> bool {
|
fn check_if_in_container() -> bool {
|
||||||
if let Ok(value) = std::env::var(ENV_KEY) {
|
std::env::var(ENV_KEY).map_or(false, |i| i == ENV_VALUE)
|
||||||
if value == ENV_VALUE {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse cli arguments
|
/// Parse cli arguments
|
||||||
@@ -97,6 +97,7 @@ impl CliArgs {
|
|||||||
in_container: Self::check_if_in_container(),
|
in_container: Self::check_if_in_container(),
|
||||||
save_dir: logs_dir,
|
save_dir: logs_dir,
|
||||||
raw: args.raw,
|
raw: args.raw,
|
||||||
|
std_err: !args.no_std_err,
|
||||||
show_self: !args.show_self,
|
show_self: !args.show_self,
|
||||||
timestamp: !args.timestamp,
|
timestamp: !args.timestamp,
|
||||||
}
|
}
|
||||||
|
|||||||
+220
-184
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -266,10 +266,9 @@ impl GuiState {
|
|||||||
self.delete_container = id;
|
self.delete_container = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the current gui_status contains any of the given status'
|
/// Return a copy of the Status HashSet
|
||||||
/// Don't really like this methodology for gui state, needs a re-think
|
pub fn get_status(&self) -> HashSet<Status> {
|
||||||
pub fn status_contains(&self, status: &[Status]) -> bool {
|
self.status.clone()
|
||||||
status.iter().any(|i| self.status.contains(i))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a gui_status into the current gui_status HashSet
|
/// Remove a gui_status into the current gui_status HashSet
|
||||||
|
|||||||
+71
-50
@@ -4,13 +4,14 @@ use crossterm::{
|
|||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use parking_lot::{Mutex, MutexGuard};
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction, Layout, Position},
|
layout::{Constraint, Direction, Layout, Position},
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
io::{self, Stdout, Write},
|
io::{self, Stdout, Write},
|
||||||
sync::{atomic::Ordering, Arc},
|
sync::{atomic::Ordering, Arc},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
@@ -26,18 +27,21 @@ mod gui_state;
|
|||||||
pub use self::color_match::*;
|
pub use self::color_match::*;
|
||||||
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
|
app_data::{
|
||||||
|
AppData, Columns, ContainerId, ContainerPorts, CpuTuple, FilterBy, Header, MemTuple,
|
||||||
|
SortedOrder, State,
|
||||||
|
},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
exec::TerminalSize,
|
exec::TerminalSize,
|
||||||
input_handler::InputMessages,
|
input_handler::InputMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
||||||
|
const POLL_RATE: Duration = std::time::Duration::from_millis(100);
|
||||||
|
|
||||||
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>>,
|
||||||
input_poll_rate: Duration,
|
|
||||||
input_tx: Sender<InputMessages>,
|
input_tx: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
@@ -59,7 +63,7 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new Ui struct, and execute the drawing loop
|
/// Create a new Ui struct, and execute the drawing loop
|
||||||
pub async fn create(
|
pub async fn start(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
input_tx: Sender<InputMessages>,
|
input_tx: Sender<InputMessages>,
|
||||||
@@ -71,7 +75,6 @@ impl Ui {
|
|||||||
app_data,
|
app_data,
|
||||||
cursor_position,
|
cursor_position,
|
||||||
gui_state,
|
gui_state,
|
||||||
input_poll_rate: std::time::Duration::from_millis(100),
|
|
||||||
input_tx,
|
input_tx,
|
||||||
is_running,
|
is_running,
|
||||||
now: Instant::now(),
|
now: Instant::now(),
|
||||||
@@ -141,7 +144,7 @@ impl Ui {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use exeternal docker cli to exec into a container
|
/// Use external docker cli to exec into a container
|
||||||
async fn exec(&mut self) {
|
async fn exec(&mut self) {
|
||||||
let exec_mode = self.gui_state.lock().get_exec_mode();
|
let exec_mode = self.gui_state.lock().get_exec_mode();
|
||||||
|
|
||||||
@@ -163,20 +166,21 @@ impl Ui {
|
|||||||
/// The loop for drawing the main UI to the terminal
|
/// The loop for drawing the main UI to the terminal
|
||||||
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
||||||
while self.is_running.load(Ordering::SeqCst) {
|
while self.is_running.load(Ordering::SeqCst) {
|
||||||
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
|
let fd = FrameData::from(&*self);
|
||||||
|
let exec = fd.status.contains(&Status::Exec);
|
||||||
if exec {
|
if exec {
|
||||||
self.exec().await;
|
self.exec().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state))
|
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state, &fd))
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return Err(AppError::Terminal);
|
return Err(AppError::Terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) {
|
if crossterm::event::poll(POLL_RATE).unwrap_or(false) {
|
||||||
if let Ok(event) = event::read() {
|
if let Ok(event) = event::read() {
|
||||||
if let Event::Key(key) = event {
|
if let Event::Key(key) = event {
|
||||||
if key.kind == event::KeyEventKind::Press {
|
if key.kind == event::KeyEventKind::Press {
|
||||||
@@ -206,11 +210,8 @@ impl Ui {
|
|||||||
|
|
||||||
/// Draw either the Error, or main oxker ui, to the terminal
|
/// Draw either the Error, or main oxker ui, to the terminal
|
||||||
async fn draw_ui(&mut self) -> Result<(), AppError> {
|
async fn draw_ui(&mut self) -> Result<(), AppError> {
|
||||||
let status_dockerconnect = self
|
let status = self.gui_state.lock().get_status();
|
||||||
.gui_state
|
if status.contains(&Status::DockerConnect) {
|
||||||
.lock()
|
|
||||||
.status_contains(&[Status::DockerConnect]);
|
|
||||||
if status_dockerconnect {
|
|
||||||
self.err_loop()?;
|
self.err_loop()?;
|
||||||
} else {
|
} else {
|
||||||
self.gui_loop().await?;
|
self.gui_loop().await?;
|
||||||
@@ -219,54 +220,73 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
|
/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FrameData {
|
pub struct FrameData {
|
||||||
|
chart_data: Option<(CpuTuple, MemTuple)>,
|
||||||
columns: Columns,
|
columns: Columns,
|
||||||
|
container_title: String,
|
||||||
delete_confirm: Option<ContainerId>,
|
delete_confirm: Option<ContainerId>,
|
||||||
|
filter_by: FilterBy,
|
||||||
|
filter_term: Option<String>,
|
||||||
has_containers: bool,
|
has_containers: bool,
|
||||||
has_error: Option<AppError>,
|
has_error: Option<AppError>,
|
||||||
height: u16,
|
height: u16,
|
||||||
help_visible: bool,
|
|
||||||
init: bool,
|
|
||||||
info_text: Option<(String, Instant)>,
|
info_text: Option<(String, Instant)>,
|
||||||
|
is_loading: bool,
|
||||||
loading_icon: String,
|
loading_icon: String,
|
||||||
|
log_title: String,
|
||||||
|
port_max_lens: (usize, usize, usize),
|
||||||
|
ports: Option<(Vec<ContainerPorts>, State)>,
|
||||||
selected_panel: SelectablePanel,
|
selected_panel: SelectablePanel,
|
||||||
sorted_by: Option<(Header, SortedOrder)>,
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
status: HashSet<Status>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
impl From<&Ui> for FrameData {
|
||||||
fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self {
|
fn from(ui: &Ui) -> Self {
|
||||||
|
let (app_data, gui_data) = (ui.app_data.lock(), ui.gui_state.lock());
|
||||||
|
|
||||||
// set max height for container section, needs +5 to deal with docker commands list and borders
|
// set max height for container section, needs +5 to deal with docker commands list and borders
|
||||||
let height = data.0.get_container_len();
|
let height = app_data.get_container_len();
|
||||||
let height = if height < 12 {
|
let height = if height < 12 {
|
||||||
u16::try_from(height + 5).unwrap_or_default()
|
u16::try_from(height + 5).unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
12
|
12
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (filter_by, filter_term) = app_data.get_filter();
|
||||||
Self {
|
Self {
|
||||||
columns: data.0.get_width(),
|
chart_data: app_data.get_chart_data(),
|
||||||
delete_confirm: data.1.get_delete_container(),
|
columns: app_data.get_width(),
|
||||||
has_containers: data.0.get_container_len() > 0,
|
container_title: app_data.get_container_title(),
|
||||||
has_error: data.0.get_error(),
|
delete_confirm: gui_data.get_delete_container(),
|
||||||
|
filter_by,
|
||||||
|
filter_term: filter_term.cloned(),
|
||||||
|
has_containers: app_data.get_container_len() > 0,
|
||||||
|
has_error: app_data.get_error(),
|
||||||
height,
|
height,
|
||||||
help_visible: data.1.status_contains(&[Status::Help]),
|
info_text: gui_data.info_box_text.clone(),
|
||||||
init: data.1.status_contains(&[Status::Init]),
|
is_loading: gui_data.is_loading(),
|
||||||
info_text: data.1.info_box_text.clone(),
|
loading_icon: gui_data.get_loading().to_string(),
|
||||||
loading_icon: data.1.get_loading().to_string(),
|
log_title: app_data.get_log_title(),
|
||||||
selected_panel: data.1.get_selected_panel(),
|
port_max_lens: app_data.get_longest_port(),
|
||||||
sorted_by: data.0.get_sorted(),
|
ports: app_data.get_selected_ports(),
|
||||||
|
selected_panel: gui_data.get_selected_panel(),
|
||||||
|
sorted_by: app_data.get_sorted(),
|
||||||
|
status: gui_data.get_status(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(
|
||||||
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
f: &mut Frame,
|
||||||
let contains_filter = gui_state.lock().status_contains(&[Status::Filter]);
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
let whole_constraints = if contains_filter {
|
fd: &FrameData,
|
||||||
|
) {
|
||||||
|
let whole_constraints = if fd.status.contains(&Status::Filter) {
|
||||||
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
||||||
} else {
|
} else {
|
||||||
vec![Constraint::Max(1), Constraint::Min(1)]
|
vec![Constraint::Max(1), Constraint::Min(1)]
|
||||||
@@ -300,21 +320,21 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
vec![Constraint::Percentage(100)]
|
vec![Constraint::Percentage(100)]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Split into 2, logs, and optional charts
|
// Split into 2, logs and charts
|
||||||
let lower_main = Layout::default()
|
let lower_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(lower_split)
|
.constraints(lower_split)
|
||||||
.split(upper_main[1]);
|
.split(upper_main[1]);
|
||||||
|
|
||||||
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state);
|
draw_blocks::containers(app_data, top_panel[0], f, fd, gui_state);
|
||||||
|
|
||||||
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
|
draw_blocks::logs(app_data, lower_main[0], f, fd, gui_state);
|
||||||
|
|
||||||
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
|
// Draw filter bar
|
||||||
if let Some(rect) = whole_layout.get(2) {
|
if let Some(rect) = whole_layout.get(2) {
|
||||||
draw_blocks::filter_bar(*rect, f, app_data);
|
draw_blocks::filter_bar(*rect, f, fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = fd.delete_confirm.as_ref() {
|
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||||
@@ -325,34 +345,35 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
gui_state.lock().set_delete_container(None);
|
gui_state.lock().set_delete_container(None);
|
||||||
},
|
},
|
||||||
|name| {
|
|name| {
|
||||||
draw_blocks::delete_confirm(f, gui_state, &name);
|
draw_blocks::delete_confirm(f, gui_state, name);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// only draw commands + charts if there are containers
|
// only draw commands + charts if there are containers
|
||||||
if let Some(rect) = top_panel.get(1) {
|
if let Some(rect) = top_panel.get(1) {
|
||||||
draw_blocks::commands(app_data, *rect, 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 ports_len =
|
||||||
let ports_len = u16::try_from(max_lens.0 + max_lens.1 + max_lens.2 + 2).unwrap_or(26);
|
u16::try_from(fd.port_max_lens.0 + fd.port_max_lens.1 + fd.port_max_lens.2 + 2)
|
||||||
|
.unwrap_or(26);
|
||||||
|
|
||||||
let lower = Layout::default()
|
let lower = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
|
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
|
||||||
.split(lower_main[1]);
|
.split(lower_main[1]);
|
||||||
|
|
||||||
draw_blocks::chart(f, lower[0], app_data);
|
draw_blocks::chart(f, lower[0], fd);
|
||||||
draw_blocks::ports(f, lower[1], app_data, max_lens);
|
draw_blocks::ports(f, lower[1], fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((text, instant)) = fd.info_text {
|
if let Some((text, instant)) = fd.info_text.as_ref() {
|
||||||
draw_blocks::info(f, &text, instant, gui_state);
|
draw_blocks::info(f, text.to_owned(), instant, gui_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if error, and show popup if so
|
// Check if error, and show popup if so
|
||||||
if fd.help_visible {
|
if fd.status.contains(&Status::Help) {
|
||||||
draw_blocks::help_box(f);
|
draw_blocks::help_box(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user