diff --git a/.github/release-body.md b/.github/release-body.md
index 2c5997b..6e8fc56 100644
--- a/.github/release-body.md
+++ b/.github/release-body.md
@@ -1,26 +1,23 @@
-### 2024-01-05
+### 2024-01-18
### Chores
-+ .devcontainer updated, [2313618eb1493ce41d70847b888c32b65fdc40ea], [5af6b8bcd31c3c38ff5a5799c76dc1cbe1167763], [9b0b6b10c3a0c1d5095490cfd3cda18d252f38f5]
-+ alpine version bump, [061de032dad935c56c6caab419ecb5c9bbac4c7e]
-+ dependencies updated, [0890991ff1a239fe2d556a0c4eac6ae05beb9b50], [0a7b266b2a358a4788ae877ca8a97f08eac4eef2], [333621f1a7321c1fdf73fd35dd7f3ab165a9dc64], [3e51889cd8a552b1da463ae6a40d5de6eec188f5], [a179bb6f6a7e076269fa830f56c0d4a31cf8488a]
-+ file formatting, [eb5e74ae67d815bf49f241d2baf319e41cf9adf8]
-+ Rust 1.75.0 linting, [81be75f27fd32a59ebff57e44c5022ff862df84b]
++ dependencies updated, [53b4bafbe53312fe41608ddf33e865d474222aaa], [58ef151600e362048a607c8ae61a5edfe80ab1dd], [b6fd35022a99ec0e982ddb154b0450d49c4840e9], [0438c108bdd9815d7eae1b89c47c4e6438f358d6]
++ files formatted, [1806165c3e266876b2d1806f7b662d09705f3aad]
++ create_release.sh check for unused lint, [d0b27211928f93f8455e1ee5a6a6485c6a21d382]
### Docs
-+ screenshot updated, [0231d1bdcda304300d289243a95044ab3bdce85c]
-+ comment typo, [0ad1ec9d85d6f0cac743b4421d0ad03432c9d717]
+= Readme updated, screenshot added, [7561a93415c1e1f596b15edba95e7b32a939cd90], [4069e5572f81cb689dbb9f735db919e4636cdccc]
### Features
-+ re-arrange columns, container name is now the first column, added a ContainerName & ContainerImage struct via `string_wrapper` macro, closes #32, [e936bb4b78980d0e34a1ef5e9f6f82a9ed0ddc7f]
++ Ports section added, closes #21, [65a1afcb0605604ede350a5630c775f94ebb74ee], [7a096a65c40924021fe643fe0aa1067095832df9]
### Fixes
-+ Docker Commands hidden, [4301e4709f99fc23ee438bf345b0dc698a05dc4e]
-+ .gitattributes, [1234ea53897b2ed6ada0eb18cd81b8783a5dc5f5]
++ sort arrow now on left of header, [40ddcb727d2c1758d6dd26a58507b85b219f51e2]
### Refactors
-+ GitHub workflow action improved, [04b66af2b60c96cfbece0b13109e30b08ef35cc4]
-+ sort_containers, [ccf8b55a7495982f72b4fb3af6e11a9bd7465216]
-+ string_wrapper .get() return `&str`, [a722731c6a77e00d1fb13967b51400aa34e72213]
++ rename string_wrapper > unit_struct, [27cf53e41f8b379f606c1c27620ee08e79bac57e]
+
+### Tests
++ Finally have tests, currently for layout and associated methods, at the moment running the tests will not interfere with any running Docker containers, [4bcf77db776a36e0a8151ecfbda722a66c4ba46c]
see CHANGELOG.md for more details
diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png
index 27a7932..383a567 100644
Binary files a/.github/screenshot_01.png and b/.github/screenshot_01.png differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfeb98b..94a81a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,26 @@
+# v0.6.0
+### 2024-01-18
+
+### Chores
++ dependencies updated, [53b4bafb](https://github.com/mrjackwills/oxker/commit/53b4bafbe53312fe41608ddf33e865d474222aaa), [58ef1516](https://github.com/mrjackwills/oxker/commit/58ef151600e362048a607c8ae61a5edfe80ab1dd), [b6fd3502](https://github.com/mrjackwills/oxker/commit/b6fd35022a99ec0e982ddb154b0450d49c4840e9), [0438c108](https://github.com/mrjackwills/oxker/commit/0438c108bdd9815d7eae1b89c47c4e6438f358d6)
++ files formatted, [1806165c](https://github.com/mrjackwills/oxker/commit/1806165c3e266876b2d1806f7b662d09705f3aad)
++ create_release.sh check for unused lint, [d0b27211](https://github.com/mrjackwills/oxker/commit/d0b27211928f93f8455e1ee5a6a6485c6a21d382)
+
+### Docs
+= Readme updated, screenshot added, [7561a934](https://github.com/mrjackwills/oxker/commit/7561a93415c1e1f596b15edba95e7b32a939cd90), [4069e557](https://github.com/mrjackwills/oxker/commit/4069e5572f81cb689dbb9f735db919e4636cdccc)
+
+### Features
++ Ports section added, closes [#21](https://github.com/mrjackwills/oxker/issues/21), [65a1afcb](https://github.com/mrjackwills/oxker/commit/65a1afcb0605604ede350a5630c775f94ebb74ee), [7a096a65](https://github.com/mrjackwills/oxker/commit/7a096a65c40924021fe643fe0aa1067095832df9)
+
+### Fixes
++ sort arrow now on left of header, [40ddcb72](https://github.com/mrjackwills/oxker/commit/40ddcb727d2c1758d6dd26a58507b85b219f51e2)
+
+### Refactors
++ rename string_wrapper > unit_struct, [27cf53e4](https://github.com/mrjackwills/oxker/commit/27cf53e41f8b379f606c1c27620ee08e79bac57e)
+
+### Tests
++ Finally have tests, currently for layout and associated methods, at the moment running the tests will not interfere with any running Docker containers, [4bcf77db](https://github.com/mrjackwills/oxker/commit/4bcf77db776a36e0a8151ecfbda722a66c4ba46c)
+
# v0.5.0
### 2024-01-05
diff --git a/Cargo.lock b/Cargo.lock
index ac77486..c67ea51 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -52,9 +52,9 @@ dependencies = [
[[package]]
name = "anstream"
-version = "0.6.5"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6"
+checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -127,9 +127,9 @@ dependencies = [
[[package]]
name = "base64"
-version = "0.21.5"
+version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bitflags"
@@ -139,9 +139,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.4.1"
+version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bollard"
@@ -237,9 +237,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.4.13"
+version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
+checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
dependencies = [
"clap_builder",
"clap_derive",
@@ -247,9 +247,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.4.12"
+version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9"
+checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
dependencies = [
"anstream",
"anstyle",
@@ -295,7 +295,7 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.4.2",
"crossterm_winapi",
"libc",
"mio",
@@ -426,9 +426,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.11"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
@@ -443,9 +443,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "h2"
-version = "0.3.22"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
+checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
dependencies = [
"bytes",
"fnv",
@@ -484,9 +484,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
-version = "0.3.3"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
+checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f"
[[package]]
name = "hex"
@@ -643,9 +643,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
-version = "0.3.66"
+version = "0.3.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
+checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1"
dependencies = [
"wasm-bindgen",
]
@@ -658,9 +658,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.151"
+version = "0.2.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4"
+checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
name = "libredox"
@@ -668,7 +668,7 @@ version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.4.2",
"libc",
"redox_syscall",
]
@@ -783,7 +783,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "oxker"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"anyhow",
"bollard",
@@ -881,9 +881,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
-version = "1.0.75"
+version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708"
+checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [
"unicode-ident",
]
@@ -933,7 +933,7 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb"
dependencies = [
- "bitflags 2.4.1",
+ "bitflags 2.4.2",
"cassowary",
"crossterm",
"indoc",
@@ -992,18 +992,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.194"
+version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773"
+checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.194"
+version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0"
+checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [
"proc-macro2",
"quote",
@@ -1110,9 +1110,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.11.2"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
+checksum = "3b187f0231d56fe41bfb12034819dd2bf336422a5866de41bc3fec4b2e3883e8"
[[package]]
name = "socket2"
@@ -1382,9 +1382,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
-version = "0.3.14"
+version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
@@ -1469,9 +1469,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.89"
+version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
+checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -1479,9 +1479,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.89"
+version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
+checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd"
dependencies = [
"bumpalo",
"log",
@@ -1494,9 +1494,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.89"
+version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
+checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1504,9 +1504,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.89"
+version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
+checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7"
dependencies = [
"proc-macro2",
"quote",
@@ -1517,9 +1517,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.89"
+version = "0.2.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
+checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
[[package]]
name = "winapi"
diff --git a/Cargo.toml b/Cargo.toml
index 1f5d3fd..5bcbd65 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "oxker"
-version = "0.5.0"
+version = "0.6.0"
edition = "2021"
authors = ["Jack Wills "]
description = "A simple tui to view & control docker containers"
@@ -17,7 +17,7 @@ unsafe_code = "forbid"
[lints.clippy]
expect_used = "warn"
nursery = "warn"
-pedantic ="warn"
+pedantic = "warn"
todo = "warn"
unused_async = "warn"
unwrap_used = "warn"
@@ -29,15 +29,15 @@ similar_names = "allow"
anyhow = "1.0"
bollard = "0.15"
cansi = "2.2"
-clap = { version = "4.4", features = ["derive", "unicode", "color"] }
+clap = { version = "4.4", features = ["color", "derive", "unicode"] }
crossterm = "0.27"
futures-util = "0.3"
-parking_lot = { version= "0.12" }
+parking_lot = { version = "0.12" }
tokio = { version = "1.35", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
ratatui = "0.25"
-uuid = { version = "1.6", features = ["v4", "fast-rng"] }
+uuid = { version = "1.6", features = ["fast-rng", "v4"] }
directories = "5.0"
[dev-dependencies]
@@ -46,6 +46,5 @@ directories = "5.0"
lto = true
codegen-units = 1
panic = 'abort'
-strip=true
+strip = true
debug = false
-
diff --git a/README.md b/README.md
index e1d47d5..806a60f 100644
--- a/README.md
+++ b/README.md
@@ -157,7 +157,8 @@ see CHANGELOG.md for more details" > .github/release-body.md
+ echo -e "${RELEASE_BODY_ADDITION}\n\nsee CHANGELOG.md for more details" >.github/release-body.md
# Add subheading with release version and date of release
- echo -e "# ${NEW_TAG_WITH_V}\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" > CHANGELOG.md
+ echo -e "# ${NEW_TAG_WITH_V}\n${DATE_SUBHEADING}${CHANGELOG_ADDITION}$(cat CHANGELOG.md)" >CHANGELOG.md
# Update changelog to add links to commits [hex:8](url_with_full_commit)
# "[aaaaaaaaaabbbbbbbbbbccccccccccddddddddd]" -> "[aaaaaaaa](https:/www.../commit/aaaaaaaaaabbbbbbbbbbccccccccccddddddddd)"
@@ -111,20 +107,19 @@ update_release_body_and_changelog () {
}
# update version in cargo.toml, to match selected current version
-update_version_number_in_files () {
+update_version_number_in_files() {
sed -i "s|^version = .*|version = \"${MAJOR}.${MINOR}.${PATCH}\"|" Cargo.toml
}
# Work out the current version, based on git tags
# create new semver version based on user input
# Set MAJOR MINOR PATCH
-check_tag () {
+check_tag() {
LATEST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)")
echo -e "\nCurrent tag: ${PURPLE}${LATEST_TAG}${RESET}\n"
echo -e "${YELLOW}Choose new tag version:${RESET}\n"
- if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]
- then
- IFS="." read -r MAJOR MINOR PATCH <<< "${LATEST_TAG:1}"
+ if [[ $LATEST_TAG =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then
+ IFS="." read -r MAJOR MINOR PATCH <<<"${LATEST_TAG:1}"
else
MAJOR="0"
MINOR="0"
@@ -134,51 +129,53 @@ check_tag () {
OP_MINOR="minor___v$(update_minor)"
OP_PATCH="patch___v$(update_patch)"
OPTIONS=("$OP_MAJOR" "$OP_MINOR" "$OP_PATCH")
- select choice in "${OPTIONS[@]}"
- do
+ select choice in "${OPTIONS[@]}"; do
case $choice in
- "$OP_MAJOR" )
- MAJOR=$((MAJOR + 1))
- MINOR=0
- PATCH=0
- break;;
- "$OP_MINOR")
- MINOR=$((MINOR + 1))
- PATCH=0
- break;;
- "$OP_PATCH")
- PATCH=$((PATCH + 1))
- break;;
- *)
- error_close "invalid option $REPLY"
+ "$OP_MAJOR")
+ MAJOR=$((MAJOR + 1))
+ MINOR=0
+ PATCH=0
+ break
+ ;;
+ "$OP_MINOR")
+ MINOR=$((MINOR + 1))
+ PATCH=0
+ break
+ ;;
+ "$OP_PATCH")
+ PATCH=$((PATCH + 1))
+ break
+ ;;
+ *)
+ error_close "invalid option $REPLY"
+ ;;
esac
done
}
# ask continue, or quit
-ask_continue () {
+ask_continue() {
ask_yn "continue"
- if [[ ! "$(user_input)" =~ ^y$ ]]
- then
+ if [[ ! "$(user_input)" =~ ^y$ ]]; then
exit
fi
}
# run all tests
-cargo_test () {
+cargo_test() {
cargo test -- --test-threads=1
ask_continue
}
# Simulate publishing to crates.io
-cargo_publish () {
+cargo_publish() {
cargo publish --dry-run
ask_continue
}
# Build all releases that GitHub workflow would
# This will download GB's of docker images
-cargo_build () {
+cargo_build() {
cargo install cross
cargo_clean
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
@@ -199,9 +196,8 @@ cargo_build () {
cargo_clean
}
-
# $1 text to colourise
-release_continue () {
+release_continue() {
echo -e "\n${PURPLE}$1${RESET}"
ask_continue
}
@@ -212,14 +208,26 @@ cargo_clean() {
cargo clean
}
# Check repository for typos
-check_typos () {
+check_typos() {
echo -e "\n${PURPLE}check typos${RESET}"
typos
ask_continue
}
+# Make sure the unused lint isn't used
+check_allow_unused() {
+ matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +)
+ matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml)
+ if [ -n "$matches_any" ]; then
+ error_close "\"#[allow(unused)]\" in ${matches_any}"
+ elif [ -n "$matches_cargo" ]; then
+ error_close "\"unused = \"allow\"\" in Cargo.toml"
+ fi
+}
+
# Full flow to create a new release
release_flow() {
+ check_allow_unused
check_typos
check_git
@@ -231,20 +239,20 @@ release_flow() {
cd "${CWD}" || error_close "Can't find ${CWD}"
check_tag
-
+
NEW_TAG_WITH_V="v${MAJOR}.${MINOR}.${PATCH}"
printf "\nnew tag chosen: %s\n\n" "${NEW_TAG_WITH_V}"
RELEASE_BRANCH=release-$NEW_TAG_WITH_V
echo -e
ask_changelog_update
-
+
release_continue "checkout ${RELEASE_BRANCH}"
git checkout -b "$RELEASE_BRANCH"
release_continue "update_version_number_in_files"
update_version_number_in_files
-
+
echo -e "\ncargo fmt"
cargo fmt
echo -e "\n${PURPLE}cargo check${RESET}\n"
@@ -283,7 +291,6 @@ release_flow() {
git branch -d "$RELEASE_BRANCH"
}
-
main() {
cmd=(dialog --backtitle "Choose option" --radiolist "choose" 14 80 16)
options=(
@@ -297,24 +304,27 @@ main() {
if [ $exitStatus -ne 0 ]; then
exit
fi
- for choice in $choices
- do
+ for choice in $choices; do
case $choice in
- 0)
- exit;;
- 1)
- cargo_test
- main
- break;;
- 2)
- release_flow
- break;;
- 3)
- cargo_build
- main
- break;;
+ 0)
+ exit
+ ;;
+ 1)
+ cargo_test
+ main
+ break
+ ;;
+ 2)
+ release_flow
+ break
+ ;;
+ 3)
+ cargo_build
+ main
+ break
+ ;;
esac
done
}
-main
\ No newline at end of file
+main
diff --git a/docker-compose.yml b/docker-compose.yml
index b2547f0..442e95e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,6 +11,8 @@ services:
ipc: private
restart: always
shm_size: 256MB
+ ports:
+ - "127.0.0.1:4040:4040"
networks:
- oxker-example-net
deploy:
diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs
index 247d6ff..53976d6 100644
--- a/src/app_data/container_state.rs
+++ b/src/app_data/container_state.rs
@@ -4,6 +4,7 @@ use std::{
fmt,
};
+use bollard::service::Port;
use ratatui::{
style::Color,
widgets::{ListItem, ListState},
@@ -49,7 +50,7 @@ impl PartialOrd for ContainerId {
/// TODO - use string_wrapper for ContainerId?
/// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them
-macro_rules! string_wrapper {
+macro_rules! unit_struct {
($name:ident) => {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct $name(String);
@@ -60,6 +61,13 @@ macro_rules! string_wrapper {
}
}
+ #[cfg(test)]
+ impl From<&str> for $name {
+ fn from(value: &str) -> Self {
+ Self(value.to_owned())
+ }
+ }
+
impl$name {
pub fn get(&self) -> &str {
self.0.as_str()
@@ -90,10 +98,51 @@ macro_rules! string_wrapper {
};
}
-string_wrapper!(ContainerName);
-string_wrapper!(ContainerImage);
+unit_struct!(ContainerName);
+unit_struct!(ContainerImage);
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ContainerPorts {
+ pub ip: Option,
+ pub private: u16,
+ pub public: Option,
+}
+
+impl From<&Port> for ContainerPorts {
+ fn from(value: &Port) -> Self {
+ Self {
+ ip: value.ip.clone(),
+ private: value.private_port,
+ public: value.public_port,
+ }
+ }
+}
+
+impl ContainerPorts {
+ pub fn len_ip(&self) -> usize {
+ self.ip.as_ref().unwrap_or(&String::new()).chars().count()
+ }
+ pub fn len_private(&self) -> usize {
+ format!("{}", self.private).chars().count()
+ }
+ pub fn len_public(&self) -> usize {
+ format!("{}", self.public.unwrap_or_default())
+ .chars()
+ .count()
+ }
+
+ pub fn print(&self) -> (String, String, String) {
+ (
+ self.ip
+ .as_ref()
+ .map_or(String::new(), std::borrow::ToOwned::to_owned),
+ format!("{}", self.private),
+ self.public.map_or(String::new(), |s| s.to_string()),
+ )
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatefulList {
pub state: ListState,
pub items: Vec,
@@ -154,7 +203,7 @@ impl StatefulList {
.state
.selected()
.map_or(0, |value| if len > 0 { value + 1 } else { value });
- format!("{c}/{}", self.items.len())
+ format!(" {c}/{}", self.items.len())
}
}
}
@@ -234,13 +283,13 @@ impl fmt::Display for State {
}
/// Items for the container control list
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockerControls {
Pause,
Restart,
Start,
Stop,
- Unpause,
+ Resume,
Delete,
}
@@ -252,7 +301,7 @@ impl DockerControls {
Self::Start => Color::Green,
Self::Stop => Color::Red,
Self::Delete => Color::Gray,
- Self::Unpause => Color::Blue,
+ Self::Resume => Color::Blue,
}
}
@@ -260,7 +309,7 @@ impl DockerControls {
pub fn gen_vec(state: State) -> Vec {
match state {
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
- State::Paused => vec![Self::Unpause, Self::Stop, Self::Delete],
+ State::Paused => vec![Self::Resume, Self::Stop, Self::Delete],
State::Restarting => vec![Self::Stop, Self::Delete],
State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete],
_ => vec![Self::Delete],
@@ -276,7 +325,7 @@ impl fmt::Display for DockerControls {
Self::Restart => "restart",
Self::Start => "start",
Self::Stop => "stop",
- Self::Unpause => "resume",
+ Self::Resume => "resume",
};
write!(f, "{disp}")
}
@@ -416,7 +465,7 @@ impl fmt::Display for LogsTz {
/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp,
/// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the
/// stateful list dependent on whethere the timestamp is in the HashSet or not
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Logs {
logs: StatefulList>,
tz: HashSet,
@@ -475,23 +524,25 @@ impl Logs {
}
/// Info for each container
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerItem {
- pub created: u64,
pub cpu_stats: VecDeque,
+ pub created: u64,
pub docker_controls: StatefulList,
pub id: ContainerId,
pub image: ContainerImage,
+ pub is_oxker: bool,
pub last_updated: u64,
pub logs: Logs,
pub mem_limit: ByteStats,
pub mem_stats: VecDeque,
pub name: ContainerName,
+ // todo remove option, can be empty vec
+ pub ports: Vec,
pub rx: ByteStats,
pub state: State,
pub status: String,
pub tx: ByteStats,
- pub is_oxker: bool,
}
/// Basic display information, for when running in debug mode
@@ -509,6 +560,7 @@ impl fmt::Display for ContainerItem {
}
impl ContainerItem {
+ #[allow(clippy::too_many_arguments)]
/// Create a new container item
pub fn new(
created: u64,
@@ -516,14 +568,16 @@ impl ContainerItem {
image: String,
is_oxker: bool,
name: String,
+ ports: Vec,
state: State,
status: String,
) -> Self {
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
docker_controls.start();
+
Self {
- created,
cpu_stats: VecDeque::with_capacity(60),
+ created,
docker_controls,
id,
image: image.into(),
@@ -533,6 +587,7 @@ impl ContainerItem {
mem_limit: ByteStats::default(),
mem_stats: VecDeque::with_capacity(60),
name: name.into(),
+ ports,
rx: ByteStats::default(),
state,
status,
@@ -594,7 +649,7 @@ impl ContainerItem {
}
/// Container information panel headings + widths, for nice pretty formatting
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Columns {
pub name: (Header, u8),
pub state: (Header, u8),
@@ -623,3 +678,98 @@ impl Columns {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use ratatui::widgets::ListItem;
+
+ use crate::{
+ app_data::{ContainerImage, Logs},
+ ui::log_sanitizer,
+ };
+
+ use super::{ByteStats, ContainerName, CpuStats, LogsTz};
+
+ #[test]
+ // Display CpuStats as a string
+ fn test_container_state_cpustats_to_string() {
+ let test = |f: f64, s: &str| {
+ assert_eq!(CpuStats::new(f).to_string(), s);
+ };
+
+ test(0.0, "00.00%");
+ test(1.5, "01.50%");
+ test(15.15, "15.15%");
+ test(150.15, "150.15%");
+ }
+
+ #[test]
+ // Display bytestats as a string, convert into correct data unit (Kb, MB, GB)
+ fn test_container_state_bytestats_to_string() {
+ let test = |u: u64, s: &str| {
+ assert_eq!(ByteStats::new(u).to_string(), s);
+ };
+
+ test(0, "0.00 kB");
+ test(150, "0.15 kB");
+ test(1500, "1.50 kB");
+ test(150_000, "150.00 kB");
+ test(1_500_000, "1.50 MB");
+ test(15_000_000, "15.00 MB");
+ test(150_000_000, "150.00 MB");
+ test(1_500_000_000, "1.50 GB");
+ test(15_000_000_000, "15.00 GB");
+ test(150_000_000_000, "150.00 GB");
+ }
+
+ #[test]
+ /// ContainerName as string truncated correctly
+ fn test_container_state_container_name_to_string() {
+ let result = ContainerName::from("name_01");
+ assert_eq!(result.to_string(), "name_01");
+
+ let result = ContainerName::from("name_01_name_01_name_01_name_01_");
+ assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…");
+
+ let result = result.get();
+ assert_eq!(result, "name_01_name_01_name_01_name_01_");
+ }
+
+ #[test]
+ /// ContainerImage as string truncated correctly
+ fn test_container_state_container_image() {
+ let result = ContainerImage::from("name_01");
+ assert_eq!(result.to_string(), "name_01");
+
+ let result = ContainerImage::from("name_01_name_01_name_01_name_01_");
+ assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…");
+
+ let result = result.get();
+ assert_eq!(result, "name_01_name_01_name_01_name_01_");
+ }
+
+ #[test]
+ /// Logs can only contain 1 entry per LogzTz
+ fn test_container_state_logz() {
+ let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet";
+ let tz = LogsTz::from(input);
+ let mut logs = Logs::default();
+ let line = log_sanitizer::remove_ansi(input);
+
+ logs.insert(ListItem::new(line.clone()), tz.clone());
+ logs.insert(ListItem::new(line.clone()), tz.clone());
+ logs.insert(ListItem::new(line), tz);
+
+ assert_eq!(logs.logs.items.len(), 1);
+
+ let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet";
+ let tz = LogsTz::from(input);
+ let line = log_sanitizer::remove_ansi(input);
+
+ logs.insert(ListItem::new(line.clone()), tz.clone());
+ logs.insert(ListItem::new(line.clone()), tz.clone());
+ logs.insert(ListItem::new(line), tz);
+
+ assert_eq!(logs.logs.items.len(), 2);
+ }
+}
diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs
index a9fcadc..557de6b 100644
--- a/src/app_data/mod.rs
+++ b/src/app_data/mod.rs
@@ -17,27 +17,6 @@ use crate::{
};
pub use container_state::*;
-#[cfg(not(debug_assertions))]
-/// Global app_state, stored in an Arc
-#[derive(Debug, Clone)]
-pub struct AppData {
- containers: StatefulList,
- error: Option,
- sorted_by: Option<(Header, SortedOrder)>,
- pub args: CliArgs,
-}
-
-#[cfg(debug_assertions)]
-/// Global app_state, stored in an Arc
-#[derive(Debug, Clone)]
-pub struct AppData {
- containers: StatefulList,
- error: Option,
- sorted_by: Option<(Header, SortedOrder)>,
- debug_string: String,
- pub args: CliArgs,
-}
-
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SortedOrder {
Asc,
@@ -75,18 +54,47 @@ impl fmt::Display for Header {
}
}
+/// Global app_state, stored in an Arc
+#[derive(Debug, Clone)]
+#[cfg(not(test))]
+pub struct AppData {
+ containers: StatefulList,
+ error: Option,
+ sorted_by: Option<(Header, SortedOrder)>,
+ pub args: CliArgs,
+}
+
+#[derive(Debug, Clone)]
+#[cfg(test)]
+pub struct AppData {
+ pub containers: StatefulList,
+ pub error: Option,
+ pub sorted_by: Option<(Header, SortedOrder)>,
+ pub args: CliArgs,
+}
+
impl AppData {
- #[cfg(debug_assertions)]
- pub fn get_debug_string(&self) -> &str {
- &self.debug_string
+ /// Generate a default app_state
+ pub fn default(args: CliArgs) -> Self {
+ Self {
+ args,
+ containers: StatefulList::new(vec![]),
+ error: None,
+ sorted_by: None,
+ }
}
- #[cfg(debug_assertions)]
- #[allow(unused)]
- pub fn push_debug_string(&mut self, x: &str) {
- self.debug_string.push_str(x);
+ /// Current time as unix timestamp
+ #[allow(clippy::expect_used)]
+ fn get_systemtime() -> u64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("In our known reality, this error should never occur")
+ .as_secs()
}
+ /// 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;
@@ -99,40 +107,6 @@ impl AppData {
}));
}
- /// Current time as unix timestamp
- #[allow(clippy::expect_used)]
- fn get_systemtime() -> u64 {
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .expect("In our known reality, this error should never occur")
- .as_secs()
- }
-
- /// Generate a default app_state
- #[cfg(not(debug_assertions))]
- pub fn default(args: CliArgs) -> Self {
- Self {
- args,
- containers: StatefulList::new(vec![]),
- error: None,
- sorted_by: None,
- }
- }
-
- /// Generate a default app_state
- #[cfg(debug_assertions)]
- pub fn default(args: CliArgs) -> Self {
- Self {
- args,
- containers: StatefulList::new(vec![]),
- error: None,
- sorted_by: None,
- debug_string: String::new(),
- }
- }
-
- /// Container sort related methods
-
/// Remove the sorted header & order, and sort by default - created datetime
pub fn reset_sorted(&mut self) {
self.set_sorted(None);
@@ -237,6 +211,11 @@ impl AppData {
self.containers.items.len()
}
+ /// Get all the ContainerItems
+ pub const fn get_container_items(&self) -> &Vec {
+ &self.containers.items
+ }
+
/// Get title for containers section
pub fn container_title(&self) -> String {
self.containers.get_state_title()
@@ -262,9 +241,9 @@ impl AppData {
self.containers.previous();
}
- /// Get Container items
- pub const fn get_container_items(&self) -> &Vec {
- &self.containers.items
+ /// Get ListState of containers
+ pub fn get_container_state(&mut self) -> &mut ListState {
+ &mut self.containers.state
}
/// Get Option of the current selected container
@@ -275,6 +254,51 @@ impl AppData {
.and_then(|i| self.containers.items.get(i))
}
+ /// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public)
+ pub fn get_longest_port(&self) -> (usize, usize, usize) {
+ let mut longest_ip = 5;
+ let mut longest_private = 10;
+ let mut longest_public = 9;
+
+ for item in &self.containers.items {
+ // if let Some(ports) = item.ports.as_ref() {
+ longest_ip = longest_ip.max(
+ item.ports
+ .iter()
+ .map(ContainerPorts::len_ip)
+ .max()
+ .unwrap_or(3),
+ );
+ longest_private = longest_private.max(
+ item.ports
+ .iter()
+ .map(ContainerPorts::len_private)
+ .max()
+ .unwrap_or(8),
+ );
+ longest_public = longest_public.max(
+ item.ports
+ .iter()
+ .map(ContainerPorts::len_public)
+ .max()
+ .unwrap_or(6),
+ );
+ }
+ // }
+
+ (longest_ip, longest_private, longest_public)
+ // )
+ }
+ /// 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() {
+ let mut ports = item.ports.clone();
+ ports.sort_by(|a, b| a.private.cmp(&b.private));
+ return Some((ports, item.state));
+ }
+ None
+ }
+
/// Get mutable Option of the current selected container
fn get_mut_selected_container(&mut self) -> Option<&mut ContainerItem> {
self.containers
@@ -283,16 +307,37 @@ impl AppData {
.and_then(|i| self.containers.items.get_mut(i))
}
- /// Get ListState of containers
- pub fn get_container_state(&mut self) -> &mut ListState {
- &mut self.containers.state
+ /// return a mutable container by given id
+ fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
+ self.containers.items.iter_mut().find(|i| &i.id == id)
}
+ /// Get the ContainerName of by ID
+ pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option {
+ self.containers
+ .items
+ .iter_mut()
+ .find(|i| &i.id == id)
+ .map(|i| i.name.clone())
+ }
+
+ /// Find the id of the currently selected container.
+ /// If any containers on system, will always return a ContainerId
+ /// Only returns None when no containers found.
+ pub fn get_selected_container_id(&self) -> Option {
+ self.get_selected_container().map(|i| i.id.clone())
+ }
+
+ /// Get the Id and State for the currently selected container - used by the exec check method
+ pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
+ self.get_selected_container()
+ .map(|i| (i.id.clone(), i.state, i.name.get().to_owned()))
+ }
/// Selected DockerCommand methods
/// Get the current selected docker command
/// So know which command to execute
- pub fn selected_docker_command(&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
@@ -302,6 +347,35 @@ impl AppData {
})
})
}
+
+ /// Change selected choice of docker commands of selected container
+ pub fn docker_controls_next(&mut self) {
+ if let Some(i) = self.get_mut_selected_container() {
+ i.docker_controls.next();
+ }
+ }
+
+ /// Change selected choice of docker commands of selected container
+ pub fn docker_controls_previous(&mut self) {
+ if let Some(i) = self.get_mut_selected_container() {
+ i.docker_controls.previous();
+ }
+ }
+
+ /// Change selected choice of docker commands of selected container
+ pub fn docker_controls_start(&mut self) {
+ if let Some(i) = self.get_mut_selected_container() {
+ i.docker_controls.start();
+ }
+ }
+
+ /// Change selected choice of docker commands of selected container
+ pub fn docker_controls_end(&mut self) {
+ if let Some(i) = self.get_mut_selected_container() {
+ i.docker_controls.end();
+ }
+ }
+
/// Get mutable Option of the currently selected container DockerControls state
pub fn get_control_state(&mut self) -> Option<&mut ListState> {
self.get_mut_selected_container()
@@ -314,34 +388,6 @@ impl AppData {
.map(|i| &mut i.docker_controls.items)
}
- /// Change selected choice of docker commands of selected container
- pub fn docker_command_next(&mut self) {
- if let Some(i) = self.get_mut_selected_container() {
- i.docker_controls.next();
- }
- }
-
- /// Change selected choice of docker commands of selected container
- pub fn docker_command_previous(&mut self) {
- if let Some(i) = self.get_mut_selected_container() {
- i.docker_controls.previous();
- }
- }
-
- /// Change selected choice of docker commands of selected container
- pub fn docker_command_start(&mut self) {
- if let Some(i) = self.get_mut_selected_container() {
- i.docker_controls.start();
- }
- }
-
- /// Change selected choice of docker commands of selected container
- pub fn docker_command_end(&mut self) {
- if let Some(i) = self.get_mut_selected_container() {
- i.docker_controls.end();
- }
- }
-
/// Logs related methods
/// Get the title for log panel for selected container, will be either
@@ -349,16 +395,16 @@ impl AppData {
/// 2) "logs - container_name" when no logs found, again 32 chars max
/// 3) "" no container currently selected - aka no containers on system
pub fn get_log_title(&self) -> String {
- self.get_selected_container().map_or_else(String::new, |y| {
- let logs_len = y.logs.get_state_title();
- // let mut name = y.name.clone();
- // name.truncate(32);
- if logs_len.is_empty() {
- format!("- {} ", y.name)
- } else {
- format!("{logs_len} - {}", y.name.get())
- }
- })
+ self.get_selected_container()
+ .map_or_else(String::new, |ci| {
+ let logs_len = ci.logs.get_state_title();
+ let prefix = if logs_len.is_empty() {
+ String::from(" ")
+ } else {
+ format!("{logs_len} ")
+ };
+ format!("{}- {}", prefix, ci.name.get())
+ })
}
/// select next selected log line
@@ -389,19 +435,6 @@ 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)> {
- self.containers
- .state
- .selected()
- .and_then(|i| self.containers.items.get_mut(i))
- .map(|i| i.get_chart_data())
- }
-
- /// Logs related methods
-
/// Get mutable Vec of current containers logs
pub fn get_logs(&mut self) -> Vec> {
self.containers
@@ -420,6 +453,17 @@ impl AppData {
.map(|i| i.logs.state())
}
+ /// Chart data related methods
+
+ /// Get mutable Option of the currently selected container chart data
+ pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> {
+ self.containers
+ .state
+ .selected()
+ .and_then(|i| self.containers.items.get_mut(i))
+ .map(|i| i.get_chart_data())
+ }
+
/// Error related methods
/// return single app_state error
@@ -445,6 +489,12 @@ impl AppData {
self.get_selected_container().map_or(false, |i| i.is_oxker)
}
+ /// Check if selected container is oxker and also that oxker is being run in a container
+ pub fn is_oxker_in_container(&self) -> bool {
+ self.get_selected_container()
+ .map_or(false, |i| i.is_oxker && self.args.in_container)
+ }
+
/// Find the widths for the strings in the containers panel.
/// So can display nicely and evenly
pub fn get_width(&self) -> Columns {
@@ -485,36 +535,9 @@ impl AppData {
/// Update related methods
- /// return a mutable container by given id
- fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
- self.containers.items.iter_mut().find(|i| &i.id == id)
- }
-
- /// Get the ContainerName of by ID
- pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option {
- self.containers
- .items
- .iter_mut()
- .find(|i| &i.id == id)
- .map(|i| i.name.clone())
- }
-
- /// Find the id of the currently selected container.
- /// If any containers on system, will always return a ContainerId
- /// Only returns None when no containers found.
- pub fn get_selected_container_id(&self) -> Option {
- self.get_selected_container().map(|i| i.id.clone())
- }
-
- /// Get the Id and State for the currently selected container - used by the exec check method
- pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
- self.get_selected_container()
- .map(|i| (i.id.clone(), i.state, i.name.get().to_owned()))
- }
-
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
/// Will also, if a sort is set, sort the containers
- pub fn update_stats(
+ pub fn update_stats_by_id(
&mut self,
id: &ContainerId,
cpu_stat: Option,
@@ -593,6 +616,10 @@ impl AppData {
})
});
+ let ports = i.ports.as_ref().map_or(vec![], |i| {
+ i.iter().map(ContainerPorts::from).collect::>()
+ });
+
let id = ContainerId::from(id.as_str());
let is_oxker = i
@@ -633,13 +660,17 @@ impl AppData {
};
item.state = state;
};
+
+ item.ports = ports;
+
if item.image.get() != image {
item.image.set(image);
};
} else {
// container not known, so make new ContainerItem and push into containers Vec
- let container =
- ContainerItem::new(created, id, image, is_oxker, name, state, status);
+ let container = ContainerItem::new(
+ created, id, image, is_oxker, name, ports, state, status,
+ );
self.containers.items.push(container);
}
}
@@ -660,7 +691,6 @@ impl AppData {
for mut i in logs {
let tz = LogsTz::from(i.as_str());
- // Strip the timestamp if `-t` flag set
if !timestamp {
i = i.replace(&tz.to_string(), "");
}
@@ -685,3 +715,1148 @@ impl AppData {
}
}
}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used, clippy::many_single_char_names)]
+mod tests {
+
+ use super::*;
+ use crate::tests::{gen_appdata, gen_container_summary, gen_containers};
+ use std::collections::VecDeque;
+
+ // ******* //
+ // Sort by //
+ // ******* //
+
+ #[test]
+ /// Sort by header: name
+ fn test_app_data_set_sort_by_header_name() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ // descending
+ app_data.set_sorted(Some((Header::Name, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("1"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Name, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ #[test]
+ /// Sort by header: state
+ fn test_app_data_set_sort_by_header_state() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ i.state = State::Exited;
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.state = State::Running;
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.state = State::Paused;
+ }
+
+ // descending
+ app_data.set_sorted(Some((Header::State, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("3"));
+ assert_eq!(c.id, ContainerId::from("2"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::State, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("3"));
+ assert_eq!(c.id, ContainerId::from("1"));
+ }
+
+ #[test]
+ /// Sort by header: status
+ fn test_app_data_set_sort_by_header_status() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.status = "Exited (0) 10 minutes ago".to_owned();
+ }
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.status = "Up 2 hours (Paused)".to_owned();
+ }
+
+ // Sort by status
+ // descending
+ app_data.set_sorted(Some((Header::Status, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("2"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Status, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ #[test]
+ /// Sort by header: cpu
+ fn test_app_data_set_sort_by_header_cpu() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ i.cpu_stats = VecDeque::from([CpuStats::new(10.1)]);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.cpu_stats = VecDeque::from([CpuStats::new(8.1)]);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.cpu_stats = VecDeque::from([CpuStats::new(20.3)]);
+ }
+
+ // descending
+ app_data.set_sorted(Some((Header::Cpu, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("2"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Cpu, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ #[test]
+ /// Sort by header: memory
+ fn test_app_data_set_sort_by_header_mem() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ i.mem_stats = VecDeque::from([ByteStats::new(40)]);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.mem_stats = VecDeque::from([ByteStats::new(80)]);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.mem_stats = VecDeque::from([ByteStats::new(2)]);
+ }
+
+ // descending
+ app_data.set_sorted(Some((Header::Memory, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("3"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Memory, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("2"));
+ }
+
+ #[test]
+ /// Sort by header: id
+ fn test_app_data_set_sort_by_header_id() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ // descending
+ app_data.set_sorted(Some((Header::Id, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("1"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Id, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ #[test]
+ /// Sort by header: image
+ fn test_app_data_set_sort_by_header_image() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ // descending
+ app_data.set_sorted(Some((Header::Image, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("1"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Image, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ #[test]
+ /// Sort by header: rx
+ fn test_app_data_set_sort_by_header_rx() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ i.rx = ByteStats::new(40);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.rx = ByteStats::new(80);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.rx = ByteStats::new(2);
+ }
+
+ // descending
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("3"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("1"));
+ assert_eq!(c.id, ContainerId::from("2"));
+ }
+
+ #[test]
+ /// Sort by header: tx
+ fn test_app_data_set_sort_by_header_tx() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ i.rx = ByteStats::new(400);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.rx = ByteStats::new(80);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.rx = ByteStats::new(83);
+ }
+
+ // descending
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("3"));
+ assert_eq!(c.id, ContainerId::from("2"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("3"));
+ assert_eq!(c.id, ContainerId::from("1"));
+ }
+
+ #[test]
+ /// Sort by header when selected headers match
+ fn test_app_data_set_sort_by_header_match() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ // descending
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("3"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("1"));
+
+ // ascending
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ #[test]
+ /// reset sorted
+ fn test_app_data_reset_sorted() {
+ let (_ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result, &containers);
+
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ i.rx = ByteStats::new(400);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
+ i.rx = ByteStats::new(80);
+ }
+ if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
+ i.rx = ByteStats::new(83);
+ }
+
+ app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc)));
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("2"));
+ assert_eq!(b.id, ContainerId::from("3"));
+ assert_eq!(c.id, ContainerId::from("1"));
+
+ app_data.set_sorted(None);
+ let result = app_data.get_container_items();
+ let (a, b, c) = (&result[0], &result[1], &result[2]);
+ assert_eq!(a.id, ContainerId::from("1"));
+ assert_eq!(b.id, ContainerId::from("2"));
+ assert_eq!(c.id, ContainerId::from("3"));
+ }
+
+ // **************** //
+ // Container state //
+ // **************** //
+
+ #[test]
+ /// Get len of current containers vec
+ fn test_app_data_get_container_len() {
+ let (_ids, containers) = gen_containers();
+ let app_data = gen_appdata(&containers);
+ assert_eq!(app_data.get_container_len(), 3);
+ }
+
+ #[test]
+ /// Select the first container
+ fn test_app_data_containers_start() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ // No container selected
+ let result = app_data.get_container_state();
+ assert_eq!(result.selected(), None);
+ assert_eq!(result.offset(), 0);
+
+ // First container selected
+ app_data.containers_start();
+ let result = app_data.get_container_state();
+ assert_eq!(result.selected(), Some(0));
+ assert_eq!(result.offset(), 0);
+
+ let result = app_data.get_selected_container_id();
+ assert_eq!(result, Some(ContainerId::from("1")));
+ let result = app_data.get_selected_container_id_state_name();
+ assert_eq!(
+ result,
+ Some((
+ ContainerId::from("1"),
+ State::Running,
+ "container_1".to_owned()
+ ))
+ );
+
+ // Calling previous when at start has no effect
+ app_data.containers_previous();
+ let result = app_data.get_selected_container_id();
+ assert_eq!(result, Some(ContainerId::from("1")));
+ let result = app_data.get_selected_container_id_state_name();
+ assert_eq!(
+ result,
+ Some((
+ ContainerId::from("1"),
+ State::Running,
+ "container_1".to_owned()
+ ))
+ );
+ }
+
+ #[test]
+ /// advance container list state by one
+ /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data
+ fn test_app_data_containers_next() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ // Advance list state by 1
+ app_data.containers_start();
+ app_data.containers_next();
+
+ let result = app_data.get_container_state();
+ assert_eq!(result.selected(), Some(1));
+ assert_eq!(result.offset(), 0);
+
+ let result = app_data.get_selected_container_id();
+ assert_eq!(result, Some(ContainerId::from("2")));
+ let result = app_data.get_selected_container_id_state_name();
+ assert_eq!(
+ result,
+ Some((
+ ContainerId::from("2"),
+ State::Running,
+ "container_2".to_owned()
+ ))
+ );
+ }
+
+ #[test]
+ /// advance container list state to the end
+ /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data
+ fn test_app_data_containers_end() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ app_data.containers_end();
+ let result = app_data.get_container_state();
+ assert_eq!(result.selected(), Some(2));
+ assert_eq!(result.offset(), 0);
+
+ let result = app_data.get_selected_container_id();
+ assert_eq!(result, Some(ContainerId::from("3")));
+ let result = app_data.get_selected_container_id_state_name();
+ assert_eq!(
+ result,
+ Some((
+ ContainerId::from("3"),
+ State::Running,
+ "container_3".to_owned()
+ ))
+ );
+
+ // Calling previous when at end has no effect
+ app_data.containers_next();
+ let result = app_data.get_selected_container_id();
+ assert_eq!(result, Some(ContainerId::from("3")));
+ let result = app_data.get_selected_container_id_state_name();
+ assert_eq!(
+ result,
+ Some((
+ ContainerId::from("3"),
+ State::Running,
+ "container_3".to_owned()
+ ))
+ );
+ }
+
+ #[test]
+ /// go to previous container
+ fn test_app_data_containers_prev() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ app_data.containers_end();
+ app_data.containers_previous();
+ let result = app_data.get_container_state();
+ assert_eq!(result.selected(), Some(1));
+ assert_eq!(result.offset(), 0);
+ }
+
+ #[test]
+ // Get the currently selected container
+ fn test_app_data_get_selected_container() {
+ let (_ids, mut containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_selected_container();
+ assert_eq!(result, None);
+
+ app_data.containers.start();
+ app_data.containers.next();
+
+ let result = app_data.get_selected_container();
+ assert_eq!(result, Some(&containers[1]));
+
+ // As above, but now as mut
+ let result = app_data.get_mut_selected_container();
+ assert_eq!(result, Some(&mut containers[1]));
+ }
+
+ #[test]
+ // Get mut container by id
+ fn test_app_data_get_container_by_id() {
+ let (_ids, mut containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_by_id(&ContainerId::from("2"));
+ assert_eq!(result, Some(&mut containers[1]));
+ }
+
+ #[test]
+ // Get just the containers name by id
+ fn test_app_data_get_container_name_by_id() {
+ let (_ids, containers) = gen_containers();
+ 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")));
+ }
+
+ #[test]
+ // Get the id of the currently selected container
+ fn test_app_data_get_selected_container_id() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ app_data.containers_end();
+
+ let result = app_data.get_selected_container_id();
+ assert_eq!(result, Some(ContainerId::from("3")));
+ }
+
+ #[test]
+ fn test_app_data_get_selected_container_id_state_name() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ app_data.containers_end();
+
+ let result = app_data.get_selected_container_id_state_name();
+ assert_eq!(
+ result,
+ Some((
+ ContainerId::from("3"),
+ State::Running,
+ "container_3".to_owned()
+ ))
+ );
+ }
+
+ // ************** //
+ // DockerControls //
+ // ************** //
+
+ #[test]
+ /// Docker commands returned correctly
+ fn test_app_data_selected_docker_command() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ // No commands when no container selected
+ let result = app_data.selected_docker_controls();
+ assert!(result.is_none());
+
+ // Correct commands returned
+ app_data.containers_start();
+ app_data.docker_controls_start();
+
+ let result = app_data.selected_docker_controls();
+ assert_eq!(result, Some(DockerControls::Pause));
+ }
+
+ #[test]
+ /// Docker command next works
+ fn test_app_data_selected_docker_command_next() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ app_data.containers_start();
+ app_data.docker_controls_start();
+ app_data.docker_controls_next();
+
+ let result = app_data.selected_docker_controls();
+ assert_eq!(result, Some(DockerControls::Restart));
+ }
+
+ #[test]
+ /// Dockercommand end works, and next has no effect when at end
+ fn test_app_data_selected_docker_command_end() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ app_data.containers_start();
+ app_data.docker_controls_end();
+
+ let result = app_data.selected_docker_controls();
+ assert_eq!(result, Some(DockerControls::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));
+ }
+
+ #[test]
+ /// Docker commands previous works, and has no effect when at start
+ fn test_app_data_selected_docker_command_previous() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ app_data.containers_start();
+ app_data.docker_controls_end();
+ app_data.docker_controls_previous();
+
+ let result = app_data.selected_docker_controls();
+ assert_eq!(result, Some(DockerControls::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));
+ }
+
+ #[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 gen_item_state = |state: State| {
+ ContainerItem::new(
+ 1,
+ ContainerId::from("1"),
+ "image_1".to_owned(),
+ false,
+ "container_1".to_owned(),
+ vec![],
+ state,
+ "Up 1 hour".to_owned(),
+ )
+ };
+ let mut app_data = gen_appdata(&vec![gen_item_state(state)]);
+ app_data.containers_start();
+ app_data.docker_controls_start();
+
+ let result = app_data.get_control_items();
+ assert_eq!(result, Some(expected));
+ };
+
+ test_state(
+ State::Dead,
+ &mut vec![
+ DockerControls::Start,
+ DockerControls::Restart,
+ DockerControls::Delete,
+ ],
+ );
+ test_state(
+ State::Exited,
+ &mut vec![
+ DockerControls::Start,
+ DockerControls::Restart,
+ DockerControls::Delete,
+ ],
+ );
+ test_state(
+ State::Paused,
+ &mut vec![
+ DockerControls::Resume,
+ DockerControls::Stop,
+ DockerControls::Delete,
+ ],
+ );
+ test_state(State::Removing, &mut vec![DockerControls::Delete]);
+ test_state(
+ State::Restarting,
+ &mut vec![DockerControls::Stop, DockerControls::Delete],
+ );
+ test_state(
+ State::Running,
+ &mut vec![
+ DockerControls::Pause,
+ DockerControls::Restart,
+ DockerControls::Stop,
+ DockerControls::Delete,
+ ],
+ );
+ test_state(State::Unknown, &mut vec![DockerControls::Delete]);
+ }
+
+ // **** //
+ // Logs //
+ // **** //
+
+ #[test]
+ /// log title string generated correctly
+ fn test_app_data_get_log_title() {
+ let (ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ // No container selected select
+ let result = app_data.get_log_title();
+ assert_eq!(result, "");
+
+ // No logs
+ app_data.containers.start();
+ let result = app_data.get_log_title();
+ assert_eq!(result, " - container_1");
+
+ // On last line of logs
+ let logs = (1..=3).map(|i| format!("{i}")).collect::>();
+ app_data.update_log_by_id(logs, &ids[0]);
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_1");
+
+ // Change log state to no longer be at the end
+ app_data.log_previous();
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 2/3 - container_1");
+ }
+
+ #[test]
+ /// log title string generated correctly after container change
+ fn test_app_data_get_log_title_after_container_change() {
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ // No container selected select
+ let result = app_data.get_log_title();
+ assert_eq!(result, "");
+
+ app_data.containers_start();
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " - container_1");
+
+ // change container
+ app_data.containers_next();
+ let result = app_data.get_log_title();
+ assert_eq!(result, " - container_2");
+
+ // On last line of logs
+ let logs = (1..=3).map(|i| format!("{i}")).collect::>();
+ app_data.update_log_by_id(logs, &ids[1]);
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_2");
+
+ // Change log state to no longer be at the end
+ app_data.log_previous();
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 2/3 - container_2");
+ }
+
+ #[test]
+ /// update logs by id works
+ fn test_app_data_update_log_by_id() {
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ // No container selected select
+ let result = app_data.get_log_title();
+ assert_eq!(result, "");
+
+ app_data.containers_start();
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
+
+ app_data.update_log_by_id(logs, &ids[0]);
+ // app_data.log_start();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(2));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_logs();
+ assert_eq!(result.len(), 3);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_1");
+ }
+
+ #[test]
+ /// logs state reset to start
+ fn test_app_data_logs_start() {
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
+ app_data.containers_start();
+ app_data.update_log_by_id(logs, &ids[0]);
+
+ app_data.log_start();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(0));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 1/3 - container_1");
+ }
+
+ #[test]
+ /// logs state end goes to the end of the logs list
+ fn test_app_data_logs_end() {
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
+ app_data.containers_start();
+ app_data.update_log_by_id(logs, &ids[0]);
+
+ app_data.log_start();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(0));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 1/3 - container_1");
+
+ app_data.log_end();
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(2));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_1");
+ }
+
+ #[test]
+ /// logs state next works
+ /// At end has no effect
+ fn test_app_data_logs_next() {
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
+ app_data.containers_start();
+ app_data.update_log_by_id(logs, &ids[0]);
+
+ app_data.log_start();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(0));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 1/3 - container_1");
+
+ app_data.log_next();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(1));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 2/3 - container_1");
+
+ app_data.log_next();
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(2));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_1");
+ app_data.log_next();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(2));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_1");
+ }
+
+ #[test]
+ /// logs state previous works
+ /// previous at start has no effect
+ fn test_app_data_logs_previous() {
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
+ app_data.containers_start();
+ app_data.update_log_by_id(logs, &ids[0]);
+
+ app_data.log_end();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(2));
+ assert_eq!(result.unwrap().offset(), 0);
+
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 3/3 - container_1");
+
+ app_data.log_previous();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(1));
+ assert_eq!(result.unwrap().offset(), 0);
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 2/3 - container_1");
+
+ app_data.log_previous();
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(0));
+ assert_eq!(result.unwrap().offset(), 0);
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 1/3 - container_1");
+
+ app_data.log_previous();
+ let result = app_data.get_log_state();
+ assert!(result.is_some());
+ assert_eq!(result.as_ref().unwrap().selected(), Some(0));
+ assert_eq!(result.unwrap().offset(), 0);
+ let result = app_data.get_log_title();
+ assert_eq!(result, " 1/3 - container_1");
+ }
+
+ // ********** //
+ // Chart data //
+ // ********** //
+
+ #[test]
+ /// Chart data returned correctly
+ fn test_app_data_get_chart_data() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_chart_data();
+ assert!(result.is_none());
+
+ app_data.containers_start();
+
+ if let Some(item) = app_data.get_container_by_id(&ContainerId::from("1")) {
+ item.cpu_stats = VecDeque::from([CpuStats::new(1.1), CpuStats::new(1.2)]);
+ item.mem_stats = VecDeque::from([ByteStats::new(1), ByteStats::new(2)]);
+ }
+
+ let result = app_data.get_chart_data();
+ assert_eq!(
+ result,
+ Some((
+ (
+ vec![(0.0, 1.1), (1.0, 1.2)],
+ CpuStats::new(1.2),
+ State::Running
+ ),
+ (
+ vec![(0.0, 1.0), (1.0, 2.0)],
+ ByteStats::new(2),
+ State::Running
+ )
+ ))
+ );
+ }
+
+ // ************* //
+ // Header Widths //
+ // ************* //
+
+ #[test]
+ /// Header widths return correctly
+ fn test_app_data_get_width() {
+ let (_ids, containers) = gen_containers();
+ let app_data = gen_appdata(&containers);
+
+ let result = app_data.get_width();
+ let expected = Columns {
+ name: (Header::Name, 11),
+ state: (Header::State, 11),
+ status: (Header::Status, 16),
+ cpu: (Header::Cpu, 7),
+ mem: (Header::Memory, 7, 7),
+ id: (Header::Id, 8),
+ image: (Header::Image, 7),
+ net_rx: (Header::Rx, 7),
+ net_tx: (Header::Tx, 7),
+ };
+ assert_eq!(result, expected);
+ }
+
+ // ***** //
+ // Ports //
+ // ***** //
+
+ #[test]
+ /// Returns selected containers ports ordered by private ip
+ fn test_app_data_get_selected_ports() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+
+ app_data.containers.items[0].ports.push(ContainerPorts {
+ ip: None,
+ private: 10,
+ public: Some(1),
+ });
+ app_data.containers.items[0].ports.push(ContainerPorts {
+ ip: None,
+ private: 11,
+ public: Some(3),
+ });
+ app_data.containers.items[0].ports.push(ContainerPorts {
+ ip: None,
+ private: 4,
+ public: Some(2),
+ });
+
+ // No containers selected
+ let result = app_data.get_selected_ports();
+ assert!(result.is_none());
+
+ // Selected container & ports
+ app_data.containers_start();
+ let result = app_data.get_selected_ports();
+
+ assert_eq!(
+ result,
+ Some((
+ vec![
+ ContainerPorts {
+ ip: None,
+ private: 4,
+ public: Some(2)
+ },
+ ContainerPorts {
+ ip: None,
+ private: 10,
+ public: Some(1)
+ },
+ ContainerPorts {
+ ip: None,
+ private: 11,
+ public: Some(3)
+ },
+ ContainerPorts {
+ ip: None,
+ private: 8001,
+ public: None
+ }
+ ],
+ State::Running
+ ))
+ );
+
+ // Selected container & no ports
+ app_data.containers_start();
+ app_data.containers.items[0].ports = vec![];
+ let result = app_data.get_selected_ports();
+
+ assert_eq!(result, Some((vec![], State::Running)));
+ }
+
+ // ************** //
+ // Update mtehods //
+ // ************** //
+
+ #[test]
+ /// Update stats functioning
+ fn test_app_data_update_stats() {
+ let (ids, containers) = gen_containers();
+
+ let mut app_data = gen_appdata(&containers);
+
+ let result = app_data.get_container_items();
+ assert_eq!(result[0], containers[0]);
+
+ app_data.update_stats_by_id(&ids[0], Some(10.0), Some(10), 10, 10, 10);
+
+ let result = app_data.get_container_items();
+ assert_ne!(result[0], containers[0]);
+ assert_eq!(result[0].cpu_stats, VecDeque::from([CpuStats::new(10.0)]));
+ assert_eq!(result[0].mem_stats, VecDeque::from([ByteStats::new(10)]));
+ assert_eq!(result[0].mem_limit, ByteStats::new(10));
+ assert_eq!(result[0].rx, ByteStats::new(10));
+ assert_eq!(result[0].tx, ByteStats::new(10));
+ }
+
+ #[test]
+ /// Update stats functioning
+ fn test_app_data_update_containers() {
+ let (_ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ let result_pre = app_data.get_container_items().clone();
+ let mut input = vec![
+ gen_container_summary(1, "paused"),
+ gen_container_summary(2, "dead"),
+ ];
+
+ app_data.update_containers(&mut input);
+ let result_post = app_data.get_container_items();
+ assert_ne!(&result_pre, result_post);
+ assert_eq!(result_post[0].state, State::Paused);
+ assert_eq!(result_post[1].state, State::Dead);
+ }
+
+ #[test]
+ /// Update logs don't work if container is_oxker: true
+ fn test_app_data_update_log_by_id_is_oxker() {
+ let (ids, mut containers) = gen_containers();
+ containers[0].is_oxker = true;
+ let mut app_data = gen_appdata(&containers);
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
+
+ app_data.update_log_by_id(logs, &ids[0]);
+ app_data.log_start();
+
+ let result = app_data.get_log_state();
+ assert!(result.is_none());
+ }
+}
diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs
index e1066c1..866aada 100644
--- a/src/docker_data/message.rs
+++ b/src/docker_data/message.rs
@@ -14,6 +14,6 @@ pub enum DockerMessage {
Restart(ContainerId),
Start(ContainerId),
Stop(ContainerId),
- Unpause(ContainerId),
+ Resume(ContainerId),
Update,
}
diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs
index 8964ebf..be8f1f2 100644
--- a/src/docker_data/mod.rs
+++ b/src/docker_data/mod.rs
@@ -71,6 +71,7 @@ pub struct DockerData {
impl DockerData {
/// Use docker stats to calculate current cpu usage
#[allow(clippy::cast_precision_loss)]
+ // 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;
@@ -150,7 +151,7 @@ impl DockerData {
app_data
.lock()
- .update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
+ .update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
}
}
spawns.lock().remove(&spawn_id);
@@ -162,7 +163,6 @@ impl DockerData {
/// Update all stats, spawn each container into own tokio::spawn thread
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
for (state, id) in all_ids {
- // let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len())));
let docker = Arc::clone(&self.docker);
let app_data = Arc::clone(&self.app_data);
let spawns = Arc::clone(&self.spawns);
@@ -387,11 +387,11 @@ impl DockerData {
});
self.update_everything().await;
}
- DockerMessage::Unpause(id) => {
+ DockerMessage::Resume(id) => {
tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker.unpause_container(id.get()).await.is_err() {
- Self::set_error(&app_data, DockerControls::Unpause, &gui_state);
+ Self::set_error(&app_data, DockerControls::Resume, &gui_state);
}
gui_state.lock().stop_loading_animation(&handle, uuid);
});
@@ -436,7 +436,7 @@ impl DockerData {
}
/// Send an update message every x ms, where x is the args.docker_interval
- fn croner(args: &CliArgs, docker_tx: Sender) {
+ fn scheduler(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 {
@@ -472,10 +472,152 @@ impl DockerData {
spawns: Arc::new(Mutex::new(HashMap::new())),
};
inner.initialise_container_data().await;
- Self::croner(&args, docker_tx);
+ Self::scheduler(&args, docker_tx);
inner.message_handler().await;
}
}
}
// 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)]
+mod tests {
+ use bollard::container::{
+ BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData,
+ };
+
+ use super::*;
+
+ #[allow(clippy::too_many_lines)]
+ fn gen_stats(x: u64, y: u64) -> Stats {
+ Stats {
+ read: String::new(),
+ preread: String::new(),
+ num_procs: 0,
+ pids_stats: PidsStats {
+ current: None,
+ limit: None,
+ },
+ network: None,
+ networks: None,
+ memory_stats: MemoryStats {
+ stats: None,
+ max_usage: None,
+ usage: None,
+ failcnt: None,
+ limit: None,
+ commit: None,
+ commit_peak: None,
+ commitbytes: None,
+ commitpeakbytes: None,
+ privateworkingset: None,
+ },
+ blkio_stats: BlkioStats {
+ io_service_bytes_recursive: None,
+ io_serviced_recursive: None,
+ io_queue_recursive: None,
+ io_service_time_recursive: None,
+ io_wait_time_recursive: None,
+ io_merged_recursive: None,
+ io_time_recursive: None,
+ sectors_recursive: None,
+ },
+ 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,
+ },
+ system_cpu_usage: Some(x),
+ online_cpus: Some(1),
+ throttling_data: ThrottlingData {
+ periods: 0,
+ throttled_periods: 0,
+ throttled_time: 0,
+ },
+ },
+ 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,
+ },
+ system_cpu_usage: Some(y),
+ online_cpus: Some(1),
+ throttling_data: ThrottlingData {
+ periods: 0,
+ throttled_periods: 0,
+ throttled_time: 0,
+ },
+ },
+ storage_stats: StorageStats {
+ read_count_normalized: None,
+ read_size_bytes: None,
+ write_count_normalized: None,
+ write_size_bytes: None,
+ },
+ name: "/container_1".to_owned(),
+ id: "1".to_owned(),
+ }
+ }
+
+ #[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);
+
+ let stats = gen_stats(1_000_000_000, 800_000_000);
+ let result = DockerData::calculate_usage(&stats);
+ assert_eq!(result, 25.0);
+
+ let stats = gen_stats(1_000_000_000, 750_000_000);
+ let result = DockerData::calculate_usage(&stats);
+ assert_eq!(result, 20.00);
+ }
+}
diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs
index 1f7fad0..8f9cc36 100644
--- a/src/input_handler/mod.rs
+++ b/src/input_handler/mod.rs
@@ -268,11 +268,11 @@ impl InputHandler {
// This isn't great, just means you can't send docker commands before full initialization of the program
let panel = self.gui_state.lock().get_selected_panel();
if panel == SelectablePanel::Commands {
- let option_command = self.app_data.lock().selected_docker_command();
+ let option_command = self.app_data.lock().selected_docker_controls();
if let Some(command) = option_command {
// Poor way of disallowing commands to be sent to a containerised okxer
- if self.app_data.lock().is_oxker() {
+ if self.app_data.lock().is_oxker_in_container() {
return;
};
let option_id = self.app_data.lock().get_selected_container_id();
@@ -286,8 +286,8 @@ impl InputHandler {
DockerControls::Pause => {
self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
}
- DockerControls::Unpause => {
- self.docker_tx.send(DockerMessage::Unpause(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()
@@ -337,7 +337,7 @@ impl InputHandler {
match selected_panel {
SelectablePanel::Containers => locked_data.containers_start(),
SelectablePanel::Logs => locked_data.log_start(),
- SelectablePanel::Commands => locked_data.docker_command_start(),
+ SelectablePanel::Commands => locked_data.docker_controls_start(),
}
}
@@ -348,7 +348,7 @@ impl InputHandler {
match selected_panel {
SelectablePanel::Containers => locked_data.containers_end(),
SelectablePanel::Logs => locked_data.log_end(),
- SelectablePanel::Commands => locked_data.docker_command_end(),
+ SelectablePanel::Commands => locked_data.docker_controls_end(),
}
}
@@ -481,7 +481,7 @@ impl InputHandler {
match selected_panel {
SelectablePanel::Containers => locked_data.containers_next(),
SelectablePanel::Logs => locked_data.log_next(),
- SelectablePanel::Commands => locked_data.docker_command_next(),
+ SelectablePanel::Commands => locked_data.docker_controls_next(),
};
}
@@ -492,7 +492,7 @@ impl InputHandler {
match selected_panel {
SelectablePanel::Containers => locked_data.containers_previous(),
SelectablePanel::Logs => locked_data.log_previous(),
- SelectablePanel::Commands => locked_data.docker_command_previous(),
+ SelectablePanel::Commands => locked_data.docker_controls_previous(),
}
}
}
diff --git a/src/main.rs b/src/main.rs
index d976325..7eb6851 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -164,3 +164,92 @@ async fn main() {
}
}
}
+
+#[cfg(test)]
+#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
+mod tests {
+ use bollard::service::{ContainerSummary, Port};
+
+ use crate::{
+ app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList},
+ parse_args::CliArgs,
+ };
+
+ pub const fn gen_args() -> CliArgs {
+ CliArgs {
+ color: false,
+ docker_interval: 1000,
+ gui: true,
+ host: None,
+ in_container: false,
+ save_dir: None,
+ raw: false,
+ show_self: false,
+ timestamp: false,
+ use_cli: false,
+ }
+ }
+
+ pub fn gen_item(id: &ContainerId, index: usize) -> ContainerItem {
+ ContainerItem::new(
+ u64::try_from(index).unwrap(),
+ id.clone(),
+ format!("image_{index}"),
+ false,
+ format!("container_{index}"),
+ vec![ContainerPorts {
+ ip: None,
+ private: u16::try_from(index).unwrap_or(1) + 8000,
+ public: None,
+ }],
+ State::Running,
+ format!("Up {index} hour"),
+ )
+ }
+
+ pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
+ AppData {
+ containers: StatefulList::new(containers.to_vec()),
+ error: None,
+ sorted_by: None,
+ args: gen_args(),
+ }
+ }
+
+ pub fn gen_containers() -> (Vec, Vec) {
+ let ids = (1..=3)
+ .map(|i| ContainerId::from(format!("{i}").as_str()))
+ .collect::>();
+ let containers = ids
+ .iter()
+ .enumerate()
+ .map(|(index, id)| gen_item(id, index + 1))
+ .collect::>();
+ (ids, containers)
+ }
+
+ pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary {
+ ContainerSummary {
+ id: Some(format!("{index}")),
+ names: Some(vec![format!("container_{}", index)]),
+ image: Some(format!("image_{index}")),
+ image_id: Some(format!("{index}")),
+ command: None,
+ created: Some(i64::try_from(index).unwrap()),
+ ports: Some(vec![Port {
+ ip: None,
+ private_port: u16::try_from(index).unwrap_or(1) + 8000,
+ public_port: None,
+ typ: None,
+ }]),
+ size_rw: None,
+ size_root_fs: None,
+ labels: None,
+ state: Some(state.to_owned()),
+ status: Some(format!("Up {index} hour")),
+ host_config: None,
+ network_settings: None,
+ mounts: None,
+ }
+ }
+}
diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs
index b35112c..a1171f2 100644
--- a/src/ui/color_match.rs
+++ b/src/ui/color_match.rs
@@ -72,3 +72,79 @@ pub mod log_sanitizer {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use ratatui::{
+ style::{Color, Style},
+ text::{Line, Span},
+ };
+
+ use super::log_sanitizer;
+
+ // This spells out "oxker", with each char having a foreground and background colour
+ const INPUT: &str = "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m";
+
+ #[test]
+ /// Return test raw, as in show escape codes
+ fn color_match_raw() {
+ let result = log_sanitizer::raw(INPUT);
+ let expected = vec![Line {
+ spans: [Span {
+ content: std::borrow::Cow::Borrowed(
+ "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m",
+ ),
+ style: Style::default(),
+ }]
+ .to_vec(),
+ alignment: None,
+ }];
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ // Use the escape codes to colorize the text
+ fn color_match_colorize() {
+ let result = log_sanitizer::colorize_logs(INPUT);
+ let expected = vec![Line {
+ spans: vec![
+ Span {
+ content: std::borrow::Cow::Borrowed("o"),
+ style: Style::default().fg(Color::Red).bg(Color::White),
+ },
+ Span {
+ content: std::borrow::Cow::Borrowed("x"),
+ style: Style::default().fg(Color::Green).bg(Color::Black),
+ },
+ Span {
+ content: std::borrow::Cow::Borrowed("k"),
+ style: Style::default().fg(Color::Yellow).bg(Color::Red),
+ },
+ Span {
+ content: std::borrow::Cow::Borrowed("e"),
+ style: Style::default().fg(Color::Blue).bg(Color::Green),
+ },
+ Span {
+ content: std::borrow::Cow::Borrowed("r"),
+ style: Style::default().fg(Color::Magenta).bg(Color::Yellow),
+ },
+ ],
+ alignment: None,
+ }];
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ // Remove all escape ansi codes from given input
+ fn color_match_remove_ansi() {
+ let result = log_sanitizer::remove_ansi(INPUT);
+ let expected = vec![Line {
+ spans: vec![Span {
+ content: std::borrow::Cow::Borrowed("oxker"),
+ style: Style::default(),
+ }],
+ alignment: None,
+ }];
+ assert_eq!(result, expected);
+ }
+}
diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs
index 8bbc176..2dacdf4 100644
--- a/src/ui/draw_blocks.rs
+++ b/src/ui/draw_blocks.rs
@@ -1,7 +1,7 @@
use parking_lot::Mutex;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
- style::{Color, Modifier, Style},
+ style::{Color, Modifier, Style, Stylize},
symbols,
text::{Line, Span},
widgets::{
@@ -66,10 +66,10 @@ fn generate_block<'a>(
.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(), app_data.lock().container_title())
}
SelectablePanel::Logs => {
- format!("{} {}", panel.title(), app_data.lock().get_log_title())
+ format!("{}{}", panel.title(), app_data.lock().get_log_title())
}
SelectablePanel::Commands => String::new(),
};
@@ -95,6 +95,7 @@ pub fn commands(
gui_state: &Arc>,
) {
let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
+ // let block = block();
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
i.iter()
.map(|c| {
@@ -115,6 +116,7 @@ pub fn commands(
if let Some(i) = app_data.lock().get_control_state() {
f.render_stateful_widget(items, area, i);
} else {
+ let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
let paragraph = Paragraph::new("")
.block(block())
.alignment(Alignment::Center);
@@ -207,7 +209,6 @@ pub fn containers(
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc>,
- widths: &Columns,
) {
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
@@ -215,7 +216,7 @@ pub fn containers(
.lock()
.get_container_items()
.iter()
- .map(|i| ListItem::new(format_containers(i, widths)))
+ .map(|i| ListItem::new(format_containers(i, &fd.columns)))
.collect::>();
if items.is_empty() {
@@ -268,6 +269,65 @@ 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),
+) {
+ if let Some(ports) = app_data.lock().get_selected_ports() {
+ let block = Block::default()
+ .borders(Borders::ALL)
+ .border_type(BorderType::Rounded)
+ .title_alignment(Alignment::Center)
+ .title(Span::styled(
+ " ports ",
+ Style::default()
+ .fg(ports.1.get_color())
+ .add_modifier(Modifier::BOLD),
+ ));
+
+ let (ip, private, public) = max_lens;
+
+ if ports.0.is_empty() {
+ let text = match ports.1 {
+ State::Running | State::Paused | State::Restarting => "no ports",
+ _ => "",
+ };
+ let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
+ .alignment(Alignment::Center)
+ .block(block);
+ f.render_widget(paragraph, area);
+ } else {
+ let mut output = vec![Line::from(
+ Span::from(format!(
+ "{:>ip$}{:>private$}{:>public$}",
+ "ip", "private", "public"
+ ))
+ .fg(Color::Yellow),
+ )];
+ for (index, item) in ports.0.iter().enumerate() {
+ let fg = if index % 2 == 0 {
+ Color::White
+ } else {
+ Color::Magenta
+ };
+ let strings = item.print();
+
+ let line = vec![
+ Span::from(format!("{:>ip$}", strings.0)).fg(fg),
+ Span::from(format!("{:>private$}", strings.1)).fg(fg),
+ Span::from(format!("{:>public$}", strings.2)).fg(fg),
+ ];
+ output.push(Line::from(line));
+ }
+ let paragraph = Paragraph::new(output).block(block);
+ f.render_widget(paragraph, area);
+ }
+ }
+}
+
/// Draw the cpu + mem charts
pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) {
if let Some((cpu, mem)) = app_data.lock().get_chart_data() {
@@ -306,10 +366,7 @@ fn make_chart<'a, T: Stats + Display>(
current: &'a T,
max: &'a T,
) -> Chart<'a> {
- let title_color = match state {
- State::Running => Color::Green,
- _ => state.get_color(),
- };
+ let title_color = state.get_color();
let label_color = match state {
State::Running => ORANGE,
_ => state.get_color(),
@@ -364,22 +421,22 @@ pub fn heading_bar(
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
let header_block = |x: &Header| {
let mut color = Color::Black;
- let mut suffix = "";
- let mut suffix_margin = 0;
+ let mut prefix = "";
+ let mut prefix_margin = 0;
if let Some((a, b)) = data.sorted_by.as_ref() {
if x == a {
match b {
- SortedOrder::Asc => suffix = " ▲",
- SortedOrder::Desc => suffix = " ▼",
+ SortedOrder::Asc => prefix = "▲ ",
+ SortedOrder::Desc => prefix = "▼ ",
}
- suffix_margin = 2;
+ prefix_margin = 2;
color = Color::White;
};
};
(
Block::default().style(Style::default().bg(Color::Magenta).fg(color)),
- suffix,
- suffix_margin,
+ prefix,
+ prefix_margin,
)
};
@@ -390,30 +447,26 @@ pub fn heading_bar(
// Yes this is a mess, needs documenting correctly
let text = match header {
Header::State => format!(
- " {:>width$}{ic}",
- header,
- ic = block.1,
- width = width - block.2,
+ " {x:>width$}",
+ x = format!("{ic}{header}", ic = block.1),
+ width = width
),
Header::Name => format!(
- " {:>width$}{ic}",
- header,
- ic = block.1,
- width = width - block.2,
+ " {x:>width$}",
+ x = format!("{ic}{header}", ic = block.1),
+ width = width
),
Header::Status => format!(
- "{} {:>width$}{ic}",
+ "{} {x:>width$}",
MARGIN,
- header,
- ic = block.1,
- width = width - block.2
+ x = format!("{ic}{header}", ic = block.1),
+ width = width
),
_ => format!(
- "{}{:>width$}{ic}",
+ "{}{x:>width$}",
MARGIN,
- header,
- ic = block.1,
- width = width - block.2
+ x = format!("{ic}{header}", ic = block.1),
+ width = width
),
};
let count = u16::try_from(text.chars().count()).unwrap_or_default();
@@ -738,7 +791,7 @@ pub fn help_box(f: &mut Frame) {
.title(title)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
- .border_style(Style::default().fg(Color::Black));
+ .border_style(Style::default().fg(Color::Black).bg(Color::Magenta));
// Order is important here
f.render_widget(Clear, area);
@@ -763,7 +816,10 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co
Span::from("Are you sure you want to delete container: "),
Span::styled(
name.to_string(),
- Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
+ Style::default()
+ .fg(Color::Red)
+ .bg(Color::White)
+ .add_modifier(Modifier::BOLD),
),
]);
@@ -780,19 +836,22 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co
Block::default()
.border_type(BorderType::Rounded)
.borders(Borders::ALL)
+ .style(Style::default().bg(Color::White))
};
let yes_para = Paragraph::new(yes_text)
.alignment(Alignment::Center)
.block(button_block());
+
// Need to add some padding for the borders
- let yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9);
+ let _yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9);
let no_para = Paragraph::new(no_text)
.alignment(Alignment::Center)
.block(button_block());
+
// Need to add some padding for the borders
- let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8);
+ // let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8);
let area = popup(
lines,
@@ -815,23 +874,15 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co
)
.split(area);
- // Should maybe have a differenet button_space IF the f.width() is within 2 chars of no_chars + yes_chars?
- let button_spacing = (max_line_width - no_chars - yes_chars) / 3;
-
- let button_spacing = if (button_spacing + max_line_width) > f.size().width {
- 1
- } else {
- button_spacing
- };
let split_buttons = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
- Constraint::Max(button_spacing),
- Constraint::Min(no_chars),
- Constraint::Max(button_spacing),
- Constraint::Min(yes_chars),
- Constraint::Max(button_spacing),
+ Constraint::Percentage(10),
+ Constraint::Percentage(35),
+ Constraint::Percentage(10),
+ Constraint::Percentage(35),
+ Constraint::Percentage(10),
]
.as_ref(),
)
@@ -893,6 +944,8 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) {
.alignment(Alignment::Center);
let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre);
+
+ // let (paragraph, area) = gen_error(f, error, seconds);
f.render_widget(Clear, area);
f.render_widget(paragraph, area);
}
@@ -952,24 +1005,2051 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio
.split(popup_layout[indexes.0])[indexes.1]
}
-#[cfg(debug_assertions)]
-// Single row at the top of the screen for debugging
-pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) {
- let block = Block::default().style(Style::default().bg(Color::Red));
- let paragraph = Paragraph::new(debug_string)
- .style(Style::default().fg(Color::White))
- .block(block);
- f.render_widget(paragraph, area);
+#[cfg(test)]
+#[allow(clippy::unwrap_used, clippy::many_single_char_names)]
+mod tests {
+
+ use std::{ops::RangeInclusive, sync::Arc};
+
+ use parking_lot::Mutex;
+ use ratatui::{
+ backend::TestBackend,
+ layout::Rect,
+ style::{Color, Modifier},
+ Terminal,
+ };
+ use uuid::Uuid;
+
+ use crate::{
+ app_data::{
+ AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, Header,
+ SortedOrder, State, StatefulList,
+ },
+ app_error::AppError,
+ tests::{gen_appdata, gen_container_summary, gen_containers},
+ ui::{draw_frame, GuiState},
+ };
+
+ use super::{FrameData, ORANGE, VERSION};
+
+ struct TuiTestSetup {
+ app_data: Arc>,
+ gui_state: Arc>,
+ fd: FrameData,
+ area: Rect,
+ terminal: Terminal,
+ ids: Vec,
+ }
+
+ const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
+
+ /// 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);
+ let terminal = Terminal::new(backend).unwrap();
+
+ let (ids, containers) = gen_containers();
+ let mut app_data = gen_appdata(&containers);
+ if control_start {
+ app_data.docker_controls_start();
+ }
+ if container_start {
+ app_data.containers_start();
+ }
+
+ let gui_state = GuiState::default();
+
+ 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 area = Rect::new(0, 0, w, h);
+ TuiTestSetup {
+ app_data,
+ gui_state,
+ fd,
+ area,
+ terminal,
+ ids,
+ }
+ }
+
+ /// Insert some logs into the first container
+ fn insert_logs(setup: &TuiTestSetup) {
+ let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::>();
+ setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]);
+ }
+
+ // ******************** //
+ // DockerControls panel //
+ // ******************** //
+
+ #[test]
+ // Test that when DockerCommands are available, they are drawn correctly, dependant on container state
+ fn test_draw_blocks_commands_none() {
+ let (w, h) = (12, 6);
+ let mut setup = test_setup(w, h, false, false);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭──────────╮",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰──────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+
+ #[test]
+ // Test that when DockerCommands are available, they are drawn correctly, dependant on container state
+ fn test_draw_blocks_commands_some() {
+ let (w, h) = (12, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭──────────╮",
+ "│▶ pause │",
+ "│ restart │",
+ "│ stop │",
+ "│ delete │",
+ "╰──────────╯",
+ ];
+ let result = &setup.terminal.backend().buffer().content;
+
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ // Check the text color is correct
+ match index {
+ // pause
+ 15..=19 => {
+ assert_eq!(result_cell.fg, Color::Yellow);
+ }
+ // restart
+ 27..=33 => {
+ assert_eq!(result_cell.fg, Color::Magenta);
+ }
+ // stop
+ 39..=42 => {
+ assert_eq!(result_cell.fg, Color::Red);
+ }
+ // delete
+ 51..=56 => {
+ assert_eq!(result_cell.fg, Color::Gray);
+ }
+ // no text
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ if result_cell.symbol().starts_with('▶') {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+
+ // Change the controls state
+ setup
+ .app_data
+ .lock()
+ .update_containers(&mut vec![gen_container_summary(1, "paused")]);
+ setup.app_data.lock().docker_controls_next();
+
+ let expected = [
+ "╭──────────╮",
+ "│ resume │",
+ "│▶ stop │",
+ "│ delete │",
+ "│ │",
+ "╰──────────╯",
+ ];
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ // Chceck the text color is correct
+ match index {
+ // resume
+ 15..=20 => {
+ assert_eq!(result_cell.fg, Color::Blue);
+ }
+ // stop
+ 27..=30 => {
+ assert_eq!(result_cell.fg, Color::Red);
+ }
+ // delete
+ 39..=44 => {
+ assert_eq!(result_cell.fg, Color::Gray);
+ }
+ // no text
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ if result_cell.symbol().starts_with('▶') {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // When control panel is selected, the border is blue, if not then white, selected text is highlighted
+ fn test_draw_blocks_commands_panel_selected_color() {
+ let (w, h) = (12, 6);
+ let mut setup = test_setup(w, h, true, true);
+ let expected = [
+ "╭──────────╮",
+ "│▶ pause │",
+ "│ restart │",
+ "│ stop │",
+ "│ delete │",
+ "╰──────────╯",
+ ];
+
+ // Unselected, has a grey border
+ setup
+ .terminal
+ .draw(|f| {
+ super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+
+ // 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()));
+ setup
+ .terminal
+ .draw(|f| {
+ super::commands(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::LightCyan);
+ }
+ // Make sure that the selected line has bold text
+ match index {
+ // pause
+ 13..=22 => {
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ _ => {
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+ }
+ }
+ }
+
+ // *********************** //
+ // Container summary panel //
+ // *********************** //
+
+ // Check that the correct solor is applied to the state/status/cpu/memory section
+ fn check_expected(expected: [&str; 6], w: u16, _h: u16, setup: &TuiTestSetup, color: Color) {
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ if (145..=207).contains(&index) {
+ assert_eq!(result_cell.fg, color);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // No containers, panel unselected, then selected, border color changes correctly
+ fn test_draw_blocks_containers_none() {
+ let (w, h) = (25, 6);
+ let mut setup = test_setup(w, h, true, true);
+ setup.app_data.lock().containers = StatefulList::new(vec![]);
+
+ let expected = [
+ "╭ Containers ───────────╮",
+ "│ no containers running │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰───────────────────────╯",
+ ];
+
+ setup.gui_state.lock().next_panel();
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+
+ setup.gui_state.lock().previous_panel();
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::LightCyan);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Containers panel drawn, selected line is bold, border is blue
+ fn test_draw_blocks_containers_some() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ // result matches expected
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ // Selected container is bold
+ match index {
+ 131 | 133..=258 => assert_eq!(result_cell.modifier, Modifier::BOLD),
+ _ => {
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+
+ // Border is blue
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::LightCyan);
+ }
+ }
+ }
+
+ // 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()));
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ // Border is gray
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+ }
+
+ #[test]
+ /// ALl columns on all rows are coloured correctly
+ fn test_draw_blocks_containers_colors() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let index_blue = [
+ 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 208, 209, 210, 211, 212, 213,
+ 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228,
+ ];
+ let index_blue = index_blue
+ .iter()
+ .flat_map(|&x| vec![x, x + 130, x + 260])
+ .collect::>();
+ let index_green = [
+ 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161,
+ 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178,
+ 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
+ 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207,
+ ];
+ let index_green = index_green
+ .iter()
+ .flat_map(|&x| vec![x, x + 130, x + 260])
+ .collect::>();
+
+ let index_rx = [229, 230, 231, 232, 233, 234, 235, 236, 237, 238];
+ let index_rx = index_rx
+ .iter()
+ .flat_map(|&x| vec![x, x + 130, x + 260])
+ .collect::>();
+
+ let index_tx = [239, 240, 241, 242, 243, 244, 245, 246, 247, 248];
+ let index_tx = index_tx
+ .iter()
+ .flat_map(|&x| vec![x, x + 130, x + 260])
+ .collect::>();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+
+ let result_cell = &result[index];
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ match index {
+ _x if index_blue.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Blue);
+ }
+ _x if index_green.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Green);
+ }
+ _x if index_rx.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193));
+ }
+ _x if index_tx.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
+ }
+ (0..=130) | (259..=260) | (389..=390) | (519..=520) | (649..=779) => {
+ assert_eq!(result_cell.fg, Color::LightCyan);
+ }
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ /// When long container/image name, it is truncated correctly
+ fn test_draw_blocks_containers_long_name_image() {
+ let (w, h) = (170, 6);
+ let mut setup = test_setup(w, h, true, true);
+ setup.app_data.lock().containers.items[0].name =
+ ContainerName::from("a_long_container_name_for_the_purposes_of_this_test");
+ setup.app_data.lock().containers.items[0].image =
+ ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test");
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ setup.app_data.lock().containers.items[0].state = State::Paused;
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ }
+ }
+
+ // THis char: …
+ }
+
+ #[test]
+ /// When container is paused, correct colors displayed
+ fn test_draw_blocks_containers_paused() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ setup.app_data.lock().containers.items[0].state = State::Paused;
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ check_expected(expected, w, h, &setup, Color::Yellow);
+ }
+
+ #[test]
+ /// When container is dead, correct colors displayed
+ fn test_draw_blocks_containers_dead() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ✖ dead Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ setup.app_data.lock().containers.items[0].state = State::Dead;
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+ check_expected(expected, w, h, &setup, Color::Red);
+ }
+
+ #[test]
+ /// When container is exited, correct colors displayed
+ fn test_draw_blocks_containers_exited() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ✖ exited Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ setup.app_data.lock().containers.items[0].state = State::Exited;
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ check_expected(expected, w, h, &setup, Color::Red);
+ }
+ #[test]
+ /// When container is paused, correct colors displayed
+ fn test_draw_blocks_containers_removing() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 removing Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ setup.app_data.lock().containers.items[0].state = State::Removing;
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ check_expected(expected, w, h, &setup, Color::LightRed);
+ }
+ #[test]
+ /// When container state is restarting, correct colors displayed
+ fn test_draw_blocks_containers_restarting() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ↻ restarting Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ setup.app_data.lock().containers.items[0].state = State::Restarting;
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ check_expected(expected, w, h, &setup, Color::LightGreen);
+ }
+ #[test]
+ /// When container state is unknown, correct colors displayed
+ fn test_draw_blocks_containers_unknown() {
+ let (w, h) = (130, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│⚪ container_1 ? unknown Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
+ "│ │",
+ "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ ];
+ setup.app_data.lock().containers.items[0].state = State::Unknown;
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+ check_expected(expected, w, h, &setup, Color::Red);
+ }
+ // ********** //
+ // Logs panel //
+ // ********** //
+
+ #[test]
+ // No logs, panel unselected, then selected, border color changes correctly
+ fn test_draw_blocks_logs_none() {
+ let (w, h) = (25, 6);
+ let mut setup = test_setup(w, h, true, true);
+ setup.app_data.lock().containers = StatefulList::new(vec![]);
+
+ let expected = [
+ "╭ Logs ─────────────────╮",
+ "│ no logs found │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰───────────────────────╯",
+ ];
+
+ let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+
+ setup.gui_state.lock().next_panel();
+ setup.gui_state.lock().next_panel();
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ // When selected, has a blue border
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::LightCyan);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Parsing logs, spinner visible, and then animates by one frame
+ fn test_draw_blocks_logs_parsing() {
+ let (w, h) = (25, 6);
+ let mut setup = test_setup(w, h, true, true);
+ let uuid = Uuid::new_v4();
+ setup.gui_state.lock().next_loading(uuid);
+
+ let expected = [
+ "╭ Logs - container_1 ───╮",
+ "│ parsing logs ⠙ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰───────────────────────╯",
+ ];
+
+ let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ fd.init = true;
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let test = |terminal: &Terminal, expected: [&str; 6]| {
+ let result = &terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ };
+
+ test(&setup.terminal, expected);
+
+ // animation moved by one frame
+ setup.gui_state.lock().next_loading(uuid);
+
+ let expected = [
+ "╭ Logs - container_1 ───╮",
+ "│ parsing logs ⠹ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰───────────────────────╯",
+ ];
+
+ let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ fd.init = true;
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ test(&setup.terminal, expected);
+ }
+
+ #[test]
+ // Logs correct displayed, changing log state also draws correctly
+ fn test_draw_blocks_logs_some() {
+ let (w, h) = (25, 6);
+ let mut setup = test_setup(w, h, true, true);
+
+ insert_logs(&setup);
+
+ let test = |terminal: &Terminal,
+ expected: [&str; 6],
+ range: RangeInclusive| {
+ let result = &terminal.backend().buffer().content;
+
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.fg, Color::Reset);
+
+ if range.contains(&index) {
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ } else {
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+ }
+ };
+
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+ let expected = [
+ "╭ Logs 3/3 - container_1╮",
+ "│ line 1 │",
+ "│ line 2 │",
+ "│▶ line 3 │",
+ "│ │",
+ "╰───────────────────────╯",
+ ];
+ test(&setup.terminal, expected, 76..=98);
+
+ // Change selected log line
+ setup.app_data.lock().log_previous();
+ let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭ Logs 2/3 - container_1╮",
+ "│ line 1 │",
+ "│▶ line 2 │",
+ "│ line 3 │",
+ "│ │",
+ "╰───────────────────────╯",
+ ];
+ test(&setup.terminal, expected, 51..=73);
+ }
+
+ #[test]
+ // Full (long) name displayed in logs border
+ fn test_draw_blocks_logs_long_name() {
+ let (w, h) = (80, 6);
+ let mut setup = test_setup(w, h, true, true);
+ setup.app_data.lock().containers.items[0].name =
+ ContainerName::from("a_long_container_name_for_the_purposes_of_this_test");
+ setup.app_data.lock().containers.items[0].image =
+ ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test");
+
+ insert_logs(&setup);
+
+ let expected = [
+ "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test ──────────────╮",
+ "│ line 1 │",
+ "│ line 2 │",
+ "│▶ line 3 │",
+ "│ │",
+ "╰──────────────────────────────────────────────────────────────────────────────╯",
+ ];
+
+ let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
+ setup
+ .terminal
+ .draw(|f| {
+ super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ }
+ }
+ }
+
+ // ************ //
+ // Charts panel //
+ // ************ //
+
+ const EXPECTED: [&str; 10] = [
+ "╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮",
+ "│10.00%│ • ││100.00 kB│ •• │",
+ "│ │ •• ││ │ •• │",
+ "│ │ ••• ││ │ • • │",
+ "│ │ • • ││ │ • • │",
+ "│ │ • •• ││ │•• •• │",
+ "│ │• • ││ │• • │",
+ "│ │• • ││ │• • │",
+ "│ │ ││ │ │",
+ "╰──────────────────────────────────────╯╰──────────────────────────────────────╯",
+ ];
+ const MEMORY_INDEX: [usize; 16] = [
+ 134, 135, 214, 215, 293, 295, 372, 375, 451, 452, 455, 456, 531, 535, 611, 615,
+ ];
+
+ const CPU_INDEX: [usize; 15] = [
+ 92, 171, 172, 250, 251, 252, 330, 332, 409, 413, 414, 488, 493, 568, 573,
+ ];
+
+ #[allow(clippy::cast_precision_loss)]
+ // Add fixed data to the cpu & mem vecdeques, that match the above data
+ fn insert_chart_data(setup: &TuiTestSetup) {
+ for i in 1..=10 {
+ setup.app_data.lock().update_stats_by_id(
+ &setup.ids[0],
+ Some(i as f64),
+ Some(i * 10000),
+ i * 10000,
+ i,
+ i,
+ );
+ }
+ for i in 1..=3 {
+ setup.app_data.lock().update_stats_by_id(
+ &setup.ids[0],
+ Some(i as f64),
+ Some(i * 10000),
+ i * 10000,
+ i,
+ i,
+ );
+ }
+ }
+ #[test]
+ // When status is Running, but not data, charts drawn without dots etc
+ fn test_draw_blocks_charts_running_none() {
+ let (w, h) = (80, 10);
+ let mut setup = test_setup(w, h, true, true);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::chart(f, setup.area, &setup.app_data);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮",
+ "│00.00%│ ││0.00 kB│ │",
+ "│ │ ││ │ │",
+ "│ │ ││ │ │",
+ "│ │ ││ │ │",
+ "│ │ ││ │ │",
+ "│ │ ││ │ │",
+ "│ │ ││ │ │",
+ "│ │ ││ │ │",
+ "╰──────────────────────────────────────╯╰──────────────────────────────────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ match index {
+ // chart tiles - cpu 03.00% && memory 30.00 kB - are green
+ 14..=25 | 52..=67 => {
+ assert_eq!(result_cell.fg, Color::Green);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ // Cpu & Memory max are orange and bold
+ 81..=86 | 121..=127 => {
+ assert_eq!(result_cell.fg, ORANGE);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ // All others
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ // When status is Running, charts correctly drawn
+ fn test_draw_blocks_charts_running_some() {
+ let (w, h) = (80, 10);
+ let mut setup = test_setup(w, h, true, true);
+
+ insert_chart_data(&setup);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::chart(f, setup.area, &setup.app_data);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in EXPECTED.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ match index {
+ // chart tiles - cpu 03.00% && memory 30.00 kB - are green
+ 14..=25 | 51..=67 => {
+ assert_eq!(result_cell.fg, Color::Green);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ // Cpu & Memory max are orange and bold
+ 81..=86 | 121..=129 => {
+ assert_eq!(result_cell.fg, ORANGE);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ // cpu dots are magenta
+ _x if CPU_INDEX.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Magenta);
+ assert!(result_cell.modifier.is_empty());
+ }
+ // memory dots are cyan
+ _x if MEMORY_INDEX.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Cyan);
+ assert!(result_cell.modifier.is_empty());
+ }
+ // All others
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Whens status paused, some text is now Yellow
+ fn test_draw_blocks_charts_paused() {
+ let (w, h) = (80, 10);
+ let mut setup = test_setup(w, h, true, true);
+
+ insert_chart_data(&setup);
+ setup.app_data.lock().containers.items[0].state = State::Paused;
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::chart(f, setup.area, &setup.app_data);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in EXPECTED.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ match index {
+ // Titles and y axis are yellow
+ 14..=25 | 51..=67 | 81..=86 | 121..=129 => {
+ assert_eq!(result_cell.fg, Color::Yellow);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ _x if CPU_INDEX.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Magenta);
+ assert!(result_cell.modifier.is_empty());
+ }
+ // memory dots are cyan
+ _x if MEMORY_INDEX.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Cyan);
+ assert!(result_cell.modifier.is_empty());
+ }
+ // All others
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ // When dead, text is read
+ fn test_draw_blocks_charts_dead() {
+ let (w, h) = (80, 10);
+ let mut setup = test_setup(w, h, true, true);
+ insert_chart_data(&setup);
+ setup.app_data.lock().containers.items[0].state = State::Dead;
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::chart(f, setup.area, &setup.app_data);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in EXPECTED.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ match index {
+ // Titles and y axis are red
+ 14..=25 | 51..=67 | 81..=86 | 121..=129 => {
+ assert_eq!(result_cell.fg, Color::Red);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ }
+ // cpu dots are magenta
+ _x if CPU_INDEX.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Magenta);
+ assert!(result_cell.modifier.is_empty());
+ }
+ // memory dots are cyan
+ _x if MEMORY_INDEX.contains(&index) => {
+ assert_eq!(result_cell.fg, Color::Cyan);
+ assert!(result_cell.modifier.is_empty());
+ }
+ // All others
+ _ => {
+ assert_eq!(result_cell.fg, Color::Reset);
+ assert!(result_cell.modifier.is_empty());
+ }
+ }
+ }
+ }
+ }
+
+ // ******* //
+ // Headers //
+ // ******* //
+
+ #[test]
+ /// Heading back only has show/exit help when no containers, correctly coloured
+ fn test_draw_blocks_headers_no_containers() {
+ let (w, h) = (140, 1);
+ 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 expected = " ( h ) show help ";
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::heading_bar(setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (index, expected_char) in expected.chars().enumerate() {
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.bg, Color::Magenta);
+ assert_eq!(result_cell.fg, Color::White);
+ }
+
+ fd.help_visible = true;
+ let expected = " ( h ) exit help ";
+ setup
+ .terminal
+ .draw(|f| {
+ super::heading_bar(setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (index, expected_char) in expected.chars().enumerate() {
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.bg, Color::Magenta);
+ assert_eq!(result_cell.fg, Color::Black);
+ }
+ }
+
+ #[test]
+ /// Show all headings when containers present, colors valid
+ 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 expected = " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ";
+ setup
+ .terminal
+ .draw(|f| {
+ super::heading_bar(setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (index, expected_char) in expected.chars().enumerate() {
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.bg, Color::Magenta);
+ assert_eq!(
+ result_cell.fg,
+ match index {
+ (2..=122) => Color::Black,
+ _ => Color::White,
+ }
+ );
+ }
+ }
+
+ #[test]
+ /// Test all combination of headers & sort by
+ 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 test = |expected: &str, range: RangeInclusive, x: (Header, SortedOrder)| {
+ fd.sorted_by = Some(x);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::heading_bar(setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (index, expected_char) in expected.chars().enumerate() {
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.bg, Color::Magenta);
+ assert_eq!(
+ result_cell.fg,
+ match index {
+ 0 | 1 => Color::White,
+ // given range | help section
+ x if range.contains(&x) || (123..=139).contains(&x) => Color::White,
+ _ => Color::Black,
+ }
+ );
+ }
+ };
+
+ // Name
+ test(" ▲ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Asc));
+ test(" ▼ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Desc));
+
+ // state
+ test(" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Asc));
+ test(" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Desc));
+
+ // status
+ test(" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Asc));
+ test(" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Desc));
+
+ // cpu
+ test(" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Asc));
+ test(" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Desc));
+
+ // mem
+ test(" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Asc));
+ test(" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Desc));
+
+ // id
+ test(" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Asc));
+ test(" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Desc));
+
+ // image
+ test(" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Asc));
+ test(" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Desc));
+
+ // rx
+ test(" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Asc));
+ test(" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Desc));
+
+ // tx
+ test(" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help ", 109..=122, (Header::Tx, SortedOrder::Asc));
+ test(" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help ", 109..=122, (Header::Tx, SortedOrder::Desc));
+ }
+
+ #[test]
+ /// Show animation
+ fn test_draw_blocks_headers_animation() {
+ let (w, h) = (140, 1);
+ 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()));
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::heading_bar(setup.area, f, &fd, &setup.gui_state);
+ })
+ .unwrap();
+
+ let expected = " ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ";
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (index, expected_char) in expected.chars().enumerate() {
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ assert_eq!(result_cell.bg, Color::Magenta);
+ assert_eq!(
+ result_cell.fg,
+ match index {
+ (2..=122) => Color::Black,
+ _ => Color::White,
+ }
+ );
+ }
+ }
+
+ // ********** //
+ // Help popup //
+ // ********** //
+ #[test]
+ // This will cause issues once the version has more than the current 5 chars (0.5.0)
+ // Help popup is drawn correctly
+ fn test_draw_blocks_help() {
+ let (w, h) = (87, 30);
+ let mut setup = test_setup(w, h, true, true);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::help_box(f);
+ })
+ .unwrap();
+
+ let expected = [
+ " ".to_owned(),
+ format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "),
+ " │ │ ".to_owned(),
+ " │ 88 │ ".to_owned(),
+ " │ 88 │ ".to_owned(),
+ " │ 88 │ ".to_owned(),
+ " │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ".to_owned(),
+ r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#.to_owned(),
+ r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#.to_owned(),
+ r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#.to_owned(),
+ r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#.to_owned(),
+ " │ │ ".to_owned(),
+ " │ A simple tui to view & control docker containers │ ".to_owned(),
+ " │ │ ".to_owned(),
+ " │ ( tab ) or ( shift+tab ) change panels │ ".to_owned(),
+ " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ".to_owned(),
+ " │ ( enter ) send docker container command │ ".to_owned(),
+ " │ ( e ) exec into a container │ ".to_owned(),
+ " │ ( h ) toggle this help information │ ".to_owned(),
+ " │ ( s ) save logs to file │ ".to_owned(),
+ " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ".to_owned(),
+ " │ ( 0 ) stop sort │ ".to_owned(),
+ " │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(),
+ " │ ( q ) quit at any time │ ".to_owned(),
+ " │ │ ".to_owned(),
+ " │ currently an early work in progress, all and any input appreciated │ ".to_owned(),
+ " │ https://github.com/mrjackwills/oxker │ ".to_owned(),
+ " │ │ ".to_owned(),
+ " ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(),
+ " ".to_owned(),
+ ];
+
+ for (row_index, row) in expected.iter().enumerate() {
+ let mut bracket_key = vec![];
+ let mut push_bracket_key = false;
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+ let result_str = result_cell.symbol();
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ // First and last row, and first char and last char in each row, is empty
+ if row_index == 0
+ || row_index == usize::from(h - 1)
+ || char_index == 0
+ || char_index == usize::from(w - 1)
+ {
+ assert_eq!(result_cell.fg, Color::Reset);
+ assert_eq!(result_cell.bg, Color::Reset);
+ // Borders
+ } else if BORDER_CHARS.contains(&result_str) {
+ assert_eq!(result_cell.fg, Color::Black);
+ assert_eq!(result_cell.bg, Color::Magenta);
+ // everything else has a magenta background
+ } else {
+ assert_eq!(result_cell.bg, Color::Magenta);
+ }
+
+ // check that ( [key] ) is white
+ if result_str == "(" {
+ push_bracket_key = true;
+ bracket_key.push(result_cell);
+ }
+ if push_bracket_key {
+ bracket_key.push(result_cell);
+ if result_str == ")" {
+ push_bracket_key = false;
+ for i in &bracket_key {
+ assert_eq!(i.fg, Color::White);
+ }
+ bracket_key.clear();
+ }
+ }
+ // TODO should really be testing every color of every str here
+ }
+ }
+ }
+
+ // ************ //
+ // Delete popup //
+ // ************ //
+
+ #[test]
+ // Delete container popup is drawn correctly
+ fn test_draw_blocks_delete() {
+ let (w, h) = (82, 10);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ " ",
+ " ╭──────────────────────── Confirm Delete ────────────────────────╮ ",
+ " │ │ ",
+ " │ Are you sure you want to delete container: container_1 │ ",
+ " │ │ ",
+ " │ ╭─────────────────────╮ ╭─────────────────────╮ │ ",
+ " │ │ (N)o │ │ (Y)es │ │ ",
+ " │ ╰─────────────────────╯ ╰─────────────────────╯ │ ",
+ " ╰────────────────────────────────────────────────────────────────╯ ",
+ " ",
+ ];
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::delete_confirm(f, &setup.gui_state, &ContainerName::from("container_1"));
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ if row_index == 0
+ || row_index == usize::from(h - 1)
+ || char_index < 8
+ || char_index > usize::from(w - 9)
+ {
+ assert_eq!(result_cell.fg, Color::Reset);
+ assert_eq!(result_cell.bg, Color::Reset);
+ } else {
+ assert_eq!(result_cell.bg, Color::White);
+ }
+
+ // Borders are black
+ if BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::Black);
+ // Container name is red
+ } else if row_index == 3 && (57..=67).contains(&char_index) {
+ assert_eq!(result_cell.fg, Color::Red);
+ // All other text is black
+ } else if !row_index == 0
+ && !row_index == usize::from(h - 1)
+ && !char_index < 8
+ && !char_index > usize::from(w - 9)
+ {
+ assert_eq!(result_cell.fg, Color::Black);
+ }
+ }
+ }
+ }
+
+ // ***** //
+ // popup //
+ // ***** //
+
+ #[test]
+ /// Info box drawn in bottom right
+ fn test_draw_blocks_info() {
+ let (w, h) = (45, 9);
+ let mut setup = test_setup(w, h, true, true);
+
+ let expected = [
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " ",
+ " test ",
+ " ",
+ ];
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::info(f, "test", std::time::Instant::now(), &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(expected_char.to_string(), result_cell.symbol());
+
+ let (fg, bg) = if row_index >= 6 && char_index >= 32 {
+ (Color::White, Color::Blue)
+ } else {
+ (Color::Reset, Color::Reset)
+ };
+
+ assert_eq!(result_cell.fg, fg);
+ assert_eq!(result_cell.bg, bg);
+ }
+ }
+ }
+
+ // *********** //
+ // Error popup //
+ // *********** //
+
+ #[test]
+ // Test that the error popup is centered, red background, white border, white text, and displays the correct text
+ fn test_draw_blocks_docker_connect_error() {
+ let (w, h) = (46, 9);
+ let mut setup = test_setup(w, h, true, true);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::error(f, AppError::DockerConnect, Some(4));
+ })
+ .unwrap();
+
+ let expected = vec![
+ " ".to_owned(),
+ " ╭───────────────── Error ──────────────────╮ ".to_owned(),
+ " │ │ ".to_owned(),
+ " │ Unable to access docker daemon │ ".to_owned(),
+ " │ │ ".to_owned(),
+ format!(" │ oxker::v{VERSION} closing in 04 seconds │ "),
+ " │ │ ".to_owned(),
+ " ╰──────────────────────────────────────────╯ ".to_owned(),
+ " ".to_owned(),
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+
+ if (1..=usize::from(h) - 2).contains(&row_index)
+ && (1..=usize::from(w) - 2).contains(&char_index)
+ {
+ assert_eq!(result_cell.bg, Color::Red);
+ }
+ if result_cell
+ .symbol()
+ .chars()
+ .next()
+ .unwrap()
+ .is_alphanumeric()
+ {
+ assert_eq!(result_cell.fg, Color::White);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text
+ fn test_draw_blocks_clearable_error() {
+ let (w, h) = (39, 10);
+ let mut setup = test_setup(w, h, true, true);
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::error(f, AppError::DockerExec, Some(4));
+ })
+ .unwrap();
+
+ let expected = [
+ " ",
+ " ╭────────────── Error ──────────────╮ ",
+ " │ │ ",
+ " │ Unable to exec into container │ ",
+ " │ │ ",
+ " │ ( c ) clear error │ ",
+ " │ ( q ) quit oxker │ ",
+ " │ │ ",
+ " ╰───────────────────────────────────╯ ",
+ " ",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string());
+ if (1..=usize::from(h) - 2).contains(&row_index)
+ && (1..=usize::from(w) - 2).contains(&char_index)
+ {
+ assert_eq!(result_cell.bg, Color::Red);
+ }
+ if result_cell
+ .symbol()
+ .chars()
+ .next()
+ .unwrap()
+ .is_alphanumeric()
+ || ["(", ")"].contains(&result_cell.symbol())
+ {
+ assert_eq!(result_cell.fg, Color::White);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Port section when container has no ports
+ fn test_draw_blocks_ports_no_ports() {
+ let (w, h) = (30, 8);
+ 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();
+ setup
+ .terminal
+ .draw(|f| {
+ super::ports(f, setup.area, &setup.app_data, max_lens);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭────────── ports ───────────╮",
+ "│ no ports │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰────────────────────────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(expected_char.to_string(), result_cell.symbol());
+ if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::Green);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ } else {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+
+ // 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();
+ setup
+ .terminal
+ .draw(|f| {
+ super::ports(f, setup.area, &setup.app_data, max_lens);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭────────── ports ───────────╮",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰────────────────────────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(expected_char.to_string(), result_cell.symbol());
+ if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) {
+ assert_eq!(result_cell.fg, Color::Red);
+ assert_eq!(result_cell.modifier, Modifier::BOLD);
+ } else {
+ assert_eq!(result_cell.fg, Color::Reset);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Port section when container has multiple ports
+ fn test_draw_blocks_ports_multiple_ports() {
+ let (w, h) = (32, 8);
+ let mut setup = test_setup(w, h, true, true);
+ setup.app_data.lock().containers.items[0]
+ .ports
+ .push(ContainerPorts {
+ ip: None,
+ private: 8002,
+ public: None,
+ });
+ setup.app_data.lock().containers.items[0]
+ .ports
+ .push(ContainerPorts {
+ ip: Some("127.0.0.1".to_owned()),
+ private: 8003,
+ public: Some(8003),
+ });
+
+ let max_lens = setup.app_data.lock().get_longest_port();
+
+ setup
+ .terminal
+ .draw(|f| {
+ super::ports(f, setup.area, &setup.app_data, max_lens);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭─────────── ports ────────────╮",
+ "│ ip private public │",
+ "│ 8001 │",
+ "│ 8002 │",
+ "│127.0.0.1 8003 8003 │",
+ "│ │",
+ "│ │",
+ "╰──────────────────────────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(expected_char.to_string(), result_cell.symbol());
+
+ let result_cell_as_char = result_cell
+ .symbol()
+ .chars()
+ .next()
+ .unwrap()
+ .is_ascii_alphanumeric();
+ if row_index == 0 && result_cell_as_char {
+ assert_eq!(result_cell.fg, Color::Green);
+ }
+ if row_index == 1 && result_cell_as_char {
+ assert_eq!(result_cell.fg, Color::Yellow);
+ }
+ if row_index == 2 && result_cell_as_char {
+ assert_eq!(result_cell.fg, Color::White);
+ }
+ if row_index == 3 && result_cell_as_char {
+ assert_eq!(result_cell.fg, Color::Magenta);
+ }
+ if row_index == 4 && result_cell_as_char {
+ assert_eq!(result_cell.fg, Color::White);
+ }
+ }
+ }
+ }
+
+ #[test]
+ // Port section title color correct dependant on state
+ 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();
+
+ setup.app_data.lock().containers.items[0].state = State::Paused;
+ setup
+ .terminal
+ .draw(|f| {
+ super::ports(f, setup.area, &setup.app_data, max_lens);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭─────────── ports ────────────╮",
+ "│ ip private public │",
+ "│ 8001 │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰──────────────────────────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(expected_char.to_string(), result_cell.symbol());
+
+ if row_index == 0
+ && result_cell
+ .symbol()
+ .chars()
+ .next()
+ .unwrap()
+ .is_ascii_alphanumeric()
+ {
+ assert_eq!(result_cell.fg, Color::Yellow);
+ }
+ }
+ }
+
+ setup.app_data.lock().containers.items[0].state = State::Dead;
+ setup
+ .terminal
+ .draw(|f| {
+ super::ports(f, setup.area, &setup.app_data, max_lens);
+ })
+ .unwrap();
+
+ let expected = [
+ "╭─────────── ports ────────────╮",
+ "│ ip private public │",
+ "│ 8001 │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰──────────────────────────────╯",
+ ];
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(expected_char.to_string(), result_cell.symbol());
+
+ if row_index == 0
+ && result_cell
+ .symbol()
+ .chars()
+ .next()
+ .unwrap()
+ .is_ascii_alphanumeric()
+ {
+ assert_eq!(result_cell.fg, Color::Red);
+ }
+ }
+ }
+ }
+
+ // *************** //
+ // The whole layout //
+ // **************** //
+ #[test]
+ // Check that the whole layout is drawn correctly
+ fn test_draw_blocks_whole_layout() {
+ let (w, h) = (160, 30);
+ let mut setup = test_setup(w, h, true, true);
+
+ insert_chart_data(&setup);
+ insert_logs(&setup);
+ setup.app_data.lock().containers.items[0]
+ .ports
+ .push(ContainerPorts {
+ ip: Some("127.0.0.1".to_owned()),
+ private: 8003,
+ public: Some(8003),
+ });
+
+ let expected = [
+ " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
+ "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮",
+ "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │",
+ "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │",
+ "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │",
+ "│ ││ delete │",
+ "│ ││ │",
+ "│ ││ │",
+ "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
+ "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
+ "│ line 1 │",
+ "│ line 2 │",
+ "│▶ line 3 │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "│ │",
+ "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
+ "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮",
+ "│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
+ "│ │ ••• • ││ │ ••• • ││ 8001 │",
+ "│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003│",
+ "│ │ ││ │ ││ │",
+ "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
+ ];
+ setup
+ .terminal
+ .draw(|f| {
+ draw_frame(f, &setup.app_data, &setup.gui_state);
+ })
+ .unwrap();
+
+ let result = &setup.terminal.backend().buffer().content;
+ for (row_index, row) in expected.iter().enumerate() {
+ for (char_index, expected_char) in row.chars().enumerate() {
+ let index = row_index * usize::from(w) + char_index;
+ let result_cell = &result[index];
+
+ assert_eq!(result_cell.symbol(), expected_char.to_string(),);
+ }
+ }
+ }
}
-
-// Draw nothing, as in a blank screen
-// pub fn nothing(f: &mut Frame) {
-// let whole_layout = Layout::default()
-// .direction(Direction::Vertical)
-// .constraints([Constraint::Min(100)].as_ref())
-// .split(f.size());
-
-// let block = Block::default()
-// .borders(Borders::NONE);
-// f.render_widget(block, whole_layout[0]);
-// }
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index 1b22599..79c3cb9 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -217,22 +217,6 @@ impl Ui {
}
}
-#[cfg(not(debug_assertions))]
-fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
- Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
- .split(f.size())
-}
-
-#[cfg(debug_assertions)]
-fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
- Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref())
- .split(f.size())
-}
-
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
#[derive(Debug)]
pub struct FrameData {
@@ -279,21 +263,16 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) {
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
- let whole_layout = get_wholelayout(f);
- #[cfg(debug_assertions)]
- draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string());
-
- #[cfg(debug_assertions)]
- let whole_layout_split = (1, 2);
-
- #[cfg(not(debug_assertions))]
- let whole_layout_split = (0, 1);
+ let whole_layout = Layout::default()
+ .direction(Direction::Vertical)
+ .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
+ .split(f.size());
// Split into 3, containers+controls, logs, then graphs
let upper_main = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
- .split(whole_layout[whole_layout_split.1]);
+ .split(whole_layout[1]);
let top_split = if fd.has_containers {
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
@@ -307,7 +286,7 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc>, gui_state: &Arc