diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 4893600..18c6da1 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -9,6 +9,4 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends build-essential pkg-config libssl-dev
USER vscode
-RUN curl --proto '=https' --tlsv1.2 -sSf curl https://sh.rustup.rs | sh -s -- -y
-# RUN rustup target add x86_64-unknown-linux-musl
-
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index d86c30d..28b0715 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -16,14 +16,11 @@
"seccomp=unconfined"
],
- "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli",
+ "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli cargo-expand",
"mounts": [
"source=/etc/timezone,target=/etc/timezone,type=bind,readonly"
],
- "containerEnv": {
- "CARGO_REGISTRIES_CRATES_IO_PROTOCOL": "sparse"
- },
"customizations": {
"vscode": {
@@ -35,7 +32,6 @@
"mutantdino.resourcemonitor",
"rangav.vscode-thunder-client",
"redhat.vscode-yaml",
- "redhat.vscode-yaml",
"rust-lang.rust-analyzer",
"serayuzgur.crates",
"tamasfe.even-better-toml",
diff --git a/.github/release-body.md b/.github/release-body.md
index 26e3358..ff13266 100644
--- a/.github/release-body.md
+++ b/.github/release-body.md
@@ -1,20 +1,17 @@
-### 2023-10-21
+### 2023-11-20
### Chores
-+ docker-compose Alpine bump, [d46c425fa29f3c1d27bd57764748bae7e0b82f69]
-+ dependencies updated, [e6eecbbdce9c0ccff42aa8806dddb6e3364f990c], [ec93115ece83002fa127f3358f573319e29357e1], [b36daa5aeaa354b6c4f45b7ae67ac1a6345ea1c0], [9c0de1f0feff3165d0f5b6cb5dda843c124bcfa4], [6dd953df458096aee5914411ce40e46c3f600ede]
-+ Rust 1.73 linting, [21234c66c3935330ccd58543dd3a915a293ac776]
++ workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558]
++ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f], [294cc2684f42daab9d51601e235a384f55617678]
++ lints moved from main.rs to Cargo.toml, [2de76e2f358be9c1500ca3dc4f9df0979ed8ed28]
++ .devcontainer updated, [37d2ee915625806dd11c2cc816a892aae12a777c]
-### Docs
-+ README.md updated, [3fd3915b3e929742d8007109fd4c7b4a345eb0fa]
-
-### Refactors
-+ LogsTZ from `&str`, [44f581f5b3652cc4e623fe145141878754dca292]
-+ from string impl, [ca79893df5f05ebf445ce194d578cb8213c9755e]
-+ env handling, [18c3ed43376a8b5e2d285d1b34a9f96843357d53]
-+ `parse_args/mod.rs` > `parse_args.rs`, [a6ff4124319ed17d3f1c46c916418f850ef1d3b0]
-+ set_info_box take `&str`, [faeaca0cd1bb243c7f4a7112b928be776b877ca1]
-+ GitHub action use concurrency matrix, re-roder workflow, [85f1982f4066bfdbc764ab7b88588eded6a17f96]
+### Features
++ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes #28, [c8077bca0b673478cfbb417e677a885136ba9eff], [0e5ee143b008c9d0ee0b681231a1568be227150b], [0e5ee143b008c9d0ee0b681231a1568be227150b]
++ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes #1, [a15da5ed43d07852504a4dd1884a189e3f5b9d84]
+### Fixes
++ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c172629dc7f7e7766f5372da9466e786d8]
++ sorted created_at clash, closes #22, [3a6489396e87702ce94b349a7f47028ece7922f6]
see CHANGELOG.md for more details
diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png
index 06b5086..2b51951 100644
Binary files a/.github/screenshot_01.png and b/.github/screenshot_01.png differ
diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml
index 81f7265..6acc896 100644
--- a/.github/workflows/create_release_and_build.yml
+++ b/.github/workflows/create_release_and_build.yml
@@ -77,7 +77,7 @@ jobs:
uses: actions/download-artifact@v3
- name: Update Release
- uses: ncipollo/release-action@v1.12.0
+ uses: ncipollo/release-action@v1
with:
makeLatest: true
name: ${{ github.ref_name }}
@@ -103,11 +103,11 @@ jobs:
uses: katyo/publish-crates@v1
with:
registry-token: ${{ secrets.CRATES_IO_TOKEN }}
-
+
#########################################
## Build images for Dockerhub & ghcr.io #
#########################################
-
+
image_build:
needs: [cargo_publish]
runs-on: ubuntu-latest
@@ -116,14 +116,14 @@ jobs:
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -133,7 +133,7 @@ jobs:
CURRENT_SEMVER=${GITHUB_REF_NAME#v}
echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV
- - uses: docker/setup-buildx-action@v2
+ - uses: docker/setup-buildx-action@v3
id: buildx
with:
install: true
@@ -146,6 +146,4 @@ jobs:
-t ghcr.io/${{ github.repository_owner }}/oxker:${{env.CURRENT_SEMVER}} \
--provenance=false --sbom=false \
--push \
- -f containerised/Dockerfile .
-
-
+ -f containerised/Dockerfile .
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1481fbc..e420888 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,20 @@
+# v0.4.0
+### 2023-11-20
+
+### Chores
++ workflow dependencies updated, [6a4cf649](https://github.com/mrjackwills/oxker/commit/6a4cf6490d08b976734e2bc8186d94c095700558)
++ dependencies updated, [e301b518](https://github.com/mrjackwills/oxker/commit/e301b51891e03ea40b2f904583119da3bc4daf53), [81d5b326](https://github.com/mrjackwills/oxker/commit/81d5b326db8881263f2c9072e1426948e41b4a0f), [294cc268](https://github.com/mrjackwills/oxker/commit/294cc2684f42daab9d51601e235a384f55617678)
++ lints moved from main.rs to Cargo.toml, [2de76e2f](https://github.com/mrjackwills/oxker/commit/2de76e2f358be9c1500ca3dc4f9df0979ed8ed28)
++ .devcontainer updated, [37d2ee91](https://github.com/mrjackwills/oxker/commit/37d2ee915625806dd11c2cc816a892aae12a777c)
+
+### Features
++ Docker exec mode - you are now able to attempt to exec into a container by pressing the `e` key, closes [#28](https://github.com/mrjackwills/oxker/issues/28), [c8077bca](https://github.com/mrjackwills/oxker/commit/c8077bca0b673478cfbb417e677a885136ba9eff), [0e5ee143](https://github.com/mrjackwills/oxker/commit/0e5ee143b008c9d0ee0b681231a1568be227150b), [0e5ee143](https://github.com/mrjackwills/oxker/commit/0e5ee143b008c9d0ee0b681231a1568be227150b)
++ Export logs feature, press `s` to save logs, use `--save-dir` cli-arg to customise output location, closes [#1](https://github.com/mrjackwills/oxker/issues/1), [a15da5ed](https://github.com/mrjackwills/oxker/commit/a15da5ed43d07852504a4dd1884a189e3f5b9d84)
+
+### Fixes
++ `as_ref()` fixed, thanks [Daniel-Boll](https://github.com/Daniel-Boll), [0e06c9c1](https://github.com/mrjackwills/oxker/commit/0e06c9c172629dc7f7e7766f5372da9466e786d8)
++ sorted created_at clash, closes [#22](https://github.com/mrjackwills/oxker/issues/22), [3a648939](https://github.com/mrjackwills/oxker/commit/3a6489396e87702ce94b349a7f47028ece7922f6)
+
# v0.3.3
### 2023-10-21
diff --git a/Cargo.lock b/Cargo.lock
index f4731fc..aa369d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,24 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+[[package]]
+name = "ahash"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -109,9 +127,9 @@ dependencies = [
[[package]]
name = "base64"
-version = "0.21.4"
+version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
+checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "bitflags"
@@ -128,8 +146,7 @@ checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "bollard"
version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f03db470b3c0213c47e978da93200259a1eb4dae2e5512cba9955e2b540a6fc6"
+source = "git+https://www.github.com/fussybeaver/bollard.git?rev=cef1cd5#cef1cd568684d0c3c497ce56221ff22ca18334f4"
dependencies = [
"base64",
"bollard-stubs",
@@ -157,8 +174,7 @@ dependencies = [
[[package]]
name = "bollard-stubs"
version = "1.43.0-rc.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b58071e8fd9ec1e930efd28e3a90c1251015872a2ce49f81f36421b86466932e"
+source = "git+https://www.github.com/fussybeaver/bollard.git?rev=cef1cd5#cef1cd568684d0c3c497ce56221ff22ca18334f4"
dependencies = [
"serde",
"serde_repr",
@@ -219,9 +235,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.4.6"
+version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
+checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
dependencies = [
"clap_builder",
"clap_derive",
@@ -229,9 +245,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.4.6"
+version = "4.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
+checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
dependencies = [
"anstream",
"anstyle",
@@ -243,9 +259,9 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.4.2"
+version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
"proc-macro2",
@@ -255,9 +271,9 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.5.1"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "colorchoice"
@@ -306,6 +322,27 @@ dependencies = [
"serde",
]
+[[package]]
+name = "directories"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
[[package]]
name = "either"
version = "1.9.0"
@@ -335,24 +372,24 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
[[package]]
name = "futures-macro"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
dependencies = [
"proc-macro2",
"quote",
@@ -361,21 +398,21 @@ dependencies = [
[[package]]
name = "futures-sink"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
[[package]]
name = "futures-task"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
[[package]]
name = "futures-util"
-version = "0.3.28"
+version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
dependencies = [
"futures-core",
"futures-macro",
@@ -387,9 +424,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.10"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
"cfg-if",
"libc",
@@ -404,9 +441,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "h2"
-version = "0.3.21"
+version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
+checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
dependencies = [
"bytes",
"fnv",
@@ -414,7 +451,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
- "indexmap 1.9.3",
+ "indexmap 2.1.0",
"slab",
"tokio",
"tokio-util",
@@ -432,6 +469,10 @@ name = "hashbrown"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
[[package]]
name = "heck"
@@ -453,9 +494,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
-version = "0.2.9"
+version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
dependencies = [
"bytes",
"fnv",
@@ -568,9 +609,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.0.2"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown 0.14.2",
@@ -600,9 +641,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
-version = "0.3.64"
+version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
+checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
dependencies = [
"wasm-bindgen",
]
@@ -615,9 +656,20 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.149"
+version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
+checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+
+[[package]]
+name = "libredox"
+version = "0.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
+dependencies = [
+ "bitflags 2.4.1",
+ "libc",
+ "redox_syscall",
+]
[[package]]
name = "lock_api"
@@ -635,6 +687,15 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+[[package]]
+name = "lru"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
+dependencies = [
+ "hashbrown 0.14.2",
+]
+
[[package]]
name = "memchr"
version = "2.6.4"
@@ -652,9 +713,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "0.8.8"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
+checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
dependencies = [
"libc",
"log",
@@ -706,6 +767,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
[[package]]
name = "overload"
version = "0.1.1"
@@ -714,13 +781,14 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "oxker"
-version = "0.3.3"
+version = "0.4.0"
dependencies = [
"anyhow",
"bollard",
"cansi",
"clap",
"crossterm",
+ "directories",
"futures-util",
"parking_lot",
"ratatui",
@@ -859,15 +927,16 @@ dependencies = [
[[package]]
name = "ratatui"
-version = "0.23.0"
+version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e2e4cd95294a85c3b4446e63ef054eea43e0205b1fd60120c16b74ff7ff96ad"
+checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425"
dependencies = [
"bitflags 2.4.1",
"cassowary",
"crossterm",
"indoc",
"itertools",
+ "lru",
"paste",
"strum",
"unicode-segmentation",
@@ -883,6 +952,17 @@ dependencies = [
"bitflags 1.3.2",
]
+[[package]]
+name = "redox_users"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.23"
@@ -909,18 +989,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.189"
+version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
+checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.189"
+version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
+checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
@@ -929,9 +1009,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.107"
+version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
dependencies = [
"itoa",
"ryu",
@@ -940,9 +1020,9 @@ dependencies = [
[[package]]
name = "serde_repr"
-version = "0.1.16"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
+checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145"
dependencies = [
"proc-macro2",
"quote",
@@ -971,7 +1051,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.0.2",
+ "indexmap 2.1.0",
"serde",
"serde_json",
"time",
@@ -1027,9 +1107,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.11.1"
+version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
+checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]]
name = "socket2"
@@ -1081,9 +1161,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.38"
+version = "2.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
+checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
dependencies = [
"proc-macro2",
"quote",
@@ -1166,9 +1246,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.33.0"
+version = "1.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
+checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
dependencies = [
"backtrace",
"bytes",
@@ -1185,9 +1265,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@@ -1196,9 +1276,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.9"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
dependencies = [
"bytes",
"futures-core",
@@ -1248,20 +1328,20 @@ dependencies = [
[[package]]
name = "tracing-log"
-version = "0.1.3"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
- "lazy_static",
"log",
+ "once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"nu-ansi-term",
"sharded-slab",
@@ -1338,9 +1418,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
+checksum = "c58fe91d841bc04822c9801002db4ea904b9e4b8e6bbad25127b46eff8dc516b"
dependencies = [
"getrandom",
"rand",
@@ -1375,9 +1455,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
-version = "0.2.87"
+version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
+checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -1385,9 +1465,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.87"
+version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
+checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
dependencies = [
"bumpalo",
"log",
@@ -1400,9 +1480,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.87"
+version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
+checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1410,9 +1490,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.87"
+version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
+checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
dependencies = [
"proc-macro2",
"quote",
@@ -1423,9 +1503,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.87"
+version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
+checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
[[package]]
name = "winapi"
@@ -1523,3 +1603,23 @@ name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
index e0055f6..9ce8d4a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "oxker"
-version = "0.3.3"
+version = "0.4.0"
edition = "2021"
authors = ["Jack Wills "]
description = "A simple tui to view & control docker containers"
@@ -11,19 +11,35 @@ readme = "README.md"
keywords = ["docker", "tui", "tokio", "terminal", "podman"]
categories = ["command-line-utilities"]
+[lints.rust]
+unsafe_code = "forbid"
+
+[lints.clippy]
+expect_used = "warn"
+nursery = "warn"
+pedantic ="warn"
+todo = "warn"
+unused_async = "warn"
+unwrap_used = "warn"
+module_name_repetitions = "allow"
+doc_markdown = "allow"
+similar_names = "allow"
+
[dependencies]
anyhow = "1.0"
-bollard = "0.15"
+# bollard = "0.15"
+bollard = { git = "https://www.github.com/fussybeaver/bollard.git", rev = "cef1cd5" }
cansi = "2.2"
clap = { version = "4.4", features = ["derive", "unicode", "color"] }
crossterm = "0.27"
futures-util = "0.3"
parking_lot = { version= "0.12" }
-tokio = { version = "1.33", features = ["full"] }
+tokio = { version = "1.34", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
-ratatui = "0.23"
-uuid = { version = "1.5", features = ["v4", "fast-rng"] }
+ratatui = "0.24"
+uuid = { version = "1.6", features = ["v4", "fast-rng"] }
+directories = "5.0"
[dev-dependencies]
diff --git a/README.md b/README.md
index 6c0140e..f093869 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,10 @@
+- [Download & install](#download--install)
+- [Run](#run)
+- [Build step](#build-step)
+- [Tests](#tests)
## Download & install
@@ -92,26 +96,30 @@ oxker
In application controls
| button| result|
|--|--|
-| ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel|
-| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| change selected line in selected panel, mouse scroll also changes selected line |
-| ```( enter )```| execute selected docker command|
-| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
-| ```( 0 )``` | stop sorting |
-| ```( h )``` | toggle help menu |
-| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected|
-| ```( q )``` | to quit at any time |
+| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel.|
+| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| Change selected line in selected panel, mouse scroll also changes selected line.|
+| ```( enter )```| Run selected docker command.|
+| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
+| ```( 0 )``` | Stop sorting.|
+| ```( e )``` | Exec into the selected container.|
+| ```( h )``` | Toggle help menu.|
+| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
+| ```( q )``` | Quit.|
+| ```( s )``` | Save logs to `$HOME/[container_name]_[timestamp].log`, or the directory set by `--save-dir`.|
Available command line arguments
| argument|result|
|--|--|
-|```-d [number > 0]```| set the minimum update interval for docker information, in ms, defaults to 1000 (1 second) |
-|```--host [hostname]```| connect to Docker with a custom hostname, defaults to `/var/run/docker.sock`, will use `$DOCKER_HOST` env if set |
-|```-r```| show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
-|```-c```| attempt to color the logs (conflicts with -r) |
-|```-t```| remove timestamps from each log entry |
-|```-s```| if running via docker, will show the oxker container |
-|```-g```| no tui, basically a pointless debugging mode, for now |
+|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).|
+|```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).|
+|```-c```| Attempt to color the logs (conflicts with `-r`).|
+|```-t```| Remove timestamps from each log entry.|
+|```-s```| If running via Docker, will display the oxker container.|
+|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
+|```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
+|```--save-dir [directory]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
+|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
## Build step
diff --git a/containerised/Dockerfile b/containerised/Dockerfile
index d3f255d..7c12c96 100644
--- a/containerised/Dockerfile
+++ b/containerised/Dockerfile
@@ -49,14 +49,13 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
## Runtime ##
#############
-FROM scratch AS RUNTIME
+FROM scratch as RUNTIME
-# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start
+# Set an ENV to indicate that we're running in a container
ENV OXKER_RUNTIME=container
-# Copy application binary from builder image
COPY --from=BUILDER /oxker /app/
# Run the application
-# this is used in the application itself, to stop itself from listing itself, so DO NOT EDIT
-ENTRYPOINT [ "/app/oxker"]
+# this is used in the application itself so DO NOT EDIT
+ENTRYPOINT [ "/app/oxker"]
\ No newline at end of file
diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev
index 6ff45ba..8a47e04 100644
--- a/containerised/Dockerfile_dev
+++ b/containerised/Dockerfile_dev
@@ -28,3 +28,23 @@ ENTRYPOINT [ "/app/oxker"]
# Buildx command to build musl version for all three platforms, should probably be executed in create_release
# docker buildx create --use
# docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile .
+
+
+# Build production version for x86 only, then run
+# docker build --platform linux/amd64 -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
+
+# docker build --platform linux/arm/v6 -t oxker_dev -f containerised/Dockerfile .
+
+### Build docker files and save to .tar file
+
+# docker build --platform linux/amd64 -t oxker_dev_amd64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_amd64.tar oxker_dev_amd64
+# docker load -i oxker_dev_amd64.tar
+# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_amd64
+
+# docker build --platform linux/arm64 -t oxker_dev_arm64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_arm64.tar oxker_dev_arm64
+# docker load -i oxker_dev_arm64.tar
+# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev oxker_dev_arm64
+
+# docker build --platform linux/arm/v6 -t oxker_dev_armv6 -f containerised/Dockerfile .; docker save -o ./oxker_dev_armv6.tar oxker_dev_armv6
+# docker load -i oxker_dev_armv6.tar
+# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_armv6
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index cbf627b..0521dea 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,7 +16,7 @@ services:
deploy:
resources:
limits:
- memory: 128M
+ memory: 1024M
redis:
image: redis:alpine3.18
container_name: redis
@@ -27,7 +27,7 @@ services:
deploy:
resources:
limits:
- memory: 16M
+ memory: 384M
rabbitmq:
image: rabbitmq:3
container_name: rabbitmq
@@ -38,6 +38,6 @@ services:
deploy:
resources:
limits:
- memory: 256M
+ memory: 512M
diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs
index 08f0785..8c4973f 100644
--- a/src/app_data/container_state.rs
+++ b/src/app_data/container_state.rs
@@ -28,6 +28,11 @@ impl ContainerId {
pub fn get(&self) -> &str {
self.0.as_str()
}
+
+ /// Only return first 8 chars of id, is usually more than enough for uniqueness
+ pub fn get_short(&self) -> String {
+ self.0.chars().take(8).collect::()
+ }
}
impl Ord for ContainerId {
@@ -121,6 +126,9 @@ pub enum State {
}
impl State {
+ pub const fn is_alive(self) -> bool {
+ matches!(self, Self::Running)
+ }
pub const fn get_color(self) -> Color {
match self {
Self::Paused => Color::Yellow,
@@ -158,6 +166,12 @@ impl From<&str> for State {
}
}
+impl From> for State {
+ fn from(input: Option) -> Self {
+ input.map_or(Self::Unknown, |input| Self::from(input.as_str()))
+ }
+}
+
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
@@ -216,7 +230,7 @@ impl fmt::Display for DockerControls {
Self::Restart => "restart",
Self::Start => "start",
Self::Stop => "stop",
- Self::Unpause => "unpause",
+ Self::Unpause => "resume",
};
write!(f, "{disp}")
}
@@ -434,6 +448,20 @@ pub struct ContainerItem {
pub is_oxker: bool,
}
+/// Basic display information, for when running in debug mode
+impl fmt::Display for ContainerItem {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}, {}, {}, {}",
+ self.id.get_short(),
+ self.name,
+ self.cpu_stats.back().unwrap_or(&CpuStats::new(0.0)),
+ self.mem_stats.back().unwrap_or(&ByteStats::new(0))
+ )
+ }
+}
+
impl ContainerItem {
/// Create a new container item
pub fn new(
diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs
index 6a851a9..745572f 100644
--- a/src/app_data/mod.rs
+++ b/src/app_data/mod.rs
@@ -17,6 +17,7 @@ use crate::{
};
pub use container_state::*;
+#[cfg(not(debug_assertions))]
/// Global app_state, stored in an Arc
#[derive(Debug, Clone)]
pub struct AppData {
@@ -26,6 +27,17 @@ pub struct AppData {
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,
@@ -64,6 +76,17 @@ impl fmt::Display for Header {
}
impl AppData {
+ #[cfg(debug_assertions)]
+ pub fn get_debug_string(&self) -> &str {
+ &self.debug_string
+ }
+
+ #[cfg(debug_assertions)]
+ #[allow(unused)]
+ pub fn push_debug_string(&mut self, x: &str) {
+ self.debug_string.push_str(x);
+ }
+
/// 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;
@@ -86,6 +109,7 @@ impl AppData {
}
/// Generate a default app_state
+ #[cfg(not(debug_assertions))]
pub fn default(args: CliArgs) -> Self {
Self {
args,
@@ -95,6 +119,18 @@ impl AppData {
}
}
+ /// 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
@@ -120,78 +156,85 @@ impl AppData {
self.sorted_by
}
- /// Sort the containers vec, based on a heading, either ascending or descending,
+ /// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending,
/// If not sort set, then sort by created time
pub fn sort_containers(&mut self) {
if let Some((head, ord)) = self.sorted_by {
- match head {
- Header::State => match ord {
- SortedOrder::Asc => self
- .containers
- .items
- .sort_by(|a, b| b.state.order().cmp(&a.state.order())),
- SortedOrder::Desc => self
- .containers
- .items
- .sort_by(|a, b| a.state.order().cmp(&b.state.order())),
- },
- Header::Status => match ord {
- SortedOrder::Asc => self
- .containers
- .items
- .sort_by(|a, b| a.status.cmp(&b.status)),
- SortedOrder::Desc => self
- .containers
- .items
- .sort_by(|a, b| b.status.cmp(&a.status)),
- },
- Header::Cpu => match ord {
- SortedOrder::Asc => self
- .containers
- .items
- .sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
- SortedOrder::Desc => self
- .containers
- .items
- .sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
- },
- Header::Memory => match ord {
- SortedOrder::Asc => self
- .containers
- .items
- .sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
- SortedOrder::Desc => self
- .containers
- .items
- .sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
- },
- Header::Id => match ord {
- SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
- SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
- },
- Header::Image => match ord {
- SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
- SortedOrder::Desc => {
- self.containers.items.sort_by(|a, b| b.image.cmp(&a.image));
- }
- },
- Header::Name => match ord {
- SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
- SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
- },
- Header::Rx => match ord {
- SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)),
- SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)),
- },
- Header::Tx => match ord {
- SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)),
- SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)),
- },
- }
+ let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
+ match head {
+ Header::State => match ord {
+ SortedOrder::Asc => a
+ .state
+ .order()
+ .cmp(&b.state.order())
+ .then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => b
+ .state
+ .order()
+ .cmp(&a.state.order())
+ .then_with(|| b.name.cmp(&a.name)),
+ },
+ Header::Status => match ord {
+ SortedOrder::Asc => {
+ a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name))
+ }
+ SortedOrder::Desc => {
+ b.status.cmp(&a.status).then_with(|| b.name.cmp(&a.name))
+ }
+ },
+ Header::Cpu => match ord {
+ SortedOrder::Asc => a
+ .cpu_stats
+ .back()
+ .cmp(&b.cpu_stats.back())
+ .then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => b
+ .cpu_stats
+ .back()
+ .cmp(&a.cpu_stats.back())
+ .then_with(|| b.name.cmp(&a.name)),
+ },
+ Header::Memory => match ord {
+ SortedOrder::Asc => a
+ .mem_stats
+ .back()
+ .cmp(&b.mem_stats.back())
+ .then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => b
+ .mem_stats
+ .back()
+ .cmp(&a.mem_stats.back())
+ .then_with(|| b.name.cmp(&a.name)),
+ },
+ Header::Id => match ord {
+ SortedOrder::Asc => a.id.cmp(&b.id).then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => b.id.cmp(&a.id).then_with(|| b.name.cmp(&a.name)),
+ },
+ Header::Image => match ord {
+ SortedOrder::Asc => a.image.cmp(&b.image).then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => {
+ b.image.cmp(&a.image).then_with(|| b.name.cmp(&a.name))
+ }
+ },
+ Header::Name => match ord {
+ SortedOrder::Asc => a.name.cmp(&b.name).then_with(|| a.id.cmp(&b.id)),
+ SortedOrder::Desc => b.name.cmp(&a.name).then_with(|| b.id.cmp(&a.id)),
+ },
+ Header::Rx => match ord {
+ SortedOrder::Asc => a.rx.cmp(&b.rx).then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => b.rx.cmp(&a.rx).then_with(|| b.name.cmp(&a.name)),
+ },
+ Header::Tx => match ord {
+ SortedOrder::Asc => a.tx.cmp(&b.tx).then_with(|| a.name.cmp(&b.name)),
+ SortedOrder::Desc => b.tx.cmp(&a.tx).then_with(|| b.name.cmp(&a.name)),
+ },
+ }
+ };
+ self.containers.items.sort_by(sort_closure);
} else {
self.containers
.items
- .sort_by(|a, b| a.created.cmp(&b.created));
+ .sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.name.cmp(&b.name)));
}
}
@@ -410,18 +453,6 @@ impl AppData {
self.get_selected_container().map_or(false, |i| i.is_oxker)
}
- /// Check if the initial parsing has been completed, by making sure that all ids given (which are running) have a non empty cpu_stats vecdec
- pub fn initialised(&mut self, all_ids: &[(bool, ContainerId)]) -> bool {
- let count_is_running = all_ids.iter().filter(|i| i.0).count();
- let number_with_cpu_status = self
- .containers
- .items
- .iter()
- .filter(|i| !i.cpu_stats.is_empty())
- .count();
- count_is_running == number_with_cpu_status
- }
-
/// Find the widths for the strings in the containers panel.
/// So can display nicely and evenly
pub fn get_width(&self) -> Columns {
@@ -482,6 +513,12 @@ impl AppData {
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.clone()))
+ }
+
/// 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(
@@ -563,6 +600,8 @@ impl AppData {
})
});
+ let id = ContainerId::from(id.as_str());
+
let is_oxker = i
.command
.as_ref()
@@ -579,8 +618,6 @@ impl AppData {
.as_ref()
.map_or(String::new(), std::clone::Clone::clone);
- let id = ContainerId::from(id.as_str());
-
let created = i
.created
.map_or(0, |i| u64::try_from(i).unwrap_or_default());
@@ -624,31 +661,33 @@ impl AppData {
let timestamp = self.args.timestamp;
if let Some(container) = self.get_container_by_id(id) {
- container.last_updated = Self::get_systemtime();
- let current_len = container.logs.len();
+ if !container.is_oxker {
+ container.last_updated = Self::get_systemtime();
+ let current_len = container.logs.len();
- 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(), "");
+ 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(), "");
+ }
+ let lines = if color {
+ log_sanitizer::colorize_logs(&i)
+ } else if raw {
+ log_sanitizer::raw(&i)
+ } else {
+ log_sanitizer::remove_ansi(&i)
+ };
+ container.logs.insert(ListItem::new(lines), tz);
}
- let lines = if color {
- log_sanitizer::colorize_logs(&i)
- } else if raw {
- log_sanitizer::raw(&i)
- } else {
- log_sanitizer::remove_ansi(&i)
- };
- container.logs.insert(ListItem::new(lines), tz);
- }
- // Set the logs selected row for each container
- // Either when no long currently selected, or currently selected (before updated) is already at end
- if container.logs.state().selected().is_none()
- || container.logs.state().selected().map_or(1, |f| f + 1) == current_len
- {
- container.logs.end();
+ // Set the logs selected row for each container
+ // Either when no long currently selected, or currently selected (before updated) is already at end
+ if container.logs.state().selected().is_none()
+ || container.logs.state().selected().map_or(1, |f| f + 1) == current_len
+ {
+ container.logs.end();
+ }
}
}
}
diff --git a/src/app_error.rs b/src/app_error.rs
index f580b06..ba0d66f 100644
--- a/src/app_error.rs
+++ b/src/app_error.rs
@@ -6,6 +6,8 @@ use std::fmt;
#[derive(Debug, Clone, Copy)]
pub enum AppError {
DockerCommand(DockerControls),
+ DockerExec,
+ DockerLogs,
DockerConnect,
DockerInterval,
InputPoll,
@@ -18,6 +20,8 @@ impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::DockerCommand(s) => write!(f, "Unable to {s} container"),
+ Self::DockerExec => write!(f, "Unable to exec into container"),
+ Self::DockerLogs => write!(f, "Unable to save logs"),
Self::DockerConnect => write!(f, "Unable to access docker daemon"),
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
Self::InputPoll => write!(f, "Unable to poll user input"),
@@ -25,7 +29,7 @@ impl fmt::Display for AppError {
let reason = if *x { "en" } else { "dis" };
write!(f, "Unable to {reason}able mouse capture")
}
- Self::Terminal => write!(f, "Unable to draw to terminal"),
+ Self::Terminal => write!(f, "Unable to fully render to terminal"),
}
}
}
diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs
index 0a6b67e..e1066c1 100644
--- a/src/docker_data/message.rs
+++ b/src/docker_data/message.rs
@@ -1,9 +1,14 @@
-use crate::app_data::ContainerId;
+use std::sync::Arc;
-#[derive(Debug, Clone)]
+use crate::app_data::ContainerId;
+use bollard::Docker;
+use tokio::sync::oneshot::Sender;
+
+#[derive(Debug)]
pub enum DockerMessage {
- Delete(ContainerId),
ConfirmDelete(ContainerId),
+ Delete(ContainerId),
+ Exec(Sender>),
Pause(ContainerId),
Quit,
Restart(ContainerId),
diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs
index 1540597..99d0bcf 100644
--- a/src/docker_data/mod.rs
+++ b/src/docker_data/mod.rs
@@ -10,13 +10,19 @@ use futures_util::StreamExt;
use parking_lot::Mutex;
use std::{
collections::HashMap,
- sync::{atomic::AtomicBool, Arc},
+ sync::{
+ atomic::{AtomicBool, AtomicUsize},
+ Arc,
+ },
+};
+use tokio::{
+ sync::mpsc::{Receiver, Sender},
+ task::JoinHandle,
};
-use tokio::{sync::mpsc::Receiver, task::JoinHandle};
use uuid::Uuid;
use crate::{
- app_data::{AppData, ContainerId, DockerControls},
+ app_data::{AppData, ContainerId, DockerControls, State},
app_error::AppError,
parse_args::CliArgs,
ui::{GuiState, Status},
@@ -54,10 +60,10 @@ pub struct DockerData {
app_data: Arc>,
args: CliArgs,
binate: Binate,
- containerised: bool,
docker: Arc,
gui_state: Arc>,
is_running: Arc,
+ init: Option>,
receiver: Receiver,
spawns: Arc>>>,
}
@@ -97,66 +103,72 @@ impl DockerData {
app_data: Arc>,
docker: Arc,
id: ContainerId,
- is_running: bool,
+ init: Option<(Arc, usize)>,
+ state: State,
spawn_id: SpawnId,
spawns: Arc>>>,
) {
- let mut stream = docker
- .stats(
- id.get(),
- Some(StatsOptions {
- stream: false,
- one_shot: !is_running,
- }),
- )
- .take(1);
+ if state.is_alive() || init.is_some() {
+ let mut stream = docker
+ .stats(
+ id.get(),
+ Some(StatsOptions {
+ stream: false,
+ one_shot: false,
+ }),
+ )
+ .take(1);
- while let Some(Ok(stats)) = stream.next().await {
- let mem_stat = stats.memory_stats.usage.unwrap_or_default();
- let mem_limit = stats.memory_stats.limit.unwrap_or_default();
+ while let Some(Ok(stats)) = stream.next().await {
+ let mem_stat = if state.is_alive() {
+ Some(stats.memory_stats.usage.unwrap_or_default())
+ } else {
+ None
+ };
- let op_key = stats
- .networks
- .as_ref()
- .and_then(|networks| networks.keys().next().cloned());
+ let mem_limit = stats.memory_stats.limit.unwrap_or_default();
- let cpu_stats = Self::calculate_usage(&stats);
-
- let (rx, tx) = if let Some(key) = op_key {
- stats
+ let op_key = stats
.networks
- .unwrap_or_default()
- .get(&key)
- .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
- } else {
- (0, 0)
- };
+ .as_ref()
+ .and_then(|networks| networks.keys().next().cloned());
+
+ let cpu_stats = if state.is_alive() {
+ Some(Self::calculate_usage(&stats))
+ } else {
+ None
+ };
+ let (rx, tx) = if let Some(key) = op_key {
+ stats
+ .networks
+ .unwrap_or_default()
+ .get(&key)
+ .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
+ } else {
+ (0, 0)
+ };
- if is_running {
- app_data.lock().update_stats(
- &id,
- Some(cpu_stats),
- Some(mem_stat),
- mem_limit,
- rx,
- tx,
- );
- } else {
app_data
.lock()
- .update_stats(&id, None, None, mem_limit, rx, tx);
+ .update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
}
- spawns.lock().remove(&spawn_id);
+ }
+ spawns.lock().remove(&spawn_id);
+ if let Some((target, _)) = init {
+ target.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
}
/// Update all stats, spawn each container into own tokio::spawn thread
- fn update_all_container_stats(&mut self, all_ids: &[(bool, ContainerId)]) {
- for (is_running, id) in all_ids {
+ 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);
let spawn_id = SpawnId::Stats((id.clone(), self.binate));
+
+ let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len()));
self.spawns
.lock()
.entry(spawn_id.clone())
@@ -165,7 +177,8 @@ impl DockerData {
app_data,
docker,
id.clone(),
- *is_running,
+ init,
+ *state,
spawn_id,
spawns,
))
@@ -177,7 +190,7 @@ impl DockerData {
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
/// Just make sure that items sent are guaranteed to have an id
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
- pub async fn update_all_containers(&mut self) -> Vec<(bool, ContainerId)> {
+ pub async fn update_all_containers(&mut self) -> Vec<(State, ContainerId)> {
let containers = self
.docker
.list_containers(Some(ListContainersOptions:: {
@@ -191,7 +204,7 @@ impl DockerData {
.into_iter()
.filter_map(|f| match f.id {
Some(_) => {
- if self.containerised
+ if self.args.in_container
&& f.command
.as_ref()
.map_or(false, |c| c.starts_with(ENTRY_POINT))
@@ -212,13 +225,7 @@ impl DockerData {
output
.into_iter()
.filter_map(|i| {
- i.id.map(|id| {
- (
- i.state == Some("running".to_owned())
- || i.state == Some("restarting".to_owned()),
- ContainerId::from(id.as_str()),
- )
- })
+ i.id.map(|id| (State::from(i.state), ContainerId::from(id.as_str())))
})
.collect::>()
}
@@ -253,7 +260,7 @@ impl DockerData {
}
/// Update all logs, spawn each container into own tokio::spawn thread
- fn init_all_logs(&mut self, all_ids: &[(bool, ContainerId)]) {
+ fn init_all_logs(&mut self, all_ids: &[(State, ContainerId)]) {
for (_, id) in all_ids {
let docker = Arc::clone(&self.docker);
let app_data = Arc::clone(&self.app_data);
@@ -275,6 +282,7 @@ impl DockerData {
.lock()
.entry(SpawnId::Log(container.id.clone()))
.or_insert_with(|| {
+ // MAYBE make a struct that can create this data?
let app_data = Arc::clone(&self.app_data);
let docker = Arc::clone(&self.docker);
let id = container.id.clone();
@@ -286,44 +294,26 @@ impl DockerData {
self.app_data.lock().sort_containers();
}
- /// Animate the loading icon
- fn loading_spin(loading_uuid: Uuid, gui_state: &Arc>) -> JoinHandle<()> {
- let gui_state = Arc::clone(gui_state);
- tokio::spawn(async move {
- loop {
- tokio::time::sleep(std::time::Duration::from_millis(100)).await;
- gui_state.lock().next_loading(loading_uuid);
- }
- })
- }
-
- /// Stop the loading_spin function, and reset gui loading status
- fn stop_loading_spin(
- gui_state: &Arc>,
- handle: &JoinHandle<()>,
- loading_uuid: Uuid,
- ) {
- handle.abort();
- gui_state.lock().remove_loading(loading_uuid);
- }
-
/// Initialize docker container data, before any messages are received
async fn initialise_container_data(&mut self) {
self.gui_state.lock().status_push(Status::Init);
let loading_uuid = Uuid::new_v4();
- let loading_spin = Self::loading_spin(loading_uuid, &Arc::clone(&self.gui_state));
-
+ let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid);
let all_ids = self.update_all_containers().await;
self.update_all_container_stats(&all_ids);
self.init_all_logs(&all_ids);
- // wait until all logs have initialised
- while !self.app_data.lock().initialised(&all_ids) {
+ while let Some(x) = self.init.as_ref() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+ if x.load(std::sync::atomic::Ordering::SeqCst) == all_ids.len() {
+ self.init = None;
+ }
}
- Self::stop_loading_spin(&self.gui_state, &loading_spin, loading_uuid);
+ self.gui_state
+ .lock()
+ .stop_loading_animation(&loading_handle, loading_uuid);
self.gui_state.lock().status_del(Status::Init);
}
@@ -340,6 +330,7 @@ impl DockerData {
/// Handle incoming messages, container controls & all container information update
/// Spawn Docker commands off into own thread
+ #[allow(clippy::too_many_lines)]
async fn message_handler(&mut self) {
while let Some(message) = self.receiver.recv().await {
let docker = Arc::clone(&self.docker);
@@ -348,29 +339,32 @@ impl DockerData {
let uuid = Uuid::new_v4();
// TODO need to refactor these
match message {
+ DockerMessage::Exec(docker_tx) => {
+ docker_tx.send(Arc::clone(&self.docker)).ok();
+ }
DockerMessage::Pause(id) => {
tokio::spawn(async move {
- let loading_spin = Self::loading_spin(uuid, &gui_state);
+ let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker.pause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
}
- Self::stop_loading_spin(&gui_state, &loading_spin, uuid);
+ gui_state.lock().stop_loading_animation(&handle, uuid);
});
self.update_everything().await;
}
DockerMessage::Restart(id) => {
tokio::spawn(async move {
- let loading_spin = Self::loading_spin(uuid, &gui_state);
+ let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker.restart_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
}
- Self::stop_loading_spin(&gui_state, &loading_spin, uuid);
+ gui_state.lock().stop_loading_animation(&handle, uuid);
});
self.update_everything().await;
}
DockerMessage::Start(id) => {
tokio::spawn(async move {
- let loading_spin = Self::loading_spin(uuid, &gui_state);
+ let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker
.start_container(id.get(), None::>)
.await
@@ -378,33 +372,33 @@ impl DockerData {
{
Self::set_error(&app_data, DockerControls::Start, &gui_state);
}
- Self::stop_loading_spin(&gui_state, &loading_spin, uuid);
+ gui_state.lock().stop_loading_animation(&handle, uuid);
});
self.update_everything().await;
}
DockerMessage::Stop(id) => {
tokio::spawn(async move {
- let loading_spin = Self::loading_spin(uuid, &gui_state);
+ let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker.stop_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
}
- Self::stop_loading_spin(&gui_state, &loading_spin, uuid);
+ gui_state.lock().stop_loading_animation(&handle, uuid);
});
self.update_everything().await;
}
DockerMessage::Unpause(id) => {
tokio::spawn(async move {
- let loading_spin = Self::loading_spin(uuid, &gui_state);
+ 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::stop_loading_spin(&gui_state, &loading_spin, uuid);
+ gui_state.lock().stop_loading_animation(&handle, uuid);
});
self.update_everything().await;
}
DockerMessage::Delete(id) => {
tokio::spawn(async move {
- let loading_spin = Self::loading_spin(uuid, &gui_state);
+ let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker
.remove_container(
id.get(),
@@ -419,7 +413,7 @@ impl DockerData {
{
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
}
- Self::stop_loading_spin(&gui_state, &loading_spin, uuid);
+ gui_state.lock().stop_loading_animation(&handle, uuid);
});
self.update_everything().await;
self.gui_state.lock().set_delete_container(None);
@@ -440,12 +434,26 @@ impl DockerData {
}
}
+ /// Send an update message every x ms, where x is the args.docker_interval
+ fn croner(args: &CliArgs, docker_tx: Sender) {
+ let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
+ let mut now = std::time::Instant::now();
+ tokio::spawn(async move {
+ loop {
+ let to_sleep = update_duration.saturating_sub(now.elapsed());
+ tokio::time::sleep(to_sleep).await;
+ docker_tx.send(DockerMessage::Update).await.ok();
+ now = std::time::Instant::now();
+ }
+ });
+ }
+
/// Initialise self, and start the message receiving loop
pub async fn init(
app_data: Arc>,
- containerised: bool,
docker: Docker,
docker_rx: Receiver,
+ docker_tx: Sender,
gui_state: Arc>,
is_running: Arc,
) {
@@ -453,16 +461,17 @@ impl DockerData {
if app_data.lock().get_error().is_none() {
let mut inner = Self {
app_data,
- containerised,
- args,
+ args: args.clone(),
binate: Binate::One,
docker: Arc::new(docker),
gui_state,
+ init: Some(Arc::new(AtomicUsize::new(0))),
is_running,
receiver: docker_rx,
spawns: Arc::new(Mutex::new(HashMap::new())),
};
inner.initialise_container_data().await;
+ Self::croner(&args, docker_tx);
inner.message_handler().await;
}
}
diff --git a/src/exec.rs b/src/exec.rs
new file mode 100644
index 0000000..cb81c28
--- /dev/null
+++ b/src/exec.rs
@@ -0,0 +1,345 @@
+use std::{
+ io::{Read, Stdout, Write},
+ sync::{atomic::AtomicBool, Arc},
+};
+
+use bollard::{
+ exec::{CreateExecOptions, ResizeExecOptions, StartExecOptions, StartExecResults},
+ Docker,
+};
+use crossterm::terminal::enable_raw_mode;
+use futures_util::StreamExt;
+use parking_lot::Mutex;
+use ratatui::{backend::CrosstermBackend, Terminal};
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+
+use crate::{
+ app_data::{AppData, ContainerId, State},
+ app_error::AppError,
+};
+
+/// TTY location
+const TTY: &str = "/dev/tty";
+
+/// This will be the start of a docker exec emssage if one is unable to actually exec into the container
+const OCI_ERROR: &str = "OCI runtime exec failed";
+
+/// Set the cursor position on the screen to (0,0)
+const CURSOR_POS: &str = "\x1B[J\x1B[H";
+
+/// This needs to be written to stdout when exiting the exec mode, else the input handler thread gets confused,
+/// see https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
+const KEYBOARD_PROTO: &str = "\x1B[?u\x1B[c";
+
+mod command {
+ pub const PWD: &str = "pwd";
+ pub const DOCKER: &str = "docker";
+ pub const EXEC: &str = "exec";
+ pub const SH: &str = "sh";
+ pub const IT: &str = "-it";
+}
+
+/// Currently known byte output after writing KEYBOARD_PROTO to stdout
+/// valid arm: [91, 63, 54, 49, 59, 54, 59, 55, 59, 50, 50, 59, 50, 51, 59, 50, 52, 59, 50, 56, 59, 51, 50, 59,52, 50] => [?61;6;7;22;23;24;28;32;2
+/// valid x86: [91, 63, 49, 59, 50, 99] => [?1;2c
+/// invalid x86: [91, 63, 49, 59, 48, 99] => [?1;0c
+enum ByteOutput {
+ Arm,
+ X86,
+}
+
+impl ByteOutput {
+ const fn len(&self) -> usize {
+ match self {
+ Self::Arm => 26,
+ Self::X86 => 6,
+ }
+ }
+ const fn last(&self) -> &[u8] {
+ match self {
+ Self::Arm => &[50],
+ Self::X86 => &[99],
+ }
+ }
+}
+
+/// Check the output from tty to see if it matches known sequence.
+/// At the moment we only need to check the length and end digit, as x86 valid and invalid match in these two regards
+fn byte_sequence_valid(bytes: &[u8]) -> bool {
+ [ByteOutput::Arm, ByteOutput::X86]
+ .iter()
+ .any(|i| i.len() == bytes.len() && bytes.ends_with(i.last()))
+}
+
+/// Check if tty is able to be written to, aka not windows
+pub fn tty_readable() -> bool {
+ std::fs::OpenOptions::new()
+ .read(true)
+ .write(false)
+ .open(TTY)
+ .is_ok()
+}
+
+/// Async tty reading, spawned into its own tokio thread
+fn tty(run: Arc) -> Option {
+ if tty_readable() {
+ let (tx, rx) = std::sync::mpsc::channel();
+ tokio::spawn(async move {
+ if let Ok(mut f) = tokio::fs::File::open(TTY).await {
+ while run.load(std::sync::atomic::Ordering::SeqCst) {
+ let mut buf = [0];
+ if tokio::time::timeout(
+ std::time::Duration::from_millis(10),
+ f.read_exact(&mut buf),
+ )
+ .await
+ .is_ok()
+ && tx.send(buf[0]).is_err()
+ {
+ run.store(false, std::sync::atomic::Ordering::SeqCst);
+ }
+ }
+ }
+ });
+ Some(AsyncTTY { rx })
+ } else {
+ None
+ }
+}
+
+struct AsyncTTY {
+ rx: std::sync::mpsc::Receiver,
+}
+
+/// This is used to set the terminal size when exec via the Internal method
+#[derive(Debug, Clone)]
+pub struct TerminalSize {
+ width: u16,
+ height: u16,
+}
+
+impl TerminalSize {
+ pub fn new(terminal: &Terminal>) -> Option {
+ terminal.size().map_or(None, |i| {
+ Some(Self {
+ width: i.width,
+ height: i.height,
+ })
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum ExecMode {
+ // use Bollard Rust library
+ Internal((ContainerId, Arc)),
+ // use the external `docker-cli`
+ External(ContainerId),
+}
+
+impl ExecMode {
+ /// Test if we can exec into the selected container, first via the Internal methods, then by the External
+ /// If the container is oxker, it will always return None
+ pub async fn new(app_data: &Arc>, docker: &Arc) -> Option {
+ let is_oxker = app_data.lock().is_oxker();
+ if is_oxker {
+ return None;
+ }
+
+ let use_cli = app_data.lock().args.use_cli;
+ let container = app_data.lock().get_selected_container_id_state_name();
+
+ if let Some((id, state, _)) = container {
+ if state == State::Running {
+ if tty_readable() && !use_cli {
+ if let Ok(exec) = docker
+ .create_exec(
+ id.get(),
+ CreateExecOptions {
+ attach_stdout: Some(true),
+ attach_stderr: Some(true),
+ cmd: Some(vec![command::PWD]),
+ ..Default::default()
+ },
+ )
+ .await
+ {
+ if let Ok(StartExecResults::Attached { mut output, .. }) =
+ docker.start_exec(&exec.id, None).await
+ {
+ if let Some(Ok(msg)) = output.next().await {
+ if !msg.to_string().starts_with(OCI_ERROR) {
+ return Some(Self::Internal((id.clone(), Arc::clone(docker))));
+ }
+ }
+ }
+ }
+ }
+
+ if let Ok(output) = std::process::Command::new(command::DOCKER)
+ .args([command::EXEC, id.get(), command::PWD])
+ .output()
+ {
+ if let Ok(output) = String::from_utf8(output.stdout) {
+ if !output.starts_with(OCI_ERROR) {
+ return Some(Self::External(id.clone()));
+ }
+ }
+ }
+ }
+ }
+ None
+ }
+
+ /// exec into the container using the external docker cli, the result it just piped into oxker
+ fn exec_external(id: &ContainerId) {
+ let mut stdout = std::io::stdout();
+ stdout.write_all(CURSOR_POS.as_bytes()).ok();
+ if let Ok(mut child) = std::process::Command::new(command::DOCKER)
+ .args([command::EXEC, command::IT, id.get(), command::SH])
+ .stdin(std::process::Stdio::inherit())
+ .stdout(std::process::Stdio::inherit())
+ .stderr(std::process::Stdio::inherit())
+ .spawn()
+ {
+ child.wait().ok();
+ if child.kill().is_err() {
+ std::process::exit(1)
+ }
+ }
+ }
+
+ /// Exec into the container via the Bollard library, stdout & stdin on different threads
+ /// Have to deal with strange output once dropped, hence the use of internal_cleanup() method
+ async fn exec_internal(
+ &self,
+ id: &ContainerId,
+ docker: &Arc,
+ terminal_size: Option,
+ ) -> Result<(), AppError> {
+ let run = Arc::new(AtomicBool::new(true));
+
+ if let Ok(exec_result) = docker
+ .create_exec(
+ id.get(),
+ CreateExecOptions {
+ attach_stdout: Some(true),
+ attach_stderr: Some(false),
+ attach_stdin: Some(true),
+ tty: Some(true),
+ cmd: Some(vec![command::SH]),
+ ..Default::default()
+ },
+ )
+ .await
+ {
+ if let Ok(StartExecResults::Attached {
+ mut output,
+ mut input,
+ }) = docker
+ .start_exec(
+ &exec_result.id,
+ Some(StartExecOptions {
+ detach: false,
+ ..Default::default()
+ }),
+ )
+ .await
+ {
+ if let Some(async_tty) = tty(Arc::clone(&run)) {
+ let run_thread = Arc::clone(&run);
+ tokio::spawn(async move {
+ enable_raw_mode().ok();
+ let mut stdout = std::io::stdout();
+ stdout.write_all(CURSOR_POS.as_bytes()).ok();
+ stdout.flush().ok();
+
+ while run_thread.load(std::sync::atomic::Ordering::SeqCst) {
+ while let Some(Ok(x)) = output.next().await {
+ stdout.write_all(&x.into_bytes()).ok();
+ stdout.flush().ok();
+ }
+ run_thread.store(false, std::sync::atomic::Ordering::SeqCst);
+ }
+ });
+
+ if let Some(terminal_size) = terminal_size {
+ docker
+ .resize_exec(
+ &exec_result.id,
+ ResizeExecOptions {
+ height: terminal_size.height,
+ width: terminal_size.width,
+ },
+ )
+ .await
+ .ok();
+ }
+
+ while let Ok(x) = async_tty.rx.recv() {
+ input.write(&[x]).await.ok();
+ }
+
+ self.internal_cleanup()?;
+ }
+ } else {
+ return Err(AppError::Terminal);
+ }
+ }
+ Ok(())
+ }
+
+ // This is the fix for key pressed not being handled correctly on quit
+ // It writes a special message to the stdout, and then listens out for a valid response
+ // afterwhich it's assumes that we're completely done with TTY
+ fn internal_cleanup(&self) -> Result<(), AppError> {
+ match self {
+ Self::External(_) => Ok(()),
+ Self::Internal(_) => {
+ let waiting = Arc::new(AtomicBool::new(true));
+ let waiting_thread = Arc::clone(&waiting);
+
+ std::thread::spawn(move || {
+ let mut bytes = Vec::with_capacity(26);
+ while waiting_thread.load(std::sync::atomic::Ordering::SeqCst) {
+ let mut buf = [0];
+ if let Ok(mut f) = std::fs::File::open(TTY) {
+ if f.read_exact(&mut buf).is_err() {
+ waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst);
+ }
+ bytes.push(buf[0]);
+ if byte_sequence_valid(&bytes) {
+ waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst);
+ }
+ };
+ }
+ });
+
+ let mut stdout = std::io::stdout();
+ stdout.write_all(KEYBOARD_PROTO.as_bytes()).ok();
+ stdout.flush().ok();
+
+ let start = std::time::Instant::now();
+ while waiting.load(std::sync::atomic::Ordering::SeqCst) {
+ if start.elapsed().as_millis() > 1500 {
+ waiting.store(false, std::sync::atomic::Ordering::SeqCst);
+ return Err(AppError::Terminal);
+ }
+ std::thread::sleep(std::time::Duration::from_millis(10));
+ }
+ Ok(())
+ }
+ }
+ }
+
+ pub async fn run(&self, tty_size: Option) -> Result<(), AppError> {
+ match self {
+ Self::External(id) => {
+ Self::exec_external(id);
+ Ok(())
+ }
+
+ Self::Internal((id, docker)) => self.exec_internal(id, docker, tty_size).await,
+ }
+ }
+}
diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs
index c05164c..d226f67 100644
--- a/src/input_handler/mod.rs
+++ b/src/input_handler/mod.rs
@@ -1,26 +1,32 @@
-use std::sync::{
- atomic::{AtomicBool, Ordering},
- Arc,
+use std::{
+ fs::OpenOptions,
+ io::{BufWriter, Write},
+ sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc,
+ },
+ time::SystemTime,
};
+use bollard::{container::LogsOptions, Docker};
+use cansi::v3::categorise_text;
use crossterm::{
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
execute,
};
+use futures_util::StreamExt;
use parking_lot::Mutex;
use ratatui::layout::Rect;
-use tokio::{
- sync::mpsc::{Receiver, Sender},
- task::JoinHandle,
-};
+use tokio::sync::mpsc::{Receiver, Sender};
+use uuid::Uuid;
mod message;
use crate::{
app_data::{AppData, DockerControls, Header},
app_error::AppError,
docker_data::DockerMessage,
+ exec::{tty_readable, ExecMode},
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
- value_capture,
};
pub use message::InputMessages;
@@ -28,9 +34,8 @@ pub use message::InputMessages;
#[derive(Debug)]
pub struct InputHandler {
app_data: Arc>,
- docker_sender: Sender,
+ docker_tx: Sender,
gui_state: Arc>,
- info_sleep: Option>,
is_running: Arc,
mouse_capture: bool,
rec: Receiver,
@@ -41,18 +46,17 @@ impl InputHandler {
pub async fn init(
app_data: Arc>,
rec: Receiver,
- docker_sender: Sender,
+ docker_tx: Sender,
gui_state: Arc>,
is_running: Arc,
) {
let mut inner = Self {
app_data,
- docker_sender,
+ docker_tx,
gui_state,
is_running,
rec,
mouse_capture: true,
- info_sleep: None,
};
inner.start().await;
}
@@ -85,6 +89,64 @@ impl InputHandler {
}
}
+ /// Sort the containers by a given header
+ fn sort(&self, selected_header: Header) {
+ self.app_data.lock().set_sort_by_header(selected_header);
+ }
+
+ /// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
+ /// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
+ async fn quit(&self) {
+ let error_init = self
+ .gui_state
+ .lock()
+ .status_contains(&[Status::Error, Status::Init]);
+ if error_init || self.docker_tx.send(DockerMessage::Quit).await.is_err() {
+ self.is_running
+ .store(false, std::sync::atomic::Ordering::SeqCst);
+ }
+ }
+
+ /// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container
+ async fn confirm_delete(&self) {
+ let id = self.gui_state.lock().get_delete_container();
+ if let Some(id) = id {
+ self.docker_tx.send(DockerMessage::Delete(id)).await.ok();
+ }
+ }
+
+ /// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel)
+ fn clear_delete(&self) {
+ self.gui_state.lock().set_delete_container(None);
+ }
+
+ /// Validate that one can exec into a Docker container
+ async fn e_key(&self) {
+ let is_oxker = self.app_data.lock().is_oxker();
+ if !is_oxker && tty_readable() {
+ let uuid = Uuid::new_v4();
+ let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
+ let (sx, rx) = tokio::sync::oneshot::channel::>();
+ self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
+
+ if let Ok(docker) = rx.await {
+ (ExecMode::new(&self.app_data, &docker).await).map_or_else(
+ || {
+ self.app_data.lock().set_error(
+ AppError::DockerExec,
+ &self.gui_state,
+ Status::Error,
+ );
+ },
+ |mode| {
+ self.gui_state.lock().set_exec_mode(mode);
+ },
+ );
+ }
+ self.gui_state.lock().stop_loading_animation(&handle, uuid);
+ }
+ }
+
/// Toggle the mouse capture (via input of the 'm' key)
fn m_key(&mut self) {
if self.mouse_capture {
@@ -111,213 +173,258 @@ impl InputHandler {
);
};
- // If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window
- // then cancel the first handle, as a new handle will be invoked
- if let Some(info_sleep_timer) = self.info_sleep.as_ref() {
- info_sleep_timer.abort();
- }
-
- let gui_state = Arc::clone(&self.gui_state);
- // Show the info box - with "mouse capture enabled / disabled", for 4000 ms
- self.info_sleep = Some(tokio::spawn(async move {
- tokio::time::sleep(std::time::Duration::from_millis(4000)).await;
- gui_state.lock().reset_info_box();
- }));
-
self.mouse_capture = !self.mouse_capture;
}
- /// Sort the containers by a given header
- fn sort(&self, selected_header: Header) {
- self.app_data.lock().set_sort_by_header(selected_header);
- }
+ /// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
+ async fn s_key(&mut self) {
+ /// This is the inner workings, *inlined* here to return a Result
+ async fn save_logs(
+ app_data: &Arc>,
+ gui_state: &Arc>,
+ docker_tx: &Sender,
+ ) -> Result<(), Box> {
+ let args = app_data.lock().args.clone();
+ let container = app_data.lock().get_selected_container_id_state_name();
+ if let Some((id, _, name)) = container {
+ if let Some(log_path) = args.save_dir {
+ let (sx, rx) = tokio::sync::oneshot::channel::>();
+ docker_tx.send(DockerMessage::Exec(sx)).await?;
- /// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
- /// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
- async fn quit(&self) {
- let error_init = self
- .gui_state
- .lock()
- .status_contains(&[Status::Error, Status::Init]);
- if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() {
- self.is_running
- .store(false, std::sync::atomic::Ordering::SeqCst);
- }
- }
+ let now = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .map_or(0, |i| i.as_secs());
- /// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container
- async fn confirm_delete(&self) {
- let id = self.gui_state.lock().get_delete_container();
- if let Some(id) = id {
- self.docker_sender
- .send(DockerMessage::Delete(id))
- .await
- .ok();
- }
- }
+ let path = log_path.join(format!("{name}_{now}.log"));
- /// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel)
- fn clear_delete(&self) {
- self.gui_state.lock().set_delete_container(None);
- }
+ let docker = rx.await?;
+ let options = Some(LogsOptions:: {
+ stdout: true,
+ timestamps: args.timestamp,
+ since: 0,
+ ..Default::default()
+ });
+ let mut logs = docker.logs(id.get(), options);
+ let mut output = vec![];
- /// Handle any keyboard button events
- #[allow(clippy::too_many_lines)]
- async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
- value_capture!(
- contains_delete,
- self.gui_state
- .lock()
- .status_contains(&[Status::DeleteConfirm])
- );
-
- value_capture!(
- contains_error,
- self.gui_state.lock().status_contains(&[Status::Error])
- );
- value_capture!(
- contains_help,
- self.gui_state.lock().status_contains(&[Status::Help])
- );
-
- // Always just quit on Ctrl + c/C or q/Q
- let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
- let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
- if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() {
- self.quit().await;
- }
-
- if contains_error {
- if let KeyCode::Char('c' | 'C') = key_code {
- self.app_data.lock().remove_error();
- self.gui_state.lock().status_del(Status::Error);
- }
- } else if contains_help {
- match key_code {
- KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
- KeyCode::Char('m' | 'M') => self.m_key(),
- _ => (),
- }
- } else if contains_delete {
- match key_code {
- KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
- KeyCode::Char('n' | 'N') => self.clear_delete(),
- _ => (),
- }
- } else {
- match key_code {
- KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
- KeyCode::Char('1') => self.sort(Header::State),
- KeyCode::Char('2') => self.sort(Header::Status),
- KeyCode::Char('3') => self.sort(Header::Cpu),
- KeyCode::Char('4') => self.sort(Header::Memory),
- KeyCode::Char('5') => self.sort(Header::Id),
- KeyCode::Char('6') => self.sort(Header::Name),
- KeyCode::Char('7') => self.sort(Header::Image),
- KeyCode::Char('8') => self.sort(Header::Rx),
- KeyCode::Char('9') => self.sort(Header::Tx),
- KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
- KeyCode::Char('m' | 'M') => self.m_key(),
- KeyCode::Tab => {
- // Skip control panel if no containers, could be refactored
- let is_containers =
- self.gui_state.lock().selected_panel == SelectablePanel::Containers;
- let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
- 2
- } else {
- 1
- };
- for _ in 0..count {
- self.gui_state.lock().next_panel();
- }
- }
- KeyCode::BackTab => {
- // Skip control panel if no containers, could be refactored
- let is_containers =
- self.gui_state.lock().selected_panel == SelectablePanel::Logs;
- let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
- 2
- } else {
- 1
- };
- for _ in 0..count {
- self.gui_state.lock().previous_panel();
- }
- }
- KeyCode::Home => {
- let mut locked_data = self.app_data.lock();
- let selected_panel = self.gui_state.lock().selected_panel;
- match selected_panel {
- SelectablePanel::Containers => locked_data.containers_start(),
- SelectablePanel::Logs => locked_data.log_start(),
- SelectablePanel::Commands => locked_data.docker_command_start(),
- }
- }
- KeyCode::End => {
- let mut locked_data = self.app_data.lock();
- let selected_panel = self.gui_state.lock().selected_panel;
- match selected_panel {
- SelectablePanel::Containers => locked_data.containers_end(),
- SelectablePanel::Logs => locked_data.log_end(),
- SelectablePanel::Commands => locked_data.docker_command_end(),
- }
- }
- KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
- KeyCode::PageUp => {
- for _ in 0..=6 {
- self.previous();
- }
- }
- KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
- KeyCode::PageDown => {
- for _ in 0..=6 {
- self.next();
- }
- }
- KeyCode::Enter => {
- // This isn't great, just means you can't send docker commands before full initialization of the program
- let panel = self.gui_state.lock().selected_panel;
- if panel == SelectablePanel::Commands {
- let option_command = self.app_data.lock().selected_docker_command();
-
- if let Some(command) = option_command {
- let option_id = self.app_data.lock().get_selected_container_id();
- // Poor way of disallowing commands to be sent to a containerised okxer
- if self.app_data.lock().is_oxker() {
- return;
- };
- if let Some(id) = option_id {
- match command {
- DockerControls::Delete => self
- .docker_sender
- .send(DockerMessage::ConfirmDelete(id))
- .await
- .ok(),
- DockerControls::Pause => {
- self.docker_sender.send(DockerMessage::Pause(id)).await.ok()
- }
- DockerControls::Unpause => self
- .docker_sender
- .send(DockerMessage::Unpause(id))
- .await
- .ok(),
- DockerControls::Start => {
- self.docker_sender.send(DockerMessage::Start(id)).await.ok()
- }
- DockerControls::Stop => {
- self.docker_sender.send(DockerMessage::Stop(id)).await.ok()
- }
- DockerControls::Restart => self
- .docker_sender
- .send(DockerMessage::Restart(id))
- .await
- .ok(),
- };
- }
+ while let Some(Ok(value)) = logs.next().await {
+ let data = value.to_string();
+ if !data.trim().is_empty() {
+ output.push(
+ categorise_text(&data)
+ .into_iter()
+ .map(|i| i.text)
+ .collect::(),
+ );
}
}
+ if !output.is_empty() {
+ let mut stream = BufWriter::new(
+ OpenOptions::new()
+ .read(true)
+ .write(true)
+ .create(true)
+ .open(&path)?,
+ );
+
+ for line in &output {
+ stream.write_all(line.as_bytes())?;
+ }
+ stream.flush()?;
+
+ gui_state
+ .lock()
+ .set_info_box(&format!("saved to {}", path.display()));
+ }
+ }
+ }
+ Ok(())
+ }
+
+ let log_status = Status::Logs;
+ let status = self.gui_state.lock().status_contains(&[log_status]);
+ if !status {
+ self.gui_state.lock().status_push(log_status);
+
+ let uuid = Uuid::new_v4();
+ let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
+ if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
+ .await
+ .is_err()
+ {
+ self.app_data.lock().set_error(
+ AppError::DockerLogs,
+ &self.gui_state,
+ Status::Error,
+ );
+ }
+ self.gui_state.lock().status_del(log_status);
+ self.gui_state.lock().stop_loading_animation(&handle, uuid);
+ }
+ }
+
+ /// Send docker command, if the Commands panel is selected
+ async fn enter_key(&mut self) {
+ // This isn't great, just means you can't send docker commands before full initialization of the program
+ let panel = self.gui_state.lock().get_selected_panel();
+ if panel == SelectablePanel::Commands {
+ let option_command = self.app_data.lock().selected_docker_command();
+
+ 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() {
+ return;
+ };
+ let option_id = self.app_data.lock().get_selected_container_id();
+ if let Some(id) = option_id {
+ match command {
+ DockerControls::Delete => self
+ .docker_tx
+ .send(DockerMessage::ConfirmDelete(id))
+ .await
+ .ok(),
+ DockerControls::Pause => {
+ self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
+ }
+ DockerControls::Unpause => {
+ self.docker_tx.send(DockerMessage::Unpause(id)).await.ok()
+ }
+ DockerControls::Start => {
+ self.docker_tx.send(DockerMessage::Start(id)).await.ok()
+ }
+ DockerControls::Stop => {
+ self.docker_tx.send(DockerMessage::Stop(id)).await.ok()
+ }
+ DockerControls::Restart => {
+ self.docker_tx.send(DockerMessage::Restart(id)).await.ok()
+ }
+ };
+ }
+ }
+ }
+ }
+
+ /// Change the the "next" seletable panel
+ fn tab_key(&mut self) {
+ let is_containers =
+ self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
+ let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
+ 2
+ } else {
+ 1
+ };
+ for _ in 0..count {
+ self.gui_state.lock().next_panel();
+ }
+ }
+
+ /// Change to previously selected panel
+ fn back_tab_key(&mut self) {
+ let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
+ let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
+ 2
+ } else {
+ 1
+ };
+ for _ in 0..count {
+ self.gui_state.lock().previous_panel();
+ }
+ }
+
+ fn home_key(&mut self) {
+ let mut locked_data = self.app_data.lock();
+ let selected_panel = self.gui_state.lock().get_selected_panel();
+ match selected_panel {
+ SelectablePanel::Containers => locked_data.containers_start(),
+ SelectablePanel::Logs => locked_data.log_start(),
+ SelectablePanel::Commands => locked_data.docker_command_start(),
+ }
+ }
+
+ /// Go to end of the list of the currently selected panel
+ fn end_key(&mut self) {
+ let mut locked_data = self.app_data.lock();
+ let selected_panel = self.gui_state.lock().get_selected_panel();
+ match selected_panel {
+ SelectablePanel::Containers => locked_data.containers_end(),
+ SelectablePanel::Logs => locked_data.log_end(),
+ SelectablePanel::Commands => locked_data.docker_command_end(),
+ }
+ }
+
+ /// Handle keyboard button events
+ async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
+ let contains_delete = self
+ .gui_state
+ .lock()
+ .status_contains(&[Status::DeleteConfirm]);
+
+ let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
+
+ let contains_error = contains(Status::Error);
+ let contains_help = contains(Status::Help);
+ let contains_exec = contains(Status::Exec);
+
+ if !contains_exec {
+ // Always just quit on Ctrl + c/C or q/Q
+ let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
+ let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
+ if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() {
+ self.quit().await;
+ }
+
+ if contains_error {
+ if let KeyCode::Char('c' | 'C') = key_code {
+ self.app_data.lock().remove_error();
+ self.gui_state.lock().status_del(Status::Error);
+ }
+ } else if contains_help {
+ match key_code {
+ KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
+ KeyCode::Char('m' | 'M') => self.m_key(),
+ _ => (),
+ }
+ } else if contains_delete {
+ match key_code {
+ KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
+ KeyCode::Char('n' | 'N') => self.clear_delete(),
+ _ => (),
+ }
+ } else {
+ match key_code {
+ KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
+ KeyCode::Char('1') => self.sort(Header::State),
+ KeyCode::Char('2') => self.sort(Header::Status),
+ KeyCode::Char('3') => self.sort(Header::Cpu),
+ KeyCode::Char('4') => self.sort(Header::Memory),
+ KeyCode::Char('5') => self.sort(Header::Id),
+ KeyCode::Char('6') => self.sort(Header::Name),
+ KeyCode::Char('7') => self.sort(Header::Image),
+ KeyCode::Char('8') => self.sort(Header::Rx),
+ KeyCode::Char('9') => self.sort(Header::Tx),
+ KeyCode::Char('e' | 'E') => self.e_key().await,
+ KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
+ KeyCode::Char('m' | 'M') => self.m_key(),
+ KeyCode::Char('s' | 'S') => self.s_key().await,
+ KeyCode::Tab => self.tab_key(),
+ KeyCode::BackTab => self.back_tab_key(),
+ KeyCode::Home => self.home_key(),
+ KeyCode::End => self.end_key(),
+ KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
+ KeyCode::PageUp => {
+ for _ in 0..=6 {
+ self.previous();
+ }
+ }
+ KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
+ KeyCode::PageDown => {
+ for _ in 0..=6 {
+ self.next();
+ }
+ }
+ KeyCode::Enter => self.enter_key().await,
+ _ => (),
}
- _ => (),
}
}
}
@@ -370,7 +477,7 @@ impl InputHandler {
/// Change state to next, depending which panel is currently in focus
fn next(&mut self) {
let mut locked_data = self.app_data.lock();
- let selected_panel = self.gui_state.lock().selected_panel;
+ let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => locked_data.containers_next(),
SelectablePanel::Logs => locked_data.log_next(),
@@ -381,7 +488,7 @@ impl InputHandler {
/// Change state to previous, depending which panel is currently in focus
fn previous(&mut self) {
let mut locked_data = self.app_data.lock();
- let selected_panel = self.gui_state.lock().selected_panel;
+ let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel {
SelectablePanel::Containers => locked_data.containers_previous(),
SelectablePanel::Logs => locked_data.log_previous(),
diff --git a/src/main.rs b/src/main.rs
index ef3e279..d976325 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,17 +1,3 @@
-#![forbid(unsafe_code)]
-#![warn(
- clippy::expect_used,
- clippy::nursery,
- clippy::pedantic,
- clippy::todo,
- clippy::unused_async,
- clippy::unwrap_used
-)]
-#![allow(
- clippy::module_name_repetitions,
- clippy::doc_markdown,
- clippy::similar_names
-)]
// Only allow when debugging
// #![allow(unused)]
@@ -35,6 +21,7 @@ use tracing::{error, info, Level};
mod app_data;
mod app_error;
mod docker_data;
+mod exec;
mod input_handler;
mod parse_args;
mod ui;
@@ -55,18 +42,6 @@ fn setup_tracing() {
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
}
-/// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately
-/// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container
-fn check_if_containerised() -> bool {
- if let Ok(value) = std::env::var(ENV_KEY) {
- if value == ENV_VALUE {
- std::thread::sleep(std::time::Duration::from_millis(250));
- return true;
- }
- }
- false
-}
-
/// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env
fn read_docker_host(args: &CliArgs) -> Option {
args.host
@@ -77,8 +52,8 @@ fn read_docker_host(args: &CliArgs) -> Option {
/// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error
async fn docker_init(
app_data: &Arc>,
- containerised: bool,
docker_rx: Receiver,
+ docker_tx: Sender,
gui_state: &Arc>,
is_running: &Arc,
host: Option,
@@ -92,13 +67,9 @@ async fn docker_init(
let app_data = Arc::clone(app_data);
let gui_state = Arc::clone(gui_state);
let is_running = Arc::clone(is_running);
+
tokio::spawn(DockerData::init(
- app_data,
- containerised,
- docker,
- docker_rx,
- gui_state,
- is_running,
+ app_data, docker, docker_rx, docker_tx, gui_state, is_running,
));
} else {
app_data
@@ -120,36 +91,40 @@ fn handler_init(
input_rx: Receiver,
is_running: &Arc,
) {
- let input_app_data = Arc::clone(app_data);
- let input_gui_state = Arc::clone(gui_state);
- let input_is_running = Arc::clone(is_running);
+ let app_data = Arc::clone(app_data);
+ let gui_state = Arc::clone(gui_state);
+ let is_running = Arc::clone(is_running);
tokio::spawn(input_handler::InputHandler::init(
- input_app_data,
+ app_data,
input_rx,
docker_sx.clone(),
- input_gui_state,
- input_is_running,
+ gui_state,
+ is_running,
));
}
#[tokio::main]
async fn main() {
- let containerised = check_if_containerised();
-
setup_tracing();
let args = CliArgs::new();
+
+ // If running via Docker image, need to sleep else program will just quit straight away, no real idea why
+ // So just sleep for small while
+ if args.in_container {
+ std::thread::sleep(std::time::Duration::from_millis(250));
+ }
let host = read_docker_host(&args);
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
let gui_state = Arc::new(Mutex::new(GuiState::default()));
let is_running = Arc::new(AtomicBool::new(true));
- let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32);
+ let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
docker_init(
&app_data,
- containerised,
docker_rx,
+ docker_tx.clone(),
&gui_state,
&is_running,
host,
@@ -157,23 +132,34 @@ async fn main() {
.await;
if args.gui {
- let (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
- handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running);
- Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await;
+ let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
+ handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
+ Ui::create(app_data, gui_state, input_tx, is_running).await;
} else {
- info!("in debug mode");
- // Debug mode for testing, mostly pointless, doesn't take terminal
+ info!("in debug mode\n");
+ // Debug mode for testing, less pointless now, will display some basic information
while is_running.load(Ordering::SeqCst) {
- loop {
- if let Some(err) = app_data.lock().get_error() {
- error!("{}", err);
- process::exit(1);
+ if let Some(err) = app_data.lock().get_error() {
+ error!("{}", err);
+ process::exit(1);
+ }
+ tokio::time::sleep(std::time::Duration::from_millis(u64::from(
+ args.docker_interval,
+ )))
+ .await;
+ let containers = app_data
+ .lock()
+ .get_container_items()
+ .clone()
+ .iter()
+ .map(|i| format!("{i}"))
+ .collect::>();
+
+ if !containers.is_empty() {
+ for item in containers {
+ info!("{item}");
}
- docker_sx.send(DockerMessage::Update).await.ok();
- tokio::time::sleep(std::time::Duration::from_millis(u64::from(
- args.docker_interval,
- )))
- .await;
+ println!();
}
}
}
diff --git a/src/parse_args.rs b/src/parse_args.rs
index 7a69053..e29d84c 100644
--- a/src/parse_args.rs
+++ b/src/parse_args.rs
@@ -1,12 +1,14 @@
-use std::process;
+use std::{path::PathBuf, process};
use clap::Parser;
use tracing::error;
+use crate::{ENV_KEY, ENV_VALUE};
+
#[derive(Parser, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
#[command(version, about)]
-pub struct CliArgs {
+pub struct Args {
/// Docker update interval in ms, minimum effectively 1000
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
pub docker_interval: u32,
@@ -19,10 +21,6 @@ pub struct CliArgs {
#[clap(short = 'c', conflicts_with = "raw")]
pub color: bool,
- /// Docker host, defaults to `/var/run/docker.sock`
- #[clap(long, short = None)]
- pub host: Option,
-
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
#[clap(short = 'r', conflicts_with = "color")]
pub raw: bool,
@@ -34,12 +32,55 @@ pub struct CliArgs {
/// Don't draw gui - for debugging - mostly pointless
#[clap(short = 'g')]
pub gui: bool,
+
+ /// Docker host, defaults to `/var/run/docker.sock`
+ #[clap(long, short = None)]
+ pub host: Option,
+
+ /// Force use of docker cli when execing into containers
+ #[clap(long="use-cli", short = None)]
+ pub use_cli: bool,
+
+ /// Directory for saving exported logs, defaults to `$HOME`
+ #[clap(long="save-dir", short = None)]
+ pub save_dir: Option,
+}
+
+#[derive(Debug, Clone)]
+#[allow(clippy::struct_excessive_bools)]
+pub struct CliArgs {
+ pub color: bool,
+ pub docker_interval: u32,
+ pub gui: bool,
+ pub host: Option,
+ pub in_container: bool,
+ pub save_dir: Option,
+ pub raw: bool,
+ pub show_self: bool,
+ pub timestamp: bool,
+ pub use_cli: bool,
}
impl CliArgs {
+ /// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately
+ /// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container
+ fn check_if_in_container() -> bool {
+ if let Ok(value) = std::env::var(ENV_KEY) {
+ if value == ENV_VALUE {
+ return true;
+ }
+ }
+ false
+ }
+
/// Parse cli arguments
pub fn new() -> Self {
- let args = Self::parse();
+ let args = Args::parse();
+
+ let logs_dir = args.save_dir.map_or_else(
+ || directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned()),
+ |logs_dir| Some(std::path::Path::new(&logs_dir).to_owned()),
+ );
// Quit the program if the docker update argument is 0
// Should maybe change it to check if less than 100
@@ -50,10 +91,13 @@ impl CliArgs {
Self {
color: args.color,
docker_interval: args.docker_interval,
- host: args.host,
+ use_cli: args.use_cli,
gui: !args.gui,
- show_self: !args.show_self,
+ host: args.host,
+ in_container: Self::check_if_in_container(),
+ save_dir: logs_dir,
raw: args.raw,
+ show_self: !args.show_self,
timestamp: !args.timestamp,
}
}
diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs
index 4080966..b38be52 100644
--- a/src/ui/draw_blocks.rs
+++ b/src/ui/draw_blocks.rs
@@ -1,6 +1,5 @@
use parking_lot::Mutex;
use ratatui::{
- backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
@@ -11,17 +10,19 @@ use ratatui::{
},
Frame,
};
-use std::default::Default;
+use std::{default::Default, time::Instant};
use std::{fmt::Display, sync::Arc};
-use crate::app_data::{Header, SortedOrder};
-use crate::ui::Status;
+use crate::app_data::{ContainerItem, Header, SortedOrder};
use crate::{
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
app_error::AppError,
};
-use super::gui_state::{BoxLocation, DeleteButton, Region};
+use super::{
+ gui_state::{BoxLocation, DeleteButton, Region},
+ FrameData,
+};
use super::{GuiState, SelectablePanel};
const NAME_TEXT: &str = r#"
@@ -40,7 +41,7 @@ const REPO: &str = env!("CARGO_PKG_REPOSITORY");
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
const ORANGE: Color = Color::Rgb(255, 178, 36);
const MARGIN: &str = " ";
-const ARROW: &str = "▶ ";
+const RIGHT_ARROW: &str = "▶ ";
const CIRCLE: &str = "⚪ ";
/// From a given &str, return the maximum number of chars on a single line
@@ -56,6 +57,7 @@ fn max_line_width(text: &str) -> usize {
fn generate_block<'a>(
app_data: &Arc>,
area: Rect,
+ fd: &FrameData,
gui_state: &Arc>,
panel: SelectablePanel,
) -> Block<'a> {
@@ -78,20 +80,21 @@ fn generate_block<'a>(
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title);
- if gui_state.lock().selected_panel == panel {
+ if fd.selected_panel == panel {
block = block.border_style(Style::default().fg(Color::LightCyan));
}
block
}
/// Draw the command panel
-pub fn commands(
+pub fn commands(
app_data: &Arc>,
area: Rect,
- f: &mut Frame<'_, B>,
+ f: &mut Frame,
+ fd: &FrameData,
gui_state: &Arc>,
) {
- let block = || generate_block(app_data, area, gui_state, SelectablePanel::Commands);
+ let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands);
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
i.iter()
.map(|c| {
@@ -107,7 +110,7 @@ pub fn commands(
let items = List::new(items)
.block(block())
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
- .highlight_symbol(ARROW);
+ .highlight_symbol(RIGHT_ARROW);
if let Some(i) = app_data.lock().get_control_state() {
f.render_stateful_widget(items, area, i);
@@ -119,88 +122,91 @@ pub fn commands(
}
}
+/// Format the container data to display nicely on the screen
+fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
+ let state_style = Style::default().fg(i.state.get_color());
+ let blue = Style::default().fg(Color::Blue);
+
+ Line::from(vec![
+ Span::styled(
+ format!(
+ "{:width$}",
+ i.status,
+ width = &widths.status.1.into()
+ ),
+ state_style,
+ ),
+ Span::styled(
+ format!(
+ "{}{:>width$}",
+ MARGIN,
+ i.cpu_stats.back().unwrap_or(&CpuStats::default()),
+ width = &widths.cpu.1.into()
+ ),
+ state_style,
+ ),
+ Span::styled(
+ format!(
+ "{MARGIN}{:>width_current$} / {:>width_limit$}",
+ i.mem_stats.back().unwrap_or(&ByteStats::default()),
+ i.mem_limit,
+ width_current = &widths.mem.1.into(),
+ width_limit = &widths.mem.2.into()
+ ),
+ state_style,
+ ),
+ Span::styled(
+ format!(
+ "{}{:>width$}",
+ MARGIN,
+ i.id.get_short(),
+ width = &widths.id.1.into()
+ ),
+ blue,
+ ),
+ Span::styled(
+ format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()),
+ blue,
+ ),
+ Span::styled(
+ format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()),
+ blue,
+ ),
+ Span::styled(
+ format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()),
+ Style::default().fg(Color::Rgb(255, 233, 193)),
+ ),
+ Span::styled(
+ format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
+ Style::default().fg(Color::Rgb(205, 140, 140)),
+ ),
+ ])
+}
+
/// Draw the containers panel
-pub fn containers(
+pub fn containers(
app_data: &Arc>,
area: Rect,
- f: &mut Frame<'_, B>,
+ f: &mut Frame,
+ fd: &FrameData,
gui_state: &Arc>,
widths: &Columns,
) {
- let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
+ let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
let items = app_data
.lock()
.get_container_items()
.iter()
- .map(|i| {
- let state_style = Style::default().fg(i.state.get_color());
- let blue = Style::default().fg(Color::Blue);
-
- let lines = Line::from(vec![
- Span::styled(
- format!(
- "{:width$}",
- i.status,
- width = &widths.status.1.into()
- ),
- state_style,
- ),
- Span::styled(
- format!(
- "{}{:>width$}",
- MARGIN,
- i.cpu_stats.back().unwrap_or(&CpuStats::default()),
- width = &widths.cpu.1.into()
- ),
- state_style,
- ),
- Span::styled(
- format!(
- "{MARGIN}{:>width_current$} / {:>width_limit$}",
- i.mem_stats.back().unwrap_or(&ByteStats::default()),
- i.mem_limit,
- width_current = &widths.mem.1.into(),
- width_limit = &widths.mem.2.into()
- ),
- state_style,
- ),
- Span::styled(
- format!(
- "{}{:>width$}",
- MARGIN,
- i.id.get().chars().take(8).collect::(),
- width = &widths.id.1.into()
- ),
- blue,
- ),
- Span::styled(
- format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()),
- blue,
- ),
- Span::styled(
- format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()),
- blue,
- ),
- Span::styled(
- format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()),
- Style::default().fg(Color::Rgb(255, 233, 193)),
- ),
- Span::styled(
- format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
- Style::default().fg(Color::Rgb(205, 140, 140)),
- ),
- ]);
- ListItem::new(lines)
- })
+ .map(|i| ListItem::new(format_containers(i, widths)))
.collect::>();
if items.is_empty() {
@@ -213,22 +219,21 @@ pub fn containers(
.block(block)
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol(CIRCLE);
-
f.render_stateful_widget(items, area, app_data.lock().get_container_state());
}
}
/// Draw the logs panel
-pub fn logs(
+pub fn logs(
app_data: &Arc>,
area: Rect,
- f: &mut Frame<'_, B>,
+ f: &mut Frame,
+ fd: &FrameData,
gui_state: &Arc>,
- loading_icon: &str,
) {
- let block = || generate_block(app_data, area, gui_state, SelectablePanel::Logs);
- if gui_state.lock().status_contains(&[Status::Init]) {
- let paragraph = Paragraph::new(format!("parsing logs {loading_icon}"))
+ let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs);
+ if fd.init {
+ let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
.style(Style::default())
.block(block())
.alignment(Alignment::Center);
@@ -244,19 +249,18 @@ pub fn logs(
} else {
let items = List::new(logs)
.block(block())
- .highlight_symbol(ARROW)
+ .highlight_symbol(RIGHT_ARROW)
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
-
// This should always return Some, as logs is not empty
- if let Some(i) = app_data.lock().get_log_state() {
- f.render_stateful_widget(items, area, i);
+ if let Some(log_state) = app_data.lock().get_log_state() {
+ f.render_stateful_widget(items, area, log_state);
}
}
}
}
/// Draw the cpu + mem charts
-pub fn chart(f: &mut Frame<'_, B>, area: Rect, app_data: &Arc>) {
+pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) {
if let Some((cpu, mem)) = app_data.lock().get_chart_data() {
let area = Layout::default()
.direction(Direction::Horizontal)
@@ -275,6 +279,7 @@ pub fn chart(f: &mut Frame<'_, B>, area: Rect, app_data: &Arc(
/// Draw heading bar at top of program, always visible
/// TODO Should separate into loading icon/headers/help functions
#[allow(clippy::too_many_lines)]
-pub fn heading_bar(
+pub fn heading_bar(
area: Rect,
- columns: &Columns,
- f: &mut Frame<'_, B>,
- has_containers: bool,
- loading_icon: &str,
- sorted_by: Option<(Header, SortedOrder)>,
+ frame: &mut Frame,
+ data: &FrameData,
gui_state: &Arc>,
) {
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
- let help_visible = gui_state.lock().status_contains(&[Status::Help]);
- f.render_widget(block(Color::Black), area);
+ frame.render_widget(block(Color::Black), area);
// 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;
- if let Some((a, b)) = sorted_by.as_ref() {
+ if let Some((a, b)) = data.sorted_by.as_ref() {
if x == a {
match b {
- SortedOrder::Asc => suffix = " ⌃",
- SortedOrder::Desc => suffix = " ⌄",
+ SortedOrder::Asc => suffix = " ▲",
+ SortedOrder::Desc => suffix = " ▼",
}
suffix_margin = 2;
color = Color::White;
@@ -408,15 +409,15 @@ pub fn heading_bar(
// Meta data to iterate over to create blocks with correct widths
let header_meta = [
- (Header::State, columns.state.1),
- (Header::Status, columns.status.1),
- (Header::Cpu, columns.cpu.1),
- (Header::Memory, columns.mem.1 + columns.mem.2 + 3),
- (Header::Id, columns.id.1),
- (Header::Name, columns.name.1),
- (Header::Image, columns.image.1),
- (Header::Rx, columns.net_rx.1),
- (Header::Tx, columns.net_tx.1),
+ (Header::State, data.columns.state.1),
+ (Header::Status, data.columns.status.1),
+ (Header::Cpu, data.columns.cpu.1),
+ (Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3),
+ (Header::Id, data.columns.id.1),
+ (Header::Name, data.columns.name.1),
+ (Header::Image, data.columns.image.1),
+ (Header::Rx, data.columns.net_rx.1),
+ (Header::Tx, data.columns.net_tx.1),
];
let header_data = header_meta
@@ -427,13 +428,13 @@ pub fn heading_bar(
})
.collect::>();
- let suffix = if help_visible { "exit" } else { "show" };
+ let suffix = if data.help_visible { "exit" } else { "show" };
let info_text = format!("( h ) {suffix} help {MARGIN}",);
let info_width = info_text.chars().count();
let column_width = usize::from(area.width).saturating_sub(info_width);
let column_width = if column_width > 0 { column_width } else { 1 };
- let splits = if has_containers {
+ let splits = if data.has_containers {
vec![
Constraint::Min(2),
Constraint::Min(column_width.try_into().unwrap_or_default()),
@@ -445,20 +446,19 @@ pub fn heading_bar(
let split_bar = Layout::default()
.direction(Direction::Horizontal)
- .constraints(splits.as_ref())
+ .constraints(splits)
.split(area);
- if has_containers {
+ if data.has_containers {
// Draw loading icon, or not, and a prefix with a single space
- let loading_icon = format!("{loading_icon:>2}");
- let loading_paragraph = Paragraph::new(loading_icon)
+ let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
.block(block(Color::White))
.alignment(Alignment::Center);
- f.render_widget(loading_paragraph, split_bar[0]);
+ frame.render_widget(loading_paragraph, split_bar[0]);
let container_splits = header_data.iter().map(|i| i.2).collect::>();
let headers_section = Layout::default()
.direction(Direction::Horizontal)
- .constraints(container_splits.as_ref())
+ .constraints(container_splits)
.split(split_bar[1]);
// draw the actual header blocks
@@ -467,12 +467,12 @@ pub fn heading_bar(
gui_state
.lock()
.update_region_map(Region::Header(header), rect);
- f.render_widget(paragraph, rect);
+ frame.render_widget(paragraph, rect);
}
}
// show/hide help
- let color = if help_visible {
+ let color = if data.help_visible {
Color::Black
} else {
Color::White
@@ -482,8 +482,8 @@ pub fn heading_bar(
.alignment(Alignment::Right);
// If no containers, don't display the headers, could maybe do this first?
- let help_index = if has_containers { 2 } else { 0 };
- f.render_widget(help_paragraph, split_bar[help_index]);
+ let help_index = if data.has_containers { 2 } else { 0 };
+ frame.render_widget(help_paragraph, split_bar[help_index]);
}
/// Help popup box needs these three pieces of information
@@ -569,7 +569,7 @@ impl HelpInfo {
button_item("tab"),
or(),
button_item("shift+tab"),
- button_desc("to change panels"),
+ button_desc("change panels"),
]),
Line::from(vec![
space(),
@@ -580,35 +580,45 @@ impl HelpInfo {
button_item("PgUp PgDown"),
or(),
button_item("Home End"),
- button_desc("to change selected line"),
+ button_desc("change selected line"),
]),
Line::from(vec![
space(),
button_item("enter"),
- button_desc("to send docker container command"),
+ button_desc("send docker container command"),
+ ]),
+ Line::from(vec![
+ space(),
+ button_item("e"),
+ button_desc("exec into a container"),
]),
Line::from(vec![
space(),
button_item("h"),
- button_desc("to toggle this help information"),
+ button_desc("toggle this help information"),
]),
- Line::from(vec![space(), button_item("0"), button_desc("to stop sort")]),
+ Line::from(vec![
+ space(),
+ button_item("s"),
+ button_desc("save logs to file"),
+ ]),
+ Line::from(vec![
+ space(),
+ button_item("m"),
+ button_desc(
+ "toggle mouse capture - if disabled, text on screen can be selected & copied",
+ ),
+ ]),
+ Line::from(vec![space(), button_item("0"), button_desc("stop sort")]),
Line::from(vec![
space(),
button_item("1 - 9"),
button_desc("sort by header - or click header"),
]),
- Line::from(vec![
- space(),
- button_item("m"),
- button_desc(
- "to toggle mouse capture - if disabled, text on screen can be selected & copied",
- ),
- ]),
Line::from(vec![
space(),
button_item("q"),
- button_desc("to quit at any time"),
+ button_desc("quit at any time"),
]),
];
@@ -646,7 +656,7 @@ impl HelpInfo {
}
/// Draw the help box in the centre of the screen
-pub fn help_box(f: &mut Frame<'_, B>) {
+pub fn help_box(f: &mut Frame) {
let title = format!(" {VERSION} ");
let name_info = HelpInfo::gen_name();
@@ -725,11 +735,7 @@ pub fn help_box(f: &mut Frame<'_, B>) {
/// Draw the delete confirm box in the centre of the screen
/// take in container id and container name here?
-pub fn delete_confirm(
- f: &mut Frame<'_, B>,
- gui_state: &Arc>,
- name: &str,
-) {
+pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &str) {
let block = Block::default()
.title(" Confirm Delete ")
.border_type(BorderType::Rounded)
@@ -834,7 +840,7 @@ pub fn delete_confirm(
}
/// Draw an error popup over whole screen
-pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option) {
+pub fn error(f: &mut Frame, error: AppError, seconds: Option) {
let block = Block::default()
.title(" Error ")
.border_type(BorderType::Rounded)
@@ -850,7 +856,7 @@ pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option<
seconds.unwrap_or(5)
)
}
- _ => String::from("\n\n ( c ) to clear error\n ( q ) to quit oxker"),
+ _ => String::from("\n\n ( c ) clear error\n ( q ) quit oxker "),
};
let mut text = format!("\n{error}");
@@ -876,13 +882,13 @@ pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option<
}
/// Draw info box in one of the 9 BoxLocations
-pub fn info(f: &mut Frame<'_, B>, text: String) {
+pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc>) {
let block = Block::default()
.title("")
.title_alignment(Alignment::Center)
.borders(Borders::NONE);
- let mut max_line_width = max_line_width(&text);
+ let mut max_line_width = max_line_width(text);
let mut lines = text.lines().count();
// Add some horizontal & vertical margins
@@ -897,6 +903,9 @@ pub fn info(f: &mut Frame<'_, B>, text: String) {
let area = popup(lines, max_line_width, f.size(), BoxLocation::BottomRight);
f.render_widget(Clear, area);
f.render_widget(paragraph, area);
+ if instant.elapsed().as_millis() > 4000 {
+ gui_state.lock().reset_info_box();
+ }
}
/// draw a box in the one of the BoxLocations, based on max line width + number of lines
@@ -927,8 +936,18 @@ 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);
+}
+
// Draw nothing, as in a blank screen
-// pub fn nothing(f: &mut Frame<'_, B>) {
+// pub fn nothing(f: &mut Frame) {
// let whole_layout = Layout::default()
// .direction(Direction::Vertical)
// .constraints([Constraint::Min(100)].as_ref())
diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs
index 18e92f5..fa2d229 100644
--- a/src/ui/gui_state.rs
+++ b/src/ui/gui_state.rs
@@ -1,8 +1,17 @@
+use parking_lot::Mutex;
use ratatui::layout::{Constraint, Rect};
-use std::collections::{HashMap, HashSet};
+use std::{
+ collections::{HashMap, HashSet},
+ sync::Arc,
+ time::Instant,
+};
+use tokio::task::JoinHandle;
use uuid::Uuid;
-use crate::app_data::{ContainerId, Header};
+use crate::{
+ app_data::{ContainerId, Header},
+ exec::ExecMode,
+};
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
pub enum SelectablePanel {
@@ -150,25 +159,28 @@ const FRAMES_LEN: u8 = 9;
/// Various functions (e.g input handler), operate differently depending upon current Status
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Status {
- Init,
- Help,
- DockerConnect,
DeleteConfirm,
+ DockerConnect,
Error,
+ Exec,
+ Help,
+ Init,
+ Logs,
}
/// Global gui_state, stored in an Arc
#[derive(Debug, Default, Clone)]
pub struct GuiState {
+ delete_container: Option,
+ delete_map: HashMap,
heading_map: HashMap,
is_loading: HashSet,
loading_index: u8,
panel_map: HashMap,
- delete_map: HashMap,
+ selected_panel: SelectablePanel,
status: HashSet,
- delete_container: Option,
- pub info_box_text: Option,
- pub selected_panel: SelectablePanel,
+ exec_mode: Option,
+ pub info_box_text: Option<(String, Instant)>,
}
impl GuiState {
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
@@ -176,6 +188,11 @@ impl GuiState {
self.panel_map.clear();
}
+ /// Get the currently selected panel
+ pub const fn get_selected_panel(&self) -> SelectablePanel {
+ self.selected_panel
+ }
+
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
pub fn panel_intersect(&mut self, rect: Rect) {
if let Some(data) = self
@@ -254,16 +271,41 @@ impl GuiState {
}
/// Remove a gui_status into the current gui_status HashSet
+ /// Remove exec mode & deleteConfirm is required
pub fn status_del(&mut self, status: Status) {
self.status.remove(&status);
- if status == Status::DeleteConfirm {
- self.status.remove(&Status::DeleteConfirm);
+ match status {
+ Status::DeleteConfirm => {
+ self.status.remove(&Status::DeleteConfirm);
+ }
+ Status::Exec => {
+ self.exec_mode = None;
+ }
+ _ => (),
}
}
+ /// Inset the ExecMode into self, and set the Status as exec
+ /// Using StatusPush with Status::Exec won't insert into the hash map
+ /// To force self.exec_mode to be set
+ pub fn set_exec_mode(&mut self, mode: ExecMode) {
+ self.exec_mode = Some(mode);
+ self.status.insert(Status::Exec);
+ }
+
+ pub fn get_exec_mode(&mut self) -> Option {
+ self.exec_mode.clone()
+ }
+
/// Insert a gui_status into the current gui_status HashSet
+ /// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
pub fn status_push(&mut self, status: Status) {
- self.status.insert(status);
+ match status {
+ Status::Exec => (),
+ _ => {
+ self.status.insert(status);
+ }
+ }
}
/// Change to next selectable panel
@@ -287,7 +329,7 @@ impl GuiState {
}
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
- pub fn get_loading(&mut self) -> char {
+ pub fn get_loading(&self) -> char {
if self.is_loading.is_empty() {
' '
} else {
@@ -296,16 +338,37 @@ impl GuiState {
}
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
- pub fn remove_loading(&mut self, uuid: Uuid) {
+ fn remove_loading(&mut self, uuid: Uuid) {
self.is_loading.remove(&uuid);
if self.is_loading.is_empty() {
self.loading_index = 0;
}
}
+ /// Animate the loading icon in its own Tokio thread
+ pub fn start_loading_animation(
+ gui_state: &Arc>,
+ loading_uuid: Uuid,
+ ) -> JoinHandle<()> {
+ gui_state.lock().next_loading(loading_uuid);
+ let gui_state = Arc::clone(gui_state);
+ tokio::spawn(async move {
+ loop {
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+ gui_state.lock().next_loading(loading_uuid);
+ }
+ })
+ }
+
+ /// Stop the loading_spin function, and reset gui loading status
+ pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) {
+ handle.abort();
+ self.remove_loading(loading_uuid);
+ }
+
/// Set info box content
pub fn set_info_box(&mut self, text: &str) {
- self.info_box_text = Some(text.to_owned());
+ self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
}
/// Remove info box content
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
index c8b8da6..2df6c51 100644
--- a/src/ui/mod.rs
+++ b/src/ui/mod.rs
@@ -4,9 +4,9 @@ use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, MutexGuard};
use ratatui::{
- backend::{Backend, CrosstermBackend},
+ backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
Frame, Terminal,
};
@@ -26,19 +26,21 @@ mod gui_state;
pub use self::color_match::*;
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
use crate::{
- app_data::AppData, app_error::AppError, docker_data::DockerMessage,
+ app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
+ app_error::AppError,
+ exec::TerminalSize,
input_handler::InputMessages,
};
pub struct Ui {
app_data: Arc>,
- docker_sx: Sender,
gui_state: Arc>,
input_poll_rate: Duration,
+ input_tx: Sender,
is_running: Arc,
now: Instant,
- sender: Sender,
terminal: Terminal>,
+ cursor_position: (u16, u16),
}
impl Ui {
@@ -57,20 +59,21 @@ impl Ui {
/// Create a new Ui struct, and execute the drawing loop
pub async fn create(
app_data: Arc>,
- docker_sx: Sender,
gui_state: Arc>,
+ input_tx: Sender,
is_running: Arc,
- sender: Sender,
) {
- if let Ok(terminal) = Self::setup_terminal() {
+ if let Ok(mut terminal) = Self::setup_terminal() {
+ // let args = app_data.lock().args.clone();
+ let cursor_position = terminal.get_cursor().unwrap_or_default();
let mut ui = Self {
app_data,
- docker_sx,
+ cursor_position,
gui_state,
input_poll_rate: std::time::Duration::from_millis(100),
+ input_tx,
is_running,
now: Instant::now(),
- sender,
terminal,
};
if let Err(e) = ui.draw_ui().await {
@@ -86,19 +89,17 @@ impl Ui {
/// Setup the terminal for full-screen drawing mode, with mouse capture
fn setup_terminal() -> Result>> {
- enable_raw_mode()?;
- let mut stdout = io::stdout();
- execute!(stdout, EnterAlternateScreen)?;
- Self::enable_mouse_capture()?;
+ let stdout = Self::init_terminal()?;
let backend = CrosstermBackend::new(stdout);
Ok(Terminal::new(backend)?)
}
- /// This is a fix for mouse-events being printed to screen, read an event and do nothing with it
- fn nullify_event_read(&self) {
- if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) {
- event::read().ok();
- }
+ fn init_terminal() -> Result {
+ enable_raw_mode()?;
+ let mut stdout = io::stdout();
+ execute!(stdout, EnterAlternateScreen)?;
+ Self::enable_mouse_capture()?;
+ Ok(stdout)
}
/// reset the terminal back to default settings
@@ -111,6 +112,9 @@ impl Ui {
DisableMouseCapture
)?;
disable_raw_mode()?;
+ self.terminal.clear().ok();
+ self.terminal
+ .set_cursor(self.cursor_position.0, self.cursor_position.1)?;
Ok(self.terminal.show_cursor()?)
}
@@ -137,12 +141,33 @@ impl Ui {
Ok(())
}
+ /// Use exeternal docker cli to exec into a container
+ async fn exec(&mut self) {
+ let exec_mode = self.gui_state.lock().get_exec_mode();
+
+ if let Some(mode) = exec_mode {
+ self.reset_terminal().ok();
+ self.terminal.clear().ok();
+ if let Err(e) = mode.run(TerminalSize::new(&self.terminal)).await {
+ self.app_data
+ .lock()
+ .set_error(e, &self.gui_state, Status::Error);
+ };
+ }
+ self.terminal.clear().ok();
+ self.reset_terminal().ok();
+ Self::init_terminal().ok();
+ self.gui_state.lock().status_del(Status::Exec);
+ }
+
/// The loop for drawing the main UI to the terminal
async fn gui_loop(&mut self) -> Result<(), AppError> {
- let update_duration =
- std::time::Duration::from_millis(u64::from(self.app_data.lock().args.docker_interval));
-
while self.is_running.load(Ordering::SeqCst) {
+ let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
+ if exec {
+ self.exec().await;
+ }
+
if self
.terminal
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state))
@@ -150,10 +175,11 @@ impl Ui {
{
return Err(AppError::Terminal);
}
+
if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) {
if let Ok(event) = event::read() {
if let Event::Key(key) = event {
- self.sender
+ self.input_tx
.send(InputMessages::ButtonPress((key.code, key.modifiers)))
.await
.ok();
@@ -162,7 +188,7 @@ impl Ui {
event::MouseEventKind::Down(_)
| event::MouseEventKind::ScrollDown
| event::MouseEventKind::ScrollUp => {
- self.sender.send(InputMessages::MouseEvent(m)).await.ok();
+ self.input_tx.send(InputMessages::MouseEvent(m)).await.ok();
}
_ => (),
}
@@ -172,11 +198,6 @@ impl Ui {
}
}
}
-
- if self.now.elapsed() >= update_duration {
- self.docker_sx.send(DockerMessage::Update).await.ok();
- self.now = Instant::now();
- }
}
Ok(())
}
@@ -192,57 +213,89 @@ impl Ui {
} else {
self.gui_loop().await?;
}
- self.nullify_event_read();
Ok(())
}
}
-#[macro_export]
-/// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
-macro_rules! value_capture {
- ($name:ident, $lock_expr:expr) => {
- let $name = || $lock_expr;
- let $name = $name();
- };
+#[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 {
+ columns: Columns,
+ delete_confirm: Option,
+ has_containers: bool,
+ has_error: Option,
+ height: u16,
+ help_visible: bool,
+ init: bool,
+ info_text: Option<(String, Instant)>,
+ loading_icon: String,
+ selected_panel: SelectablePanel,
+ sorted_by: Option<(Header, SortedOrder)>,
+}
+
+impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
+ fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self {
+ // set max height for container section, needs +5 to deal with docker commands list and borders
+ let height = data.0.get_container_len();
+ let height = if height < 12 {
+ u16::try_from(height + 5).unwrap_or_default()
+ } else {
+ 12
+ };
+
+ Self {
+ columns: data.0.get_width(),
+ delete_confirm: data.1.get_delete_container(),
+ has_containers: data.0.get_container_len() > 1,
+ has_error: data.0.get_error(),
+ height,
+ help_visible: data.1.status_contains(&[Status::Help]),
+ init: data.1.status_contains(&[Status::Init]),
+ info_text: data.1.info_box_text.clone(),
+ loading_icon: data.1.get_loading().to_string(),
+ selected_panel: data.1.get_selected_panel(),
+ sorted_by: data.0.get_sorted(),
+ }
+ }
}
/// Draw the main ui to a frame of the terminal
-/// TODO add a single line area for debug message - if not in release mode?
-fn draw_frame(
- f: &mut Frame<'_, B>,
- app_data: &Arc>,
- gui_state: &Arc>,
-) {
- value_capture!(height, app_data.lock().get_container_len());
- value_capture!(column_widths, app_data.lock().get_width());
- value_capture!(has_containers, app_data.lock().get_container_len() > 0);
- value_capture!(sorted_by, app_data.lock().get_sorted());
- value_capture!(delete_confirm, gui_state.lock().get_delete_container());
- value_capture!(has_error, app_data.lock().get_error());
- value_capture!(info_text, gui_state.lock().info_box_text.clone());
- value_capture!(loading_icon, gui_state.lock().get_loading().to_string());
+fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) {
+ let fd = FrameData::from((app_data.lock(), gui_state.lock()));
- // set max height for container section, needs +5 to deal with docker commands list and borders
- let height = if height < 12 { height + 5 } else { 12 };
+ let whole_layout = get_wholelayout(f);
+ #[cfg(debug_assertions)]
+ draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string());
- let whole_layout = Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
- .split(f.size());
+ #[cfg(debug_assertions)]
+ let whole_layout_split = (1, 2);
+
+ #[cfg(not(debug_assertions))]
+ let whole_layout_split = (0, 1);
// Split into 3, containers+controls, logs, then graphs
let upper_main = Layout::default()
.direction(Direction::Vertical)
- .constraints(
- [
- Constraint::Max(height.try_into().unwrap_or_default()),
- Constraint::Percentage(50),
- ]
- .as_ref(),
- )
- .split(whole_layout[1]);
+ .constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
+ .split(whole_layout[whole_layout_split.1]);
- let top_split = if has_containers {
+ let top_split = if fd.has_containers {
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
} else {
vec![Constraint::Percentage(100)]
@@ -250,10 +303,10 @@ fn draw_frame(
// Containers + docker commands
let top_panel = Layout::default()
.direction(Direction::Horizontal)
- .constraints(top_split.as_ref())
+ .constraints(top_split)
.split(upper_main[0]);
- let lower_split = if has_containers {
+ let lower_split = if fd.has_containers {
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
} else {
vec![Constraint::Percentage(100)]
@@ -262,25 +315,17 @@ fn draw_frame(
// Split into 2, logs, and optional charts
let lower_main = Layout::default()
.direction(Direction::Vertical)
- .constraints(lower_split.as_ref())
+ .constraints(lower_split)
.split(upper_main[1]);
- draw_blocks::containers(app_data, top_panel[0], f, gui_state, &column_widths);
+ draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state, &fd.columns);
- draw_blocks::logs(app_data, lower_main[0], f, gui_state, &loading_icon);
+ draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
- draw_blocks::heading_bar(
- whole_layout[0],
- &column_widths,
- f,
- has_containers,
- &loading_icon,
- sorted_by,
- gui_state,
- );
+ draw_blocks::heading_bar(whole_layout[whole_layout_split.0], f, &fd, gui_state);
- if let Some(id) = delete_confirm {
- app_data.lock().get_container_name_by_id(&id).map_or_else(
+ if let Some(id) = fd.delete_confirm.as_ref() {
+ app_data.lock().get_container_name_by_id(id).map_or_else(
|| {
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
// so if in that unique situation, just clear the delete_container id
@@ -293,21 +338,21 @@ fn draw_frame(
}
// only draw commands + charts if there are containers
- if has_containers {
- draw_blocks::commands(app_data, top_panel[1], f, gui_state);
+ if fd.has_containers {
+ draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
draw_blocks::chart(f, lower_main[1], app_data);
}
- if let Some(info) = info_text {
- draw_blocks::info(f, info);
+ if let Some((text, instant)) = fd.info_text {
+ draw_blocks::info(f, &text, instant, gui_state);
}
// Check if error, and show popup if so
- if gui_state.lock().status_contains(&[Status::Help]) {
+ if fd.help_visible {
draw_blocks::help_box(f);
}
- if let Some(error) = has_error {
+ if let Some(error) = fd.has_error {
draw_blocks::error(f, error, None);
}
}