diff --git a/.github/release-body.md b/.github/release-body.md
index 679805e..166376e 100644
--- a/.github/release-body.md
+++ b/.github/release-body.md
@@ -1,14 +1,42 @@
-### 2024-10-22
+### 2024-12-05
### Chores
-+ dependencies updated, [ea877d23711b98ffd1108a74206d93d43482d44d], [af609c0dbf0caab4a073f822166de34999afb41b]
-+ .devcontainer updated, [a9844436d003b84a3e9d8b600ea029b232566f3a]
-+ create_release.sh updated, [c4943370f4a67f6c01c75a8a7f825912427666a2], [1389d8adbba75fef480eb1de09337eb7beb10ba3]
++ dependencies updated, [b78713579c4706d605e5b35fcd832610a0152294], [c6200e8f77f8bb1f0152cb9374029d15cc45df9d]
++ Rust 1.83 linting, [751d997a3dac823e144ae62e6c1455676e50ddb8]
### 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
-+ 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 CHANGELOG.md for more details
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 858c2c6..05e5d8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,45 @@
+# v0.9.0
+### 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)
+
# v0.8.0
### 2024-10-22
diff --git a/Cargo.lock b/Cargo.lock
index 0bfe203..1da5da6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "addr2line"
@@ -19,9 +19,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "allocator-api2"
-version = "0.2.18"
+version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
@@ -40,9 +40,9 @@ dependencies = [
[[package]]
name = "anstream"
-version = "0.6.15"
+version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -55,43 +55,43 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.8"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
-version = "0.2.5"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
-version = "1.1.1"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
-version = "3.0.4"
+version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
-version = "1.0.90"
+version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95"
+checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "autocfg"
@@ -128,9 +128,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bollard"
-version = "0.17.1"
+version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a"
+checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30"
dependencies = [
"base64",
"bollard-stubs",
@@ -151,7 +151,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serde_urlencoded",
- "thiserror",
+ "thiserror 2.0.4",
"tokio",
"tokio-util",
"tower-service",
@@ -161,9 +161,9 @@ dependencies = [
[[package]]
name = "bollard-stubs"
-version = "1.45.0-rc.26.0.1"
+version = "1.47.1-rc.27.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4"
+checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da"
dependencies = [
"serde",
"serde_repr",
@@ -184,9 +184,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cansi"
@@ -211,9 +211,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.1.31"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
+checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
dependencies = [
"shlex",
]
@@ -239,9 +239,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.20"
+version = "4.5.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
+checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
dependencies = [
"clap_builder",
"clap_derive",
@@ -249,9 +249,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.20"
+version = "4.5.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
+checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
dependencies = [
"anstream",
"anstyle",
@@ -275,15 +275,15 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.7.2"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
[[package]]
name = "colorchoice"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "compact_str"
@@ -330,6 +330,41 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "darling"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "deranged"
version = "0.3.11"
@@ -340,6 +375,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
[[package]]
name = "directories"
version = "5.0.1"
@@ -361,6 +402,17 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "either"
version = "1.13.0"
@@ -375,12 +427,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
-version = "0.3.9"
+version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -481,9 +533,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
-version = "0.15.0"
+version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
@@ -496,12 +548,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-[[package]]
-name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
[[package]]
name = "hex"
version = "0.4.3"
@@ -510,9 +556,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
-version = "1.1.0"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
@@ -556,9 +602,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
+checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
dependencies = [
"bytes",
"futures-channel",
@@ -591,9 +637,9 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.9"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
@@ -647,13 +693,148 @@ dependencies = [
]
[[package]]
-name = "idna"
-version = "0.5.0"
+name = "icu_collections"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
- "unicode-bidi",
- "unicode-normalization",
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
]
[[package]]
@@ -669,12 +850,12 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
- "hashbrown 0.15.0",
+ "hashbrown 0.15.2",
"serde",
]
@@ -686,10 +867,14 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "instability"
-version = "0.3.2"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c"
+checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e"
dependencies = [
+ "darling",
+ "indoc",
+ "pretty_assertions",
+ "proc-macro2",
"quote",
"syn",
]
@@ -711,16 +896,17 @@ dependencies = [
[[package]]
name = "itoa"
-version = "1.0.11"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "js-sys"
-version = "0.3.72"
+version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
+checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705"
dependencies = [
+ "once_cell",
"wasm-bindgen",
]
@@ -732,9 +918,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.161"
+version = "0.2.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
+checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
[[package]]
name = "libredox"
@@ -752,6 +938,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+[[package]]
+name = "litemap"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -774,7 +966,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
- "hashbrown 0.15.0",
+ "hashbrown 0.15.2",
]
[[package]]
@@ -794,11 +986,10 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
- "hermit-abi",
"libc",
"log",
"wasi",
@@ -859,7 +1050,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "oxker"
-version = "0.8.0"
+version = "0.9.0"
dependencies = [
"anyhow",
"bollard",
@@ -914,9 +1105,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
-version = "0.2.14"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
@@ -940,10 +1131,20 @@ dependencies = [
]
[[package]]
-name = "proc-macro2"
-version = "1.0.88"
+name = "pretty_assertions"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
@@ -1025,7 +1226,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
@@ -1036,9 +1237,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
-version = "0.38.37"
+version = "0.38.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
+checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
dependencies = [
"bitflags",
"errno",
@@ -1067,18 +1268,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.211"
+version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793"
+checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.211"
+version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff"
+checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
@@ -1087,9 +1288,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.132"
+version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
+checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@@ -1130,7 +1331,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.6.0",
+ "indexmap 2.7.0",
"serde",
"serde_derive",
"serde_json",
@@ -1199,14 +1400,20 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
-version = "0.5.7"
+version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -1243,9 +1450,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.82"
+version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
+checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
@@ -1253,19 +1460,50 @@ dependencies = [
]
[[package]]
-name = "thiserror"
-version = "1.0.64"
+name = "synstructure"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
- "thiserror-impl",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490"
+dependencies = [
+ "thiserror-impl 2.0.4",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.64"
+version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061"
dependencies = [
"proc-macro2",
"quote",
@@ -1284,9 +1522,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.36"
+version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
@@ -1305,34 +1543,29 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
-version = "0.2.18"
+version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
-name = "tinyvec"
-version = "1.8.0"
+name = "tinystr"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
- "tinyvec_macros",
+ "displaydoc",
+ "zerovec",
]
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
[[package]]
name = "tokio"
-version = "1.41.0"
+version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
+checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
dependencies = [
"backtrace",
"bytes",
@@ -1359,9 +1592,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.12"
+version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
+checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
dependencies = [
"bytes",
"futures-core",
@@ -1378,9 +1611,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.40"
+version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -1389,9 +1622,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.27"
+version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
@@ -1400,9 +1633,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.32"
+version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@@ -1421,9 +1654,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.18"
+version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",
@@ -1445,26 +1678,11 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
-[[package]]
-name = "unicode-bidi"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
-
[[package]]
name = "unicode-ident"
-version = "1.0.13"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
-
-[[package]]
-name = "unicode-normalization"
-version = "0.1.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
-dependencies = [
- "tinyvec",
-]
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-segmentation"
@@ -1497,15 +1715,27 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "url"
-version = "2.5.2"
+version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -1545,9 +1775,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.95"
+version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
+checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c"
dependencies = [
"cfg-if",
"once_cell",
@@ -1556,9 +1786,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.95"
+version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
+checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd"
dependencies = [
"bumpalo",
"log",
@@ -1571,9 +1801,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.95"
+version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
+checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1581,9 +1811,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.95"
+version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
+checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
dependencies = [
"proc-macro2",
"quote",
@@ -1594,9 +1824,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.95"
+version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
+checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49"
[[package]]
name = "winapi"
@@ -1647,6 +1877,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -1768,6 +2007,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yoke"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
[[package]]
name = "zerocopy"
version = "0.7.35"
@@ -1788,3 +2069,46 @@ dependencies = [
"quote",
"syn",
]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
index dec1d8e..e9cc624 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "oxker"
-version = "0.8.0"
+version = "0.9.0"
edition = "2021"
authors = ["Jack Wills "]
description = "A simple tui to view & control docker containers"
@@ -27,7 +27,7 @@ similar_names = "allow"
[dependencies]
anyhow = "1.0"
-bollard = "0.17"
+bollard = "0.18"
cansi = "2.2"
clap = { version = "4.5", features = ["color", "derive", "unicode"] }
crossterm = "0.28"
@@ -35,7 +35,7 @@ directories = "5.0"
futures-util = "0.3"
parking_lot = { version = "0.12" }
ratatui = "0.29"
-tokio = { version = "1.41", features = ["full"] }
+tokio = { version = "1.42", features = ["full"] }
tokio-util = "0.7"
tracing = "0.1"
tracing-subscriber = "0.3"
diff --git a/README.md b/README.md
index 49c2938..b239e57 100644
--- a/README.md
+++ b/README.md
@@ -127,6 +127,7 @@ Available command line arguments
|```-s```| If running via Docker, will display the oxker container.|
|```-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.|
+|```--no-stderr```| Do not include stderr output in logs.|
|```--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.|
diff --git a/containerised/Dockerfile b/containerised/Dockerfile
index 3c2c7b8..e05760a 100644
--- a/containerised/Dockerfile
+++ b/containerised/Dockerfile
@@ -2,7 +2,7 @@
## Builder ##
#############
-FROM --platform=linux/amd64 rust:slim AS builder
+FROM --platform=$BUILDPLATFORM rust:slim AS builder
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_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
@@ -29,10 +29,10 @@ COPY Cargo.* /usr/src/oxker/
WORKDIR /usr/src/oxker
# 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
-RUN cargo build --target $(cat /.platform) --release
+RUN cargo build --target $(cat /.target) --release
# Now copy in the rest of the sources
COPY src /usr/src/oxker/src/
@@ -41,9 +41,9 @@ COPY src /usr/src/oxker/src/
RUN touch /usr/src/oxker/src/main.rs
# 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 ##
diff --git a/containerised/platform.sh b/containerised/target.sh
similarity index 62%
rename from containerised/platform.sh
rename to containerised/target.sh
index 9ada7c8..40ba3d6 100644
--- a/containerised/platform.sh
+++ b/containerised/target.sh
@@ -4,15 +4,15 @@
case $TARGETARCH in
"amd64")
- echo "x86_64-unknown-linux-musl" >/.platform
+ echo "x86_64-unknown-linux-musl" >/.target
echo "" >/.compiler
;;
"arm64")
- echo "aarch64-unknown-linux-musl" >/.platform
+ echo "aarch64-unknown-linux-musl" >/.target
echo "gcc-aarch64-linux-gnu" >/.compiler
;;
"arm")
- echo "arm-unknown-linux-musleabihf" >/.platform
+ echo "arm-unknown-linux-musleabihf" >/.target
echo "gcc-arm-linux-gnueabihf" >/.compiler
;;
esac
diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs
index 1dd717a..69b5c5e 100644
--- a/src/app_data/container_state.rs
+++ b/src/app_data/container_state.rs
@@ -2,6 +2,7 @@ use std::{
cmp::Ordering,
collections::{HashSet, VecDeque},
fmt,
+ net::IpAddr,
};
use bollard::service::Port;
@@ -103,17 +104,17 @@ macro_rules! unit_struct {
unit_struct!(ContainerName);
unit_struct!(ContainerImage);
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContainerPorts {
- pub ip: Option,
+ pub ip: Option,
pub private: u16,
pub public: Option,
}
-impl From<&Port> for ContainerPorts {
- fn from(value: &Port) -> Self {
+impl From for ContainerPorts {
+ fn from(value: Port) -> Self {
Self {
- ip: value.ip.clone(),
+ ip: value.ip.and_then(|i| i.parse::().ok()),
private: value.private_port,
public: value.public_port,
}
@@ -122,7 +123,9 @@ impl From<&Port> for ContainerPorts {
impl ContainerPorts {
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 {
format!("{}", self.private).chars().count()
@@ -133,11 +136,12 @@ impl ContainerPorts {
.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
.as_ref()
- .map_or(String::new(), std::borrow::ToOwned::to_owned),
+ .map_or(String::new(), std::string::ToString::to_string),
format!("{}", self.private),
self.public.map_or(String::new(), |s| s.to_string()),
)
@@ -171,27 +175,25 @@ impl StatefulList {
pub fn next(&mut self) {
if !self.items.is_empty() {
- let i = match self.state.selected() {
- Some(i) => {
- if i < self.items.len() - 1 {
- i + 1
- } else {
- i
- }
+ self.state.select(Some(self.state.selected().map_or(0, |i| {
+ if i < self.items.len() - 1 {
+ i + 1
+ } else {
+ i
}
- None => 0,
- };
- self.state.select(Some(i));
+ })));
}
}
pub fn previous(&mut self) {
if !self.items.is_empty() {
- let i = self
- .state
- .selected()
- .map_or(0, |i| if i == 0 { 0 } else { i - 1 });
- self.state.select(Some(i));
+ self.state.select(Some(self.state.selected().map_or(0, |i| {
+ if i == 0 {
+ 0
+ } else {
+ i - 1
+ }
+ })));
}
}
@@ -201,11 +203,11 @@ impl StatefulList {
String::new()
} else {
let len = self.items.len();
- let c = self
+ let count = self
.state
.selected()
.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 {
+ /// The container is alive if the start is Running, either healthy or unhealthy
pub const fn is_alive(self) -> bool {
matches!(self, Self::Running(_))
}
+ /// Color of the state for the containers section
+ /// TODO allow usable editable colours
pub const fn get_color(self) -> Color {
match self {
Self::Paused => Color::Yellow,
@@ -333,7 +338,7 @@ impl fmt::Display for State {
/// Items for the container control list
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum DockerControls {
+pub enum DockerCommand {
Pause,
Restart,
Start,
@@ -342,7 +347,7 @@ pub enum DockerControls {
Delete,
}
-impl DockerControls {
+impl DockerCommand {
pub const fn get_color(self) -> Color {
match self {
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 {
let disp = match self {
Self::Pause => "pause",
@@ -577,7 +582,7 @@ impl Logs {
pub struct ContainerItem {
pub cpu_stats: VecDeque,
pub created: u64,
- pub docker_controls: StatefulList,
+ pub docker_controls: StatefulList,
pub id: ContainerId,
pub image: ContainerImage,
pub is_oxker: bool,
@@ -620,7 +625,7 @@ impl ContainerItem {
state: State,
status: ContainerStatus,
) -> Self {
- let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
+ let mut docker_controls = StatefulList::new(DockerCommand::gen_vec(state));
docker_controls.start();
Self {
diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs
index d21c232..f355f2b 100644
--- a/src/app_data/mod.rs
+++ b/src/app_data/mod.rs
@@ -143,10 +143,10 @@ impl AppData {
Self {
args,
containers: StatefulList::new(vec![]),
- hidden_containers: vec![],
error: None,
- sorted_by: None,
filter: Filter::new(),
+ hidden_containers: vec![],
+ sorted_by: None,
}
}
@@ -160,15 +160,9 @@ impl AppData {
}
/// Filter related methods
-
- /// Get the current filter term
- pub const fn get_filter_term(&self) -> Option<&String> {
- self.filter.term.as_ref()
- }
-
- /// Get the current filter by choice
- pub const fn get_filter_by(&self) -> FilterBy {
- self.filter.by
+ /// Get the filterby and filter_term
+ pub const fn get_filter(&self) -> (FilterBy, Option<&String>) {
+ (self.filter.by, self.filter.term.as_ref())
}
/// 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();
}
- // change the filter_by option
+ /// change the filter_by option
pub fn filter_by_next(&mut self) {
if let Some(by) = self.filter.by.next() {
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) {
if let Some(by) = self.filter.by.prev() {
self.filter.by = by;
@@ -280,7 +274,6 @@ impl AppData {
}
/// Container sort related methods
-
/// Change the sorted order, also set the selected container state to match new order
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
self.sorted_by = x;
@@ -350,7 +343,6 @@ impl AppData {
.back()
.cmp(&item_ord.1.mem_stats.back())
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
-
Header::Id => item_ord
.0
.id
@@ -372,7 +364,6 @@ impl AppData {
.tx
.cmp(&item_ord.1.tx)
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
-
Header::Name => item_ord
.0
.name
@@ -392,19 +383,26 @@ impl AppData {
}
/// Container state methods
-
/// Get the total number of none "hidden" containers
pub fn get_container_len(&self) -> usize {
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::>()
+ }
+
/// Get all the ContainerItems
pub fn get_container_items(&self) -> &[ContainerItem] {
&self.containers.items
}
/// 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() {
" - filtered"
} 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)
+ ///display like this: "│ ip, private, public│", so (5,10,9) are the minimum lengths required
pub fn get_longest_port(&self) -> (usize, usize, usize) {
- let mut longest_ip = 5;
- let mut longest_private = 10;
- let mut longest_public = 9;
+ let mut output = (5, 10, 9);
for item in [&self.containers.items, &self.hidden_containers] {
for item in item {
- longest_ip = longest_ip.max(
+ output.0 = output.0.max(
item.ports
.iter()
.map(ContainerPorts::len_ip)
.max()
- .unwrap_or(3),
+ .unwrap_or(output.0),
);
- longest_private = longest_private.max(
+ output.1 = output.1.max(
item.ports
.iter()
.map(ContainerPorts::len_private)
.max()
- .unwrap_or(8),
+ .unwrap_or(output.1),
);
- longest_public = longest_public.max(
+ output.2 = output.2.max(
item.ports
.iter()
.map(ContainerPorts::len_public)
.max()
- .unwrap_or(6),
+ .unwrap_or(output.2),
);
}
}
-
- (longest_ip, longest_private, longest_public)
+ output
}
/// Get Option of the current selected container's ports, sorted by private port
- pub fn get_selected_ports(&mut self) -> Option<(Vec, State)> {
- if let Some(item) = self.get_mut_selected_container() {
+ pub fn get_selected_ports(&self) -> Option<(Vec, State)> {
+ if let Some(item) = self.get_selected_container() {
let mut ports = item.ports.clone();
ports.sort_by(|a, b| a.private.cmp(&b.private));
return Some((ports, item.state));
@@ -510,12 +506,12 @@ impl AppData {
}
/// Get the ContainerName of by ID
- pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option {
+ pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<&ContainerName> {
self.containers
.items
.iter_mut()
.find(|i| &i.id == id)
- .map(|i| i.name.clone())
+ .map(|i| &i.name)
}
/// Find the id of the currently selected container.
@@ -532,10 +528,9 @@ impl AppData {
}
/// Selected DockerCommand methods
-
/// Get the current selected docker command
/// So know which command to execute
- pub fn selected_docker_controls(&self) -> Option {
+ pub fn selected_docker_controls(&self) -> Option {
self.get_selected_container().and_then(|i| {
i.docker_controls.state.selected().and_then(|x| {
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> {
self.get_mut_selected_container()
.map(|i| &mut i.docker_controls.state)
}
- /// Get mutable Option of the currently selected container DockerControls items
- /// TODO command or control, need a uniform name across the application
- pub fn get_control_items(&mut self) -> Option<&mut Vec> {
+ /// Get mutable Option of the currently selected container DockerConmand items
+ pub fn get_control_items(&mut self) -> Option<&mut Vec> {
self.get_mut_selected_container()
.map(|i| &mut i.docker_controls.items)
}
/// Logs related methods
-
/// Get the title for log panel for selected container, will be either
/// 1) "logs x/x - container_name - container_image"
/// 2) "logs - container_name - container_image" when no logs found
@@ -635,11 +628,11 @@ impl AppData {
}
/// Get mutable Vec of current containers logs
- pub fn get_logs(&mut self) -> Vec> {
+ pub fn get_logs(&self) -> Vec> {
self.containers
.state
.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())
}
@@ -653,18 +646,16 @@ impl AppData {
}
/// Chart data related methods
-
/// 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
.state
.selected()
- .and_then(|i| self.containers.items.get_mut(i))
- .map(|i| i.get_chart_data())
+ .and_then(|i| self.containers.items.get(i))
+ .map(container_state::ContainerItem::get_chart_data)
}
/// Error related methods
-
/// Get single app_state error
pub const fn get_error(&self) -> Option {
self.error
@@ -701,7 +692,6 @@ impl AppData {
let mut columns = Columns::new();
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 container {
let cpu_count = container.cpu_stats.back().map_or_else(
@@ -729,7 +719,6 @@ impl AppData {
}
/// Update related methods
-
/// 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> {
if self.get_hidden_container_by_id(id).is_some() {
@@ -769,12 +758,11 @@ impl AppData {
container.tx.update(tx);
container.mem_limit.update(mem_limit);
}
- // need to benchmark this?
self.sort_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) {
let all_ids = self
.containers
.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() {
let name = i.names.as_mut().map_or(String::new(), |names| {
names.first_mut().map_or(String::new(), |f| {
@@ -820,8 +808,8 @@ impl AppData {
})
});
- let ports = i.ports.as_ref().map_or(vec![], |i| {
- i.iter().map(ContainerPorts::from).collect::>()
+ let ports = i.ports.map_or(vec![], |i| {
+ i.into_iter().map(ContainerPorts::from).collect::>()
});
let id = ContainerId::from(id.as_str());
@@ -855,7 +843,7 @@ impl AppData {
item.status = status;
};
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
match state {
State::Removing | State::Restarting | State::Unknown => {
@@ -1476,7 +1464,7 @@ mod tests {
let mut app_data = gen_appdata(&containers);
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]
@@ -1526,7 +1514,7 @@ mod tests {
app_data.docker_controls_start();
let result = app_data.selected_docker_controls();
- assert_eq!(result, Some(DockerControls::Pause));
+ assert_eq!(result, Some(DockerCommand::Pause));
}
#[test]
@@ -1539,7 +1527,7 @@ mod tests {
app_data.docker_controls_next();
let result = app_data.selected_docker_controls();
- assert_eq!(result, Some(DockerControls::Restart));
+ assert_eq!(result, Some(DockerCommand::Restart));
}
#[test]
@@ -1551,12 +1539,12 @@ mod tests {
app_data.docker_controls_end();
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
app_data.docker_controls_next();
let result = app_data.selected_docker_controls();
- assert_eq!(result, Some(DockerControls::Delete));
+ assert_eq!(result, Some(DockerCommand::Delete));
}
#[test]
@@ -1569,19 +1557,19 @@ mod tests {
app_data.docker_controls_previous();
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
app_data.docker_controls_start();
app_data.docker_controls_previous();
let result = app_data.selected_docker_controls();
- assert_eq!(result, Some(DockerControls::Pause));
+ assert_eq!(result, Some(DockerCommand::Pause));
}
#[test]
/// DockerCommands get correct controls dependant on container state
fn test_app_data_get_control_items() {
- let test_state = |state: State, expected: &mut Vec| {
+ let test_state = |state: State, expected: &mut Vec| {
let gen_item_state = |state: State| {
ContainerItem::new(
1,
@@ -1605,42 +1593,42 @@ mod tests {
test_state(
State::Dead,
&mut vec![
- DockerControls::Start,
- DockerControls::Restart,
- DockerControls::Delete,
+ DockerCommand::Start,
+ DockerCommand::Restart,
+ DockerCommand::Delete,
],
);
test_state(
State::Exited,
&mut vec![
- DockerControls::Start,
- DockerControls::Restart,
- DockerControls::Delete,
+ DockerCommand::Start,
+ DockerCommand::Restart,
+ DockerCommand::Delete,
],
);
test_state(
State::Paused,
&mut vec![
- DockerControls::Resume,
- DockerControls::Stop,
- DockerControls::Delete,
+ DockerCommand::Resume,
+ DockerCommand::Stop,
+ DockerCommand::Delete,
],
);
- test_state(State::Removing, &mut vec![DockerControls::Delete]);
+ test_state(State::Removing, &mut vec![DockerCommand::Delete]);
test_state(
State::Restarting,
- &mut vec![DockerControls::Stop, DockerControls::Delete],
+ &mut vec![DockerCommand::Stop, DockerCommand::Delete],
);
test_state(
State::Running(RunningState::Healthy),
&mut vec![
- DockerControls::Pause,
- DockerControls::Restart,
- DockerControls::Stop,
- DockerControls::Delete,
+ DockerCommand::Pause,
+ DockerCommand::Restart,
+ DockerCommand::Stop,
+ 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);
- assert!(app_data.get_filter_term().is_none());
+ assert!(app_data.get_filter().1.is_none());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('_');
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();
let post_len = app_data.containers.items.len();
@@ -1680,7 +1668,7 @@ mod tests {
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();
for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] {
@@ -1689,8 +1677,10 @@ mod tests {
// app_data.filter_term_push('2');
app_data.filter_by_next();
- assert_eq!(app_data.get_filter_by(), FilterBy::Image);
- assert_eq!(app_data.get_filter_term(), Some(&"image_2".to_string()));
+ assert_eq!(
+ app_data.get_filter(),
+ (FilterBy::Image, Some(&"image_2".to_string()))
+ );
app_data.filter_containers();
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);
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();
app_data.filter_term_push('x');
@@ -1717,8 +1707,10 @@ mod tests {
app_data.filter_by_next();
app_data.filter_by_next();
- assert_eq!(app_data.get_filter_by(), FilterBy::Status);
- assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
+ assert_eq!(
+ app_data.get_filter(),
+ (FilterBy::Status, Some(&"x".to_string()))
+ );
app_data.filter_containers();
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);
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();
app_data.filter_term_push('x');
@@ -1746,8 +1738,10 @@ mod tests {
app_data.filter_by_next();
app_data.filter_by_next();
- assert_eq!(app_data.get_filter_by(), FilterBy::All);
- assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
+ assert_eq!(
+ app_data.get_filter(),
+ (FilterBy::All, Some(&"x".to_string()))
+ );
app_data.filter_containers();
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);
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();
app_data.filter_term_push('x');
@@ -1774,8 +1768,10 @@ mod tests {
app_data.filter_by_next();
app_data.filter_by_next();
- assert_eq!(app_data.get_filter_by(), FilterBy::Status);
- assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
+ assert_eq!(
+ app_data.get_filter(),
+ (FilterBy::Status, Some(&"x".to_string()))
+ );
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -1787,8 +1783,10 @@ mod tests {
assert!(!app_data.can_insert(&containers[2]));
app_data.filter_by_prev();
- assert_eq!(app_data.get_filter_by(), FilterBy::Image);
- assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
+ assert_eq!(
+ app_data.get_filter(),
+ (FilterBy::Image, Some(&"x".to_string()))
+ );
app_data.filter_containers();
let post_len = app_data.containers.items.len();
@@ -2230,12 +2228,12 @@ mod tests {
let (_ids, containers) = gen_containers();
let mut app_data = gen_appdata(&containers);
let result_pre = app_data.get_container_items().to_owned();
- let mut input = [
+ let input = vec![
gen_container_summary(1, "paused"),
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();
assert_ne!(result_pre, result_post);
assert_eq!(result_post[0].state, State::Paused);
diff --git a/src/app_error.rs b/src/app_error.rs
index ba0d66f..e192ad8 100644
--- a/src/app_error.rs
+++ b/src/app_error.rs
@@ -1,16 +1,13 @@
-use crate::app_data::DockerControls;
+use crate::app_data::DockerCommand;
use std::fmt;
/// app errors to set in global state
-#[allow(unused)]
#[derive(Debug, Clone, Copy)]
pub enum AppError {
- DockerCommand(DockerControls),
+ DockerCommand(DockerCommand),
DockerExec,
DockerLogs,
DockerConnect,
- DockerInterval,
- InputPoll,
MouseCapture(bool),
Terminal,
}
@@ -23,8 +20,6 @@ impl fmt::Display for AppError {
Self::DockerExec => write!(f, "Unable to exec into container"),
Self::DockerLogs => write!(f, "Unable to save logs"),
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) => {
let reason = if *x { "en" } else { "dis" };
write!(f, "Unable to {reason}able mouse capture")
diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs
index 866aada..b0af01a 100644
--- a/src/docker_data/message.rs
+++ b/src/docker_data/message.rs
@@ -1,19 +1,13 @@
use std::sync::Arc;
-use crate::app_data::ContainerId;
+use crate::app_data::{ContainerId, DockerCommand};
use bollard::Docker;
use tokio::sync::oneshot::Sender;
#[derive(Debug)]
pub enum DockerMessage {
ConfirmDelete(ContainerId),
- Delete(ContainerId),
+ Control((DockerCommand, ContainerId)),
Exec(Sender>),
- Pause(ContainerId),
- Quit,
- Restart(ContainerId),
- Start(ContainerId),
- Stop(ContainerId),
- Resume(ContainerId),
Update,
}
diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs
index b97102c..b288af3 100644
--- a/src/docker_data/mod.rs
+++ b/src/docker_data/mod.rs
@@ -10,10 +10,7 @@ use futures_util::StreamExt;
use parking_lot::Mutex;
use std::{
collections::HashMap,
- sync::{
- atomic::{AtomicBool, AtomicUsize},
- Arc,
- },
+ sync::{atomic::AtomicUsize, Arc},
};
use tokio::{
sync::mpsc::{Receiver, Sender},
@@ -22,7 +19,7 @@ use tokio::{
use uuid::Uuid;
use crate::{
- app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State},
+ app_data::{AppData, ContainerId, DockerCommand, State},
app_error::AppError,
parse_args::CliArgs,
ui::{GuiState, Status},
@@ -37,6 +34,15 @@ enum SpawnId {
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
/// 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
@@ -62,8 +68,6 @@ pub struct DockerData {
binate: Binate,
docker: Arc,
gui_state: Arc>,
- is_running: Arc,
- init: Option>,
receiver: Receiver,
spawns: Arc>>>,
}
@@ -71,24 +75,30 @@ pub struct DockerData {
impl DockerData {
/// Use docker stats to calculate current cpu usage
#[allow(clippy::cast_precision_loss)]
- // TODO FIX: this can overflow
fn calculate_usage(stats: &Stats) -> f64 {
let mut cpu_percentage = 0.0;
- let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
- let cpu_delta = stats.cpu_stats.cpu_usage.total_usage as f64 - previous_cpu as f64;
+ let cpu_delta = stats
+ .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)) = (
stats.cpu_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(|| {
- stats
- .cpu_stats
- .cpu_usage
- .percpu_usage
- .as_ref()
- .map_or(0, std::vec::Vec::len) as u64
+ u64::try_from(
+ stats
+ .cpu_stats
+ .cpu_usage
+ .percpu_usage
+ .as_ref()
+ .map_or(0, std::vec::Vec::len),
+ )
+ .unwrap_or_default()
}) as f64;
if system_delta > 0.0 && cpu_delta > 0.0 {
cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0;
@@ -103,97 +113,86 @@ impl DockerData {
async fn update_container_stat(
app_data: Arc>,
docker: Arc,
- id: ContainerId,
- init: Option<(Arc, usize)>,
state: State,
spawn_id: SpawnId,
spawns: Arc>>>,
) {
- if state.is_alive() || init.is_some() {
- let mut stream = docker
- .stats(
- id.get(),
- Some(StatsOptions {
- stream: false,
- one_shot: false,
- }),
- )
- .take(1);
+ let id = spawn_id.get_id();
+ let mut stream = docker
+ .stats(
+ id.get(),
+ Some(StatsOptions {
+ stream: false,
+ one_shot: false,
+ }),
+ )
+ .take(1);
- while let Some(Ok(stats)) = stream.next().await {
- // 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_cache = stats.memory_stats.stats.map_or(0, |i| match i {
- MemoryStatsStats::V1(x) => x.inactive_file,
- MemoryStatsStats::V2(x) => x.inactive_file,
- });
+ while let Some(Ok(stats)) = stream.next().await {
+ // Memory stats are only collected if the container is alive - is this the behaviour we want?
+ let (mem_stat, cpu_stats) = if state.is_alive() {
+ let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i {
+ MemoryStatsStats::V1(x) => x.inactive_file,
+ MemoryStatsStats::V2(x) => x.inactive_file,
+ });
+ (
Some(
stats
.memory_stats
.usage
.unwrap_or_default()
.saturating_sub(mem_cache),
- )
- } else {
- None
- };
+ ),
+ Some(Self::calculate_usage(&stats)),
+ )
+ } 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
- .as_ref()
- .and_then(|networks| networks.keys().next().cloned());
+ .unwrap_or_default()
+ .get(&key)
+ .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
+ } else {
+ (0, 0)
+ };
- let cpu_stats = if state.is_alive() {
- Some(Self::calculate_usage(&stats))
- } else {
- None
- };
- let (rx, tx) = if let Some(key) = op_key {
- stats
- .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);
- }
+ app_data.lock().update_stats_by_id(
+ id,
+ cpu_stats,
+ mem_stat,
+ stats.memory_stats.limit.unwrap_or_default(),
+ rx,
+ tx,
+ );
}
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
- 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 {
- let docker = Arc::clone(&self.docker);
- let app_data = Arc::clone(&self.app_data);
- let spawns = Arc::clone(&self.spawns);
- let spawn_id = SpawnId::Stats((id.clone(), self.binate));
+ let spawn_id = SpawnId::Stats((id, self.binate));
- let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len()));
- self.spawns
- .lock()
- .entry(spawn_id.clone())
- .or_insert_with(|| {
- tokio::spawn(Self::update_container_stat(
- app_data,
- docker,
- id.clone(),
- init,
- *state,
- spawn_id,
- spawns,
- ))
- });
+ if let std::collections::hash_map::Entry::Vacant(spawns) =
+ self.spawns.lock().entry(spawn_id.clone())
+ {
+ spawns.insert(tokio::spawn(Self::update_container_stat(
+ Arc::clone(&self.app_data),
+ Arc::clone(&self.docker),
+ state,
+ spawn_id,
+ Arc::clone(&self.spawns),
+ )));
+ }
}
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
/// Just make sure that items sent are guaranteed to have an id
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
- pub async fn update_all_containers(&self) -> Vec<(State, ContainerId)> {
+ async fn update_all_containers(&self) {
let containers = self
.docker
.list_containers(Some(ListContainersOptions:: {
@@ -211,7 +210,7 @@ impl DockerData {
.await
.unwrap_or_default();
- let mut output = containers
+ let output = containers
.into_iter()
.filter_map(|f| match f.id {
Some(_) => {
@@ -230,23 +229,7 @@ impl DockerData {
})
.collect::>();
- self.app_data.lock().update_containers(&mut 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::>()
+ self.app_data.lock().update_containers(output);
}
/// Update single container logs
@@ -257,10 +240,11 @@ impl DockerData {
id: ContainerId,
since: u64,
spawns: Arc>>>,
+ stderr: bool,
) {
let options = Some(LogsOptions:: {
stdout: true,
- stderr: true,
+ stderr,
timestamps: true,
since: i64::try_from(since).unwrap_or_default(),
..Default::default()
@@ -275,44 +259,29 @@ impl DockerData {
output.push(data);
}
}
- spawns.lock().remove(&SpawnId::Log(id.clone()));
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
- fn init_all_logs(&self, all_ids: &[(State, ContainerId)]) {
+ fn init_all_logs(&self, all_ids: Vec<(State, ContainerId)>) -> Arc {
+ let init = Arc::new(AtomicUsize::new(0));
for (_, id) in all_ids {
+ let app_data: Arc> =
+ Arc::clone(&self.app_data);
let docker = Arc::clone(&self.docker);
- let app_data = Arc::clone(&self.app_data);
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(
- key,
- tokio::spawn(Self::update_log(app_data, docker, id.clone(), 0, spawns)),
+ SpawnId::Log(id.clone()),
+ 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);
+ }),
);
}
- }
-
- /// 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());
+ init
}
/// Initialize docker container data, before any messages are received
@@ -320,27 +289,48 @@ impl DockerData {
self.gui_state.lock().status_push(Status::Init);
let loading_uuid = Uuid::new_v4();
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);
-
- self.init_all_logs(&all_ids);
-
- while let Some(x) = self.init.as_ref() {
+ while init.load(std::sync::atomic::Ordering::SeqCst) != all_ids_len {
self.app_data.lock().sort_containers();
- tokio::time::sleep(std::time::Duration::from_millis(100)).await;
- if x.load(std::sync::atomic::Ordering::SeqCst) == all_ids.len() {
- self.init = None;
- }
+ tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
self.gui_state.lock().stop_loading_animation(loading_uuid);
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
fn set_error(
app_data: &Arc>,
- error: DockerControls,
+ error: DockerCommand,
gui_state: &Arc>,
) {
app_data
@@ -348,150 +338,102 @@ impl DockerData {
.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::>)
+ .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
/// Spawn Docker commands off into own thread
- #[allow(clippy::too_many_lines)]
async fn message_handler(&mut self) {
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 {
- 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::>)
- .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) => {
self.gui_state.lock().set_delete_container(Some(id));
}
- DockerMessage::Update => self.update_everything().await,
- DockerMessage::Quit => {
- self.spawns
- .lock()
- .values()
- .for_each(tokio::task::JoinHandle::abort);
- self.is_running
- .store(false, std::sync::atomic::Ordering::SeqCst);
+ DockerMessage::Control((command, id)) => self.execute_command(command, id).await,
+ DockerMessage::Exec(docker_tx) => {
+ docker_tx.send(Arc::clone(&self.docker)).ok();
}
+ DockerMessage::Update => self.update_everything().await,
}
}
}
/// Send an update message every x ms, where x is the args.docker_interval
- fn scheduler(args: &CliArgs, docker_tx: Sender) {
+ fn heartbeat(args: &CliArgs, docker_tx: Sender) {
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
let mut now = std::time::Instant::now();
tokio::spawn(async move {
loop {
- let to_sleep = update_duration.saturating_sub(now.elapsed());
- tokio::time::sleep(to_sleep).await;
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();
}
});
}
/// Initialise self, and start the message receiving loop
- pub async fn init(
+ pub async fn start(
app_data: Arc>,
docker: Docker,
docker_rx: Receiver,
docker_tx: Sender,
gui_state: Arc>,
- is_running: Arc,
) {
let args = app_data.lock().args.clone();
if app_data.lock().get_error().is_none() {
let mut inner = Self {
app_data,
- args: args.clone(),
+ args,
binate: Binate::One,
docker: Arc::new(docker),
gui_state,
- init: Some(Arc::new(AtomicUsize::new(0))),
- is_running,
receiver: docker_rx,
spawns: Arc::new(Mutex::new(HashMap::new())),
};
inner.initialise_container_data().await;
- Self::scheduler(&args, docker_tx);
+ Self::heartbeat(&inner.args, docker_tx);
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
#[cfg(test)]
+#[allow(clippy::float_cmp)]
mod tests {
use bollard::container::{
- BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData,
+ BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, Stats, StorageStats, ThrottlingData,
};
use super::*;
- #[allow(clippy::too_many_lines)]
- fn gen_stats(x: u64, y: u64) -> Stats {
+ fn gen_stats() -> Stats {
Stats {
read: String::new(),
preread: String::new(),
- num_procs: 0,
+ num_procs: 1,
pids_stats: PidsStats {
current: None,
limit: None,
@@ -542,33 +484,12 @@ mod tests {
},
cpu_stats: CPUStats {
cpu_usage: CPUUsage {
- percpu_usage: Some(vec![
- 291_593_800,
- 182_192_900,
- 195_048_700,
- 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,
+ percpu_usage: Some(vec![50]),
+ usage_in_usermode: 10,
+ total_usage: 100,
+ usage_in_kernelmode: 20,
},
- system_cpu_usage: Some(x),
+ system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
@@ -578,33 +499,12 @@ mod tests {
},
precpu_stats: CPUStats {
cpu_usage: CPUUsage {
- percpu_usage: Some(vec![
- 291_593_800,
- 182_192_900,
- 195_048_700,
- 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,
+ percpu_usage: Some(vec![50]),
+ usage_in_usermode: 10,
+ total_usage: 100,
+ usage_in_kernelmode: 20,
},
- system_cpu_usage: Some(y),
+ system_cpu_usage: Some(400),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
@@ -618,25 +518,198 @@ mod tests {
write_count_normalized: None,
write_size_bytes: None,
},
- name: "/container_1".to_owned(),
- id: "1".to_owned(),
+ name: String::new(),
+ id: String::new(),
}
}
#[test]
- #[allow(clippy::float_cmp)]
- /// Test the stats calculator, had to cheat here to get round input/outputs
- fn test_calculate_usage_no_previous_cpu() {
- let stats = gen_stats(1_000_000_000, 900_000_000);
- let result = DockerData::calculate_usage(&stats);
- assert_eq!(result, 50.0);
+ fn test_calculate_usage_50() {
+ 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![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);
- let result = DockerData::calculate_usage(&stats);
- assert_eq!(result, 25.0);
+ #[test]
+ fn test_calculate_usage_25() {
+ 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 result = DockerData::calculate_usage(&stats);
- assert_eq!(result, 20.00);
+ let cpu_percentage = DockerData::calculate_usage(&stats);
+ assert_eq!(25.0, cpu_percentage);
+ }
+
+ #[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);
}
}
diff --git a/src/exec.rs b/src/exec.rs
index 283a945..ba77737 100644
--- a/src/exec.rs
+++ b/src/exec.rs
@@ -144,9 +144,9 @@ impl TerminalSize {
#[derive(Debug, Clone)]
pub enum ExecMode {
// use Bollard Rust library
- Internal((ContainerId, Arc)),
+ Internal((Arc, Arc)),
// use the external `docker-cli`
- External(ContainerId),
+ External(Arc),
}
impl ExecMode {
@@ -186,7 +186,10 @@ impl ExecMode {
{
if let Some(Ok(msg)) = output.next().await {
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 !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(())
}
- // 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
- // afterwhich it's assumes that we're completely done with TTY
+ /// 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
+ /// afterwhich it's assumes that we're completely done with TTY
fn internal_cleanup(&self) -> Result<(), AppError> {
match self {
Self::External(_) => Ok(()),
diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs
index 8228f59..1670410 100644
--- a/src/input_handler/mod.rs
+++ b/src/input_handler/mod.rs
@@ -1,14 +1,11 @@
use std::{
fs::OpenOptions,
io::{BufWriter, Write},
- sync::{
- atomic::{AtomicBool, Ordering},
- Arc,
- },
+ sync::{atomic::AtomicBool, Arc},
time::SystemTime,
};
-use bollard::{container::LogsOptions, Docker};
+use bollard::container::LogsOptions;
use cansi::v3::categorise_text;
use crossterm::{
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
@@ -22,7 +19,7 @@ use uuid::Uuid;
mod message;
use crate::{
- app_data::{AppData, DockerControls, Header},
+ app_data::{AppData, DockerCommand, Header},
app_error::AppError,
docker_data::DockerMessage,
exec::{tty_readable, ExecMode},
@@ -43,7 +40,7 @@ pub struct InputHandler {
impl InputHandler {
/// Initialize self, and running the message handling loop
- pub async fn init(
+ pub async fn start(
app_data: Arc>,
rec: Receiver,
docker_tx: Sender,
@@ -58,35 +55,30 @@ impl InputHandler {
rec,
mouse_capture: true,
};
- inner.start().await;
+ inner.message_handler().await;
}
/// check for incoming messages
- async fn start(&mut self) {
+ async fn message_handler(&mut self) {
while let Some(message) = self.rec.recv().await {
match message {
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
InputMessages::MouseEvent(mouse_event) => {
- if !self.gui_state.lock().status_contains(&[
- Status::Error,
- Status::Help,
- Status::DeleteConfirm,
- Status::Filter,
- ]) {
+ let status = self.gui_state.lock().get_status();
+ let contains = |s: Status| status.contains(&s);
+
+ if !contains(Status::Error)
+ | !contains(Status::Help)
+ | !contains(Status::DeleteConfirm)
+ | !contains(Status::Filter)
+ {
self.mouse_press(mouse_event);
}
- let delete_confirm = self
- .gui_state
- .lock()
- .status_contains(&[Status::DeleteConfirm]);
- if delete_confirm {
+ if contains(Status::DeleteConfirm) {
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
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
- async fn quit(&self) {
- let error_init = self
- .gui_state
- .lock()
- .status_contains(&[Status::Error, Status::Init]);
- if error_init || self.docker_tx.send(DockerMessage::Quit).await.is_err() {
+ fn quit(&self) {
+ let status = self.gui_state.lock().get_status();
+ let contains = |s: Status| status.contains(&s);
+ if !contains(Status::Error) | !contains(Status::Init) {
self.is_running
.store(false, std::sync::atomic::Ordering::SeqCst);
}
@@ -112,7 +102,10 @@ impl InputHandler {
async fn confirm_delete(&self) {
let id = self.gui_state.lock().get_delete_container();
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() {
let uuid = Uuid::new_v4();
GuiState::start_loading_animation(&self.gui_state, uuid);
- let (sx, rx) = tokio::sync::oneshot::channel::>();
+ let (sx, rx) = tokio::sync::oneshot::channel();
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
if let Ok(docker) = rx.await {
@@ -150,118 +143,109 @@ impl InputHandler {
/// Toggle the mouse capture (via input of the 'm' key)
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 execute!(std::io::stdout(), DisableMouseCapture).is_ok() {
self.gui_state
.lock()
.set_info_box("✖ mouse capture disabled");
} else {
- self.app_data.lock().set_error(
- AppError::MouseCapture(false),
- &self.gui_state,
- Status::Error,
- );
+ err();
}
} else if Ui::enable_mouse_capture().is_ok() {
self.gui_state
.lock()
.set_info_box("✓ mouse capture enabled");
} else {
- self.app_data.lock().set_error(
- AppError::MouseCapture(true),
- &self.gui_state,
- Status::Error,
- );
+ err();
};
self.mouse_capture = !self.mouse_capture;
}
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
- async fn s_key(&self) {
- /// This is the inner workings, *inlined* here to return a Result
- async fn save_logs(
- app_data: &Arc>,
- gui_state: &Arc>,
- docker_tx: &Sender,
- ) -> Result<(), Box> {
- 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::>();
- docker_tx.send(DockerMessage::Exec(sx)).await?;
+ async fn save_logs(&self) -> Result<(), Box> {
+ let args = self.app_data.lock().args.clone();
+ let container = self.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();
+ self.docker_tx.send(DockerMessage::Exec(sx)).await?;
- let now = SystemTime::now()
- .duration_since(SystemTime::UNIX_EPOCH)
- .map_or(0, |i| i.as_secs());
+ let now = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .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:: {
- stderr: true,
- stdout: true,
- timestamps: args.timestamp,
- since: 0,
- ..Default::default()
- });
- let mut logs = docker.logs(id.get(), options);
- let mut output = vec![];
+ let options = Some(LogsOptions:: {
+ stderr: true,
+ stdout: true,
+ timestamps: args.timestamp,
+ since: 0,
+ ..Default::default()
+ });
+ let mut logs = rx.await?.logs(id.get(), options);
+ let mut output = vec![];
- while let Some(Ok(value)) = logs.next().await {
- let data = value.to_string();
- if !data.trim().is_empty() {
- output.push(
- categorise_text(&data)
- .into_iter()
- .map(|i| i.text)
- .collect::(),
- );
- }
- }
- if !output.is_empty() {
- let mut stream = BufWriter::new(
- OpenOptions::new()
- .read(true)
- .write(true)
- .create(true)
- .truncate(true)
- .open(&path)?,
+ while let Some(Ok(value)) = logs.next().await {
+ let data = value.to_string();
+ if !data.trim().is_empty() {
+ output.push(
+ categorise_text(&data)
+ .into_iter()
+ .map(|i| i.text)
+ .collect::(),
);
-
- 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;
- let status = self.gui_state.lock().status_contains(&[log_status]);
- if !status {
- self.gui_state.lock().status_push(log_status);
+ /// Attempt to save the currently selected container logs to a file
+ async fn s_key(&self) {
+ let status = self.gui_state.lock().get_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();
GuiState::start_loading_animation(&self.gui_state, uuid);
- if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
- .await
- .is_err()
- {
+ if self.save_logs().await.is_err() {
self.app_data.lock().set_error(
AppError::DockerLogs,
&self.gui_state,
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);
}
}
@@ -281,26 +265,17 @@ impl InputHandler {
let option_id = self.app_data.lock().get_selected_container_id();
if let Some(id) = option_id {
match command {
- DockerControls::Delete => self
+ DockerCommand::Delete => self
.docker_tx
.send(DockerMessage::ConfirmDelete(id))
.await
.ok(),
- DockerControls::Pause => {
- self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
- }
- DockerControls::Resume => {
- self.docker_tx.send(DockerMessage::Resume(id)).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()
- }
+
+ _ => self
+ .docker_tx
+ .send(DockerMessage::Control((command, id)))
+ .await
+ .ok(),
};
}
}
@@ -308,50 +283,43 @@ impl InputHandler {
}
/// 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) {
- let is_containers =
- self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
- let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
- 2
- } else {
- 1
- };
- for _ in 0..count {
+ self.gui_state.lock().next_panel();
+ if self.app_data.lock().get_container_len() == 0
+ && self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
+ {
self.gui_state.lock().next_panel();
}
}
/// Change to previously selected panel
+ /// Need to skip the commands planel if there no are current containers running
fn back_tab_key(&self) {
- let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
- let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
- 2
- } else {
- 1
- };
- for _ in 0..count {
+ self.gui_state.lock().previous_panel();
+ if self.app_data.lock().get_container_len() == 0
+ && self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands
+ {
self.gui_state.lock().previous_panel();
}
}
fn home_key(&self) {
- let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
- SelectablePanel::Containers => locked_data.containers_start(),
- SelectablePanel::Logs => locked_data.log_start(),
- SelectablePanel::Commands => locked_data.docker_controls_start(),
+ SelectablePanel::Containers => self.app_data.lock().containers_start(),
+ SelectablePanel::Logs => self.app_data.lock().log_start(),
+ SelectablePanel::Commands => self.app_data.lock().docker_controls_start(),
}
}
/// Go to end of the list of the currently selected panel
fn end_key(&self) {
- let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
- SelectablePanel::Containers => locked_data.containers_end(),
- SelectablePanel::Logs => locked_data.log_end(),
- SelectablePanel::Commands => locked_data.docker_controls_end(),
+ SelectablePanel::Containers => self.app_data.lock().containers_end(),
+ SelectablePanel::Logs => self.app_data.lock().log_end(),
+ SelectablePanel::Commands => self.app_data.lock().docker_controls_end(),
}
}
@@ -455,24 +423,21 @@ impl InputHandler {
}
/// Handle keyboard button events
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
- let contains_delete = self
- .gui_state
- .lock()
- .status_contains(&[Status::DeleteConfirm]);
-
- let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
+ let status = self.gui_state.lock().get_status();
+ let contains = |s: Status| status.contains(&s);
let contains_error = contains(Status::Error);
let contains_help = contains(Status::Help);
let contains_exec = contains(Status::Exec);
- let contains_filter: bool = contains(Status::Filter);
+ let contains_filter = contains(Status::Filter);
+ let contains_delete = contains(Status::DeleteConfirm);
if !contains_exec {
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() && !contains_filter {
- // Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active
- self.quit().await;
+ // Always just quit on Ctrl + c/C or q/Q, unless in Filter status active
+ self.quit();
}
if contains_error {
@@ -514,22 +479,13 @@ impl InputHandler {
MouseEventKind::ScrollUp => self.previous(),
MouseEventKind::ScrollDown => self.next(),
MouseEventKind::Down(MouseButton::Left) => {
- let header = self.gui_state.lock().header_intersect(Rect::new(
- mouse_event.column,
- mouse_event.row,
- 1,
- 1,
- ));
+ let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
+ let header = self.gui_state.lock().header_intersect(mouse_point);
if let Some(header) = header {
self.sort(header);
}
- self.gui_state.lock().panel_intersect(Rect::new(
- mouse_event.column,
- mouse_event.row,
- 1,
- 1,
- ));
+ self.gui_state.lock().panel_intersect(mouse_point);
}
_ => (),
}
@@ -537,23 +493,21 @@ impl InputHandler {
/// Change state to next, depending which panel is currently in focus
fn next(&self) {
- let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
- SelectablePanel::Containers => locked_data.containers_next(),
- SelectablePanel::Logs => locked_data.log_next(),
- SelectablePanel::Commands => locked_data.docker_controls_next(),
+ SelectablePanel::Containers => self.app_data.lock().containers_next(),
+ SelectablePanel::Logs => self.app_data.lock().log_next(),
+ SelectablePanel::Commands => self.app_data.lock().docker_controls_next(),
};
}
/// Change state to previous, depending which panel is currently in focus
fn previous(&self) {
- let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
- SelectablePanel::Containers => locked_data.containers_previous(),
- SelectablePanel::Logs => locked_data.log_previous(),
- SelectablePanel::Commands => locked_data.docker_controls_previous(),
+ SelectablePanel::Containers => self.app_data.lock().containers_previous(),
+ SelectablePanel::Logs => self.app_data.lock().log_previous(),
+ SelectablePanel::Commands => self.app_data.lock().docker_controls_previous(),
}
}
}
diff --git a/src/main.rs b/src/main.rs
index 81a0639..3ad0af4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -52,32 +52,28 @@ async fn docker_init(
docker_rx: Receiver,
docker_tx: Sender,
gui_state: &Arc>,
- is_running: &Arc,
- host: Option,
) {
+ let host = read_docker_host(&app_data.lock().args);
+
let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| {
Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION)
});
if let Ok(docker) = connection {
if docker.ping().await.is_ok() {
- let app_data = Arc::clone(app_data);
- let gui_state = Arc::clone(gui_state);
- let is_running = Arc::clone(is_running);
-
- tokio::spawn(DockerData::init(
- app_data, docker, docker_rx, docker_tx, gui_state, is_running,
+ tokio::spawn(DockerData::start(
+ Arc::clone(app_data),
+ docker,
+ docker_rx,
+ docker_tx,
+ Arc::clone(gui_state),
));
- } else {
- app_data
- .lock()
- .set_error(AppError::DockerConnect, gui_state, Status::DockerConnect);
+ return;
}
- } 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
@@ -88,15 +84,12 @@ fn handler_init(
input_rx: Receiver,
is_running: &Arc,
) {
- let app_data = Arc::clone(app_data);
- let gui_state = Arc::clone(gui_state);
- let is_running = Arc::clone(is_running);
- tokio::spawn(input_handler::InputHandler::init(
- app_data,
+ tokio::spawn(input_handler::InputHandler::start(
+ Arc::clone(app_data),
input_rx,
docker_sx.clone(),
- gui_state,
- is_running,
+ Arc::clone(gui_state),
+ Arc::clone(is_running),
));
}
@@ -106,34 +99,20 @@ async fn main() {
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 gui_state = Arc::new(Mutex::new(GuiState::default()));
let is_running = Arc::new(AtomicBool::new(true));
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
- docker_init(
- &app_data,
- docker_rx,
- docker_tx.clone(),
- &gui_state,
- &is_running,
- host,
- )
- .await;
+ docker_init(&app_data, docker_rx, docker_tx.clone(), &gui_state).await;
if args.gui {
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
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 {
info!("in debug mode\n");
+ let mut now = std::time::Instant::now();
// Debug mode for testing, less pointless now, will display some basic information
while is_running.load(Ordering::SeqCst) {
let err = app_data.lock().get_error();
@@ -141,10 +120,12 @@ async fn main() {
error!("{}", err);
process::exit(1);
}
- tokio::time::sleep(std::time::Duration::from_millis(u64::from(
- args.docker_interval,
- )))
- .await;
+ if let Some(Ok(to_sleep)) = u128::from(args.docker_interval)
+ .checked_sub(now.elapsed().as_millis())
+ .map(u64::try_from)
+ {
+ tokio::time::sleep(std::time::Duration::from_millis(to_sleep)).await;
+ }
let containers = app_data
.lock()
.get_container_items()
@@ -158,6 +139,7 @@ async fn main() {
}
println!();
}
+ now = std::time::Instant::now();
}
}
}
@@ -182,6 +164,7 @@ mod tests {
docker_interval: 1000,
gui: true,
host: None,
+ std_err: false,
in_container: false,
save_dir: None,
raw: false,
diff --git a/src/parse_args.rs b/src/parse_args.rs
index e29d84c..911ba20 100644
--- a/src/parse_args.rs
+++ b/src/parse_args.rs
@@ -37,13 +37,17 @@ pub struct Args {
#[clap(long, short = None)]
pub host: Option,
- /// Force use of docker cli when execing into containers
- #[clap(long="use-cli", short = None)]
- pub use_cli: bool,
+ /// Do not include stderr output in logs
+ #[clap(long = "no-stderr")]
+ pub no_std_err: bool,
/// Directory for saving exported logs, defaults to `$HOME`
#[clap(long="save-dir", short = None)]
pub save_dir: Option,
+
+ /// Force use of docker cli when execing into containers
+ #[clap(long="use-cli", short = None)]
+ pub use_cli: bool,
}
#[derive(Debug, Clone)]
@@ -58,6 +62,7 @@ pub struct CliArgs {
pub raw: bool,
pub show_self: bool,
pub timestamp: bool,
+ pub std_err: 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
/// 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 {
- if let Ok(value) = std::env::var(ENV_KEY) {
- if value == ENV_VALUE {
- return true;
- }
- }
- false
+ std::env::var(ENV_KEY).map_or(false, |i| i == ENV_VALUE)
}
/// Parse cli arguments
@@ -97,6 +97,7 @@ impl CliArgs {
in_container: Self::check_if_in_container(),
save_dir: logs_dir,
raw: args.raw,
+ std_err: !args.no_std_err,
show_self: !args.show_self,
timestamp: !args.timestamp,
}
diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs
index 7423418..d939255 100644
--- a/src/ui/draw_blocks.rs
+++ b/src/ui/draw_blocks.rs
@@ -43,6 +43,9 @@ const MARGIN: &str = " ";
const RIGHT_ARROW: &str = "▶ ";
const CIRCLE: &str = "⚪ ";
+const COLOR_RX: Color = Color::Rgb(255, 233, 193);
+const COLOR_TX: Color = Color::Rgb(205, 140, 140);
+
const CONSTRAINT_50_50: [Constraint; 2] = [Constraint::Percentage(50), Constraint::Percentage(50)];
const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)];
const CONSTRAINT_POPUP: [Constraint; 5] = [
@@ -72,7 +75,6 @@ fn max_line_width(text: &str) -> usize {
/// Generate block, add a border if is the selected panel,
/// add custom title based on state of each panel
fn generate_block<'a>(
- app_data: &Arc>,
area: Rect,
fd: &FrameData,
gui_state: &Arc>,
@@ -81,12 +83,13 @@ fn generate_block<'a>(
gui_state
.lock()
.update_region_map(Region::Panel(panel), area);
+
let mut title = match panel {
SelectablePanel::Containers => {
- format!("{}{}", panel.title(), app_data.lock().container_title())
+ format!("{}{}", panel.title(), fd.container_title)
}
SelectablePanel::Logs => {
- format!("{}{}", panel.title(), app_data.lock().get_log_title())
+ format!("{}{}", panel.title(), fd.log_title)
}
SelectablePanel::Commands => String::new(),
};
@@ -97,7 +100,7 @@ fn generate_block<'a>(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title);
- if fd.selected_panel == panel && !gui_state.lock().status_contains(&[Status::Filter]) {
+ if fd.selected_panel == panel && !fd.status.contains(&Status::Filter) {
block = block.border_style(Style::default().fg(Color::LightCyan));
}
block
@@ -111,7 +114,7 @@ pub fn commands(
fd: &FrameData,
gui_state: &Arc>,
) {
- let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
+ let block = generate_block(area, fd, gui_state, SelectablePanel::Commands);
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
i.iter()
.map(|c| {
@@ -141,7 +144,6 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
let state_style = Style::default().fg(i.state.get_color());
let blue = Style::default().fg(Color::Blue);
- // Truncate?
Line::from(vec![
Span::styled(
format!(
@@ -203,11 +205,11 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
),
Span::styled(
format!("{:>width$}{MARGIN}", i.rx, width = widths.net_rx.1.into()),
- Style::default().fg(Color::Rgb(255, 233, 193)),
+ Style::default().fg(COLOR_RX),
),
Span::styled(
format!("{:>width$}{MARGIN}", i.tx, width = widths.net_tx.1.into()),
- Style::default().fg(Color::Rgb(205, 140, 140)),
+ Style::default().fg(COLOR_TX),
),
])
}
@@ -220,7 +222,7 @@ pub fn containers(
fd: &FrameData,
gui_state: &Arc>,
) {
- let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
+ let block = generate_block(area, fd, gui_state, SelectablePanel::Containers);
let items = app_data
.lock()
@@ -230,9 +232,9 @@ pub fn containers(
.collect::>();
if items.is_empty() {
- let text = if app_data.lock().get_filter_term().is_some() {
+ let text = if fd.filter_term.is_some() {
"no containers match filter"
- } else if gui_state.lock().is_loading() {
+ } else if fd.is_loading {
&format!("loading {}", fd.loading_icon)
} else {
"no containers running"
@@ -259,8 +261,8 @@ pub fn logs(
fd: &FrameData,
gui_state: &Arc>,
) {
- let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs);
- if fd.init {
+ let block = generate_block(area, fd, gui_state, SelectablePanel::Logs);
+ if fd.status.contains(&Status::Init) {
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
.style(Style::default())
.block(block)
@@ -268,7 +270,6 @@ pub fn logs(
f.render_widget(paragraph, area);
} else {
let logs = app_data.lock().get_logs();
-
if logs.is_empty() {
let paragraph = Paragraph::new("no logs found")
.block(block)
@@ -287,15 +288,9 @@ pub fn logs(
}
}
-// Display the ports in a formatted list
-pub fn ports(
- f: &mut Frame,
- area: Rect,
- app_data: &Arc>,
- max_lens: (usize, usize, usize),
-) {
- let ports = app_data.lock().get_selected_ports();
- if let Some(ports) = ports {
+/// Display the ports in a formatted list
+pub fn ports(f: &mut Frame, area: Rect, fd: &FrameData) {
+ if let Some(ports) = fd.ports.as_ref() {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
@@ -307,7 +302,7 @@ pub fn ports(
.add_modifier(Modifier::BOLD),
));
- let (ip, private, public) = max_lens;
+ let (ip, private, public) = fd.port_max_lens;
if ports.0.is_empty() {
let text = match ports.1 {
@@ -328,7 +323,7 @@ pub fn ports(
)];
for item in &ports.0 {
let fg = Color::White;
- let strings = item.print();
+ let strings = item.get_all();
let line = vec![
Span::from(format!("{:>ip$}", strings.0)).fg(fg),
@@ -344,9 +339,8 @@ pub fn ports(
}
/// Draw the cpu + mem charts
-pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) {
- let cpu_mem = app_data.lock().get_chart_data();
- if let Some((cpu, mem)) = cpu_mem {
+pub fn chart(f: &mut Frame, area: Rect, fd: &FrameData) {
+ if let Some((cpu, mem)) = fd.chart_data.as_ref() {
let area = Layout::default()
.direction(Direction::Horizontal)
.constraints(CONSTRAINT_50_50)
@@ -422,45 +416,30 @@ fn make_chart<'a, T: Stats + Display>(
}
/// Create the filter_by by spans, coloured dependant on which one is selected
-fn filter_by_spans(app_data: &Arc>) -> [Span; 4] {
- let filter_by = app_data.lock().get_filter_by();
-
+fn filter_by_spans(fd: &FrameData) -> [Span; 4] {
let selected = Style::default().bg(Color::Gray).fg(Color::Black);
let not_selected = Style::default().bg(Color::Reset).fg(Color::Reset);
- // This should be refactored somehow
let name = [" Name ", " Image ", " Status ", " All "];
- match filter_by {
- FilterBy::Name => [
- Span::styled(name[0], selected),
- Span::styled(name[1], not_selected),
- Span::styled(name[2], not_selected),
- Span::styled(name[3], not_selected),
- ],
- FilterBy::Image => [
- Span::styled(name[0], not_selected),
- Span::styled(name[1], selected),
- Span::styled(name[2], not_selected),
- Span::styled(name[3], not_selected),
- ],
- FilterBy::Status => [
- Span::styled(name[0], not_selected),
- Span::styled(name[1], not_selected),
- Span::styled(name[2], selected),
- Span::styled(name[3], not_selected),
- ],
- FilterBy::All => [
- Span::styled(name[0], not_selected),
- Span::styled(name[1], not_selected),
- Span::styled(name[2], not_selected),
- Span::styled(name[3], selected),
- ],
+ let mut filter_spans = [
+ Span::styled(name[0], not_selected),
+ Span::styled(name[1], not_selected),
+ Span::styled(name[2], not_selected),
+ Span::styled(name[3], not_selected),
+ ];
+
+ match fd.filter_by {
+ FilterBy::Name => filter_spans[0] = Span::styled(name[0], selected),
+ FilterBy::Image => filter_spans[1] = Span::styled(name[1], selected),
+ FilterBy::Status => filter_spans[2] = Span::styled(name[2], selected),
+ FilterBy::All => filter_spans[3] = Span::styled(name[3], selected),
}
+ filter_spans
}
/// Draw the filter bar
-pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) {
+pub fn filter_bar(area: Rect, frame: &mut Frame, fd: &FrameData) {
let style_but = Style::default().fg(Color::Black).bg(Color::Magenta);
let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset);
@@ -470,7 +449,7 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>)
Span::styled(" ← by → ", style_but),
Span::from(" "),
];
- line.extend_from_slice(&filter_by_spans(app_data));
+ line.extend_from_slice(&filter_by_spans(fd));
line.extend_from_slice(&[
Span::styled(
" term: ",
@@ -479,10 +458,9 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>)
.add_modifier(Modifier::BOLD),
),
Span::styled(
- app_data
- .lock()
- .get_filter_term()
- .map_or(String::new(), std::borrow::ToOwned::to_owned),
+ fd.filter_term
+ .as_ref()
+ .map_or(String::new(), std::clone::Clone::clone),
Style::default().fg(Color::Gray),
),
]);
@@ -491,10 +469,11 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>)
/// Draw heading bar at top of program, always visible
/// TODO Should separate into loading icon/headers/help functions
+#[allow(clippy::too_many_lines)]
pub fn heading_bar(
area: Rect,
frame: &mut Frame,
- data: &FrameData,
+ fd: &FrameData,
gui_state: &Arc>,
) {
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
@@ -505,7 +484,7 @@ pub fn heading_bar(
let header_block = |x: &Header| {
let mut color = Color::Black;
let mut suffix = "";
- if let Some((a, b)) = &data.sorted_by {
+ if let Some((a, b)) = &fd.sorted_by {
if x == a {
match b {
SortedOrder::Asc => suffix = " ▲",
@@ -523,6 +502,7 @@ pub fn heading_bar(
let gen_header = |header: &Header, width: usize| {
let block = header_block(header);
+ // TODO
// Yes this is a mess, needs documenting correctly
let text = format!(
@@ -538,26 +518,30 @@ pub fn heading_bar(
// Meta data to iterate over to create blocks with correct widths
let header_meta = [
- (Header::Name, data.columns.name.1),
- (Header::State, data.columns.state.1),
- (Header::Status, data.columns.status.1),
- (Header::Cpu, data.columns.cpu.1),
- (Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3),
- (Header::Id, data.columns.id.1),
- (Header::Image, data.columns.image.1),
- (Header::Rx, data.columns.net_rx.1),
- (Header::Tx, data.columns.net_tx.1),
+ (Header::Name, fd.columns.name.1),
+ (Header::State, fd.columns.state.1),
+ (Header::Status, fd.columns.status.1),
+ (Header::Cpu, fd.columns.cpu.1),
+ (Header::Memory, fd.columns.mem.1 + fd.columns.mem.2 + 3),
+ (Header::Id, fd.columns.id.1),
+ (Header::Image, fd.columns.image.1),
+ (Header::Rx, fd.columns.net_rx.1),
+ (Header::Tx, fd.columns.net_tx.1),
];
// Need to add widths to this
- let suffix = if data.help_visible { "exit" } else { "show" };
+ let suffix = if fd.status.contains(&Status::Help) {
+ "exit"
+ } else {
+ "show"
+ };
let info_text = format!("( h ) {suffix} help{MARGIN}",);
let info_width = info_text.chars().count();
let column_width = usize::from(area.width).saturating_sub(info_width);
let column_width = if column_width > 0 { column_width } else { 1 };
- let splits = if data.has_containers {
+ let splits = if fd.has_containers {
vec![
Constraint::Max(4),
Constraint::Max(column_width.try_into().unwrap_or_default()),
@@ -573,11 +557,11 @@ pub fn heading_bar(
.split(area);
// Draw loading icon, or not, and a prefix with a single space
- let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
+ let loading_paragraph = Paragraph::new(format!("{:>2}", fd.loading_icon))
.block(block(Color::White))
.alignment(Alignment::Left);
frame.render_widget(loading_paragraph, split_bar[0]);
- if data.has_containers {
+ if fd.has_containers {
let header_section_width = split_bar[1].width;
let mut counter = 0;
@@ -612,7 +596,7 @@ pub fn heading_bar(
}
// show/hide help
- let color = if data.help_visible {
+ let color = if fd.status.contains(&Status::Help) {
Color::Black
} else {
Color::White
@@ -622,7 +606,7 @@ pub fn heading_bar(
.alignment(Alignment::Right);
// If no containers, don't display the headers, could maybe do this first?
- let help_index = if data.has_containers { 2 } else { 0 };
+ let help_index = if fd.has_containers { 2 } else { 0 };
frame.render_widget(help_paragraph, split_bar[help_index]);
}
@@ -851,22 +835,19 @@ pub fn help_box(f: &mut Frame) {
let name_paragraph = Paragraph::new(name_info.lines)
.style(Style::default().bg(Color::Magenta).fg(Color::White))
- .block(Block::default())
.alignment(Alignment::Center);
+ let style = || Style::default().bg(Color::Magenta).fg(Color::Black);
let description_paragraph = Paragraph::new(description_info.lines)
- .style(Style::default().bg(Color::Magenta).fg(Color::Black))
- .block(Block::default())
+ .style(style())
.alignment(Alignment::Center);
let help_paragraph = Paragraph::new(button_info.lines)
- .style(Style::default().bg(Color::Magenta).fg(Color::Black))
- .block(Block::default())
+ .style(style())
.alignment(Alignment::Left);
let final_paragraph = Paragraph::new(final_info.lines)
- .style(Style::default().bg(Color::Magenta).fg(Color::Black))
- .block(Block::default())
+ .style(style())
.alignment(Alignment::Center);
let block = Block::default()
@@ -1003,20 +984,19 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) {
let area = popup(lines, max_line_width, f.area(), BoxLocation::MiddleCentre);
- // let (paragraph, area) = gen_error(f, error, seconds);
f.render_widget(Clear, area);
f.render_widget(paragraph, area);
}
/// Draw info box in one of the 9 BoxLocations
// TODO is this broken - I don't think so
-pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc>) {
+pub fn info(f: &mut Frame, text: String, instant: &Instant, gui_state: &Arc>) {
let block = Block::default()
.title("")
.title_alignment(Alignment::Center)
.borders(Borders::NONE);
- let mut max_line_width = max_line_width(text);
+ let mut max_line_width = max_line_width(&text);
let mut lines = text.lines().count();
// Add some horizontal & vertical margins
@@ -1068,7 +1048,11 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio
#[allow(clippy::unwrap_used)]
mod tests {
- use std::{ops::RangeInclusive, sync::Arc};
+ use std::{
+ net::{IpAddr, Ipv4Addr},
+ ops::RangeInclusive,
+ sync::Arc,
+ };
use parking_lot::Mutex;
use ratatui::{
@@ -1086,7 +1070,10 @@ mod tests {
},
app_error::AppError,
tests::{gen_appdata, gen_container_summary, gen_containers},
- ui::{draw_frame, GuiState},
+ ui::{
+ draw_blocks::{COLOR_RX, COLOR_TX},
+ draw_frame, GuiState, Status,
+ },
};
use super::{FrameData, ORANGE, VERSION};
@@ -1102,6 +1089,42 @@ mod tests {
const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
+ impl From<(&Arc>, &Arc>)> for FrameData {
+ fn from(data: (&Arc>, &Arc>)) -> Self {
+ let (app_data, gui_data) = (data.0.lock(), data.1.lock());
+
+ // set max height for container section, needs +5 to deal with docker commands list and borders
+ let height = app_data.get_container_len();
+ let height = if height < 12 {
+ u16::try_from(height + 5).unwrap_or_default()
+ } else {
+ 12
+ };
+
+ let (filter_by, filter_term) = app_data.get_filter();
+ Self {
+ chart_data: app_data.get_chart_data(),
+ columns: app_data.get_width(),
+ container_title: app_data.get_container_title(),
+ 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,
+ ports: app_data.get_selected_ports(),
+ port_max_lens: app_data.get_longest_port(),
+ info_text: gui_data.info_box_text.clone(),
+ is_loading: gui_data.is_loading(),
+ loading_icon: gui_data.get_loading().to_string(),
+ log_title: app_data.get_log_title(),
+ selected_panel: gui_data.get_selected_panel(),
+ sorted_by: app_data.get_sorted(),
+ status: gui_data.get_status(),
+ }
+ }
+ }
+
/// Generate state to be used in *most* gui tests
fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup {
let backend = TestBackend::new(w, h);
@@ -1120,8 +1143,7 @@ mod tests {
let app_data = Arc::new(Mutex::new(app_data));
let gui_state = Arc::new(Mutex::new(gui_state));
-
- let fd = FrameData::from((app_data.lock(), gui_state.lock()));
+ let fd = FrameData::from((&app_data, &gui_state));
let area = Rect::new(0, 0, w, h);
TuiTestSetup {
app_data,
@@ -1248,7 +1270,7 @@ mod tests {
setup
.app_data
.lock()
- .update_containers(&mut vec![gen_container_summary(1, "paused")]);
+ .update_containers(vec![gen_container_summary(1, "paused")]);
setup.app_data.lock().docker_controls_next();
let expected = [
@@ -1327,7 +1349,7 @@ mod tests {
// Control panel now selected, should have a blue border
setup.gui_state.lock().next_panel();
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
@@ -1376,7 +1398,7 @@ mod tests {
];
setup.gui_state.lock().next_panel();
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1394,7 +1416,7 @@ mod tests {
}
setup.gui_state.lock().previous_panel();
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1463,7 +1485,7 @@ mod tests {
// Change selected panel, border is now no longer blue
setup.gui_state.lock().next_panel();
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
@@ -1497,7 +1519,7 @@ mod tests {
"│ │",
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
];
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1527,11 +1549,11 @@ mod tests {
}
// rx column
(1..=3, 92..=101) => {
- assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193));
+ assert_eq!(result_cell.fg, COLOR_RX);
}
// tx column
(1..=3, 102..=111) => {
- assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
+ assert_eq!(result_cell.fg, COLOR_TX);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
@@ -1557,7 +1579,7 @@ mod tests {
"│ │",
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup.app_data.lock().containers.items[0].state = State::Paused;
setup
@@ -1601,11 +1623,11 @@ mod tests {
}
// rx column
(1..=3, 92..=101) => {
- assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193));
+ assert_eq!(result_cell.fg, COLOR_RX);
}
// tx column
(1..=3, 102..=111) => {
- assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
+ assert_eq!(result_cell.fg, COLOR_TX);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
@@ -1627,7 +1649,7 @@ mod tests {
"│ │",
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup.app_data.lock().containers.items[0].state = State::Paused;
setup
@@ -1655,7 +1677,7 @@ mod tests {
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
setup.app_data.lock().containers.items[0].state = State::Dead;
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1682,7 +1704,7 @@ mod tests {
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
setup.app_data.lock().containers.items[0].state = State::Exited;
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1708,7 +1730,7 @@ mod tests {
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
setup.app_data.lock().containers.items[0].state = State::Removing;
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1735,7 +1757,7 @@ mod tests {
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
setup.app_data.lock().containers.items[0].state = State::Restarting;
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1768,11 +1790,11 @@ mod tests {
}
// rx column
(1..=3, 95..=104) => {
- assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193));
+ assert_eq!(result_cell.fg, COLOR_RX);
}
// tx column
(1..=3, 105..=114) => {
- assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
+ assert_eq!(result_cell.fg, COLOR_TX);
}
_ => {
assert_eq!(result_cell.fg, Color::Reset);
@@ -1800,7 +1822,7 @@ mod tests {
"│ │",
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
];
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1832,11 +1854,11 @@ mod tests {
}
// rx column
(1..=3, 104..=113) => {
- assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193));
+ assert_eq!(result_cell.fg, COLOR_RX);
}
// tx column
(1..=3, 114..=123) => {
- assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
+ assert_eq!(result_cell.fg, COLOR_TX);
}
_ => assert_eq!(result_cell.fg, Color::Reset),
}
@@ -1859,7 +1881,7 @@ mod tests {
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
];
setup.app_data.lock().containers.items[0].state = State::Unknown;
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
@@ -1878,21 +1900,18 @@ mod tests {
#[test]
/// No logs, panel unselected, then selected, border color changes correctly
fn test_draw_blocks_logs_none() {
- let (w, h) = (25, 6);
+ let (w, h) = (35, 6);
let mut setup = test_setup(w, h, true, true);
- setup.app_data.lock().containers = StatefulList::new(vec![]);
let expected = [
- "╭ Logs ─────────────────╮",
- "│ no logs found │",
- "│ │",
- "│ │",
- "│ │",
- "╰───────────────────────╯",
+ "╭ Logs - container_1 - image_1 ───╮",
+ "│ no logs found │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰─────────────────────────────────╯",
];
- let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
-
setup
.terminal
.draw(|f| {
@@ -1910,7 +1929,7 @@ mod tests {
setup.gui_state.lock().next_panel();
setup.gui_state.lock().next_panel();
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
// When selected, has a blue border
setup
@@ -1924,7 +1943,6 @@ mod tests {
let expected_row = expected_to_vec(&expected, row_index);
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
-
if BORDER_CHARS.contains(&result_cell.symbol()) {
assert_eq!(result_cell.fg, Color::LightCyan);
}
@@ -1949,8 +1967,8 @@ mod tests {
"╰──────────────────────────────╯",
];
- let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
- fd.init = true;
+ let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
+ fd.status.insert(Status::Init);
setup
.terminal
@@ -1979,8 +1997,8 @@ mod tests {
"╰──────────────────────────────╯",
];
- let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
- fd.init = true;
+ let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
+ fd.status.insert(Status::Init);
setup
.terminal
.draw(|f| {
@@ -2000,12 +2018,12 @@ mod tests {
#[test]
/// Logs correct displayed, changing log state also draws correctly
fn test_draw_blocks_logs_some() {
- let (w, h) = (25, 6);
+ let (w, h) = (36, 6);
let mut setup = test_setup(w, h, true, true);
insert_logs(&setup);
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
@@ -2013,12 +2031,12 @@ mod tests {
})
.unwrap();
let expected = [
- "╭ Logs 3/3 - container_1╮",
- "│ line 1 │",
- "│ line 2 │",
- "│▶ line 3 │",
- "│ │",
- "╰───────────────────────╯",
+ "╭ Logs 3/3 - container_1 - image_1 ╮",
+ "│ line 1 │",
+ "│ line 2 │",
+ "│▶ line 3 │",
+ "│ │",
+ "╰──────────────────────────────────╯",
];
for (row_index, result_row) in get_result(&setup, w) {
@@ -2027,7 +2045,7 @@ mod tests {
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
assert_eq!(result_cell.fg, Color::Reset);
- if row_index == 3 && (1..=23).contains(&result_cell_index) {
+ if row_index == 3 && (1..=34).contains(&result_cell_index) {
assert_eq!(result_cell.modifier, Modifier::BOLD);
} else {
assert!(result_cell.modifier.is_empty());
@@ -2037,22 +2055,22 @@ mod tests {
// Change selected log line
setup.app_data.lock().log_previous();
- _ = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
})
.unwrap();
let expected = [
- "╭ Logs 2/3 - container_1╮",
- "│ line 1 │",
- "│▶ line 2 │",
- "│ line 3 │",
- "│ │",
- "╰───────────────────────╯",
+ "╭ Logs 2/3 - container_1 - image_1 ╮",
+ "│ line 1 │",
+ "│▶ line 2 │",
+ "│ line 3 │",
+ "│ │",
+ "╰──────────────────────────────────╯",
];
for (row_index, result_row) in get_result(&setup, w) {
let expected_row = expected_to_vec(&expected, row_index);
@@ -2060,7 +2078,7 @@ mod tests {
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
assert_eq!(result_cell.fg, Color::Reset);
- if row_index == 2 && (1..=23).contains(&result_cell_index) {
+ if row_index == 2 && (1..=34).contains(&result_cell_index) {
assert_eq!(result_cell.modifier, Modifier::BOLD);
} else {
assert!(result_cell.modifier.is_empty());
@@ -2090,7 +2108,7 @@ mod tests {
"╰──────────────────────────────────────────────────────────────────────────────╯",
];
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
@@ -2194,10 +2212,11 @@ mod tests {
let (w, h) = (80, 10);
let mut setup = test_setup(w, h, true, true);
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::chart(f, setup.area, &setup.app_data);
+ super::chart(f, setup.area, &fd);
})
.unwrap();
@@ -2244,11 +2263,12 @@ mod tests {
let mut setup = test_setup(w, h, true, true);
insert_chart_data(&setup);
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::chart(f, setup.area, &setup.app_data);
+ super::chart(f, setup.area, &fd);
})
.unwrap();
@@ -2291,11 +2311,12 @@ mod tests {
insert_chart_data(&setup);
setup.app_data.lock().containers.items[0].state = State::Paused;
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::chart(f, setup.area, &setup.app_data);
+ super::chart(f, setup.area, &fd);
})
.unwrap();
@@ -2333,11 +2354,12 @@ mod tests {
let mut setup = test_setup(w, h, true, true);
insert_chart_data(&setup);
setup.app_data.lock().containers.items[0].state = State::Dead;
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::chart(f, setup.area, &setup.app_data);
+ super::chart(f, setup.area, &fd);
})
.unwrap();
@@ -2379,7 +2401,7 @@ mod tests {
let mut setup = test_setup(w, h, true, true);
setup.app_data.lock().containers = StatefulList::new(vec![]);
- let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
let expected = [" ( h ) show help "];
@@ -2399,7 +2421,7 @@ mod tests {
}
}
- fd.help_visible = true;
+ fd.status.insert(Status::Help);
let expected = [" ( h ) exit help "];
setup
.terminal
@@ -2423,7 +2445,7 @@ mod tests {
fn test_draw_blocks_headers_some_containers() {
let (w, h) = (140, 1);
let mut setup = test_setup(w, h, true, true);
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
setup
@@ -2454,7 +2476,7 @@ mod tests {
fn test_draw_blocks_headers_some_containers_reduced_width() {
let (w, h) = (80, 1);
let mut setup = test_setup(w, h, true, true);
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
let expected =
[" name state status cpu ( h ) show help "];
@@ -2486,7 +2508,7 @@ mod tests {
fn test_draw_blocks_headers_sort_containers() {
let (w, h) = (140, 1);
let mut setup = test_setup(w, h, true, true);
- let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
// Actual test, used for each header and sorted type
let mut test =
@@ -2555,7 +2577,7 @@ mod tests {
let mut setup = test_setup(w, h, true, true);
let uuid = Uuid::new_v4();
setup.gui_state.lock().next_loading(uuid);
- let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
@@ -2810,7 +2832,12 @@ mod tests {
setup
.terminal
.draw(|f| {
- super::info(f, "test", std::time::Instant::now(), &setup.gui_state);
+ super::info(
+ f,
+ "test".to_owned(),
+ &std::time::Instant::now(),
+ &setup.gui_state,
+ );
})
.unwrap();
@@ -2848,7 +2875,7 @@ mod tests {
setup
.terminal
.draw(|f| {
- super::filter_bar(setup.area, f, &setup.app_data);
+ super::filter_bar(setup.area, f, &setup.fd);
})
.unwrap();
@@ -2889,11 +2916,12 @@ mod tests {
// Test when char added to search term
setup.app_data.lock().filter_term_push('c');
setup.app_data.lock().filter_term_push('d');
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::filter_bar(setup.area, f, &setup.app_data);
+ super::filter_bar(setup.area, f, &fd);
})
.unwrap();
@@ -2934,10 +2962,11 @@ mod tests {
// Test when filter_by chances
setup.app_data.lock().filter_by_next();
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::filter_bar(setup.area, f, &setup.app_data);
+ super::filter_bar(setup.area, f, &fd);
})
.unwrap();
@@ -3078,11 +3107,11 @@ mod tests {
let mut setup = test_setup(w, h, true, true);
setup.app_data.lock().containers.items[0].ports = vec![];
- let max_lens = setup.app_data.lock().get_longest_port();
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::ports(f, setup.area, &setup.app_data, max_lens);
+ super::ports(f, setup.area, &fd);
})
.unwrap();
@@ -3123,11 +3152,12 @@ mod tests {
// When state is "State::Running | State::Paused | State::Restarting, won't show "no ports"
setup.app_data.lock().containers.items[0].state = State::Dead;
- let max_lens = setup.app_data.lock().get_longest_port();
+
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::ports(f, setup.area, &setup.app_data, max_lens);
+ super::ports(f, setup.area, &fd);
})
.unwrap();
@@ -3173,17 +3203,16 @@ mod tests {
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
- ip: Some("127.0.0.1".to_owned()),
+ ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
private: 8003,
public: Some(8003),
});
- let max_lens = setup.app_data.lock().get_longest_port();
-
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::ports(f, setup.area, &setup.app_data, max_lens);
+ super::ports(f, setup.area, &fd);
})
.unwrap();
@@ -3232,12 +3261,12 @@ mod tests {
fn test_draw_blocks_ports_container_state() {
let (w, h) = (32, 8);
let mut setup = test_setup(w, h, true, true);
- let max_lens = setup.app_data.lock().get_longest_port();
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::ports(f, setup.area, &setup.app_data, max_lens);
+ super::ports(f, setup.area, &fd);
})
.unwrap();
@@ -3265,10 +3294,11 @@ mod tests {
}
setup.app_data.lock().containers.items[0].state = State::Paused;
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::ports(f, setup.area, &setup.app_data, max_lens);
+ super::ports(f, setup.area, &fd);
})
.unwrap();
@@ -3285,10 +3315,11 @@ mod tests {
}
setup.app_data.lock().containers.items[0].state = State::Exited;
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- super::ports(f, setup.area, &setup.app_data, max_lens);
+ super::ports(f, setup.area, &fd);
})
.unwrap();
@@ -3319,7 +3350,7 @@ mod tests {
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
- ip: Some("127.0.0.1".to_owned()),
+ ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
private: 8003,
public: Some(8003),
});
@@ -3356,10 +3387,12 @@ mod tests {
"│ │ ││ │ ││ │",
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
];
+
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- draw_frame(f, &setup.app_data, &setup.gui_state);
+ draw_frame(f, &setup.app_data, &setup.gui_state, &fd);
})
.unwrap();
@@ -3383,7 +3416,7 @@ mod tests {
setup.app_data.lock().containers.items[1]
.ports
.push(ContainerPorts {
- ip: Some("127.0.0.1".to_owned()),
+ ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
private: 8003,
public: Some(8003),
});
@@ -3420,10 +3453,11 @@ mod tests {
"│ │ ││ │ ││ │",
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
];
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- draw_frame(f, &setup.app_data, &setup.gui_state);
+ draw_frame(f, &setup.app_data, &setup.gui_state, &fd);
})
.unwrap();
@@ -3474,10 +3508,11 @@ mod tests {
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
" Esc clear ← by → Name Image Status All term: r_1 ",
];
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- draw_frame(f, &setup.app_data, &setup.gui_state);
+ draw_frame(f, &setup.app_data, &setup.gui_state, &fd);
})
.unwrap();
@@ -3500,7 +3535,7 @@ mod tests {
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
- ip: Some("127.0.0.1".to_owned()),
+ ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
private: 8003,
public: Some(8003),
});
@@ -3542,10 +3577,11 @@ mod tests {
"│ │ ││ │ ││ │",
"╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
];
+ let fd = FrameData::from((&setup.app_data, &setup.gui_state));
setup
.terminal
.draw(|f| {
- draw_frame(f, &setup.app_data, &setup.gui_state);
+ draw_frame(f, &setup.app_data, &setup.gui_state, &fd);
})
.unwrap();
diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs
index ce007e8..b7b1850 100644
--- a/src/ui/gui_state.rs
+++ b/src/ui/gui_state.rs
@@ -266,10 +266,9 @@ impl GuiState {
self.delete_container = id;
}
- /// Check if the current gui_status contains any of the given status'
- /// Don't really like this methodology for gui state, needs a re-think
- pub fn status_contains(&self, status: &[Status]) -> bool {
- status.iter().any(|i| self.status.contains(i))
+ /// Return a copy of the Status HashSet
+ pub fn get_status(&self) -> HashSet {
+ self.status.clone()
}
/// Remove a gui_status into the current gui_status HashSet
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 2158fef..fc3244c 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -4,13 +4,14 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
-use parking_lot::{Mutex, MutexGuard};
+use parking_lot::Mutex;
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Position},
Frame, Terminal,
};
use std::{
+ collections::HashSet,
io::{self, Stdout, Write},
sync::{atomic::Ordering, Arc},
time::Duration,
@@ -26,18 +27,21 @@ mod gui_state;
pub use self::color_match::*;
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
use crate::{
- app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
+ app_data::{
+ AppData, Columns, ContainerId, ContainerPorts, CpuTuple, FilterBy, Header, MemTuple,
+ SortedOrder, State,
+ },
app_error::AppError,
exec::TerminalSize,
input_handler::InputMessages,
};
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 {
app_data: Arc>,
gui_state: Arc>,
- input_poll_rate: Duration,
input_tx: Sender,
is_running: Arc,
now: Instant,
@@ -59,7 +63,7 @@ impl Ui {
}
/// Create a new Ui struct, and execute the drawing loop
- pub async fn create(
+ pub async fn start(
app_data: Arc>,
gui_state: Arc>,
input_tx: Sender,
@@ -71,7 +75,6 @@ impl Ui {
app_data,
cursor_position,
gui_state,
- input_poll_rate: std::time::Duration::from_millis(100),
input_tx,
is_running,
now: Instant::now(),
@@ -141,7 +144,7 @@ impl Ui {
Ok(())
}
- /// Use exeternal docker cli to exec into a container
+ /// Use external docker cli to exec into a container
async fn exec(&mut self) {
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
async fn gui_loop(&mut self) -> Result<(), AppError> {
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 {
self.exec().await;
}
if self
.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()
{
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 Event::Key(key) = event {
if key.kind == event::KeyEventKind::Press {
@@ -206,11 +210,8 @@ impl Ui {
/// Draw either the Error, or main oxker ui, to the terminal
async fn draw_ui(&mut self) -> Result<(), AppError> {
- let status_dockerconnect = self
- .gui_state
- .lock()
- .status_contains(&[Status::DockerConnect]);
- if status_dockerconnect {
+ let status = self.gui_state.lock().get_status();
+ if status.contains(&Status::DockerConnect) {
self.err_loop()?;
} else {
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
-#[derive(Debug)]
+/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
+#[derive(Debug, Clone)]
pub struct FrameData {
+ chart_data: Option<(CpuTuple, MemTuple)>,
columns: Columns,
+ container_title: String,
delete_confirm: Option,
+ filter_by: FilterBy,
+ filter_term: Option,
has_containers: bool,
has_error: Option,
height: u16,
- help_visible: bool,
- init: bool,
info_text: Option<(String, Instant)>,
+ is_loading: bool,
loading_icon: String,
+ log_title: String,
+ port_max_lens: (usize, usize, usize),
+ ports: Option<(Vec, State)>,
selected_panel: SelectablePanel,
sorted_by: Option<(Header, SortedOrder)>,
+ status: HashSet,
}
-impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
- fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self {
+impl From<&Ui> for FrameData {
+ 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
- let height = data.0.get_container_len();
+ let height = app_data.get_container_len();
let height = if height < 12 {
u16::try_from(height + 5).unwrap_or_default()
} else {
12
};
+ let (filter_by, filter_term) = app_data.get_filter();
Self {
- columns: data.0.get_width(),
- delete_confirm: data.1.get_delete_container(),
- has_containers: data.0.get_container_len() > 0,
- has_error: data.0.get_error(),
+ chart_data: app_data.get_chart_data(),
+ columns: app_data.get_width(),
+ container_title: app_data.get_container_title(),
+ 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,
- help_visible: data.1.status_contains(&[Status::Help]),
- init: data.1.status_contains(&[Status::Init]),
- info_text: data.1.info_box_text.clone(),
- loading_icon: data.1.get_loading().to_string(),
- selected_panel: data.1.get_selected_panel(),
- sorted_by: data.0.get_sorted(),
+ info_text: gui_data.info_box_text.clone(),
+ is_loading: gui_data.is_loading(),
+ loading_icon: gui_data.get_loading().to_string(),
+ log_title: app_data.get_log_title(),
+ port_max_lens: app_data.get_longest_port(),
+ 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
-fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) {
- let fd = FrameData::from((app_data.lock(), gui_state.lock()));
- let contains_filter = gui_state.lock().status_contains(&[Status::Filter]);
-
- let whole_constraints = if contains_filter {
+fn draw_frame(
+ f: &mut Frame,
+ app_data: &Arc>,
+ gui_state: &Arc>,
+ fd: &FrameData,
+) {
+ let whole_constraints = if fd.status.contains(&Status::Filter) {
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
} else {
vec![Constraint::Max(1), Constraint::Min(1)]
@@ -300,21 +320,21 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc