chore: merge release-v0.4.0 into main
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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 }}
|
||||
@@ -103,11 +103,11 @@ jobs:
|
||||
uses: katyo/publish-crates@v1
|
||||
with:
|
||||
registry-token: ${{ secrets.CRATES_IO_TOKEN }}
|
||||
|
||||
|
||||
#########################################
|
||||
## Build images for Dockerhub & ghcr.io #
|
||||
#########################################
|
||||
|
||||
|
||||
image_build:
|
||||
needs: [cargo_publish]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -116,14 +116,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
CURRENT_SEMVER=${GITHUB_REF_NAME#v}
|
||||
echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV
|
||||
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
@@ -146,6 +146,4 @@ jobs:
|
||||
-t ghcr.io/${{ github.repository_owner }}/oxker:${{env.CURRENT_SEMVER}} \
|
||||
--provenance=false --sbom=false \
|
||||
--push \
|
||||
-f containerised/Dockerfile .
|
||||
|
||||
|
||||
-f containerised/Dockerfile .
|
||||
@@ -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
@@ -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
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -49,14 +49,13 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
|
||||
## Runtime ##
|
||||
#############
|
||||
|
||||
FROM scratch AS RUNTIME
|
||||
FROM scratch as RUNTIME
|
||||
|
||||
# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start
|
||||
# Set an ENV to indicate that we're running in a container
|
||||
ENV OXKER_RUNTIME=container
|
||||
|
||||
# Copy application binary from builder image
|
||||
COPY --from=BUILDER /oxker /app/
|
||||
|
||||
# Run the application
|
||||
# this is used in the application itself, to stop itself from listing itself, so DO NOT EDIT
|
||||
ENTRYPOINT [ "/app/oxker"]
|
||||
# this is used in the application itself so DO NOT EDIT
|
||||
ENTRYPOINT [ "/app/oxker"]
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
+141
-102
@@ -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 {
|
||||
match head {
|
||||
Header::State => match ord {
|
||||
SortedOrder::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| b.state.order().cmp(&a.state.order())),
|
||||
SortedOrder::Desc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.state.order().cmp(&b.state.order())),
|
||||
},
|
||||
Header::Status => match ord {
|
||||
SortedOrder::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.status.cmp(&b.status)),
|
||||
SortedOrder::Desc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| b.status.cmp(&a.status)),
|
||||
},
|
||||
Header::Cpu => match ord {
|
||||
SortedOrder::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
|
||||
SortedOrder::Desc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
|
||||
},
|
||||
Header::Memory => match ord {
|
||||
SortedOrder::Asc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
|
||||
SortedOrder::Desc => self
|
||||
.containers
|
||||
.items
|
||||
.sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
|
||||
},
|
||||
Header::Id => match ord {
|
||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
|
||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
|
||||
},
|
||||
Header::Image => match ord {
|
||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
|
||||
SortedOrder::Desc => {
|
||||
self.containers.items.sort_by(|a, b| b.image.cmp(&a.image));
|
||||
}
|
||||
},
|
||||
Header::Name => match ord {
|
||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
|
||||
},
|
||||
Header::Rx => match ord {
|
||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)),
|
||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)),
|
||||
},
|
||||
Header::Tx => match ord {
|
||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)),
|
||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)),
|
||||
},
|
||||
}
|
||||
let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
|
||||
match head {
|
||||
Header::State => match ord {
|
||||
SortedOrder::Asc => a
|
||||
.state
|
||||
.order()
|
||||
.cmp(&b.state.order())
|
||||
.then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => b
|
||||
.state
|
||||
.order()
|
||||
.cmp(&a.state.order())
|
||||
.then_with(|| b.name.cmp(&a.name)),
|
||||
},
|
||||
Header::Status => match ord {
|
||||
SortedOrder::Asc => {
|
||||
a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name))
|
||||
}
|
||||
SortedOrder::Desc => {
|
||||
b.status.cmp(&a.status).then_with(|| b.name.cmp(&a.name))
|
||||
}
|
||||
},
|
||||
Header::Cpu => match ord {
|
||||
SortedOrder::Asc => a
|
||||
.cpu_stats
|
||||
.back()
|
||||
.cmp(&b.cpu_stats.back())
|
||||
.then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => b
|
||||
.cpu_stats
|
||||
.back()
|
||||
.cmp(&a.cpu_stats.back())
|
||||
.then_with(|| b.name.cmp(&a.name)),
|
||||
},
|
||||
Header::Memory => match ord {
|
||||
SortedOrder::Asc => a
|
||||
.mem_stats
|
||||
.back()
|
||||
.cmp(&b.mem_stats.back())
|
||||
.then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => b
|
||||
.mem_stats
|
||||
.back()
|
||||
.cmp(&a.mem_stats.back())
|
||||
.then_with(|| b.name.cmp(&a.name)),
|
||||
},
|
||||
Header::Id => match ord {
|
||||
SortedOrder::Asc => a.id.cmp(&b.id).then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => b.id.cmp(&a.id).then_with(|| b.name.cmp(&a.name)),
|
||||
},
|
||||
Header::Image => match ord {
|
||||
SortedOrder::Asc => a.image.cmp(&b.image).then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => {
|
||||
b.image.cmp(&a.image).then_with(|| b.name.cmp(&a.name))
|
||||
}
|
||||
},
|
||||
Header::Name => match ord {
|
||||
SortedOrder::Asc => a.name.cmp(&b.name).then_with(|| a.id.cmp(&b.id)),
|
||||
SortedOrder::Desc => b.name.cmp(&a.name).then_with(|| b.id.cmp(&a.id)),
|
||||
},
|
||||
Header::Rx => match ord {
|
||||
SortedOrder::Asc => a.rx.cmp(&b.rx).then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => b.rx.cmp(&a.rx).then_with(|| b.name.cmp(&a.name)),
|
||||
},
|
||||
Header::Tx => match ord {
|
||||
SortedOrder::Asc => a.tx.cmp(&b.tx).then_with(|| a.name.cmp(&b.name)),
|
||||
SortedOrder::Desc => b.tx.cmp(&a.tx).then_with(|| b.name.cmp(&a.name)),
|
||||
},
|
||||
}
|
||||
};
|
||||
self.containers.items.sort_by(sort_closure);
|
||||
} else {
|
||||
self.containers
|
||||
.items
|
||||
.sort_by(|a, b| a.created.cmp(&b.created));
|
||||
.sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.name.cmp(&b.name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,18 +453,6 @@ impl AppData {
|
||||
self.get_selected_container().map_or(false, |i| i.is_oxker)
|
||||
}
|
||||
|
||||
/// Check if the initial parsing has been completed, by making sure that all ids given (which are running) have a non empty cpu_stats vecdec
|
||||
pub fn initialised(&mut self, all_ids: &[(bool, ContainerId)]) -> bool {
|
||||
let count_is_running = all_ids.iter().filter(|i| i.0).count();
|
||||
let number_with_cpu_status = self
|
||||
.containers
|
||||
.items
|
||||
.iter()
|
||||
.filter(|i| !i.cpu_stats.is_empty())
|
||||
.count();
|
||||
count_is_running == number_with_cpu_status
|
||||
}
|
||||
|
||||
/// Find the widths for the strings in the containers panel.
|
||||
/// So can display nicely and evenly
|
||||
pub fn get_width(&self) -> Columns {
|
||||
@@ -482,6 +513,12 @@ impl AppData {
|
||||
self.get_selected_container().map(|i| i.id.clone())
|
||||
}
|
||||
|
||||
/// Get the Id and State for the currently selected container - used by the exec check method
|
||||
pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
|
||||
self.get_selected_container()
|
||||
.map(|i| (i.id.clone(), i.state, i.name.clone()))
|
||||
}
|
||||
|
||||
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
|
||||
/// Will also, if a sort is set, sort the containers
|
||||
pub fn update_stats(
|
||||
@@ -563,6 +600,8 @@ impl AppData {
|
||||
})
|
||||
});
|
||||
|
||||
let id = ContainerId::from(id.as_str());
|
||||
|
||||
let is_oxker = i
|
||||
.command
|
||||
.as_ref()
|
||||
@@ -579,8 +618,6 @@ impl AppData {
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone);
|
||||
|
||||
let id = ContainerId::from(id.as_str());
|
||||
|
||||
let created = i
|
||||
.created
|
||||
.map_or(0, |i| u64::try_from(i).unwrap_or_default());
|
||||
@@ -624,31 +661,33 @@ impl AppData {
|
||||
let timestamp = self.args.timestamp;
|
||||
|
||||
if let Some(container) = self.get_container_by_id(id) {
|
||||
container.last_updated = Self::get_systemtime();
|
||||
let current_len = container.logs.len();
|
||||
if !container.is_oxker {
|
||||
container.last_updated = Self::get_systemtime();
|
||||
let current_len = container.logs.len();
|
||||
|
||||
for mut i in logs {
|
||||
let tz = LogsTz::from(i.as_str());
|
||||
// Strip the timestamp if `-t` flag set
|
||||
if !timestamp {
|
||||
i = i.replace(&tz.to_string(), "");
|
||||
for mut i in logs {
|
||||
let tz = LogsTz::from(i.as_str());
|
||||
// Strip the timestamp if `-t` flag set
|
||||
if !timestamp {
|
||||
i = i.replace(&tz.to_string(), "");
|
||||
}
|
||||
let lines = if color {
|
||||
log_sanitizer::colorize_logs(&i)
|
||||
} else if raw {
|
||||
log_sanitizer::raw(&i)
|
||||
} else {
|
||||
log_sanitizer::remove_ansi(&i)
|
||||
};
|
||||
container.logs.insert(ListItem::new(lines), tz);
|
||||
}
|
||||
let lines = if color {
|
||||
log_sanitizer::colorize_logs(&i)
|
||||
} else if raw {
|
||||
log_sanitizer::raw(&i)
|
||||
} else {
|
||||
log_sanitizer::remove_ansi(&i)
|
||||
};
|
||||
container.logs.insert(ListItem::new(lines), tz);
|
||||
}
|
||||
|
||||
// Set the logs selected row for each container
|
||||
// Either when no long currently selected, or currently selected (before updated) is already at end
|
||||
if container.logs.state().selected().is_none()
|
||||
|| container.logs.state().selected().map_or(1, |f| f + 1) == current_len
|
||||
{
|
||||
container.logs.end();
|
||||
// Set the logs selected row for each container
|
||||
// Either when no long currently selected, or currently selected (before updated) is already at end
|
||||
if container.logs.state().selected().is_none()
|
||||
|| container.logs.state().selected().map_or(1, |f| f + 1) == current_len
|
||||
{
|
||||
container.logs.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
+106
-97
@@ -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,66 +103,72 @@ 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<()>>>>,
|
||||
) {
|
||||
let mut stream = docker
|
||||
.stats(
|
||||
id.get(),
|
||||
Some(StatsOptions {
|
||||
stream: false,
|
||||
one_shot: !is_running,
|
||||
}),
|
||||
)
|
||||
.take(1);
|
||||
if state.is_alive() || init.is_some() {
|
||||
let mut stream = docker
|
||||
.stats(
|
||||
id.get(),
|
||||
Some(StatsOptions {
|
||||
stream: false,
|
||||
one_shot: false,
|
||||
}),
|
||||
)
|
||||
.take(1);
|
||||
|
||||
while let Some(Ok(stats)) = stream.next().await {
|
||||
let mem_stat = stats.memory_stats.usage.unwrap_or_default();
|
||||
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
|
||||
while let Some(Ok(stats)) = stream.next().await {
|
||||
let mem_stat = if state.is_alive() {
|
||||
Some(stats.memory_stats.usage.unwrap_or_default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let op_key = stats
|
||||
.networks
|
||||
.as_ref()
|
||||
.and_then(|networks| networks.keys().next().cloned());
|
||||
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
|
||||
|
||||
let cpu_stats = Self::calculate_usage(&stats);
|
||||
|
||||
let (rx, tx) = if let Some(key) = op_key {
|
||||
stats
|
||||
let op_key = stats
|
||||
.networks
|
||||
.unwrap_or_default()
|
||||
.get(&key)
|
||||
.map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
.as_ref()
|
||||
.and_then(|networks| networks.keys().next().cloned());
|
||||
|
||||
let cpu_stats = if state.is_alive() {
|
||||
Some(Self::calculate_usage(&stats))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (rx, tx) = if let Some(key) = op_key {
|
||||
stats
|
||||
.networks
|
||||
.unwrap_or_default()
|
||||
.get(&key)
|
||||
.map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes))
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
if is_running {
|
||||
app_data.lock().update_stats(
|
||||
&id,
|
||||
Some(cpu_stats),
|
||||
Some(mem_stat),
|
||||
mem_limit,
|
||||
rx,
|
||||
tx,
|
||||
);
|
||||
} else {
|
||||
app_data
|
||||
.lock()
|
||||
.update_stats(&id, None, None, mem_limit, rx, tx);
|
||||
.update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
|
||||
}
|
||||
spawns.lock().remove(&spawn_id);
|
||||
}
|
||||
spawns.lock().remove(&spawn_id);
|
||||
if let Some((target, _)) = init {
|
||||
target.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||
fn update_all_container_stats(&mut self, all_ids: &[(bool, ContainerId)]) {
|
||||
for (is_running, id) in all_ids {
|
||||
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
||||
for (state, id) in all_ids {
|
||||
// let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len())));
|
||||
let docker = Arc::clone(&self.docker);
|
||||
let app_data = Arc::clone(&self.app_data);
|
||||
let spawns = Arc::clone(&self.spawns);
|
||||
let spawn_id = SpawnId::Stats((id.clone(), self.binate));
|
||||
|
||||
let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len()));
|
||||
self.spawns
|
||||
.lock()
|
||||
.entry(spawn_id.clone())
|
||||
@@ -165,7 +177,8 @@ impl DockerData {
|
||||
app_data,
|
||||
docker,
|
||||
id.clone(),
|
||||
*is_running,
|
||||
init,
|
||||
*state,
|
||||
spawn_id,
|
||||
spawns,
|
||||
))
|
||||
@@ -177,7 +190,7 @@ impl DockerData {
|
||||
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
|
||||
/// Just make sure that items sent are guaranteed to have an id
|
||||
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
|
||||
pub async fn update_all_containers(&mut self) -> Vec<(bool, ContainerId)> {
|
||||
pub async fn update_all_containers(&mut self) -> Vec<(State, ContainerId)> {
|
||||
let containers = self
|
||||
.docker
|
||||
.list_containers(Some(ListContainersOptions::<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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+319
-212
@@ -1,26 +1,32 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{BufWriter, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use bollard::{container::LogsOptions, Docker};
|
||||
use cansi::v3::categorise_text;
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
||||
execute,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use ratatui::layout::Rect;
|
||||
use tokio::{
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod message;
|
||||
use crate::{
|
||||
app_data::{AppData, DockerControls, Header},
|
||||
app_error::AppError,
|
||||
docker_data::DockerMessage,
|
||||
exec::{tty_readable, ExecMode},
|
||||
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
||||
value_capture,
|
||||
};
|
||||
pub use message::InputMessages;
|
||||
|
||||
@@ -28,9 +34,8 @@ pub use message::InputMessages;
|
||||
#[derive(Debug)]
|
||||
pub struct InputHandler {
|
||||
app_data: Arc<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,213 +173,258 @@ impl InputHandler {
|
||||
);
|
||||
};
|
||||
|
||||
// If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window
|
||||
// then cancel the first handle, as a new handle will be invoked
|
||||
if let Some(info_sleep_timer) = self.info_sleep.as_ref() {
|
||||
info_sleep_timer.abort();
|
||||
}
|
||||
|
||||
let gui_state = Arc::clone(&self.gui_state);
|
||||
// Show the info box - with "mouse capture enabled / disabled", for 4000 ms
|
||||
self.info_sleep = Some(tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(4000)).await;
|
||||
gui_state.lock().reset_info_box();
|
||||
}));
|
||||
|
||||
self.mouse_capture = !self.mouse_capture;
|
||||
}
|
||||
|
||||
/// Sort the containers by a given header
|
||||
fn sort(&self, selected_header: Header) {
|
||||
self.app_data.lock().set_sort_by_header(selected_header);
|
||||
}
|
||||
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
|
||||
async fn s_key(&mut self) {
|
||||
/// This is the inner workings, *inlined* here to return a Result
|
||||
async fn save_logs(
|
||||
app_data: &Arc<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?;
|
||||
|
||||
/// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
|
||||
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
|
||||
async fn quit(&self) {
|
||||
let error_init = self
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_contains(&[Status::Error, Status::Init]);
|
||||
if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() {
|
||||
self.is_running
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |i| i.as_secs());
|
||||
|
||||
/// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container
|
||||
async fn confirm_delete(&self) {
|
||||
let id = self.gui_state.lock().get_delete_container();
|
||||
if let Some(id) = id {
|
||||
self.docker_sender
|
||||
.send(DockerMessage::Delete(id))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
let path = log_path.join(format!("{name}_{now}.log"));
|
||||
|
||||
/// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel)
|
||||
fn clear_delete(&self) {
|
||||
self.gui_state.lock().set_delete_container(None);
|
||||
}
|
||||
let docker = rx.await?;
|
||||
let options = Some(LogsOptions::<String> {
|
||||
stdout: true,
|
||||
timestamps: args.timestamp,
|
||||
since: 0,
|
||||
..Default::default()
|
||||
});
|
||||
let mut logs = docker.logs(id.get(), options);
|
||||
let mut output = vec![];
|
||||
|
||||
/// Handle any keyboard button events
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
||||
value_capture!(
|
||||
contains_delete,
|
||||
self.gui_state
|
||||
.lock()
|
||||
.status_contains(&[Status::DeleteConfirm])
|
||||
);
|
||||
|
||||
value_capture!(
|
||||
contains_error,
|
||||
self.gui_state.lock().status_contains(&[Status::Error])
|
||||
);
|
||||
value_capture!(
|
||||
contains_help,
|
||||
self.gui_state.lock().status_contains(&[Status::Help])
|
||||
);
|
||||
|
||||
// Always just quit on Ctrl + c/C or q/Q
|
||||
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||
if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() {
|
||||
self.quit().await;
|
||||
}
|
||||
|
||||
if contains_error {
|
||||
if let KeyCode::Char('c' | 'C') = key_code {
|
||||
self.app_data.lock().remove_error();
|
||||
self.gui_state.lock().status_del(Status::Error);
|
||||
}
|
||||
} else if contains_help {
|
||||
match key_code {
|
||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
_ => (),
|
||||
}
|
||||
} else if contains_delete {
|
||||
match key_code {
|
||||
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||
KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
match key_code {
|
||||
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
||||
KeyCode::Char('1') => self.sort(Header::State),
|
||||
KeyCode::Char('2') => self.sort(Header::Status),
|
||||
KeyCode::Char('3') => self.sort(Header::Cpu),
|
||||
KeyCode::Char('4') => self.sort(Header::Memory),
|
||||
KeyCode::Char('5') => self.sort(Header::Id),
|
||||
KeyCode::Char('6') => self.sort(Header::Name),
|
||||
KeyCode::Char('7') => self.sort(Header::Image),
|
||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
KeyCode::Tab => {
|
||||
// Skip control panel if no containers, could be refactored
|
||||
let is_containers =
|
||||
self.gui_state.lock().selected_panel == SelectablePanel::Containers;
|
||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
for _ in 0..count {
|
||||
self.gui_state.lock().next_panel();
|
||||
}
|
||||
}
|
||||
KeyCode::BackTab => {
|
||||
// Skip control panel if no containers, could be refactored
|
||||
let is_containers =
|
||||
self.gui_state.lock().selected_panel == SelectablePanel::Logs;
|
||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
for _ in 0..count {
|
||||
self.gui_state.lock().previous_panel();
|
||||
}
|
||||
}
|
||||
KeyCode::Home => {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
let selected_panel = self.gui_state.lock().selected_panel;
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => locked_data.containers_start(),
|
||||
SelectablePanel::Logs => locked_data.log_start(),
|
||||
SelectablePanel::Commands => locked_data.docker_command_start(),
|
||||
}
|
||||
}
|
||||
KeyCode::End => {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
let selected_panel = self.gui_state.lock().selected_panel;
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => locked_data.containers_end(),
|
||||
SelectablePanel::Logs => locked_data.log_end(),
|
||||
SelectablePanel::Commands => locked_data.docker_command_end(),
|
||||
}
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
||||
KeyCode::PageUp => {
|
||||
for _ in 0..=6 {
|
||||
self.previous();
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
||||
KeyCode::PageDown => {
|
||||
for _ in 0..=6 {
|
||||
self.next();
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||
let panel = self.gui_state.lock().selected_panel;
|
||||
if panel == SelectablePanel::Commands {
|
||||
let option_command = self.app_data.lock().selected_docker_command();
|
||||
|
||||
if let Some(command) = option_command {
|
||||
let option_id = self.app_data.lock().get_selected_container_id();
|
||||
// Poor way of disallowing commands to be sent to a containerised okxer
|
||||
if self.app_data.lock().is_oxker() {
|
||||
return;
|
||||
};
|
||||
if let Some(id) = option_id {
|
||||
match command {
|
||||
DockerControls::Delete => self
|
||||
.docker_sender
|
||||
.send(DockerMessage::ConfirmDelete(id))
|
||||
.await
|
||||
.ok(),
|
||||
DockerControls::Pause => {
|
||||
self.docker_sender.send(DockerMessage::Pause(id)).await.ok()
|
||||
}
|
||||
DockerControls::Unpause => self
|
||||
.docker_sender
|
||||
.send(DockerMessage::Unpause(id))
|
||||
.await
|
||||
.ok(),
|
||||
DockerControls::Start => {
|
||||
self.docker_sender.send(DockerMessage::Start(id)).await.ok()
|
||||
}
|
||||
DockerControls::Stop => {
|
||||
self.docker_sender.send(DockerMessage::Stop(id)).await.ok()
|
||||
}
|
||||
DockerControls::Restart => self
|
||||
.docker_sender
|
||||
.send(DockerMessage::Restart(id))
|
||||
.await
|
||||
.ok(),
|
||||
};
|
||||
}
|
||||
while let Some(Ok(value)) = logs.next().await {
|
||||
let data = value.to_string();
|
||||
if !data.trim().is_empty() {
|
||||
output.push(
|
||||
categorise_text(&data)
|
||||
.into_iter()
|
||||
.map(|i| i.text)
|
||||
.collect::<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(())
|
||||
}
|
||||
|
||||
let log_status = Status::Logs;
|
||||
let status = self.gui_state.lock().status_contains(&[log_status]);
|
||||
if !status {
|
||||
self.gui_state.lock().status_push(log_status);
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
self.app_data.lock().set_error(
|
||||
AppError::DockerLogs,
|
||||
&self.gui_state,
|
||||
Status::Error,
|
||||
);
|
||||
}
|
||||
self.gui_state.lock().status_del(log_status);
|
||||
self.gui_state.lock().stop_loading_animation(&handle, uuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send docker command, if the Commands panel is selected
|
||||
async fn enter_key(&mut self) {
|
||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
||||
let panel = self.gui_state.lock().get_selected_panel();
|
||||
if panel == SelectablePanel::Commands {
|
||||
let option_command = self.app_data.lock().selected_docker_command();
|
||||
|
||||
if let Some(command) = option_command {
|
||||
// Poor way of disallowing commands to be sent to a containerised okxer
|
||||
if self.app_data.lock().is_oxker() {
|
||||
return;
|
||||
};
|
||||
let option_id = self.app_data.lock().get_selected_container_id();
|
||||
if let Some(id) = option_id {
|
||||
match command {
|
||||
DockerControls::Delete => self
|
||||
.docker_tx
|
||||
.send(DockerMessage::ConfirmDelete(id))
|
||||
.await
|
||||
.ok(),
|
||||
DockerControls::Pause => {
|
||||
self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
|
||||
}
|
||||
DockerControls::Unpause => {
|
||||
self.docker_tx.send(DockerMessage::Unpause(id)).await.ok()
|
||||
}
|
||||
DockerControls::Start => {
|
||||
self.docker_tx.send(DockerMessage::Start(id)).await.ok()
|
||||
}
|
||||
DockerControls::Stop => {
|
||||
self.docker_tx.send(DockerMessage::Stop(id)).await.ok()
|
||||
}
|
||||
DockerControls::Restart => {
|
||||
self.docker_tx.send(DockerMessage::Restart(id)).await.ok()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the the "next" seletable panel
|
||||
fn tab_key(&mut self) {
|
||||
let is_containers =
|
||||
self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
|
||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
for _ in 0..count {
|
||||
self.gui_state.lock().next_panel();
|
||||
}
|
||||
}
|
||||
|
||||
/// Change to previously selected panel
|
||||
fn back_tab_key(&mut self) {
|
||||
let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
|
||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
for _ in 0..count {
|
||||
self.gui_state.lock().previous_panel();
|
||||
}
|
||||
}
|
||||
|
||||
fn home_key(&mut self) {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => locked_data.containers_start(),
|
||||
SelectablePanel::Logs => locked_data.log_start(),
|
||||
SelectablePanel::Commands => locked_data.docker_command_start(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to end of the list of the currently selected panel
|
||||
fn end_key(&mut self) {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => locked_data.containers_end(),
|
||||
SelectablePanel::Logs => locked_data.log_end(),
|
||||
SelectablePanel::Commands => locked_data.docker_command_end(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle keyboard button events
|
||||
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
||||
let contains_delete = self
|
||||
.gui_state
|
||||
.lock()
|
||||
.status_contains(&[Status::DeleteConfirm]);
|
||||
|
||||
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
||||
|
||||
let contains_error = contains(Status::Error);
|
||||
let contains_help = contains(Status::Help);
|
||||
let contains_exec = contains(Status::Exec);
|
||||
|
||||
if !contains_exec {
|
||||
// Always just quit on Ctrl + c/C or q/Q
|
||||
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||
if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() {
|
||||
self.quit().await;
|
||||
}
|
||||
|
||||
if contains_error {
|
||||
if let KeyCode::Char('c' | 'C') = key_code {
|
||||
self.app_data.lock().remove_error();
|
||||
self.gui_state.lock().status_del(Status::Error);
|
||||
}
|
||||
} else if contains_help {
|
||||
match key_code {
|
||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
_ => (),
|
||||
}
|
||||
} else if contains_delete {
|
||||
match key_code {
|
||||
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||
KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
match key_code {
|
||||
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
||||
KeyCode::Char('1') => self.sort(Header::State),
|
||||
KeyCode::Char('2') => self.sort(Header::Status),
|
||||
KeyCode::Char('3') => self.sort(Header::Cpu),
|
||||
KeyCode::Char('4') => self.sort(Header::Memory),
|
||||
KeyCode::Char('5') => self.sort(Header::Id),
|
||||
KeyCode::Char('6') => self.sort(Header::Name),
|
||||
KeyCode::Char('7') => self.sort(Header::Image),
|
||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||
KeyCode::Char('e' | 'E') => self.e_key().await,
|
||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
KeyCode::Char('s' | 'S') => self.s_key().await,
|
||||
KeyCode::Tab => self.tab_key(),
|
||||
KeyCode::BackTab => self.back_tab_key(),
|
||||
KeyCode::Home => self.home_key(),
|
||||
KeyCode::End => self.end_key(),
|
||||
KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
||||
KeyCode::PageUp => {
|
||||
for _ in 0..=6 {
|
||||
self.previous();
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
||||
KeyCode::PageDown => {
|
||||
for _ in 0..=6 {
|
||||
self.next();
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => self.enter_key().await,
|
||||
_ => (),
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,7 +477,7 @@ impl InputHandler {
|
||||
/// Change state to next, depending which panel is currently in focus
|
||||
fn next(&mut self) {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
let selected_panel = self.gui_state.lock().selected_panel;
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => locked_data.containers_next(),
|
||||
SelectablePanel::Logs => locked_data.log_next(),
|
||||
@@ -381,7 +488,7 @@ impl InputHandler {
|
||||
/// Change state to previous, depending which panel is currently in focus
|
||||
fn previous(&mut self) {
|
||||
let mut locked_data = self.app_data.lock();
|
||||
let selected_panel = self.gui_state.lock().selected_panel;
|
||||
let selected_panel = self.gui_state.lock().get_selected_panel();
|
||||
match selected_panel {
|
||||
SelectablePanel::Containers => locked_data.containers_previous(),
|
||||
SelectablePanel::Logs => locked_data.log_previous(),
|
||||
|
||||
+43
-57
@@ -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);
|
||||
if let Some(err) = app_data.lock().get_error() {
|
||||
error!("{}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||
args.docker_interval,
|
||||
)))
|
||||
.await;
|
||||
let containers = app_data
|
||||
.lock()
|
||||
.get_container_items()
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|i| format!("{i}"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !containers.is_empty() {
|
||||
for item in containers {
|
||||
info!("{item}");
|
||||
}
|
||||
docker_sx.send(DockerMessage::Update).await.ok();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||
args.docker_interval,
|
||||
)))
|
||||
.await;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-9
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+169
-150
@@ -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,88 +122,91 @@ pub fn commands<B: Backend>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the container data to display nicely on the screen
|
||||
fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
||||
let state_style = Style::default().fg(i.state.get_color());
|
||||
let blue = Style::default().fg(Color::Blue);
|
||||
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
format!(
|
||||
"{:<width$}",
|
||||
i.state.to_string(),
|
||||
width = widths.state.1.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{MARGIN}{:>width$}",
|
||||
i.status,
|
||||
width = &widths.status.1.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.cpu_stats.back().unwrap_or(&CpuStats::default()),
|
||||
width = &widths.cpu.1.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{MARGIN}{:>width_current$} / {:>width_limit$}",
|
||||
i.mem_stats.back().unwrap_or(&ByteStats::default()),
|
||||
i.mem_limit,
|
||||
width_current = &widths.mem.1.into(),
|
||||
width_limit = &widths.mem.2.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.id.get_short(),
|
||||
width = &widths.id.1.into()
|
||||
),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()),
|
||||
Style::default().fg(Color::Rgb(255, 233, 193)),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
|
||||
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
/// Draw the containers panel
|
||||
pub fn containers<B: Backend>(
|
||||
pub fn containers(
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
area: Rect,
|
||||
f: &mut Frame<'_, B>,
|
||||
f: &mut Frame,
|
||||
fd: &FrameData,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
widths: &Columns,
|
||||
) {
|
||||
let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
|
||||
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
|
||||
|
||||
let items = app_data
|
||||
.lock()
|
||||
.get_container_items()
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let state_style = Style::default().fg(i.state.get_color());
|
||||
let blue = Style::default().fg(Color::Blue);
|
||||
|
||||
let lines = Line::from(vec![
|
||||
Span::styled(
|
||||
format!(
|
||||
"{:<width$}",
|
||||
i.state.to_string(),
|
||||
width = widths.state.1.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{MARGIN}{:>width$}",
|
||||
i.status,
|
||||
width = &widths.status.1.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.cpu_stats.back().unwrap_or(&CpuStats::default()),
|
||||
width = &widths.cpu.1.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{MARGIN}{:>width_current$} / {:>width_limit$}",
|
||||
i.mem_stats.back().unwrap_or(&ByteStats::default()),
|
||||
i.mem_limit,
|
||||
width_current = &widths.mem.1.into(),
|
||||
width_limit = &widths.mem.2.into()
|
||||
),
|
||||
state_style,
|
||||
),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{}{:>width$}",
|
||||
MARGIN,
|
||||
i.id.get().chars().take(8).collect::<String>(),
|
||||
width = &widths.id.1.into()
|
||||
),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.name, width = widths.name.1.into()),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.image, width = widths.image.1.into()),
|
||||
blue,
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.rx, width = widths.net_rx.1.into()),
|
||||
Style::default().fg(Color::Rgb(255, 233, 193)),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
|
||||
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||
),
|
||||
]);
|
||||
ListItem::new(lines)
|
||||
})
|
||||
.map(|i| ListItem::new(format_containers(i, widths)))
|
||||
.collect::<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())
|
||||
|
||||
+78
-15
@@ -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,16 +271,41 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Remove a gui_status into the current gui_status HashSet
|
||||
/// Remove exec mode & deleteConfirm is required
|
||||
pub fn status_del(&mut self, status: Status) {
|
||||
self.status.remove(&status);
|
||||
if status == Status::DeleteConfirm {
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
match status {
|
||||
Status::DeleteConfirm => {
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
}
|
||||
Status::Exec => {
|
||||
self.exec_mode = None;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inset the ExecMode into self, and set the Status as exec
|
||||
/// Using StatusPush with Status::Exec won't insert into the hash map
|
||||
/// To force self.exec_mode to be set
|
||||
pub fn set_exec_mode(&mut self, mode: ExecMode) {
|
||||
self.exec_mode = Some(mode);
|
||||
self.status.insert(Status::Exec);
|
||||
}
|
||||
|
||||
pub fn get_exec_mode(&mut self) -> Option<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) {
|
||||
self.status.insert(status);
|
||||
match status {
|
||||
Status::Exec => (),
|
||||
_ => {
|
||||
self.status.insert(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change to next selectable panel
|
||||
@@ -287,7 +329,7 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
||||
pub fn get_loading(&mut self) -> char {
|
||||
pub fn get_loading(&self) -> char {
|
||||
if self.is_loading.is_empty() {
|
||||
' '
|
||||
} else {
|
||||
@@ -296,16 +338,37 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
|
||||
pub fn remove_loading(&mut self, uuid: Uuid) {
|
||||
fn remove_loading(&mut self, uuid: Uuid) {
|
||||
self.is_loading.remove(&uuid);
|
||||
if self.is_loading.is_empty() {
|
||||
self.loading_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the loading icon in its own Tokio thread
|
||||
pub fn start_loading_animation(
|
||||
gui_state: &Arc<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
|
||||
|
||||
+133
-88
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user