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
|
&& apt-get -y install --no-install-recommends build-essential pkg-config libssl-dev
|
||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
RUN curl --proto '=https' --tlsv1.2 -sSf curl https://sh.rustup.rs | sh -s -- -y
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
# RUN rustup target add x86_64-unknown-linux-musl
|
|
||||||
|
|
||||||
@@ -16,14 +16,11 @@
|
|||||||
"seccomp=unconfined"
|
"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": [
|
"mounts": [
|
||||||
"source=/etc/timezone,target=/etc/timezone,type=bind,readonly"
|
"source=/etc/timezone,target=/etc/timezone,type=bind,readonly"
|
||||||
],
|
],
|
||||||
"containerEnv": {
|
|
||||||
"CARGO_REGISTRIES_CRATES_IO_PROTOCOL": "sparse"
|
|
||||||
},
|
|
||||||
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
@@ -35,7 +32,6 @@
|
|||||||
"mutantdino.resourcemonitor",
|
"mutantdino.resourcemonitor",
|
||||||
"rangav.vscode-thunder-client",
|
"rangav.vscode-thunder-client",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"redhat.vscode-yaml",
|
|
||||||
"rust-lang.rust-analyzer",
|
"rust-lang.rust-analyzer",
|
||||||
"serayuzgur.crates",
|
"serayuzgur.crates",
|
||||||
"tamasfe.even-better-toml",
|
"tamasfe.even-better-toml",
|
||||||
|
|||||||
+11
-14
@@ -1,20 +1,17 @@
|
|||||||
### 2023-10-21
|
### 2023-11-20
|
||||||
|
|
||||||
### Chores
|
### Chores
|
||||||
+ docker-compose Alpine bump, [d46c425fa29f3c1d27bd57764748bae7e0b82f69]
|
+ workflow dependencies updated, [6a4cf6490d08b976734e2bc8186d94c095700558]
|
||||||
+ dependencies updated, [e6eecbbdce9c0ccff42aa8806dddb6e3364f990c], [ec93115ece83002fa127f3358f573319e29357e1], [b36daa5aeaa354b6c4f45b7ae67ac1a6345ea1c0], [9c0de1f0feff3165d0f5b6cb5dda843c124bcfa4], [6dd953df458096aee5914411ce40e46c3f600ede]
|
+ dependencies updated, [e301b51891e03ea40b2f904583119da3bc4daf53], [81d5b326db8881263f2c9072e1426948e41b4a0f], [294cc2684f42daab9d51601e235a384f55617678]
|
||||||
+ Rust 1.73 linting, [21234c66c3935330ccd58543dd3a915a293ac776]
|
+ lints moved from main.rs to Cargo.toml, [2de76e2f358be9c1500ca3dc4f9df0979ed8ed28]
|
||||||
|
+ .devcontainer updated, [37d2ee915625806dd11c2cc816a892aae12a777c]
|
||||||
|
|
||||||
### Docs
|
### Features
|
||||||
+ README.md updated, [3fd3915b3e929742d8007109fd4c7b4a345eb0fa]
|
+ 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]
|
||||||
### 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]
|
|
||||||
|
|
||||||
|
### 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
|
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
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: Update Release
|
- name: Update Release
|
||||||
uses: ncipollo/release-action@v1.12.0
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
makeLatest: true
|
makeLatest: true
|
||||||
name: ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
@@ -116,14 +116,14 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@@ -133,7 +133,7 @@ jobs:
|
|||||||
CURRENT_SEMVER=${GITHUB_REF_NAME#v}
|
CURRENT_SEMVER=${GITHUB_REF_NAME#v}
|
||||||
echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV
|
echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v3
|
||||||
id: buildx
|
id: buildx
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
@@ -147,5 +147,3 @@ jobs:
|
|||||||
--provenance=false --sbom=false \
|
--provenance=false --sbom=false \
|
||||||
--push \
|
--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>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.3.3'>v0.3.3</a>
|
||||||
### 2023-10-21
|
### 2023-10-21
|
||||||
|
|
||||||
|
|||||||
Generated
+180
-80
@@ -17,6 +17,24 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
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]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -109,9 +127,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.4"
|
version = "0.21.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
|
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
@@ -128,8 +146,7 @@ checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "bollard"
|
name = "bollard"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://www.github.com/fussybeaver/bollard.git?rev=cef1cd5#cef1cd568684d0c3c497ce56221ff22ca18334f4"
|
||||||
checksum = "f03db470b3c0213c47e978da93200259a1eb4dae2e5512cba9955e2b540a6fc6"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bollard-stubs",
|
"bollard-stubs",
|
||||||
@@ -157,8 +174,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "bollard-stubs"
|
name = "bollard-stubs"
|
||||||
version = "1.43.0-rc.2"
|
version = "1.43.0-rc.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://www.github.com/fussybeaver/bollard.git?rev=cef1cd5#cef1cd568684d0c3c497ce56221ff22ca18334f4"
|
||||||
checksum = "b58071e8fd9ec1e930efd28e3a90c1251015872a2ce49f81f36421b86466932e"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -219,9 +235,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.4.6"
|
version = "4.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956"
|
checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -229,9 +245,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.4.6"
|
version = "4.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45"
|
checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -243,9 +259,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.4.2"
|
version = "4.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
|
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -255,9 +271,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
|
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
@@ -306,6 +322,27 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -335,24 +372,24 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
|
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -361,21 +398,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
|
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
|
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.28"
|
version = "0.3.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
|
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -387,9 +424,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.10"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -404,9 +441,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.21"
|
version = "0.3.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
|
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -414,7 +451,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 1.9.3",
|
"indexmap 2.1.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -432,6 +469,10 @@ name = "hashbrown"
|
|||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
|
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@@ -453,9 +494,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.9"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -568,9 +609,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.0.2"
|
version = "2.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
|
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.2",
|
"hashbrown 0.14.2",
|
||||||
@@ -600,9 +641,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.64"
|
version = "0.3.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
|
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -615,9 +656,20 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.149"
|
version = "0.2.150"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -635,6 +687,15 @@ version = "0.4.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
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]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.6.4"
|
version = "2.6.4"
|
||||||
@@ -652,9 +713,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -706,6 +767,12 @@ version = "1.18.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -714,13 +781,14 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.3.3"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bollard",
|
"bollard",
|
||||||
"cansi",
|
"cansi",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"directories",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
@@ -859,15 +927,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ratatui"
|
name = "ratatui"
|
||||||
version = "0.23.0"
|
version = "0.24.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2e2e4cd95294a85c3b4446e63ef054eea43e0205b1fd60120c16b74ff7ff96ad"
|
checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.4.1",
|
"bitflags 2.4.1",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"lru",
|
||||||
"paste",
|
"paste",
|
||||||
"strum",
|
"strum",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
@@ -883,6 +952,17 @@ dependencies = [
|
|||||||
"bitflags 1.3.2",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
@@ -909,18 +989,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.189"
|
version = "1.0.192"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
|
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.189"
|
version = "1.0.192"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
|
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -929,9 +1009,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.107"
|
version = "1.0.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
|
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -940,9 +1020,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.16"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
|
checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -971,7 +1051,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.0.2",
|
"indexmap 2.1.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"time",
|
"time",
|
||||||
@@ -1027,9 +1107,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.11.1"
|
version = "1.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
|
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -1081,9 +1161,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.38"
|
version = "2.0.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
|
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1166,9 +1246,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.33.0"
|
version = "1.34.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
|
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1185,9 +1265,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.1.0"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1196,9 +1276,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.9"
|
version = "0.7.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
|
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -1248,20 +1328,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
|
||||||
"log",
|
"log",
|
||||||
|
"once_cell",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
@@ -1338,9 +1418,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.5.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
checksum = "c58fe91d841bc04822c9801002db4ea904b9e4b8e6bbad25127b46eff8dc516b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -1375,9 +1455,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.87"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
|
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
@@ -1385,9 +1465,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.87"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
|
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
@@ -1400,9 +1480,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.87"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
|
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -1410,9 +1490,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.87"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1423,9 +1503,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.87"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
|
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
@@ -1523,3 +1603,23 @@ name = "windows_x86_64_msvc"
|
|||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
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]
|
[package]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.3.3"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Jack Wills <email@mrjackwills.com>"]
|
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||||
description = "A simple tui to view & control docker containers"
|
description = "A simple tui to view & control docker containers"
|
||||||
@@ -11,19 +11,35 @@ readme = "README.md"
|
|||||||
keywords = ["docker", "tui", "tokio", "terminal", "podman"]
|
keywords = ["docker", "tui", "tokio", "terminal", "podman"]
|
||||||
categories = ["command-line-utilities"]
|
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]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bollard = "0.15"
|
# bollard = "0.15"
|
||||||
|
bollard = { git = "https://www.github.com/fussybeaver/bollard.git", rev = "cef1cd5" }
|
||||||
cansi = "2.2"
|
cansi = "2.2"
|
||||||
clap = { version = "4.4", features = ["derive", "unicode", "color"] }
|
clap = { version = "4.4", features = ["derive", "unicode", "color"] }
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
parking_lot = { version= "0.12" }
|
parking_lot = { version= "0.12" }
|
||||||
tokio = { version = "1.33", features = ["full"] }
|
tokio = { version = "1.34", features = ["full"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
ratatui = "0.23"
|
ratatui = "0.24"
|
||||||
uuid = { version = "1.5", features = ["v4", "fast-rng"] }
|
uuid = { version = "1.6", features = ["v4", "fast-rng"] }
|
||||||
|
directories = "5.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
- [Download & install](#download--install)
|
||||||
|
- [Run](#run)
|
||||||
|
- [Build step](#build-step)
|
||||||
|
- [Tests](#tests)
|
||||||
|
|
||||||
## Download & install
|
## Download & install
|
||||||
|
|
||||||
@@ -92,26 +96,30 @@ oxker
|
|||||||
In application controls
|
In application controls
|
||||||
| button| result|
|
| button| result|
|
||||||
|--|--|
|
|--|--|
|
||||||
| ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel|
|
| ```( 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 |
|
| ```( ↑ ↓ )``` 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|
|
| ```( enter )```| Run selected docker command.|
|
||||||
| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
|
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
||||||
| ```( 0 )``` | stop sorting |
|
| ```( 0 )``` | Stop sorting.|
|
||||||
| ```( h )``` | toggle help menu |
|
| ```( e )``` | Exec into the selected container.|
|
||||||
| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected|
|
| ```( h )``` | Toggle help menu.|
|
||||||
| ```( q )``` | to quit at any time |
|
| ```( 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
|
Available command line arguments
|
||||||
| argument|result|
|
| argument|result|
|
||||||
|--|--|
|
|--|--|
|
||||||
|```-d [number > 0]```| set the minimum update interval for docker information, in ms, defaults to 1000 (1 second) |
|
|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. 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, removes ANSI formatting (conflicts with `-c`).|
|
||||||
|```-r```| show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
|
|```-c```| Attempt to color the logs (conflicts with `-r`).|
|
||||||
|```-c```| attempt to color the logs (conflicts with -r) |
|
|```-t```| Remove timestamps from each log entry.|
|
||||||
|```-t```| remove timestamps from each log entry |
|
|```-s```| If running via Docker, will display the oxker container.|
|
||||||
|```-s```| if running via docker, will show the oxker container |
|
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
||||||
|```-g```| no tui, basically a pointless debugging mode, 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
|
## Build step
|
||||||
|
|
||||||
|
|||||||
@@ -49,14 +49,13 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
|
|||||||
## Runtime ##
|
## 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
|
ENV OXKER_RUNTIME=container
|
||||||
|
|
||||||
# Copy application binary from builder image
|
|
||||||
COPY --from=BUILDER /oxker /app/
|
COPY --from=BUILDER /oxker /app/
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
# this is used in the application itself, to stop itself from listing itself, so DO NOT EDIT
|
# this is used in the application itself so DO NOT EDIT
|
||||||
ENTRYPOINT [ "/app/oxker"]
|
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
|
# Buildx command to build musl version for all three platforms, should probably be executed in create_release
|
||||||
# docker buildx create --use
|
# 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 .
|
# 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:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 128M
|
memory: 1024M
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine3.18
|
image: redis:alpine3.18
|
||||||
container_name: redis
|
container_name: redis
|
||||||
@@ -27,7 +27,7 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 16M
|
memory: 384M
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3
|
image: rabbitmq:3
|
||||||
container_name: rabbitmq
|
container_name: rabbitmq
|
||||||
@@ -38,6 +38,6 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 256M
|
memory: 512M
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ impl ContainerId {
|
|||||||
pub fn get(&self) -> &str {
|
pub fn get(&self) -> &str {
|
||||||
self.0.as_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 {
|
impl Ord for ContainerId {
|
||||||
@@ -121,6 +126,9 @@ pub enum State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
pub const fn is_alive(self) -> bool {
|
||||||
|
matches!(self, Self::Running)
|
||||||
|
}
|
||||||
pub const fn get_color(self) -> Color {
|
pub const fn get_color(self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Self::Paused => Color::Yellow,
|
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 {
|
impl fmt::Display for State {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let disp = match self {
|
let disp = match self {
|
||||||
@@ -216,7 +230,7 @@ impl fmt::Display for DockerControls {
|
|||||||
Self::Restart => "restart",
|
Self::Restart => "restart",
|
||||||
Self::Start => "start",
|
Self::Start => "start",
|
||||||
Self::Stop => "stop",
|
Self::Stop => "stop",
|
||||||
Self::Unpause => "unpause",
|
Self::Unpause => "resume",
|
||||||
};
|
};
|
||||||
write!(f, "{disp}")
|
write!(f, "{disp}")
|
||||||
}
|
}
|
||||||
@@ -434,6 +448,20 @@ pub struct ContainerItem {
|
|||||||
pub is_oxker: bool,
|
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 {
|
impl ContainerItem {
|
||||||
/// Create a new container item
|
/// Create a new container item
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|||||||
+97
-58
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
pub use container_state::*;
|
pub use container_state::*;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
/// Global app_state, stored in an Arc<Mutex>
|
/// Global app_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
@@ -26,6 +27,17 @@ pub struct AppData {
|
|||||||
pub args: CliArgs,
|
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)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum SortedOrder {
|
pub enum SortedOrder {
|
||||||
Asc,
|
Asc,
|
||||||
@@ -64,6 +76,17 @@ impl fmt::Display for Header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
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
|
/// Change the sorted order, also set the selected container state to match new order
|
||||||
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
|
||||||
self.sorted_by = x;
|
self.sorted_by = x;
|
||||||
@@ -86,6 +109,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a default app_state
|
/// Generate a default app_state
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
pub fn default(args: CliArgs) -> Self {
|
pub fn default(args: CliArgs) -> Self {
|
||||||
Self {
|
Self {
|
||||||
args,
|
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
|
/// Container sort related methods
|
||||||
|
|
||||||
/// Remove the sorted header & order, and sort by default - created datetime
|
/// Remove the sorted header & order, and sort by default - created datetime
|
||||||
@@ -120,78 +156,85 @@ impl AppData {
|
|||||||
self.sorted_by
|
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
|
/// If not sort set, then sort by created time
|
||||||
pub fn sort_containers(&mut self) {
|
pub fn sort_containers(&mut self) {
|
||||||
if let Some((head, ord)) = self.sorted_by {
|
if let Some((head, ord)) = self.sorted_by {
|
||||||
|
let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
|
||||||
match head {
|
match head {
|
||||||
Header::State => match ord {
|
Header::State => match ord {
|
||||||
SortedOrder::Asc => self
|
SortedOrder::Asc => a
|
||||||
.containers
|
.state
|
||||||
.items
|
.order()
|
||||||
.sort_by(|a, b| b.state.order().cmp(&a.state.order())),
|
.cmp(&b.state.order())
|
||||||
SortedOrder::Desc => self
|
.then_with(|| a.name.cmp(&b.name)),
|
||||||
.containers
|
SortedOrder::Desc => b
|
||||||
.items
|
.state
|
||||||
.sort_by(|a, b| a.state.order().cmp(&b.state.order())),
|
.order()
|
||||||
|
.cmp(&a.state.order())
|
||||||
|
.then_with(|| b.name.cmp(&a.name)),
|
||||||
},
|
},
|
||||||
Header::Status => match ord {
|
Header::Status => match ord {
|
||||||
SortedOrder::Asc => self
|
SortedOrder::Asc => {
|
||||||
.containers
|
a.status.cmp(&b.status).then_with(|| a.name.cmp(&b.name))
|
||||||
.items
|
}
|
||||||
.sort_by(|a, b| a.status.cmp(&b.status)),
|
SortedOrder::Desc => {
|
||||||
SortedOrder::Desc => self
|
b.status.cmp(&a.status).then_with(|| b.name.cmp(&a.name))
|
||||||
.containers
|
}
|
||||||
.items
|
|
||||||
.sort_by(|a, b| b.status.cmp(&a.status)),
|
|
||||||
},
|
},
|
||||||
Header::Cpu => match ord {
|
Header::Cpu => match ord {
|
||||||
SortedOrder::Asc => self
|
SortedOrder::Asc => a
|
||||||
.containers
|
.cpu_stats
|
||||||
.items
|
.back()
|
||||||
.sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
|
.cmp(&b.cpu_stats.back())
|
||||||
SortedOrder::Desc => self
|
.then_with(|| a.name.cmp(&b.name)),
|
||||||
.containers
|
SortedOrder::Desc => b
|
||||||
.items
|
.cpu_stats
|
||||||
.sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
|
.back()
|
||||||
|
.cmp(&a.cpu_stats.back())
|
||||||
|
.then_with(|| b.name.cmp(&a.name)),
|
||||||
},
|
},
|
||||||
Header::Memory => match ord {
|
Header::Memory => match ord {
|
||||||
SortedOrder::Asc => self
|
SortedOrder::Asc => a
|
||||||
.containers
|
.mem_stats
|
||||||
.items
|
.back()
|
||||||
.sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
|
.cmp(&b.mem_stats.back())
|
||||||
SortedOrder::Desc => self
|
.then_with(|| a.name.cmp(&b.name)),
|
||||||
.containers
|
SortedOrder::Desc => b
|
||||||
.items
|
.mem_stats
|
||||||
.sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
|
.back()
|
||||||
|
.cmp(&a.mem_stats.back())
|
||||||
|
.then_with(|| b.name.cmp(&a.name)),
|
||||||
},
|
},
|
||||||
Header::Id => match ord {
|
Header::Id => match ord {
|
||||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
|
SortedOrder::Asc => a.id.cmp(&b.id).then_with(|| a.name.cmp(&b.name)),
|
||||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
|
SortedOrder::Desc => b.id.cmp(&a.id).then_with(|| b.name.cmp(&a.name)),
|
||||||
},
|
},
|
||||||
Header::Image => match ord {
|
Header::Image => match ord {
|
||||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
|
SortedOrder::Asc => a.image.cmp(&b.image).then_with(|| a.name.cmp(&b.name)),
|
||||||
SortedOrder::Desc => {
|
SortedOrder::Desc => {
|
||||||
self.containers.items.sort_by(|a, b| b.image.cmp(&a.image));
|
b.image.cmp(&a.image).then_with(|| b.name.cmp(&a.name))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Header::Name => match ord {
|
Header::Name => match ord {
|
||||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
|
SortedOrder::Asc => a.name.cmp(&b.name).then_with(|| a.id.cmp(&b.id)),
|
||||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
|
SortedOrder::Desc => b.name.cmp(&a.name).then_with(|| b.id.cmp(&a.id)),
|
||||||
},
|
},
|
||||||
Header::Rx => match ord {
|
Header::Rx => match ord {
|
||||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.rx.cmp(&b.rx)),
|
SortedOrder::Asc => a.rx.cmp(&b.rx).then_with(|| a.name.cmp(&b.name)),
|
||||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.rx.cmp(&a.rx)),
|
SortedOrder::Desc => b.rx.cmp(&a.rx).then_with(|| b.name.cmp(&a.name)),
|
||||||
},
|
},
|
||||||
Header::Tx => match ord {
|
Header::Tx => match ord {
|
||||||
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.tx.cmp(&b.tx)),
|
SortedOrder::Asc => a.tx.cmp(&b.tx).then_with(|| a.name.cmp(&b.name)),
|
||||||
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.tx.cmp(&a.tx)),
|
SortedOrder::Desc => b.tx.cmp(&a.tx).then_with(|| b.name.cmp(&a.name)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
self.containers.items.sort_by(sort_closure);
|
||||||
} else {
|
} else {
|
||||||
self.containers
|
self.containers
|
||||||
.items
|
.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)
|
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.
|
/// Find the widths for the strings in the containers panel.
|
||||||
/// So can display nicely and evenly
|
/// So can display nicely and evenly
|
||||||
pub fn get_width(&self) -> Columns {
|
pub fn get_width(&self) -> Columns {
|
||||||
@@ -482,6 +513,12 @@ impl AppData {
|
|||||||
self.get_selected_container().map(|i| i.id.clone())
|
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
|
/// 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
|
/// Will also, if a sort is set, sort the containers
|
||||||
pub fn update_stats(
|
pub fn update_stats(
|
||||||
@@ -563,6 +600,8 @@ impl AppData {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let id = ContainerId::from(id.as_str());
|
||||||
|
|
||||||
let is_oxker = i
|
let is_oxker = i
|
||||||
.command
|
.command
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -579,8 +618,6 @@ impl AppData {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(String::new(), std::clone::Clone::clone);
|
.map_or(String::new(), std::clone::Clone::clone);
|
||||||
|
|
||||||
let id = ContainerId::from(id.as_str());
|
|
||||||
|
|
||||||
let created = i
|
let created = i
|
||||||
.created
|
.created
|
||||||
.map_or(0, |i| u64::try_from(i).unwrap_or_default());
|
.map_or(0, |i| u64::try_from(i).unwrap_or_default());
|
||||||
@@ -624,6 +661,7 @@ impl AppData {
|
|||||||
let timestamp = self.args.timestamp;
|
let timestamp = self.args.timestamp;
|
||||||
|
|
||||||
if let Some(container) = self.get_container_by_id(id) {
|
if let Some(container) = self.get_container_by_id(id) {
|
||||||
|
if !container.is_oxker {
|
||||||
container.last_updated = Self::get_systemtime();
|
container.last_updated = Self::get_systemtime();
|
||||||
let current_len = container.logs.len();
|
let current_len = container.logs.len();
|
||||||
|
|
||||||
@@ -653,3 +691,4 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -6,6 +6,8 @@ use std::fmt;
|
|||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
DockerCommand(DockerControls),
|
DockerCommand(DockerControls),
|
||||||
|
DockerExec,
|
||||||
|
DockerLogs,
|
||||||
DockerConnect,
|
DockerConnect,
|
||||||
DockerInterval,
|
DockerInterval,
|
||||||
InputPoll,
|
InputPoll,
|
||||||
@@ -18,6 +20,8 @@ impl fmt::Display for AppError {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::DockerCommand(s) => write!(f, "Unable to {s} container"),
|
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::DockerConnect => write!(f, "Unable to access docker daemon"),
|
||||||
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
|
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
|
||||||
Self::InputPoll => write!(f, "Unable to poll user input"),
|
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" };
|
let reason = if *x { "en" } else { "dis" };
|
||||||
write!(f, "Unable to {reason}able mouse capture")
|
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 {
|
pub enum DockerMessage {
|
||||||
Delete(ContainerId),
|
|
||||||
ConfirmDelete(ContainerId),
|
ConfirmDelete(ContainerId),
|
||||||
|
Delete(ContainerId),
|
||||||
|
Exec(Sender<Arc<Docker>>),
|
||||||
Pause(ContainerId),
|
Pause(ContainerId),
|
||||||
Quit,
|
Quit,
|
||||||
Restart(ContainerId),
|
Restart(ContainerId),
|
||||||
|
|||||||
+83
-74
@@ -10,13 +10,19 @@ use futures_util::StreamExt;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
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 uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, DockerControls},
|
app_data::{AppData, ContainerId, DockerControls, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
parse_args::CliArgs,
|
parse_args::CliArgs,
|
||||||
ui::{GuiState, Status},
|
ui::{GuiState, Status},
|
||||||
@@ -54,10 +60,10 @@ pub struct DockerData {
|
|||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
args: CliArgs,
|
args: CliArgs,
|
||||||
binate: Binate,
|
binate: Binate,
|
||||||
containerised: bool,
|
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
|
init: Option<Arc<AtomicUsize>>,
|
||||||
receiver: Receiver<DockerMessage>,
|
receiver: Receiver<DockerMessage>,
|
||||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||||
}
|
}
|
||||||
@@ -97,22 +103,29 @@ impl DockerData {
|
|||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
id: ContainerId,
|
id: ContainerId,
|
||||||
is_running: bool,
|
init: Option<(Arc<AtomicUsize>, usize)>,
|
||||||
|
state: State,
|
||||||
spawn_id: SpawnId,
|
spawn_id: SpawnId,
|
||||||
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
|
||||||
) {
|
) {
|
||||||
|
if state.is_alive() || init.is_some() {
|
||||||
let mut stream = docker
|
let mut stream = docker
|
||||||
.stats(
|
.stats(
|
||||||
id.get(),
|
id.get(),
|
||||||
Some(StatsOptions {
|
Some(StatsOptions {
|
||||||
stream: false,
|
stream: false,
|
||||||
one_shot: !is_running,
|
one_shot: false,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.take(1);
|
.take(1);
|
||||||
|
|
||||||
while let Some(Ok(stats)) = stream.next().await {
|
while let Some(Ok(stats)) = stream.next().await {
|
||||||
let mem_stat = stats.memory_stats.usage.unwrap_or_default();
|
let mem_stat = if state.is_alive() {
|
||||||
|
Some(stats.memory_stats.usage.unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
|
let mem_limit = stats.memory_stats.limit.unwrap_or_default();
|
||||||
|
|
||||||
let op_key = stats
|
let op_key = stats
|
||||||
@@ -120,8 +133,11 @@ impl DockerData {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|networks| networks.keys().next().cloned());
|
.and_then(|networks| networks.keys().next().cloned());
|
||||||
|
|
||||||
let cpu_stats = Self::calculate_usage(&stats);
|
let cpu_stats = if state.is_alive() {
|
||||||
|
Some(Self::calculate_usage(&stats))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let (rx, tx) = if let Some(key) = op_key {
|
let (rx, tx) = if let Some(key) = op_key {
|
||||||
stats
|
stats
|
||||||
.networks
|
.networks
|
||||||
@@ -132,31 +148,27 @@ impl DockerData {
|
|||||||
(0, 0)
|
(0, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_running {
|
|
||||||
app_data.lock().update_stats(
|
|
||||||
&id,
|
|
||||||
Some(cpu_stats),
|
|
||||||
Some(mem_stat),
|
|
||||||
mem_limit,
|
|
||||||
rx,
|
|
||||||
tx,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
app_data
|
app_data
|
||||||
.lock()
|
.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
|
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||||
fn update_all_container_stats(&mut self, all_ids: &[(bool, ContainerId)]) {
|
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
||||||
for (is_running, id) in all_ids {
|
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 docker = Arc::clone(&self.docker);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
let spawns = Arc::clone(&self.spawns);
|
let spawns = Arc::clone(&self.spawns);
|
||||||
let spawn_id = SpawnId::Stats((id.clone(), self.binate));
|
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
|
self.spawns
|
||||||
.lock()
|
.lock()
|
||||||
.entry(spawn_id.clone())
|
.entry(spawn_id.clone())
|
||||||
@@ -165,7 +177,8 @@ impl DockerData {
|
|||||||
app_data,
|
app_data,
|
||||||
docker,
|
docker,
|
||||||
id.clone(),
|
id.clone(),
|
||||||
*is_running,
|
init,
|
||||||
|
*state,
|
||||||
spawn_id,
|
spawn_id,
|
||||||
spawns,
|
spawns,
|
||||||
))
|
))
|
||||||
@@ -177,7 +190,7 @@ impl DockerData {
|
|||||||
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here
|
/// 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
|
/// 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
|
/// 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
|
let containers = self
|
||||||
.docker
|
.docker
|
||||||
.list_containers(Some(ListContainersOptions::<String> {
|
.list_containers(Some(ListContainersOptions::<String> {
|
||||||
@@ -191,7 +204,7 @@ impl DockerData {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|f| match f.id {
|
.filter_map(|f| match f.id {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
if self.containerised
|
if self.args.in_container
|
||||||
&& f.command
|
&& f.command
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |c| c.starts_with(ENTRY_POINT))
|
.map_or(false, |c| c.starts_with(ENTRY_POINT))
|
||||||
@@ -212,13 +225,7 @@ impl DockerData {
|
|||||||
output
|
output
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|i| {
|
.filter_map(|i| {
|
||||||
i.id.map(|id| {
|
i.id.map(|id| (State::from(i.state), ContainerId::from(id.as_str())))
|
||||||
(
|
|
||||||
i.state == Some("running".to_owned())
|
|
||||||
|| i.state == Some("restarting".to_owned()),
|
|
||||||
ContainerId::from(id.as_str()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
@@ -253,7 +260,7 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update all logs, spawn each container into own tokio::spawn thread
|
/// 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 {
|
for (_, id) in all_ids {
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
@@ -275,6 +282,7 @@ impl DockerData {
|
|||||||
.lock()
|
.lock()
|
||||||
.entry(SpawnId::Log(container.id.clone()))
|
.entry(SpawnId::Log(container.id.clone()))
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
|
// MAYBE make a struct that can create this data?
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let id = container.id.clone();
|
let id = container.id.clone();
|
||||||
@@ -286,44 +294,26 @@ impl DockerData {
|
|||||||
self.app_data.lock().sort_containers();
|
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
|
/// Initialize docker container data, before any messages are received
|
||||||
async fn initialise_container_data(&mut self) {
|
async fn initialise_container_data(&mut self) {
|
||||||
self.gui_state.lock().status_push(Status::Init);
|
self.gui_state.lock().status_push(Status::Init);
|
||||||
let loading_uuid = Uuid::new_v4();
|
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;
|
let all_ids = self.update_all_containers().await;
|
||||||
|
|
||||||
self.update_all_container_stats(&all_ids);
|
self.update_all_container_stats(&all_ids);
|
||||||
|
|
||||||
self.init_all_logs(&all_ids);
|
self.init_all_logs(&all_ids);
|
||||||
|
|
||||||
// wait until all logs have initialised
|
while let Some(x) = self.init.as_ref() {
|
||||||
while !self.app_data.lock().initialised(&all_ids) {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
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);
|
self.gui_state.lock().status_del(Status::Init);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +330,7 @@ impl DockerData {
|
|||||||
|
|
||||||
/// Handle incoming messages, container controls & all container information update
|
/// Handle incoming messages, container controls & all container information update
|
||||||
/// Spawn Docker commands off into own thread
|
/// Spawn Docker commands off into own thread
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn message_handler(&mut self) {
|
async fn message_handler(&mut self) {
|
||||||
while let Some(message) = self.receiver.recv().await {
|
while let Some(message) = self.receiver.recv().await {
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
@@ -348,29 +339,32 @@ impl DockerData {
|
|||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
// TODO need to refactor these
|
// TODO need to refactor these
|
||||||
match message {
|
match message {
|
||||||
|
DockerMessage::Exec(docker_tx) => {
|
||||||
|
docker_tx.send(Arc::clone(&self.docker)).ok();
|
||||||
|
}
|
||||||
DockerMessage::Pause(id) => {
|
DockerMessage::Pause(id) => {
|
||||||
tokio::spawn(async move {
|
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() {
|
if docker.pause_container(id.get()).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
|
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;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Restart(id) => {
|
DockerMessage::Restart(id) => {
|
||||||
tokio::spawn(async move {
|
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() {
|
if docker.restart_container(id.get(), None).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
|
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;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Start(id) => {
|
DockerMessage::Start(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let loading_spin = Self::loading_spin(uuid, &gui_state);
|
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker
|
if docker
|
||||||
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
||||||
.await
|
.await
|
||||||
@@ -378,33 +372,33 @@ impl DockerData {
|
|||||||
{
|
{
|
||||||
Self::set_error(&app_data, DockerControls::Start, &gui_state);
|
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;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Stop(id) => {
|
DockerMessage::Stop(id) => {
|
||||||
tokio::spawn(async move {
|
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() {
|
if docker.stop_container(id.get(), None).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
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.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Unpause(id) => {
|
DockerMessage::Unpause(id) => {
|
||||||
tokio::spawn(async move {
|
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() {
|
if docker.unpause_container(id.get()).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Unpause, &gui_state);
|
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;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Delete(id) => {
|
DockerMessage::Delete(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let loading_spin = Self::loading_spin(uuid, &gui_state);
|
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker
|
if docker
|
||||||
.remove_container(
|
.remove_container(
|
||||||
id.get(),
|
id.get(),
|
||||||
@@ -419,7 +413,7 @@ impl DockerData {
|
|||||||
{
|
{
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
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.update_everything().await;
|
||||||
self.gui_state.lock().set_delete_container(None);
|
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
|
/// Initialise self, and start the message receiving loop
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
containerised: bool,
|
|
||||||
docker: Docker,
|
docker: Docker,
|
||||||
docker_rx: Receiver<DockerMessage>,
|
docker_rx: Receiver<DockerMessage>,
|
||||||
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
@@ -453,16 +461,17 @@ impl DockerData {
|
|||||||
if app_data.lock().get_error().is_none() {
|
if app_data.lock().get_error().is_none() {
|
||||||
let mut inner = Self {
|
let mut inner = Self {
|
||||||
app_data,
|
app_data,
|
||||||
containerised,
|
args: args.clone(),
|
||||||
args,
|
|
||||||
binate: Binate::One,
|
binate: Binate::One,
|
||||||
docker: Arc::new(docker),
|
docker: Arc::new(docker),
|
||||||
gui_state,
|
gui_state,
|
||||||
|
init: Some(Arc::new(AtomicUsize::new(0))),
|
||||||
is_running,
|
is_running,
|
||||||
receiver: docker_rx,
|
receiver: docker_rx,
|
||||||
spawns: Arc::new(Mutex::new(HashMap::new())),
|
spawns: Arc::new(Mutex::new(HashMap::new())),
|
||||||
};
|
};
|
||||||
inner.initialise_container_data().await;
|
inner.initialise_container_data().await;
|
||||||
|
Self::croner(&args, docker_tx);
|
||||||
inner.message_handler().await;
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+266
-159
@@ -1,26 +1,32 @@
|
|||||||
use std::sync::{
|
use std::{
|
||||||
|
fs::OpenOptions,
|
||||||
|
io::{BufWriter, Write},
|
||||||
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
|
},
|
||||||
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bollard::{container::LogsOptions, Docker};
|
||||||
|
use cansi::v3::categorise_text;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
||||||
execute,
|
execute,
|
||||||
};
|
};
|
||||||
|
use futures_util::StreamExt;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use tokio::{
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
sync::mpsc::{Receiver, Sender},
|
use uuid::Uuid;
|
||||||
task::JoinHandle,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, DockerControls, Header},
|
app_data::{AppData, DockerControls, Header},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
docker_data::DockerMessage,
|
docker_data::DockerMessage,
|
||||||
|
exec::{tty_readable, ExecMode},
|
||||||
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
||||||
value_capture,
|
|
||||||
};
|
};
|
||||||
pub use message::InputMessages;
|
pub use message::InputMessages;
|
||||||
|
|
||||||
@@ -28,9 +34,8 @@ pub use message::InputMessages;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InputHandler {
|
pub struct InputHandler {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker_sender: Sender<DockerMessage>,
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
info_sleep: Option<JoinHandle<()>>,
|
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
mouse_capture: bool,
|
mouse_capture: bool,
|
||||||
rec: Receiver<InputMessages>,
|
rec: Receiver<InputMessages>,
|
||||||
@@ -41,18 +46,17 @@ impl InputHandler {
|
|||||||
pub async fn init(
|
pub async fn init(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
rec: Receiver<InputMessages>,
|
rec: Receiver<InputMessages>,
|
||||||
docker_sender: Sender<DockerMessage>,
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let mut inner = Self {
|
let mut inner = Self {
|
||||||
app_data,
|
app_data,
|
||||||
docker_sender,
|
docker_tx,
|
||||||
gui_state,
|
gui_state,
|
||||||
is_running,
|
is_running,
|
||||||
rec,
|
rec,
|
||||||
mouse_capture: true,
|
mouse_capture: true,
|
||||||
info_sleep: None,
|
|
||||||
};
|
};
|
||||||
inner.start().await;
|
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)
|
/// Toggle the mouse capture (via input of the 'm' key)
|
||||||
fn m_key(&mut self) {
|
fn m_key(&mut self) {
|
||||||
if self.mouse_capture {
|
if self.mouse_capture {
|
||||||
@@ -111,75 +173,199 @@ impl InputHandler {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window
|
|
||||||
// then cancel the first handle, as a new handle will be invoked
|
|
||||||
if let Some(info_sleep_timer) = self.info_sleep.as_ref() {
|
|
||||||
info_sleep_timer.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
let gui_state = Arc::clone(&self.gui_state);
|
|
||||||
// Show the info box - with "mouse capture enabled / disabled", for 4000 ms
|
|
||||||
self.info_sleep = Some(tokio::spawn(async move {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(4000)).await;
|
|
||||||
gui_state.lock().reset_info_box();
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.mouse_capture = !self.mouse_capture;
|
self.mouse_capture = !self.mouse_capture;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort the containers by a given header
|
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
|
||||||
fn sort(&self, selected_header: Header) {
|
async fn s_key(&mut self) {
|
||||||
self.app_data.lock().set_sort_by_header(selected_header);
|
/// This is the inner workings, *inlined* here to return a Result
|
||||||
|
async fn save_logs(
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
docker_tx: &Sender<DockerMessage>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args = app_data.lock().args.clone();
|
||||||
|
let container = app_data.lock().get_selected_container_id_state_name();
|
||||||
|
if let Some((id, _, name)) = container {
|
||||||
|
if let Some(log_path) = args.save_dir {
|
||||||
|
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
|
||||||
|
docker_tx.send(DockerMessage::Exec(sx)).await?;
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.map_or(0, |i| i.as_secs());
|
||||||
|
|
||||||
|
let path = log_path.join(format!("{name}_{now}.log"));
|
||||||
|
|
||||||
|
let docker = rx.await?;
|
||||||
|
let options = Some(LogsOptions::<String> {
|
||||||
|
stdout: true,
|
||||||
|
timestamps: args.timestamp,
|
||||||
|
since: 0,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let mut logs = docker.logs(id.get(), options);
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
while let Some(Ok(value)) = logs.next().await {
|
||||||
|
let data = value.to_string();
|
||||||
|
if !data.trim().is_empty() {
|
||||||
|
output.push(
|
||||||
|
categorise_text(&data)
|
||||||
|
.into_iter()
|
||||||
|
.map(|i| i.text)
|
||||||
|
.collect::<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !output.is_empty() {
|
||||||
|
let mut stream = BufWriter::new(
|
||||||
|
OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.open(&path)?,
|
||||||
|
);
|
||||||
|
|
||||||
|
for line in &output {
|
||||||
|
stream.write_all(line.as_bytes())?;
|
||||||
|
}
|
||||||
|
stream.flush()?;
|
||||||
|
|
||||||
|
gui_state
|
||||||
|
.lock()
|
||||||
|
.set_info_box(&format!("saved to {}", path.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead
|
let log_status = Status::Logs;
|
||||||
/// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit
|
let status = self.gui_state.lock().status_contains(&[log_status]);
|
||||||
async fn quit(&self) {
|
if !status {
|
||||||
let error_init = self
|
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
|
.gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.status_contains(&[Status::Error, Status::Init]);
|
.status_contains(&[Status::DeleteConfirm]);
|
||||||
if error_init || self.docker_sender.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
|
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel)
|
let contains_error = contains(Status::Error);
|
||||||
fn clear_delete(&self) {
|
let contains_help = contains(Status::Help);
|
||||||
self.gui_state.lock().set_delete_container(None);
|
let contains_exec = contains(Status::Exec);
|
||||||
}
|
|
||||||
|
|
||||||
/// 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])
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if !contains_exec {
|
||||||
// Always just quit on Ctrl + c/C or q/Q
|
// 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_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||||
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||||
@@ -216,52 +402,14 @@ impl InputHandler {
|
|||||||
KeyCode::Char('7') => self.sort(Header::Image),
|
KeyCode::Char('7') => self.sort(Header::Image),
|
||||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
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('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
KeyCode::Tab => {
|
KeyCode::Char('s' | 'S') => self.s_key().await,
|
||||||
// Skip control panel if no containers, could be refactored
|
KeyCode::Tab => self.tab_key(),
|
||||||
let is_containers =
|
KeyCode::BackTab => self.back_tab_key(),
|
||||||
self.gui_state.lock().selected_panel == SelectablePanel::Containers;
|
KeyCode::Home => self.home_key(),
|
||||||
let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
|
KeyCode::End => self.end_key(),
|
||||||
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::Up | KeyCode::Char('k' | 'K') => self.previous(),
|
||||||
KeyCode::PageUp => {
|
KeyCode::PageUp => {
|
||||||
for _ in 0..=6 {
|
for _ in 0..=6 {
|
||||||
@@ -274,53 +422,12 @@ impl InputHandler {
|
|||||||
self.next();
|
self.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => self.enter_key().await,
|
||||||
// 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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
|
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
|
||||||
async fn button_intersect(&mut self, mouse_event: MouseEvent) {
|
async fn button_intersect(&mut self, mouse_event: MouseEvent) {
|
||||||
@@ -370,7 +477,7 @@ impl InputHandler {
|
|||||||
/// Change state to next, depending which panel is currently in focus
|
/// Change state to next, depending which panel is currently in focus
|
||||||
fn next(&mut self) {
|
fn next(&mut self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
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 {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_next(),
|
SelectablePanel::Containers => locked_data.containers_next(),
|
||||||
SelectablePanel::Logs => locked_data.log_next(),
|
SelectablePanel::Logs => locked_data.log_next(),
|
||||||
@@ -381,7 +488,7 @@ impl InputHandler {
|
|||||||
/// Change state to previous, depending which panel is currently in focus
|
/// Change state to previous, depending which panel is currently in focus
|
||||||
fn previous(&mut self) {
|
fn previous(&mut self) {
|
||||||
let mut locked_data = self.app_data.lock();
|
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 {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_previous(),
|
SelectablePanel::Containers => locked_data.containers_previous(),
|
||||||
SelectablePanel::Logs => locked_data.log_previous(),
|
SelectablePanel::Logs => locked_data.log_previous(),
|
||||||
|
|||||||
+36
-50
@@ -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
|
// Only allow when debugging
|
||||||
// #![allow(unused)]
|
// #![allow(unused)]
|
||||||
|
|
||||||
@@ -35,6 +21,7 @@ use tracing::{error, info, Level};
|
|||||||
mod app_data;
|
mod app_data;
|
||||||
mod app_error;
|
mod app_error;
|
||||||
mod docker_data;
|
mod docker_data;
|
||||||
|
mod exec;
|
||||||
mod input_handler;
|
mod input_handler;
|
||||||
mod parse_args;
|
mod parse_args;
|
||||||
mod ui;
|
mod ui;
|
||||||
@@ -55,18 +42,6 @@ fn setup_tracing() {
|
|||||||
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
|
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
|
/// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env
|
||||||
fn read_docker_host(args: &CliArgs) -> Option<String> {
|
fn read_docker_host(args: &CliArgs) -> Option<String> {
|
||||||
args.host
|
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
|
/// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error
|
||||||
async fn docker_init(
|
async fn docker_init(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
containerised: bool,
|
|
||||||
docker_rx: Receiver<DockerMessage>,
|
docker_rx: Receiver<DockerMessage>,
|
||||||
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
is_running: &Arc<AtomicBool>,
|
is_running: &Arc<AtomicBool>,
|
||||||
host: Option<String>,
|
host: Option<String>,
|
||||||
@@ -92,13 +67,9 @@ async fn docker_init(
|
|||||||
let app_data = Arc::clone(app_data);
|
let app_data = Arc::clone(app_data);
|
||||||
let gui_state = Arc::clone(gui_state);
|
let gui_state = Arc::clone(gui_state);
|
||||||
let is_running = Arc::clone(is_running);
|
let is_running = Arc::clone(is_running);
|
||||||
|
|
||||||
tokio::spawn(DockerData::init(
|
tokio::spawn(DockerData::init(
|
||||||
app_data,
|
app_data, docker, docker_rx, docker_tx, gui_state, is_running,
|
||||||
containerised,
|
|
||||||
docker,
|
|
||||||
docker_rx,
|
|
||||||
gui_state,
|
|
||||||
is_running,
|
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
app_data
|
app_data
|
||||||
@@ -120,36 +91,40 @@ fn handler_init(
|
|||||||
input_rx: Receiver<InputMessages>,
|
input_rx: Receiver<InputMessages>,
|
||||||
is_running: &Arc<AtomicBool>,
|
is_running: &Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
let input_app_data = Arc::clone(app_data);
|
let app_data = Arc::clone(app_data);
|
||||||
let input_gui_state = Arc::clone(gui_state);
|
let gui_state = Arc::clone(gui_state);
|
||||||
let input_is_running = Arc::clone(is_running);
|
let is_running = Arc::clone(is_running);
|
||||||
tokio::spawn(input_handler::InputHandler::init(
|
tokio::spawn(input_handler::InputHandler::init(
|
||||||
input_app_data,
|
app_data,
|
||||||
input_rx,
|
input_rx,
|
||||||
docker_sx.clone(),
|
docker_sx.clone(),
|
||||||
input_gui_state,
|
gui_state,
|
||||||
input_is_running,
|
is_running,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let containerised = check_if_containerised();
|
|
||||||
|
|
||||||
setup_tracing();
|
setup_tracing();
|
||||||
|
|
||||||
let args = CliArgs::new();
|
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 host = read_docker_host(&args);
|
||||||
|
|
||||||
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
||||||
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
||||||
let is_running = Arc::new(AtomicBool::new(true));
|
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(
|
docker_init(
|
||||||
&app_data,
|
&app_data,
|
||||||
containerised,
|
|
||||||
docker_rx,
|
docker_rx,
|
||||||
|
docker_tx.clone(),
|
||||||
&gui_state,
|
&gui_state,
|
||||||
&is_running,
|
&is_running,
|
||||||
host,
|
host,
|
||||||
@@ -157,23 +132,34 @@ async fn main() {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if args.gui {
|
if args.gui {
|
||||||
let (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
|
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
|
||||||
handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running);
|
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
||||||
Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await;
|
Ui::create(app_data, gui_state, input_tx, is_running).await;
|
||||||
} else {
|
} else {
|
||||||
info!("in debug mode");
|
info!("in debug mode\n");
|
||||||
// Debug mode for testing, mostly pointless, doesn't take terminal
|
// Debug mode for testing, less pointless now, will display some basic information
|
||||||
while is_running.load(Ordering::SeqCst) {
|
while is_running.load(Ordering::SeqCst) {
|
||||||
loop {
|
|
||||||
if let Some(err) = app_data.lock().get_error() {
|
if let Some(err) = app_data.lock().get_error() {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
docker_sx.send(DockerMessage::Update).await.ok();
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||||
args.docker_interval,
|
args.docker_interval,
|
||||||
)))
|
)))
|
||||||
.await;
|
.await;
|
||||||
|
let containers = app_data
|
||||||
|
.lock()
|
||||||
|
.get_container_items()
|
||||||
|
.clone()
|
||||||
|
.iter()
|
||||||
|
.map(|i| format!("{i}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !containers.is_empty() {
|
||||||
|
for item in containers {
|
||||||
|
info!("{item}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-9
@@ -1,12 +1,14 @@
|
|||||||
use std::process;
|
use std::{path::PathBuf, process};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{ENV_KEY, ENV_VALUE};
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[command(version, about)]
|
#[command(version, about)]
|
||||||
pub struct CliArgs {
|
pub struct Args {
|
||||||
/// Docker update interval in ms, minimum effectively 1000
|
/// Docker update interval in ms, minimum effectively 1000
|
||||||
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
|
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
|
||||||
pub docker_interval: u32,
|
pub docker_interval: u32,
|
||||||
@@ -19,10 +21,6 @@ pub struct CliArgs {
|
|||||||
#[clap(short = 'c', conflicts_with = "raw")]
|
#[clap(short = 'c', conflicts_with = "raw")]
|
||||||
pub color: bool,
|
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"
|
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
|
||||||
#[clap(short = 'r', conflicts_with = "color")]
|
#[clap(short = 'r', conflicts_with = "color")]
|
||||||
pub raw: bool,
|
pub raw: bool,
|
||||||
@@ -34,12 +32,55 @@ pub struct CliArgs {
|
|||||||
/// Don't draw gui - for debugging - mostly pointless
|
/// Don't draw gui - for debugging - mostly pointless
|
||||||
#[clap(short = 'g')]
|
#[clap(short = 'g')]
|
||||||
pub gui: bool,
|
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 {
|
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
|
/// Parse cli arguments
|
||||||
pub fn new() -> Self {
|
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
|
// Quit the program if the docker update argument is 0
|
||||||
// Should maybe change it to check if less than 100
|
// Should maybe change it to check if less than 100
|
||||||
@@ -50,10 +91,13 @@ impl CliArgs {
|
|||||||
Self {
|
Self {
|
||||||
color: args.color,
|
color: args.color,
|
||||||
docker_interval: args.docker_interval,
|
docker_interval: args.docker_interval,
|
||||||
host: args.host,
|
use_cli: args.use_cli,
|
||||||
gui: !args.gui,
|
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,
|
raw: args.raw,
|
||||||
|
show_self: !args.show_self,
|
||||||
timestamp: !args.timestamp,
|
timestamp: !args.timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-99
@@ -1,6 +1,5 @@
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
symbols,
|
symbols,
|
||||||
@@ -11,17 +10,19 @@ use ratatui::{
|
|||||||
},
|
},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
use std::default::Default;
|
use std::{default::Default, time::Instant};
|
||||||
use std::{fmt::Display, sync::Arc};
|
use std::{fmt::Display, sync::Arc};
|
||||||
|
|
||||||
use crate::app_data::{Header, SortedOrder};
|
use crate::app_data::{ContainerItem, Header, SortedOrder};
|
||||||
use crate::ui::Status;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::gui_state::{BoxLocation, DeleteButton, Region};
|
use super::{
|
||||||
|
gui_state::{BoxLocation, DeleteButton, Region},
|
||||||
|
FrameData,
|
||||||
|
};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
const NAME_TEXT: &str = r#"
|
const NAME_TEXT: &str = r#"
|
||||||
@@ -40,7 +41,7 @@ const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
|||||||
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||||
const MARGIN: &str = " ";
|
const MARGIN: &str = " ";
|
||||||
const ARROW: &str = "▶ ";
|
const RIGHT_ARROW: &str = "▶ ";
|
||||||
const CIRCLE: &str = "⚪ ";
|
const CIRCLE: &str = "⚪ ";
|
||||||
|
|
||||||
/// From a given &str, return the maximum number of chars on a single line
|
/// 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>(
|
fn generate_block<'a>(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
panel: SelectablePanel,
|
panel: SelectablePanel,
|
||||||
) -> Block<'a> {
|
) -> Block<'a> {
|
||||||
@@ -78,20 +80,21 @@ fn generate_block<'a>(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(title);
|
.title(title);
|
||||||
if gui_state.lock().selected_panel == panel {
|
if fd.selected_panel == panel {
|
||||||
block = block.border_style(Style::default().fg(Color::LightCyan));
|
block = block.border_style(Style::default().fg(Color::LightCyan));
|
||||||
}
|
}
|
||||||
block
|
block
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the command panel
|
/// Draw the command panel
|
||||||
pub fn commands<B: Backend>(
|
pub fn commands(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
f: &mut Frame<'_, B>,
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
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| {
|
let items = app_data.lock().get_control_items().map_or(vec![], |i| {
|
||||||
i.iter()
|
i.iter()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
@@ -107,7 +110,7 @@ pub fn commands<B: Backend>(
|
|||||||
let items = List::new(items)
|
let items = List::new(items)
|
||||||
.block(block())
|
.block(block())
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
.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() {
|
if let Some(i) = app_data.lock().get_control_state() {
|
||||||
f.render_stateful_widget(items, area, i);
|
f.render_stateful_widget(items, area, i);
|
||||||
@@ -119,25 +122,12 @@ pub fn commands<B: Backend>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the containers panel
|
/// Format the container data to display nicely on the screen
|
||||||
pub fn containers<B: Backend>(
|
fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
|
||||||
area: Rect,
|
|
||||||
f: &mut Frame<'_, B>,
|
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
|
||||||
widths: &Columns,
|
|
||||||
) {
|
|
||||||
let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
|
|
||||||
|
|
||||||
let items = app_data
|
|
||||||
.lock()
|
|
||||||
.get_container_items()
|
|
||||||
.iter()
|
|
||||||
.map(|i| {
|
|
||||||
let state_style = Style::default().fg(i.state.get_color());
|
let state_style = Style::default().fg(i.state.get_color());
|
||||||
let blue = Style::default().fg(Color::Blue);
|
let blue = Style::default().fg(Color::Blue);
|
||||||
|
|
||||||
let lines = Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(
|
format!(
|
||||||
"{:<width$}",
|
"{:<width$}",
|
||||||
@@ -177,7 +167,7 @@ pub fn containers<B: Backend>(
|
|||||||
format!(
|
format!(
|
||||||
"{}{:>width$}",
|
"{}{:>width$}",
|
||||||
MARGIN,
|
MARGIN,
|
||||||
i.id.get().chars().take(8).collect::<String>(),
|
i.id.get_short(),
|
||||||
width = &widths.id.1.into()
|
width = &widths.id.1.into()
|
||||||
),
|
),
|
||||||
blue,
|
blue,
|
||||||
@@ -198,9 +188,25 @@ pub fn containers<B: Backend>(
|
|||||||
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
|
format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()),
|
||||||
Style::default().fg(Color::Rgb(205, 140, 140)),
|
Style::default().fg(Color::Rgb(205, 140, 140)),
|
||||||
),
|
),
|
||||||
]);
|
])
|
||||||
ListItem::new(lines)
|
}
|
||||||
})
|
|
||||||
|
/// Draw the containers panel
|
||||||
|
pub fn containers(
|
||||||
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
|
area: Rect,
|
||||||
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
widths: &Columns,
|
||||||
|
) {
|
||||||
|
let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers);
|
||||||
|
|
||||||
|
let items = app_data
|
||||||
|
.lock()
|
||||||
|
.get_container_items()
|
||||||
|
.iter()
|
||||||
|
.map(|i| ListItem::new(format_containers(i, widths)))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
@@ -213,22 +219,21 @@ pub fn containers<B: Backend>(
|
|||||||
.block(block)
|
.block(block)
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
.highlight_symbol(CIRCLE);
|
.highlight_symbol(CIRCLE);
|
||||||
|
|
||||||
f.render_stateful_widget(items, area, app_data.lock().get_container_state());
|
f.render_stateful_widget(items, area, app_data.lock().get_container_state());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the logs panel
|
/// Draw the logs panel
|
||||||
pub fn logs<B: Backend>(
|
pub fn logs(
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
f: &mut Frame<'_, B>,
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
loading_icon: &str,
|
|
||||||
) {
|
) {
|
||||||
let block = || generate_block(app_data, area, gui_state, SelectablePanel::Logs);
|
let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs);
|
||||||
if gui_state.lock().status_contains(&[Status::Init]) {
|
if fd.init {
|
||||||
let paragraph = Paragraph::new(format!("parsing logs {loading_icon}"))
|
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
||||||
.style(Style::default())
|
.style(Style::default())
|
||||||
.block(block())
|
.block(block())
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
@@ -244,19 +249,18 @@ pub fn logs<B: Backend>(
|
|||||||
} else {
|
} else {
|
||||||
let items = List::new(logs)
|
let items = List::new(logs)
|
||||||
.block(block())
|
.block(block())
|
||||||
.highlight_symbol(ARROW)
|
.highlight_symbol(RIGHT_ARROW)
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||||
|
|
||||||
// This should always return Some, as logs is not empty
|
// This should always return Some, as logs is not empty
|
||||||
if let Some(i) = app_data.lock().get_log_state() {
|
if let Some(log_state) = app_data.lock().get_log_state() {
|
||||||
f.render_stateful_widget(items, area, i);
|
f.render_stateful_widget(items, area, log_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the cpu + mem charts
|
/// 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() {
|
if let Some((cpu, mem)) = app_data.lock().get_chart_data() {
|
||||||
let area = Layout::default()
|
let area = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.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)];
|
.data(&mem.0)];
|
||||||
|
|
||||||
let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
|
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 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 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);
|
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
|
/// Draw heading bar at top of program, always visible
|
||||||
/// TODO Should separate into loading icon/headers/help functions
|
/// TODO Should separate into loading icon/headers/help functions
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
pub fn heading_bar<B: Backend>(
|
pub fn heading_bar(
|
||||||
area: Rect,
|
area: Rect,
|
||||||
columns: &Columns,
|
frame: &mut Frame,
|
||||||
f: &mut Frame<'_, B>,
|
data: &FrameData,
|
||||||
has_containers: bool,
|
|
||||||
loading_icon: &str,
|
|
||||||
sorted_by: Option<(Header, SortedOrder)>,
|
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
) {
|
) {
|
||||||
let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg));
|
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
|
// 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 header_block = |x: &Header| {
|
||||||
let mut color = Color::Black;
|
let mut color = Color::Black;
|
||||||
let mut suffix = "";
|
let mut suffix = "";
|
||||||
let mut suffix_margin = 0;
|
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 {
|
if x == a {
|
||||||
match b {
|
match b {
|
||||||
SortedOrder::Asc => suffix = " ⌃",
|
SortedOrder::Asc => suffix = " ▲",
|
||||||
SortedOrder::Desc => suffix = " ⌄",
|
SortedOrder::Desc => suffix = " ▼",
|
||||||
}
|
}
|
||||||
suffix_margin = 2;
|
suffix_margin = 2;
|
||||||
color = Color::White;
|
color = Color::White;
|
||||||
@@ -408,15 +409,15 @@ pub fn heading_bar<B: Backend>(
|
|||||||
|
|
||||||
// Meta data to iterate over to create blocks with correct widths
|
// Meta data to iterate over to create blocks with correct widths
|
||||||
let header_meta = [
|
let header_meta = [
|
||||||
(Header::State, columns.state.1),
|
(Header::State, data.columns.state.1),
|
||||||
(Header::Status, columns.status.1),
|
(Header::Status, data.columns.status.1),
|
||||||
(Header::Cpu, columns.cpu.1),
|
(Header::Cpu, data.columns.cpu.1),
|
||||||
(Header::Memory, columns.mem.1 + columns.mem.2 + 3),
|
(Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3),
|
||||||
(Header::Id, columns.id.1),
|
(Header::Id, data.columns.id.1),
|
||||||
(Header::Name, columns.name.1),
|
(Header::Name, data.columns.name.1),
|
||||||
(Header::Image, columns.image.1),
|
(Header::Image, data.columns.image.1),
|
||||||
(Header::Rx, columns.net_rx.1),
|
(Header::Rx, data.columns.net_rx.1),
|
||||||
(Header::Tx, columns.net_tx.1),
|
(Header::Tx, data.columns.net_tx.1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let header_data = header_meta
|
let header_data = header_meta
|
||||||
@@ -427,13 +428,13 @@ pub fn heading_bar<B: Backend>(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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_text = format!("( h ) {suffix} help {MARGIN}",);
|
||||||
let info_width = info_text.chars().count();
|
let info_width = info_text.chars().count();
|
||||||
|
|
||||||
let column_width = usize::from(area.width).saturating_sub(info_width);
|
let column_width = usize::from(area.width).saturating_sub(info_width);
|
||||||
let column_width = if column_width > 0 { column_width } else { 1 };
|
let column_width = if column_width > 0 { column_width } else { 1 };
|
||||||
let splits = if has_containers {
|
let splits = if data.has_containers {
|
||||||
vec![
|
vec![
|
||||||
Constraint::Min(2),
|
Constraint::Min(2),
|
||||||
Constraint::Min(column_width.try_into().unwrap_or_default()),
|
Constraint::Min(column_width.try_into().unwrap_or_default()),
|
||||||
@@ -445,20 +446,19 @@ pub fn heading_bar<B: Backend>(
|
|||||||
|
|
||||||
let split_bar = Layout::default()
|
let split_bar = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(splits.as_ref())
|
.constraints(splits)
|
||||||
.split(area);
|
.split(area);
|
||||||
if has_containers {
|
if data.has_containers {
|
||||||
// Draw loading icon, or not, and a prefix with a single space
|
// Draw loading icon, or not, and a prefix with a single space
|
||||||
let loading_icon = format!("{loading_icon:>2}");
|
let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
|
||||||
let loading_paragraph = Paragraph::new(loading_icon)
|
|
||||||
.block(block(Color::White))
|
.block(block(Color::White))
|
||||||
.alignment(Alignment::Center);
|
.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 container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||||
let headers_section = Layout::default()
|
let headers_section = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(container_splits.as_ref())
|
.constraints(container_splits)
|
||||||
.split(split_bar[1]);
|
.split(split_bar[1]);
|
||||||
|
|
||||||
// draw the actual header blocks
|
// draw the actual header blocks
|
||||||
@@ -467,12 +467,12 @@ pub fn heading_bar<B: Backend>(
|
|||||||
gui_state
|
gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.update_region_map(Region::Header(header), rect);
|
.update_region_map(Region::Header(header), rect);
|
||||||
f.render_widget(paragraph, rect);
|
frame.render_widget(paragraph, rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show/hide help
|
// show/hide help
|
||||||
let color = if help_visible {
|
let color = if data.help_visible {
|
||||||
Color::Black
|
Color::Black
|
||||||
} else {
|
} else {
|
||||||
Color::White
|
Color::White
|
||||||
@@ -482,8 +482,8 @@ pub fn heading_bar<B: Backend>(
|
|||||||
.alignment(Alignment::Right);
|
.alignment(Alignment::Right);
|
||||||
|
|
||||||
// If no containers, don't display the headers, could maybe do this first?
|
// If no containers, don't display the headers, could maybe do this first?
|
||||||
let help_index = if has_containers { 2 } else { 0 };
|
let help_index = if data.has_containers { 2 } else { 0 };
|
||||||
f.render_widget(help_paragraph, split_bar[help_index]);
|
frame.render_widget(help_paragraph, split_bar[help_index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Help popup box needs these three pieces of information
|
/// Help popup box needs these three pieces of information
|
||||||
@@ -569,7 +569,7 @@ impl HelpInfo {
|
|||||||
button_item("tab"),
|
button_item("tab"),
|
||||||
or(),
|
or(),
|
||||||
button_item("shift+tab"),
|
button_item("shift+tab"),
|
||||||
button_desc("to change panels"),
|
button_desc("change panels"),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
@@ -580,35 +580,45 @@ impl HelpInfo {
|
|||||||
button_item("PgUp PgDown"),
|
button_item("PgUp PgDown"),
|
||||||
or(),
|
or(),
|
||||||
button_item("Home End"),
|
button_item("Home End"),
|
||||||
button_desc("to change selected line"),
|
button_desc("change selected line"),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
button_item("enter"),
|
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![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
button_item("h"),
|
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![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
button_item("1 - 9"),
|
button_item("1 - 9"),
|
||||||
button_desc("sort by header - or click header"),
|
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![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
button_item("q"),
|
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
|
/// 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 title = format!(" {VERSION} ");
|
||||||
|
|
||||||
let name_info = HelpInfo::gen_name();
|
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
|
/// Draw the delete confirm box in the centre of the screen
|
||||||
/// take in container id and container name here?
|
/// take in container id and container name here?
|
||||||
pub fn delete_confirm<B: Backend>(
|
pub fn delete_confirm(f: &mut Frame, gui_state: &Arc<Mutex<GuiState>>, name: &str) {
|
||||||
f: &mut Frame<'_, B>,
|
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
|
||||||
name: &str,
|
|
||||||
) {
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(" Confirm Delete ")
|
.title(" Confirm Delete ")
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
@@ -834,7 +840,7 @@ pub fn delete_confirm<B: Backend>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw an error popup over whole screen
|
/// 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()
|
let block = Block::default()
|
||||||
.title(" Error ")
|
.title(" Error ")
|
||||||
.border_type(BorderType::Rounded)
|
.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)
|
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}");
|
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
|
/// 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()
|
let block = Block::default()
|
||||||
.title("")
|
.title("")
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.borders(Borders::NONE);
|
.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();
|
let mut lines = text.lines().count();
|
||||||
|
|
||||||
// Add some horizontal & vertical margins
|
// 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);
|
let area = popup(lines, max_line_width, f.size(), BoxLocation::BottomRight);
|
||||||
f.render_widget(Clear, area);
|
f.render_widget(Clear, area);
|
||||||
f.render_widget(paragraph, 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
|
/// 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]
|
.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
|
// 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()
|
// let whole_layout = Layout::default()
|
||||||
// .direction(Direction::Vertical)
|
// .direction(Direction::Vertical)
|
||||||
// .constraints([Constraint::Min(100)].as_ref())
|
// .constraints([Constraint::Min(100)].as_ref())
|
||||||
|
|||||||
+76
-13
@@ -1,8 +1,17 @@
|
|||||||
|
use parking_lot::Mutex;
|
||||||
use ratatui::layout::{Constraint, Rect};
|
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 uuid::Uuid;
|
||||||
|
|
||||||
use crate::app_data::{ContainerId, Header};
|
use crate::{
|
||||||
|
app_data::{ContainerId, Header},
|
||||||
|
exec::ExecMode,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
pub enum SelectablePanel {
|
pub enum SelectablePanel {
|
||||||
@@ -150,25 +159,28 @@ const FRAMES_LEN: u8 = 9;
|
|||||||
/// Various functions (e.g input handler), operate differently depending upon current Status
|
/// Various functions (e.g input handler), operate differently depending upon current Status
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Init,
|
|
||||||
Help,
|
|
||||||
DockerConnect,
|
|
||||||
DeleteConfirm,
|
DeleteConfirm,
|
||||||
|
DockerConnect,
|
||||||
Error,
|
Error,
|
||||||
|
Exec,
|
||||||
|
Help,
|
||||||
|
Init,
|
||||||
|
Logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct GuiState {
|
pub struct GuiState {
|
||||||
|
delete_container: Option<ContainerId>,
|
||||||
|
delete_map: HashMap<DeleteButton, Rect>,
|
||||||
heading_map: HashMap<Header, Rect>,
|
heading_map: HashMap<Header, Rect>,
|
||||||
is_loading: HashSet<Uuid>,
|
is_loading: HashSet<Uuid>,
|
||||||
loading_index: u8,
|
loading_index: u8,
|
||||||
panel_map: HashMap<SelectablePanel, Rect>,
|
panel_map: HashMap<SelectablePanel, Rect>,
|
||||||
delete_map: HashMap<DeleteButton, Rect>,
|
selected_panel: SelectablePanel,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
delete_container: Option<ContainerId>,
|
exec_mode: Option<ExecMode>,
|
||||||
pub info_box_text: Option<String>,
|
pub info_box_text: Option<(String, Instant)>,
|
||||||
pub selected_panel: SelectablePanel,
|
|
||||||
}
|
}
|
||||||
impl GuiState {
|
impl GuiState {
|
||||||
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||||
@@ -176,6 +188,11 @@ impl GuiState {
|
|||||||
self.panel_map.clear();
|
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
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
pub fn panel_intersect(&mut self, rect: Rect) {
|
pub fn panel_intersect(&mut self, rect: Rect) {
|
||||||
if let Some(data) = self
|
if let Some(data) = self
|
||||||
@@ -254,17 +271,42 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a gui_status into the current gui_status HashSet
|
/// Remove a gui_status into the current gui_status HashSet
|
||||||
|
/// Remove exec mode & deleteConfirm is required
|
||||||
pub fn status_del(&mut self, status: Status) {
|
pub fn status_del(&mut self, status: Status) {
|
||||||
self.status.remove(&status);
|
self.status.remove(&status);
|
||||||
if status == Status::DeleteConfirm {
|
match status {
|
||||||
|
Status::DeleteConfirm => {
|
||||||
self.status.remove(&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
|
/// 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) {
|
pub fn status_push(&mut self, status: Status) {
|
||||||
|
match status {
|
||||||
|
Status::Exec => (),
|
||||||
|
_ => {
|
||||||
self.status.insert(status);
|
self.status.insert(status);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Change to next selectable panel
|
/// Change to next selectable panel
|
||||||
pub fn next_panel(&mut self) {
|
pub fn next_panel(&mut self) {
|
||||||
@@ -287,7 +329,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
/// 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() {
|
if self.is_loading.is_empty() {
|
||||||
' '
|
' '
|
||||||
} else {
|
} else {
|
||||||
@@ -296,16 +338,37 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
|
/// 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);
|
self.is_loading.remove(&uuid);
|
||||||
if self.is_loading.is_empty() {
|
if self.is_loading.is_empty() {
|
||||||
self.loading_index = 0;
|
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
|
/// Set info box content
|
||||||
pub fn set_info_box(&mut self, text: &str) {
|
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
|
/// Remove info box content
|
||||||
|
|||||||
+132
-87
@@ -4,9 +4,9 @@ use crossterm::{
|
|||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, MutexGuard};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
};
|
};
|
||||||
@@ -26,19 +26,21 @@ mod gui_state;
|
|||||||
pub use self::color_match::*;
|
pub use self::color_match::*;
|
||||||
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||||
use crate::{
|
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,
|
input_handler::InputMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker_sx: Sender<DockerMessage>,
|
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
input_poll_rate: Duration,
|
input_poll_rate: Duration,
|
||||||
|
input_tx: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
sender: Sender<InputMessages>,
|
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
cursor_position: (u16, u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
@@ -57,20 +59,21 @@ impl Ui {
|
|||||||
/// Create a new Ui struct, and execute the drawing loop
|
/// Create a new Ui struct, and execute the drawing loop
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
docker_sx: Sender<DockerMessage>,
|
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
input_tx: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
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 {
|
let mut ui = Self {
|
||||||
app_data,
|
app_data,
|
||||||
docker_sx,
|
cursor_position,
|
||||||
gui_state,
|
gui_state,
|
||||||
input_poll_rate: std::time::Duration::from_millis(100),
|
input_poll_rate: std::time::Duration::from_millis(100),
|
||||||
|
input_tx,
|
||||||
is_running,
|
is_running,
|
||||||
now: Instant::now(),
|
now: Instant::now(),
|
||||||
sender,
|
|
||||||
terminal,
|
terminal,
|
||||||
};
|
};
|
||||||
if let Err(e) = ui.draw_ui().await {
|
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
|
/// Setup the terminal for full-screen drawing mode, with mouse capture
|
||||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||||
enable_raw_mode()?;
|
let stdout = Self::init_terminal()?;
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
|
||||||
Self::enable_mouse_capture()?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
Ok(Terminal::new(backend)?)
|
Ok(Terminal::new(backend)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is a fix for mouse-events being printed to screen, read an event and do nothing with it
|
fn init_terminal() -> Result<Stdout> {
|
||||||
fn nullify_event_read(&self) {
|
enable_raw_mode()?;
|
||||||
if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) {
|
let mut stdout = io::stdout();
|
||||||
event::read().ok();
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
}
|
Self::enable_mouse_capture()?;
|
||||||
|
Ok(stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reset the terminal back to default settings
|
/// reset the terminal back to default settings
|
||||||
@@ -111,6 +112,9 @@ impl Ui {
|
|||||||
DisableMouseCapture
|
DisableMouseCapture
|
||||||
)?;
|
)?;
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
self.terminal.clear().ok();
|
||||||
|
self.terminal
|
||||||
|
.set_cursor(self.cursor_position.0, self.cursor_position.1)?;
|
||||||
Ok(self.terminal.show_cursor()?)
|
Ok(self.terminal.show_cursor()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,12 +141,33 @@ impl Ui {
|
|||||||
Ok(())
|
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
|
/// The loop for drawing the main UI to the terminal
|
||||||
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
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) {
|
while self.is_running.load(Ordering::SeqCst) {
|
||||||
|
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
|
||||||
|
if exec {
|
||||||
|
self.exec().await;
|
||||||
|
}
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state))
|
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state))
|
||||||
@@ -150,10 +175,11 @@ impl Ui {
|
|||||||
{
|
{
|
||||||
return Err(AppError::Terminal);
|
return Err(AppError::Terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) {
|
if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) {
|
||||||
if let Ok(event) = event::read() {
|
if let Ok(event) = event::read() {
|
||||||
if let Event::Key(key) = event {
|
if let Event::Key(key) = event {
|
||||||
self.sender
|
self.input_tx
|
||||||
.send(InputMessages::ButtonPress((key.code, key.modifiers)))
|
.send(InputMessages::ButtonPress((key.code, key.modifiers)))
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
@@ -162,7 +188,7 @@ impl Ui {
|
|||||||
event::MouseEventKind::Down(_)
|
event::MouseEventKind::Down(_)
|
||||||
| event::MouseEventKind::ScrollDown
|
| event::MouseEventKind::ScrollDown
|
||||||
| event::MouseEventKind::ScrollUp => {
|
| 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -192,57 +213,89 @@ impl Ui {
|
|||||||
} else {
|
} else {
|
||||||
self.gui_loop().await?;
|
self.gui_loop().await?;
|
||||||
}
|
}
|
||||||
self.nullify_event_read();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[cfg(not(debug_assertions))]
|
||||||
/// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
|
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
|
||||||
macro_rules! value_capture {
|
Layout::default()
|
||||||
($name:ident, $lock_expr:expr) => {
|
.direction(Direction::Vertical)
|
||||||
let $name = || $lock_expr;
|
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
||||||
let $name = $name();
|
.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
|
/// Draw the main ui to a frame of the terminal
|
||||||
/// TODO add a single line area for debug message - if not in release mode?
|
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
||||||
fn draw_frame<B: Backend>(
|
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
||||||
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());
|
|
||||||
|
|
||||||
// set max height for container section, needs +5 to deal with docker commands list and borders
|
let whole_layout = get_wholelayout(f);
|
||||||
let height = if height < 12 { height + 5 } else { 12 };
|
#[cfg(debug_assertions)]
|
||||||
|
draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string());
|
||||||
|
|
||||||
let whole_layout = Layout::default()
|
#[cfg(debug_assertions)]
|
||||||
.direction(Direction::Vertical)
|
let whole_layout_split = (1, 2);
|
||||||
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
|
||||||
.split(f.size());
|
#[cfg(not(debug_assertions))]
|
||||||
|
let whole_layout_split = (0, 1);
|
||||||
|
|
||||||
// Split into 3, containers+controls, logs, then graphs
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
let upper_main = Layout::default()
|
let upper_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
|
||||||
[
|
.split(whole_layout[whole_layout_split.1]);
|
||||||
Constraint::Max(height.try_into().unwrap_or_default()),
|
|
||||||
Constraint::Percentage(50),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(whole_layout[1]);
|
|
||||||
|
|
||||||
let top_split = if has_containers {
|
let top_split = if fd.has_containers {
|
||||||
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
||||||
} else {
|
} else {
|
||||||
vec![Constraint::Percentage(100)]
|
vec![Constraint::Percentage(100)]
|
||||||
@@ -250,10 +303,10 @@ fn draw_frame<B: Backend>(
|
|||||||
// Containers + docker commands
|
// Containers + docker commands
|
||||||
let top_panel = Layout::default()
|
let top_panel = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(top_split.as_ref())
|
.constraints(top_split)
|
||||||
.split(upper_main[0]);
|
.split(upper_main[0]);
|
||||||
|
|
||||||
let lower_split = if has_containers {
|
let lower_split = if fd.has_containers {
|
||||||
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
|
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
|
||||||
} else {
|
} else {
|
||||||
vec![Constraint::Percentage(100)]
|
vec![Constraint::Percentage(100)]
|
||||||
@@ -262,25 +315,17 @@ fn draw_frame<B: Backend>(
|
|||||||
// Split into 2, logs, and optional charts
|
// Split into 2, logs, and optional charts
|
||||||
let lower_main = Layout::default()
|
let lower_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(lower_split.as_ref())
|
.constraints(lower_split)
|
||||||
.split(upper_main[1]);
|
.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(
|
draw_blocks::heading_bar(whole_layout[whole_layout_split.0], f, &fd, gui_state);
|
||||||
whole_layout[0],
|
|
||||||
&column_widths,
|
|
||||||
f,
|
|
||||||
has_containers,
|
|
||||||
&loading_icon,
|
|
||||||
sorted_by,
|
|
||||||
gui_state,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(id) = delete_confirm {
|
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||||
app_data.lock().get_container_name_by_id(&id).map_or_else(
|
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
|
// 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
|
// 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
|
// only draw commands + charts if there are containers
|
||||||
if has_containers {
|
if fd.has_containers {
|
||||||
draw_blocks::commands(app_data, top_panel[1], f, gui_state);
|
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
|
||||||
draw_blocks::chart(f, lower_main[1], app_data);
|
draw_blocks::chart(f, lower_main[1], app_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(info) = info_text {
|
if let Some((text, instant)) = fd.info_text {
|
||||||
draw_blocks::info(f, info);
|
draw_blocks::info(f, &text, instant, gui_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if error, and show popup if so
|
// 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);
|
draw_blocks::help_box(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = has_error {
|
if let Some(error) = fd.has_error {
|
||||||
draw_blocks::error(f, error, None);
|
draw_blocks::error(f, error, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user