chore: merge release-v0.4.0 into main

This commit is contained in:
Jack Wills
2023-11-20 17:10:26 +00:00
24 changed files with 1717 additions and 874 deletions
+1 -3
View File
@@ -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
+1 -5
View File
@@ -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",
+11 -14
View File
@@ -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 <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 95 KiB

@@ -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 }}
@@ -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
@@ -147,5 +147,3 @@ jobs:
--provenance=false --sbom=false \
--push \
-f containerised/Dockerfile .
+17
View File
@@ -1,3 +1,20 @@
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.4.0'>v0.4.0</a>
### 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)
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.3'>v0.3.3</a>
### 2023-10-21
Generated
+180 -80
View File
@@ -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",
]
+21 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "oxker"
version = "0.3.3"
version = "0.4.0"
edition = "2021"
authors = ["Jack Wills <email@mrjackwills.com>"]
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]
+23 -15
View File
@@ -15,6 +15,10 @@
</a>
</p>
- [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
+3 -4
View File
@@ -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
# this is used in the application itself so DO NOT EDIT
ENTRYPOINT [ "/app/oxker"]
+20
View File
@@ -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
+3 -3
View File
@@ -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
+29 -1
View File
@@ -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::<String>()
}
}
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<Option<String>> for State {
fn from(input: Option<String>) -> 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(
+97 -58
View File
@@ -17,6 +17,7 @@ use crate::{
};
pub use container_state::*;
#[cfg(not(debug_assertions))]
/// Global app_state, stored in an Arc<Mutex>
#[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<Mutex>
#[derive(Debug, Clone)]
pub struct AppData {
containers: StatefulList<ContainerItem>,
error: Option<AppError>,
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 {
let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
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())),
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 => 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)),
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 => 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())),
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 => 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())),
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 => 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)),
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 => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
SortedOrder::Asc => a.image.cmp(&b.image).then_with(|| a.name.cmp(&b.name)),
SortedOrder::Desc => {
self.containers.items.sort_by(|a, b| b.image.cmp(&a.image));
b.image.cmp(&a.image).then_with(|| b.name.cmp(&a.name))
}
},
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)),
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 => 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)),
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 => 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)),
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,6 +661,7 @@ impl AppData {
let timestamp = self.args.timestamp;
if let Some(container) = self.get_container_by_id(id) {
if !container.is_oxker {
container.last_updated = Self::get_systemtime();
let current_len = container.logs.len();
@@ -652,4 +690,5 @@ impl AppData {
}
}
}
}
}
+5 -1
View File
@@ -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"),
}
}
}
+8 -3
View File
@@ -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<Arc<Docker>>),
Pause(ContainerId),
Quit,
Restart(ContainerId),
+83 -74
View File
@@ -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<Mutex<AppData>>,
args: CliArgs,
binate: Binate,
containerised: bool,
docker: Arc<Docker>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
init: Option<Arc<AtomicUsize>>,
receiver: Receiver<DockerMessage>,
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
}
@@ -97,22 +103,29 @@ impl DockerData {
app_data: Arc<Mutex<AppData>>,
docker: Arc<Docker>,
id: ContainerId,
is_running: bool,
init: Option<(Arc<AtomicUsize>, usize)>,
state: State,
spawn_id: SpawnId,
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
) {
if state.is_alive() || init.is_some() {
let mut stream = docker
.stats(
id.get(),
Some(StatsOptions {
stream: false,
one_shot: !is_running,
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_stat = if state.is_alive() {
Some(stats.memory_stats.usage.unwrap_or_default())
} else {
None
};
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
let op_key = stats
@@ -120,8 +133,11 @@ impl DockerData {
.as_ref()
.and_then(|networks| networks.keys().next().cloned());
let cpu_stats = Self::calculate_usage(&stats);
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
@@ -132,31 +148,27 @@ impl DockerData {
(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);
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::<String> {
@@ -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::<Vec<_>>()
}
@@ -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<Mutex<GuiState>>) -> 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<Mutex<GuiState>>,
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::<StartContainerOptions<String>>)
.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<DockerMessage>) {
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<Mutex<AppData>>,
containerised: bool,
docker: Docker,
docker_rx: Receiver<DockerMessage>,
docker_tx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
) {
@@ -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;
}
}
+345
View File
@@ -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<AtomicBool>) -> Option<AsyncTTY> {
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<u8>,
}
/// 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<CrosstermBackend<Stdout>>) -> Option<Self> {
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<Docker>)),
// 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<Mutex<AppData>>, docker: &Arc<Docker>) -> Option<Self> {
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<Docker>,
terminal_size: Option<TerminalSize>,
) -> 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<TerminalSize>) -> Result<(), AppError> {
match self {
Self::External(id) => {
Self::exec_external(id);
Ok(())
}
Self::Internal((id, docker)) => self.exec_internal(id, docker, tty_size).await,
}
}
}
+266 -159
View File
@@ -1,26 +1,32 @@
use std::sync::{
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<Mutex<AppData>>,
docker_sender: Sender<DockerMessage>,
docker_tx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
info_sleep: Option<JoinHandle<()>>,
is_running: Arc<AtomicBool>,
mouse_capture: bool,
rec: Receiver<InputMessages>,
@@ -41,18 +46,17 @@ impl InputHandler {
pub async fn init(
app_data: Arc<Mutex<AppData>>,
rec: Receiver<InputMessages>,
docker_sender: Sender<DockerMessage>,
docker_tx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>,
) {
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::<Arc<Docker>>();
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,75 +173,199 @@ 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<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>,
docker_tx: &Sender<DockerMessage>,
) -> Result<(), Box<dyn std::error::Error>> {
let args = app_data.lock().args.clone();
let container = app_data.lock().get_selected_container_id_state_name();
if let Some((id, _, name)) = container {
if let Some(log_path) = args.save_dir {
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
docker_tx.send(DockerMessage::Exec(sx)).await?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |i| i.as_secs());
let path = log_path.join(format!("{name}_{now}.log"));
let docker = rx.await?;
let options = Some(LogsOptions::<String> {
stdout: true,
timestamps: args.timestamp,
since: 0,
..Default::default()
});
let mut logs = docker.logs(id.get(), options);
let mut output = vec![];
while let Some(Ok(value)) = logs.next().await {
let data = value.to_string();
if !data.trim().is_empty() {
output.push(
categorise_text(&data)
.into_iter()
.map(|i| i.text)
.collect::<String>(),
);
}
}
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(())
}
/// 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
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::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);
}
}
.status_contains(&[Status::DeleteConfirm]);
/// 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 contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
/// 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);
}
/// 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])
);
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');
@@ -216,52 +402,14 @@ impl InputHandler {
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::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::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 {
@@ -274,53 +422,12 @@ impl InputHandler {
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(),
};
}
}
}
}
KeyCode::Enter => self.enter_key().await,
_ => (),
}
}
}
}
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
async fn button_intersect(&mut self, mouse_event: MouseEvent) {
@@ -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(),
+36 -50
View File
@@ -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<String> {
args.host
@@ -77,8 +52,8 @@ fn read_docker_host(args: &CliArgs) -> Option<String> {
/// 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<Mutex<AppData>>,
containerised: bool,
docker_rx: Receiver<DockerMessage>,
docker_tx: Sender<DockerMessage>,
gui_state: &Arc<Mutex<GuiState>>,
is_running: &Arc<AtomicBool>,
host: Option<String>,
@@ -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<InputMessages>,
is_running: &Arc<AtomicBool>,
) {
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);
}
docker_sx.send(DockerMessage::Update).await.ok();
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::<Vec<_>>();
if !containers.is_empty() {
for item in containers {
info!("{item}");
}
println!();
}
}
}
+53 -9
View File
@@ -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<String>,
/// 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<String>,
/// 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<String>,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct CliArgs {
pub color: bool,
pub docker_interval: u32,
pub gui: bool,
pub host: Option<String>,
pub in_container: bool,
pub save_dir: Option<PathBuf>,
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,
}
}
+118 -99
View File
@@ -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<Mutex<AppData>>,
area: Rect,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
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<B: Backend>(
pub fn commands(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame<'_, B>,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
) {
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<B: Backend>(
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,25 +122,12 @@ pub fn commands<B: Backend>(
}
}
/// Draw the containers panel
pub fn containers<B: Backend>(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame<'_, B>,
gui_state: &Arc<Mutex<GuiState>>,
widths: &Columns,
) {
let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
let items = app_data
.lock()
.get_container_items()
.iter()
.map(|i| {
/// 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);
let lines = Line::from(vec![
Line::from(vec![
Span::styled(
format!(
"{:<width$}",
@@ -177,7 +167,7 @@ pub fn containers<B: Backend>(
format!(
"{}{:>width$}",
MARGIN,
i.id.get().chars().take(8).collect::<String>(),
i.id.get_short(),
width = &widths.id.1.into()
),
blue,
@@ -198,9 +188,25 @@ pub fn containers<B: Backend>(
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
Style::default().fg(Color::Rgb(205, 140, 140)),
),
]);
ListItem::new(lines)
})
])
}
/// Draw the containers panel
pub fn containers(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
widths: &Columns,
) {
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
let items = app_data
.lock()
.get_container_items()
.iter()
.map(|i| ListItem::new(format_containers(i, widths)))
.collect::<Vec<_>>();
if items.is_empty() {
@@ -213,22 +219,21 @@ pub fn containers<B: Backend>(
.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<B: Backend>(
pub fn logs(
app_data: &Arc<Mutex<AppData>>,
area: Rect,
f: &mut Frame<'_, B>,
f: &mut Frame,
fd: &FrameData,
gui_state: &Arc<Mutex<GuiState>>,
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<B: Backend>(
} 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<B: Backend>(f: &mut Frame<'_, B>, area: Rect, app_data: &Arc<Mutex<AppData>>) {
pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) {
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<B: Backend>(f: &mut Frame<'_, B>, area: Rect, app_data: &Arc<Mutex<
.data(&mem.0)];
let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64));
let cpu_chart = make_chart(cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1);
let mem_chart = make_chart(mem.2, "memory", mem_dataset, &mem_stats, &mem.1);
@@ -337,30 +342,26 @@ fn make_chart<'a, T: Stats + Display>(
/// 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<B: Backend>(
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<Mutex<GuiState>>,
) {
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<B: Backend>(
// 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<B: Backend>(
})
.collect::<Vec<_>>();
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<B: Backend>(
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::<Vec<_>>();
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<B: Backend>(
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<B: Backend>(
.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<B: Backend>(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<B: Backend>(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<B: Backend>(
f: &mut Frame<'_, B>,
gui_state: &Arc<Mutex<GuiState>>,
name: &str,
) {
pub fn delete_confirm(f: &mut Frame, gui_state: &Arc<Mutex<GuiState>>, name: &str) {
let block = Block::default()
.title(" Confirm Delete ")
.border_type(BorderType::Rounded)
@@ -834,7 +840,7 @@ pub fn delete_confirm<B: Backend>(
}
/// Draw an error popup over whole screen
pub fn error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<u8>) {
pub fn error(f: &mut Frame, error: AppError, seconds: Option<u8>) {
let block = Block::default()
.title(" Error ")
.border_type(BorderType::Rounded)
@@ -850,7 +856,7 @@ pub fn error<B: Backend>(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<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<
}
/// Draw info box in one of the 9 BoxLocations
pub fn info<B: Backend>(f: &mut Frame<'_, B>, text: String) {
pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc<Mutex<GuiState>>) {
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<B: Backend>(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<B: Backend>(f: &mut Frame<'_, B>) {
// pub fn nothing(f: &mut Frame) {
// let whole_layout = Layout::default()
// .direction(Direction::Vertical)
// .constraints([Constraint::Min(100)].as_ref())
+76 -13
View File
@@ -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<Mutex>
#[derive(Debug, Default, Clone)]
pub struct GuiState {
delete_container: Option<ContainerId>,
delete_map: HashMap<DeleteButton, Rect>,
heading_map: HashMap<Header, Rect>,
is_loading: HashSet<Uuid>,
loading_index: u8,
panel_map: HashMap<SelectablePanel, Rect>,
delete_map: HashMap<DeleteButton, Rect>,
selected_panel: SelectablePanel,
status: HashSet<Status>,
delete_container: Option<ContainerId>,
pub info_box_text: Option<String>,
pub selected_panel: SelectablePanel,
exec_mode: Option<ExecMode>,
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,17 +271,42 @@ 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 {
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<ExecMode> {
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) {
match status {
Status::Exec => (),
_ => {
self.status.insert(status);
}
}
}
/// Change to next selectable panel
pub fn next_panel(&mut self) {
@@ -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<Mutex<Self>>,
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
+132 -87
View File
@@ -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<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
input_poll_rate: Duration,
input_tx: Sender<InputMessages>,
is_running: Arc<AtomicBool>,
now: Instant,
sender: Sender<InputMessages>,
terminal: Terminal<CrosstermBackend<Stdout>>,
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<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>,
input_tx: Sender<InputMessages>,
is_running: Arc<AtomicBool>,
sender: Sender<InputMessages>,
) {
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<Terminal<CrosstermBackend<Stdout>>> {
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<Stdout> {
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<ContainerId>,
has_containers: bool,
has_error: Option<AppError>,
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<B: Backend>(
f: &mut Frame<'_, B>,
app_data: &Arc<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>,
) {
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<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
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<B: Backend>(
// 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<B: Backend>(
// 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<B: Backend>(
}
// 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);
}
}