diff --git a/.github/release-body.md b/.github/release-body.md index 28640bb..f9cc715 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,25 +1,23 @@ -### 2023-03-13 +### 2023-03-30 ### Chores -+ Rust 1.68.0 clippy linting, [5582c45403413d3355bbcd629cfad559296f5e5b] -+ devcontainer use sparse protocol index, [20b79e9cd5bf75bb253158c0b590284139e0291d] -+ dependencies updated, [0c07d4b40607a0eba003b6dcd0345ec0543c6264], [601a73d2c830043a25d64922c4d4aa38f8801912], [5aaa3c1ab08b0c85df9bfce18a3e60206556fa58], [7a1563030e48499da7f41033673c70deefe3de8a], [457157755baa1f9e9cfef9315a7940c357b0953d] ++ dependencies updated, [7a9bdc9699594532e17a33e044ca0678693c8d3f], [58e03a750fe89b914b9069cb0c6c02a3d0929439], [b246e8c25af0c5136953afca7c694cda66550d9b] + +### Docs ++ README.md and screenshot updated, [73ab7580c61dd59c59f10872629111360afb9033] ### Features -+ increase mpsc channel size from 16 to 32 messages, [924f14e998f79f731447a2eded038eab51f2e932] -+ KeyEvents send modifier, so can quit on `ctrl + c`, [598f67c6f6a8713102bcc415f0409911763bb914] -+ only send relevant mouse events to input handler, [507660d835d0beaa8cd021110401ecc58c0613c6] ++ Ability to delete a container, be warned, as this will force delete, closes #27, [937202fe34d1692693c62dd1a7ad19db37651233], [b25f8b18f4f2acd5c9af4a1d40655761d1bd720e] ++ Publish images to `ghcr.io` as well as Docker Hub, and correctly tag images with `latest` and the current sermver, [cb1271cf7f21c898020481ad85914a3dcc83ec93] ++ Replace `tui-rs` with [ratatui](https://github.com/tui-rs-revival/ratatui), [d431f850219b28af2bc45f3b6917377604596a40] ### Fixes -+ GitHub workflow on SEMEVR tag only, [140773865165bf006e74f9d436fc744220f5eae7] ++ out of bound bug in `heading_bar()`, [b9c125da46fe0eb4aae15c354d87ac824e9cb83a] ++ `-d` arg error text updated, [e0b49be84062abdfcb636418f57043fad37d06ec] ### Refactors -+ replace `unwrap_or(())` with `.ok()`, [8ba37a165bb89277ab957194da6464bdb35be2e6] -+ use `unwrap_or_default()`, [79de92c3921702417bb2df1f44939a7b09cb7fa0] -+ Result return, [d9f0bd5566e27218b8c8eaba6ece237907771c1d] - -### Reverts -+ temporary devcontainer buildkit fix removed, [d1497a4451f4de54d3cc26c5a3957cd636c29118] ++ `popup()` use `saturating_x()` rather than `checked_x()`, [d628e8029942916053b3b7e72d363b1290fc5711] ++ button_item() include brackets, [7c92ffef7da20143a31706a310b5e6f2c3e0554f] see CHANGELOG.md for more details diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index d3b2c72..ac502ab 100644 Binary files a/.github/screenshot_01.png and b/.github/screenshot_01.png differ diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index bd9eb27..df4c890 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -5,6 +5,7 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+' jobs: deploy: + # Change this to latest - or ubuntu 20.04? runs-on: ubuntu-18.04 steps: - name: Checkout @@ -64,23 +65,39 @@ jobs: - name: compress windows_x86_64 binary run: zip -j ./oxker_windows_x86_64.zip target/x86_64-pc-windows-gnu/release/oxker.exe - ############################### - ## Build images for Dockerhub # - ############################### + ######################################### + ## Build images for Dockerhub & ghcr.io # + ######################################### + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Login to DockerHub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Write release version to env + run: | + CURRENT_SEMVER=${GITHUB_REF_NAME#v} + echo "CURRENT_SEMVER=$CURRENT_SEMVER" >> $GITHUB_ENV + - uses: docker/setup-buildx-action@v2 id: buildx with: install: true - - name: Build for Docker Hub + - name: Build for Dockerhub & ghcr.io run: | docker build --platform linux/arm/v6,linux/arm64,linux/amd64 \ -t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:latest \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/oxker:${{env.CURRENT_SEMVER}} \ + -t ghcr.io/${{ github.repository_owner }}/oxker:latest \ + -t ghcr.io/${{ github.repository_owner }}/oxker:${{env.CURRENT_SEMVER}} \ --push \ -f containerised/Dockerfile . diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ad202..067694d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# v0.3.0 +### 2023-03-30 + +### Chores ++ dependencies updated, [7a9bdc96](https://github.com/mrjackwills/oxker/commit/7a9bdc9699594532e17a33e044ca0678693c8d3f), [58e03a75](https://github.com/mrjackwills/oxker/commit/58e03a750fe89b914b9069cb0c6c02a3d0929439), [b246e8c2](https://github.com/mrjackwills/oxker/commit/b246e8c25af0c5136953afca7c694cda66550d9b) + +### Docs ++ README.md and screenshot updated, [73ab7580](https://github.com/mrjackwills/oxker/commit/73ab7580c61dd59c59f10872629111360afb9033) + +### Features ++ Ability to delete a container, be warned, as this will force delete, closes [#27](https://github.com/mrjackwills/oxker/issues/27), [937202fe](https://github.com/mrjackwills/oxker/commit/937202fe34d1692693c62dd1a7ad19db37651233), [b25f8b18](https://github.com/mrjackwills/oxker/commit/b25f8b18f4f2acd5c9af4a1d40655761d1bd720e) ++ Publish images to `ghcr.io` as well as Docker Hub, and correctly tag images with `latest` and the current sermver, [cb1271cf](https://github.com/mrjackwills/oxker/commit/cb1271cf7f21c898020481ad85914a3dcc83ec93) ++ Replace `tui-rs` with [ratatui](https://github.com/tui-rs-revival/ratatui), [d431f850](https://github.com/mrjackwills/oxker/commit/d431f850219b28af2bc45f3b6917377604596a40) + +### Fixes ++ out of bound bug in `heading_bar()`, [b9c125da](https://github.com/mrjackwills/oxker/commit/b9c125da46fe0eb4aae15c354d87ac824e9cb83a) ++ `-d` arg error text updated, [e0b49be8](https://github.com/mrjackwills/oxker/commit/e0b49be84062abdfcb636418f57043fad37d06ec) + +### Refactors ++ `popup()` use `saturating_x()` rather than `checked_x()`, [d628e802](https://github.com/mrjackwills/oxker/commit/d628e8029942916053b3b7e72d363b1290fc5711) ++ button_item() include brackets, [7c92ffef](https://github.com/mrjackwills/oxker/commit/7c92ffef7da20143a31706a310b5e6f2c3e0554f) + # v0.2.5 ### 2023-03-13 diff --git a/Cargo.lock b/Cargo.lock index 048dded..f10e5dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,10 +12,50 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.69" +name = "anstream" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-wincon", + "concolor-override", + "concolor-query", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" + +[[package]] +name = "anstyle-parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-wincon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "autocfg" @@ -131,42 +171,47 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.8" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" dependencies = [ - "bitflags", + "clap_builder", "clap_derive", - "clap_lex", - "is-terminal", "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", "strsim", - "termcolor", "unicase", "unicode-width", ] [[package]] name = "clap_derive" -version = "4.1.8" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] name = "clap_lex" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" -dependencies = [ - "os_str_bytes", -] +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" [[package]] name = "codespan-reporting" @@ -178,28 +223,27 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "concolor-override" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" -[[package]] -name = "crossterm" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" -dependencies = [ - "bitflags", - "crossterm_winapi", - "libc", - "mio", - "parking_lot", - "signal-hook", - "signal-hook-mio", - "winapi", -] - [[package]] name = "crossterm" version = "0.26.1" @@ -227,9 +271,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -239,9 +283,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", @@ -249,35 +293,35 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 2.0.11", ] [[package]] name = "cxxbridge-flags" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] name = "errno" -version = "0.2.8" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys", ] [[package]] @@ -307,47 +351,47 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-macro", @@ -493,16 +537,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "716f12fbcfac6ffab0a5e9ec51d0a0ff70503742bb2dc7b99396394c9dc323f0" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", ] [[package]] @@ -527,9 +571,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -538,19 +582,20 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ + "hermit-abi 0.3.1", "libc", "windows-sys", ] [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", @@ -596,9 +641,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "cd550e73688e6d578f0ac2119e32b797a327631a42f9433e59d02e139c8df60d" [[package]] name = "lock_api" @@ -619,12 +664,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - [[package]] name = "mio" version = "0.8.6" @@ -682,12 +721,6 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "overload" version = "0.1.1" @@ -696,19 +729,19 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.2.5" +version = "0.3.0" dependencies = [ "anyhow", "bollard", "cansi", "clap", - "crossterm 0.26.1", + "crossterm", "futures-util", "parking_lot", + "ratatui", "tokio", "tracing", "tracing-subscriber", - "tui", "uuid", ] @@ -758,7 +791,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -779,35 +812,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" -version = "1.0.52" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" dependencies = [ "unicode-ident", ] @@ -851,6 +860,19 @@ dependencies = [ "getrandom", ] +[[package]] +name = "ratatui" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc0d032bccba900ee32151ec0265667535c230169f5a011154cdcd984e16829" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -862,9 +884,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.9" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +checksum = "0e78cc525325c06b4a7ff02db283472f3c042b7ff0c391f96c6d5ac6f4f91b75" dependencies = [ "bitflags", "errno", @@ -894,29 +916,29 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "serde" -version = "1.0.155" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.155" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -925,13 +947,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] @@ -1042,6 +1064,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -1053,22 +1086,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] @@ -1125,14 +1158,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", @@ -1145,13 +1177,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.11", ] [[package]] @@ -1194,7 +1226,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1238,19 +1270,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags", - "cassowary", - "crossterm 0.25.0", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "unicase" version = "2.6.0" @@ -1262,9 +1281,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" @@ -1304,6 +1323,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.3.0" @@ -1363,7 +1388,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -1385,7 +1410,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1427,13 +1452,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2649ff315bee4c98757f15dac226efe3d81927adbb6e882084bb1ee3e0c330a7" +dependencies = [ + "windows-targets 0.47.0", +] + [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", ] [[package]] @@ -1442,13 +1476,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8996d3f43b4b2d44327cd71b7b0efd1284ab60e6e9d0e8b630e18555d87d3e" +dependencies = [ + "windows_aarch64_gnullvm 0.47.0", + "windows_aarch64_msvc 0.47.0", + "windows_i686_gnu 0.47.0", + "windows_i686_msvc 0.47.0", + "windows_x86_64_gnu 0.47.0", + "windows_x86_64_gnullvm 0.47.0", + "windows_x86_64_msvc 0.47.0", ] [[package]] @@ -1457,38 +1506,80 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831d567d53d4f3cb1db332b68e6e2b6260228eb4d99a777d8b2e8ed794027c90" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a42d54a417c60ce4f0e31661eed628f0fa5aca73448c093ec4d45fab4c51cdf" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1925beafdbb22201a53a483db861a5644123157c1c3cee83323a2ed565d71e3" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8ef8f2f1711b223947d9b69b596cf5a4e452c930fb58b6fc3fdae7d0ec6b31" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acaa0c2cf0d2ef99b61c308a0c3dbae430a51b7345dedec470bd8f53f5a3642" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a0628f71be1d11e17ca4a0e9e15b3a5180f6fbf1c2d55e3ba3f850378052c1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6e62c256dc6d40b8c8707df17df8d774e60e39db723675241e7c15e910bce7" diff --git a/Cargo.toml b/Cargo.toml index 02a5f28..8d339ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.2.5" +version = "0.3.0" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" @@ -15,14 +15,14 @@ categories = ["command-line-utilities"] anyhow = "1.0" bollard = "0.14" cansi = "2.2" -clap={version="4.1", features = ["derive", "unicode", "color"] } +clap={version="4.2", features = ["derive", "unicode", "color"] } crossterm = "0.26" futures-util = "0.3" parking_lot = {version= "0.12"} -tokio = {version = "1.26", features=["full"]} +tokio = {version = "1.27", features=["full"]} tracing = "0.1" tracing-subscriber = "0.3" -tui = "0.19" +ratatui = "0.20" uuid = {version = "1.3", features = ["v4", "fast-rng"]} [dev-dependencies] diff --git a/README.md b/README.md index aa4ee99..11a9d48 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- Built in Rust, making heavy use of tui-rs & Bollard + Built in Rust, making heavy use of ratatui & Bollard

@@ -27,12 +27,21 @@ cargo install oxker ``` ### Docker -Published on Docker Hub, with images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6` +Published on Docker Hub and ghcr.io, +with images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6` + +**via Docker Hub** ```shell docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker ``` +**via ghcr.io** + +```shell +docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always ghcr.io/mrjackwills/oxker +``` + ### Nix Using nix flakes, oxker can be ran directly with @@ -68,7 +77,7 @@ rm oxker_linux_x86_64.tar.gz oxker or, for automatic platform selection, download, and installation (to `$HOME/.local/bin`) -*One should always verify script content before running in a shell* +*One should always verify script content before running in a shell* ```shell curl https://raw.githubusercontent.com/mrjackwills/oxker/main/install.sh | bash diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index b1e1ba6..974356b 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -4,7 +4,7 @@ use std::{ fmt, }; -use tui::{ +use ratatui::{ style::Color, widgets::{ListItem, ListState}, }; @@ -207,6 +207,7 @@ pub enum DockerControls { Start, Stop, Unpause, + Delete, } impl DockerControls { @@ -216,6 +217,7 @@ impl DockerControls { Self::Restart => Color::Magenta, Self::Start => Color::Green, Self::Stop => Color::Red, + Self::Delete => Color::Gray, Self::Unpause => Color::Blue, } } @@ -223,11 +225,11 @@ impl DockerControls { /// Docker commands available depending on the containers state pub fn gen_vec(state: State) -> Vec { match state { - State::Dead | State::Exited => vec![Self::Start, Self::Restart], - State::Paused => vec![Self::Unpause, Self::Stop], - State::Restarting => vec![Self::Stop], - State::Running => vec![Self::Pause, Self::Restart, Self::Stop], - _ => vec![], + State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete], + State::Paused => vec![Self::Unpause, Self::Stop, Self::Delete], + State::Restarting => vec![Self::Stop, Self::Delete], + State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete], + _ => vec![Self::Delete], } } } @@ -236,6 +238,7 @@ impl fmt::Display for DockerControls { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let disp = match self { Self::Pause => "pause", + Self::Delete => "delete", Self::Restart => "restart", Self::Start => "start", Self::Stop => "stop", diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 93d5769..fda6cbb 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -1,7 +1,7 @@ use bollard::models::ContainerSummary; use core::fmt; +use ratatui::widgets::{ListItem, ListState}; use std::time::{SystemTime, UNIX_EPOCH}; -use tui::widgets::{ListItem, ListState}; mod container_state; @@ -456,6 +456,15 @@ impl AppData { self.containers.items.iter_mut().find(|i| &i.id == id) } + /// return a mutable container by given id + pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { + self.containers + .items + .iter_mut() + .find(|i| &i.id == id) + .map(|i| i.name.clone()) + } + /// Find the id of the currently selected container. /// If any containers on system, will always return a ContainerId /// Only returns None when no containers found. @@ -532,6 +541,7 @@ impl AppData { } } } + // Trim a &String and return String let trim_owned = |x: &String| x.trim().to_owned(); diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs index f7de6f5..0a6b67e 100644 --- a/src/docker_data/message.rs +++ b/src/docker_data/message.rs @@ -2,11 +2,13 @@ use crate::app_data::ContainerId; #[derive(Debug, Clone)] pub enum DockerMessage { - Update, - Start(ContainerId), - Restart(ContainerId), + Delete(ContainerId), + ConfirmDelete(ContainerId), Pause(ContainerId), - Unpause(ContainerId), - Stop(ContainerId), Quit, + Restart(ContainerId), + Start(ContainerId), + Stop(ContainerId), + Unpause(ContainerId), + Update, } diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 8104ccc..92fd28c 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -1,5 +1,8 @@ use bollard::{ - container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions}, + container::{ + ListContainersOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions, Stats, + StatsOptions, + }, service::ContainerSummary, Docker, }; @@ -335,13 +338,14 @@ impl DockerData { } /// Handle incoming messages, container controls & all container information update - /// Spawn dowcker commands off into own thread + /// Spawn Docker commands off into own thread async fn message_handler(&mut self) { while let Some(message) = self.receiver.recv().await { let docker = Arc::clone(&self.docker); let gui_state = Arc::clone(&self.gui_state); let app_data = Arc::clone(&self.app_data); let uuid = Uuid::new_v4(); + // TODO need to refactor these match message { DockerMessage::Pause(id) => { tokio::spawn(async move { @@ -397,6 +401,31 @@ impl DockerData { }); self.update_everything().await; } + DockerMessage::Delete(id) => { + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + if docker + .remove_container( + id.get(), + Some(RemoveContainerOptions { + v: false, + force: true, + link: false, + }), + ) + .await + .is_err() + { + Self::set_error(&app_data, DockerControls::Stop, &gui_state); + } + Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + }); + self.update_everything().await; + self.gui_state.lock().set_delete_container(None); + } + DockerMessage::ConfirmDelete(id) => { + self.gui_state.lock().set_delete_container(Some(id)); + } DockerMessage::Update => self.update_everything().await, DockerMessage::Quit => { self.spawns diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index bb60bdd..421f86a 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -8,18 +8,18 @@ use crossterm::{ execute, }; use parking_lot::Mutex; +use ratatui::layout::Rect; use tokio::{ sync::mpsc::{Receiver, Sender}, task::JoinHandle, }; -use tui::layout::Rect; mod message; use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{GuiState, SelectablePanel, Status, Ui}, + ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui}, }; pub use message::InputMessages; @@ -62,13 +62,21 @@ impl InputHandler { match message { InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await, InputMessages::MouseEvent(mouse_event) => { - let error_or_help = self - .gui_state - .lock() - .status_contains(&[Status::Error, Status::Help]); + let error_or_help = self.gui_state.lock().status_contains(&[ + Status::Error, + Status::Help, + Status::DeleteConfirm, + ]); if !error_or_help { self.mouse_press(mouse_event); } + let delete_confirm = self + .gui_state + .lock() + .status_contains(&[Status::DeleteConfirm]); + if delete_confirm { + self.button_intersect(mouse_event).await; + } } } if !self.is_running.load(Ordering::SeqCst) { @@ -133,41 +141,59 @@ impl InputHandler { } } + /// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container + async fn confirm_delete(&self) { + let id = self.gui_state.lock().get_delete_container(); + if let Some(id) = id { + self.docker_sender + .send(DockerMessage::Delete(id)) + .await + .ok(); + } + } + + /// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel) + fn clear_delete(&self) { + self.gui_state.lock().set_delete_container(None); + } + /// Handle any keyboard button events #[allow(clippy::too_many_lines)] async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { // TODO - refactor this to a single call, maybe return Error, Help or Normal let contains_error = self.gui_state.lock().status_contains(&[Status::Error]); let contains_help = self.gui_state.lock().status_contains(&[Status::Help]); + let contains_delete = self + .gui_state + .lock() + .status_contains(&[Status::DeleteConfirm]); - // Quit on Ctrl + c/C + // Always just quit on Ctrl + c/C or q/Q let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); - if key_modififer == KeyModifiers::CONTROL && is_c() { + let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); + if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() { self.quit().await; } if contains_error { - match key_code { - KeyCode::Char('q' | 'Q') => self.quit().await, - KeyCode::Char('c' | 'C') => { - self.app_data.lock().remove_error(); - self.gui_state.lock().status_del(Status::Error); - } - _ => (), + if let KeyCode::Char('c' | 'C') = key_code { + self.app_data.lock().remove_error(); + self.gui_state.lock().status_del(Status::Error); } } else if contains_help { match key_code { - KeyCode::Char('q' | 'Q') => self.quit().await, KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help), KeyCode::Char('m' | 'M') => self.m_key(), _ => (), } - } else { - // let abc = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::Ctrl); + } else if contains_delete { + match key_code { + KeyCode::Char('y' | 'Y') => self.confirm_delete().await, + KeyCode::Char('n' | 'N') => self.clear_delete(), + _ => (), + } + } else { match key_code { - // KeyCode::Ctrl('c') => { - // self.quit().await; - // } KeyCode::Char('0') => self.app_data.lock().reset_sorted(), KeyCode::Char('1') => self.sort(Header::State), KeyCode::Char('2') => self.sort(Header::Status), @@ -178,7 +204,6 @@ impl InputHandler { KeyCode::Char('7') => self.sort(Header::Image), KeyCode::Char('8') => self.sort(Header::Rx), KeyCode::Char('9') => self.sort(Header::Tx), - KeyCode::Char('q' | 'Q') => self.quit().await, KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Tab => { @@ -251,6 +276,11 @@ impl InputHandler { }; 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() } @@ -280,6 +310,25 @@ impl InputHandler { } } + /// 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) { + if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) { + let intersect = self.gui_state.lock().button_intersect(Rect::new( + mouse_event.column, + mouse_event.row, + 1, + 1, + )); + + if let Some(button) = intersect { + match button { + DeleteButton::Yes => self.confirm_delete().await, + DeleteButton::No => self.clear_delete(), + } + } + } + } + /// Handle mouse button events fn mouse_press(&mut self, mouse_event: MouseEvent) { match mouse_event.kind { diff --git a/src/main.rs b/src/main.rs index fd99297..29db59b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ use ui::{GuiState, Status, Ui}; use crate::docker_data::DockerMessage; -// this is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container +/// This is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container const ENTRY_POINT: &str = "/app/oxker"; const ENV_KEY: &str = "OXKER_RUNTIME"; const ENV_VALUE: &str = "container"; diff --git a/src/parse_args/mod.rs b/src/parse_args/mod.rs index 2a881ba..d6c823d 100644 --- a/src/parse_args/mod.rs +++ b/src/parse_args/mod.rs @@ -40,7 +40,7 @@ impl CliArgs { // Quit the program if the docker update argument is 0 // Should maybe change it to check if less than 100 if args.docker_interval == 0 { - error!("docker args needs to be greater than 0"); + error!("\"-d\" argument needs to be greater than 0"); process::exit(1) } Self { diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs index d77a40e..2e349f6 100644 --- a/src/ui/color_match.rs +++ b/src/ui/color_match.rs @@ -1,7 +1,7 @@ pub mod log_sanitizer { use cansi::{v3::categorise_text, Color as CansiColor, Intensity}; - use tui::{ + use ratatui::{ style::{Color, Modifier, Style}, text::{Span, Spans}, }; diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index d09105b..29e5589 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1,7 +1,5 @@ use parking_lot::Mutex; -use std::default::Default; -use std::{fmt::Display, sync::Arc}; -use tui::{ +use ratatui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -13,6 +11,8 @@ use tui::{ }, Frame, }; +use std::default::Default; +use std::{fmt::Display, sync::Arc}; use crate::app_data::{Header, SortedOrder}; use crate::ui::Status; @@ -21,7 +21,7 @@ use crate::{ app_error::AppError, }; -use super::gui_state::{BoxLocation, Region}; +use super::gui_state::{BoxLocation, DeleteButton, Region}; use super::{GuiState, SelectablePanel}; const NAME_TEXT: &str = r#" @@ -43,6 +43,14 @@ const MARGIN: &str = " "; const ARROW: &str = "▶ "; const CIRCLE: &str = "⚪ "; +/// From a given &str, return the maximum number of chars on a single line +fn max_line_width(text: &str) -> usize { + text.lines() + .map(|i| i.chars().count()) + .max() + .unwrap_or_default() +} + /// Generate block, add a border if is the selected panel, /// add custom title based on state of each panel fn generate_block<'a>( @@ -53,7 +61,7 @@ fn generate_block<'a>( ) -> Block<'a> { gui_state .lock() - .update_heading_map(Region::Panel(panel), area); + .update_region_map(Region::Panel(panel), area); let current_selected_panel = gui_state.lock().selected_panel; let mut title = match panel { SelectablePanel::Containers => { @@ -424,7 +432,7 @@ pub fn heading_bar( let info_text = format!("( h ) {suffix} help {MARGIN}",); let info_width = info_text.chars().count(); - let column_width = usize::from(area.width) - 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 splits = if has_containers { vec![ @@ -459,7 +467,7 @@ pub fn heading_bar( let rect = headers_section[index]; gui_state .lock() - .update_heading_map(Region::Header(header), rect); + .update_region_map(Region::Header(header), rect); f.render_widget(paragraph, rect); } } @@ -479,14 +487,6 @@ pub fn heading_bar( f.render_widget(help_paragraph, split_bar[help_index]); } -/// From a given &str, return the maximum number of chars on a single line -fn max_line_width(text: &str) -> usize { - text.lines() - .map(|i| i.chars().count()) - .max() - .unwrap_or_default() -} - /// Help popup box needs these three pieces of information struct HelpInfo { spans: Vec>, @@ -500,7 +500,7 @@ impl HelpInfo { spans .iter() .flat_map(|x| x.0.iter()) - .map(tui::text::Span::width) + .map(ratatui::text::Span::width) .max() .unwrap_or(1) } @@ -515,12 +515,12 @@ impl HelpInfo { Span::styled(input.to_owned(), Style::default().fg(color)) } - /// Span to black text span + /// &str to black text span fn black_span<'a>(input: &str) -> Span<'a> { Self::span(input, Color::Black) } - /// Span to white text span + /// &str to white text span fn white_span<'a>(input: &str) -> Span<'a> { Self::span(input, Color::White) } @@ -559,7 +559,7 @@ impl HelpInfo { /// Generate the button information span + metadata fn gen_button() -> Self { - let button_item = |x: &str| Self::white_span(&format!(" {x} ")); + let button_item = |x: &str| Self::white_span(&format!(" ( {x} ) ")); let button_desc = |x: &str| Self::black_span(x); let or = || button_desc("or"); let space = || button_desc(" "); @@ -567,52 +567,48 @@ impl HelpInfo { let spans = [ Spans::from(vec![ space(), - button_item("( tab )"), + button_item("tab"), or(), - button_item("( shift+tab )"), + button_item("shift+tab"), button_desc("to change panels"), ]), Spans::from(vec![ space(), - button_item("( ↑ ↓ )"), + button_item("↑ ↓"), or(), - button_item("( j k )"), + button_item("j k"), or(), - button_item("( PgUp PgDown )"), + button_item("PgUp PgDown"), or(), - button_item("( Home End )"), + button_item("Home End"), button_desc("to change selected line"), ]), Spans::from(vec![ space(), - button_item("( enter )"), + button_item("enter"), button_desc("to send docker container command"), ]), Spans::from(vec![ space(), - button_item("( h )"), + button_item("h"), button_desc("to toggle this help information"), ]), + Spans::from(vec![space(), button_item("0"), button_desc("to stop sort")]), Spans::from(vec![ space(), - button_item("( 0 )"), - button_desc("to stop sort"), - ]), - Spans::from(vec![ - space(), - button_item("( 1 - 9 )"), + button_item("1 - 9"), button_desc("sort by header - or click header"), ]), Spans::from(vec![ space(), - button_item("( m )"), + button_item("m"), button_desc( "to toggle mouse capture - if disabled, text on screen can be selected & copied", ), ]), Spans::from(vec![ space(), - button_item("( q )"), + button_item("q"), button_desc("to quit at any time"), ]), ]; @@ -636,8 +632,7 @@ impl HelpInfo { Spans::from(vec![Span::styled( REPO.to_owned(), Style::default() - .bg(Color::Magenta) - .fg(Color::Black) + .fg(Color::White) .add_modifier(Modifier::UNDERLINED), )]), ]; @@ -729,6 +724,117 @@ pub fn help_box(f: &mut Frame<'_, B>) { f.render_widget(block, area); } +/// Draw the delete confirm box in the centre of the screen +/// take in container id and container name here? +pub fn delete_confirm( + f: &mut Frame<'_, B>, + gui_state: &Arc>, + name: &str, +) { + let block = Block::default() + .title(" Confirm Delete ") + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::White).fg(Color::Black)) + .title_alignment(Alignment::Center) + .borders(Borders::ALL); + + let confirm = Spans::from(vec![ + Span::from("Are you sure you want to delete container: "), + Span::styled( + name, + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ), + ]); + + let yes_text = " (Y)es "; + let no_text = " (N)o "; + + // Find the maximum line width & height, and add some padding + let max_line_width = u16::try_from(confirm.width()).unwrap_or(64) + 12; + let lines = 8; + + let confirm_para = Paragraph::new(confirm).alignment(Alignment::Center); + + let button_block = || { + Block::default() + .border_type(BorderType::Rounded) + .borders(Borders::ALL) + }; + + let yes_para = Paragraph::new(yes_text) + .alignment(Alignment::Center) + .block(button_block()); + // Need to add some padding for the borders + let yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9); + + let no_para = Paragraph::new(no_text) + .alignment(Alignment::Center) + .block(button_block()); + // Need to add some padding for the borders + let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8); + + let area = popup( + lines, + max_line_width.into(), + f.size(), + BoxLocation::MiddleCentre, + ); + + let split_popup = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Min(2), + Constraint::Max(1), + Constraint::Max(1), + Constraint::Max(3), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(area); + + // Should maybe have a differenet button_space IF the f.width() is within 2 chars of no_chars + yes_chars? + let button_spacing = (max_line_width - no_chars - yes_chars) / 3; + + let button_spacing = if (button_spacing + max_line_width) > f.size().width { + 1 + } else { + button_spacing + }; + let split_buttons = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Max(button_spacing), + Constraint::Min(no_chars), + Constraint::Max(button_spacing), + Constraint::Min(yes_chars), + Constraint::Max(button_spacing), + ] + .as_ref(), + ) + .split(split_popup[3]); + + let no_area = split_buttons[1]; + let yes_area = split_buttons[3]; + + // Insert button areas into region map, so can interact with them on click + gui_state + .lock() + .update_region_map(Region::Delete(DeleteButton::No), no_area); + + gui_state + .lock() + .update_region_map(Region::Delete(DeleteButton::Yes), yes_area); + + f.render_widget(Clear, area); + f.render_widget(block, area); + f.render_widget(confirm_para, split_popup[1]); + f.render_widget(no_para, no_area); + f.render_widget(yes_para, yes_area); +} + /// Draw an error popup over whole screen pub fn error(f: &mut Frame<'_, B>, error: AppError, seconds: Option) { let block = Block::default() @@ -798,11 +904,7 @@ pub fn info(f: &mut Frame<'_, B>, text: String) { /// draw a box in the one of the BoxLocations, based on max line width + number of lines fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocation) -> Rect { // Make sure blank_space can't be an negative, as will crash - let calc = |x: u16, y: usize| { - (usize::from(x).checked_sub(y).map_or(1usize, |f| f)) - .checked_div(2) - .map_or(1usize, |f| f) - }; + let calc = |x: u16, y: usize| usize::from(x).saturating_sub(y).saturating_div(2); let blank_vertical = calc(r.height, text_lines); let blank_horizontal = calc(r.width, text_width); diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 134c112..5a6c128 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -1,11 +1,11 @@ +use ratatui::layout::{Constraint, Rect}; use std::{ collections::{HashMap, HashSet}, fmt, }; -use tui::layout::{Constraint, Rect}; use uuid::Uuid; -use crate::app_data::Header; +use crate::app_data::{ContainerId, Header}; #[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] pub enum SelectablePanel { @@ -43,6 +43,13 @@ impl SelectablePanel { pub enum Region { Panel(SelectablePanel), Header(Header), + Delete(DeleteButton), +} + +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] +pub enum DeleteButton { + Yes, + No, } #[allow(unused)] @@ -191,11 +198,13 @@ impl fmt::Display for Loading { /// The application gui state can be in multiple of these four states at the same time /// Various functions (e.g input handler), operate differently depending upon current Status +// Copy #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Status { Init, Help, DockerConnect, + DeleteConfirm, Error, } @@ -206,7 +215,9 @@ pub struct GuiState { is_loading: HashSet, loading_icon: Loading, panel_map: HashMap, + delete_map: HashMap, status: HashSet, + delete_container: Option, pub info_box_text: Option, pub selected_panel: SelectablePanel, } @@ -229,6 +240,16 @@ impl GuiState { } } + /// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button + pub fn button_intersect(&mut self, rect: Rect) -> Option { + self.delete_map + .iter() + .filter(|i| i.1.intersects(rect)) + .collect::>() + .get(0) + .map(|data| *data.0) + } + /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels pub fn header_intersect(&mut self, rect: Rect) -> Option

{ self.heading_map @@ -240,7 +261,7 @@ impl GuiState { } /// Insert, or updates header area panel into heading_map - pub fn update_heading_map(&mut self, region: Region, area: Rect) { + pub fn update_region_map(&mut self, region: Region, area: Rect) { match region { Region::Header(header) => self .heading_map @@ -252,9 +273,31 @@ impl GuiState { .entry(panel) .and_modify(|w| *w = area) .or_insert(area), + Region::Delete(button) => self + .delete_map + .entry(button) + .and_modify(|w| *w = area) + .or_insert(area), }; } + /// Check if an ContainerId is set in the delete_container field + pub fn get_delete_container(&self) -> Option { + self.delete_container.clone() + } + + /// Set either a ContainerId, or None, to the delete_container field + /// If Some, will also insert the DeleteConfirm status into self.status + pub fn set_delete_container(&mut self, id: Option) { + if id.is_some() { + self.status.insert(Status::DeleteConfirm); + } else { + self.delete_map.clear(); + self.status.remove(&Status::DeleteConfirm); + } + self.delete_container = id; + } + /// Check if the current gui_status contains any of the given status' /// Don't really like this methodology for gui state, needs a re-think pub fn status_contains(&self, status: &[Status]) -> bool { @@ -264,6 +307,9 @@ impl GuiState { /// Remove a gui_status into the current gui_status HashSet pub fn status_del(&mut self, status: Status) { self.status.remove(&status); + if status == Status::DeleteConfirm { + self.status.remove(&Status::DeleteConfirm); + } } /// Insert a gui_status into the current gui_status HashSet diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5f9ee65..53ec6a1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -5,6 +5,11 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use parking_lot::Mutex; +use ratatui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout}, + Frame, Terminal, +}; use std::{ io::{self, Stdout, Write}, sync::{atomic::Ordering, Arc}, @@ -13,18 +18,13 @@ use std::{ use std::{sync::atomic::AtomicBool, time::Instant}; use tokio::sync::mpsc::Sender; use tracing::error; -use tui::{ - backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout}, - Frame, Terminal, -}; mod color_match; mod draw_blocks; mod gui_state; pub use self::color_match::*; -pub use self::gui_state::{GuiState, SelectablePanel, Status}; +pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::AppData, app_error::AppError, docker_data::DockerMessage, input_handler::InputMessages, @@ -198,20 +198,23 @@ impl Ui { } /// Draw the main ui to a frame of the terminal +/// TODO add a single line area for debug message - if not in release mode, maybe with #[cfg(debug_assertions)] ? fn draw_frame( f: &mut Frame<'_, B>, app_data: &Arc>, gui_state: &Arc>, ) { - // set max height for container section, needs +4 to deal with docker commands list and borders + // set max height for container section, needs +5 to deal with docker commands list and borders let height = app_data.lock().get_container_len(); - let height = if height < 12 { height + 4 } else { 12 }; + let height = if height < 12 { height + 5 } else { 12 }; let column_widths = app_data.lock().get_width(); let has_containers = app_data.lock().get_container_len() > 0; let has_error = app_data.lock().get_error(); let sorted_by = app_data.lock().get_sorted(); + let delete_confirm = gui_state.lock().get_delete_container(); + let show_help = gui_state.lock().status_contains(&[Status::Help]); let info_text = gui_state.lock().info_box_text.clone(); let loading_icon = gui_state.lock().get_loading(); @@ -274,6 +277,20 @@ fn draw_frame( gui_state, ); + if let Some(id) = delete_confirm { + let name = app_data.lock().get_container_name_by_id(&id); + name.map_or_else( + || { + // If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation + // so if in that unique situation, just clear the delete_container id + gui_state.lock().set_delete_container(None); + }, + |name| { + draw_blocks::delete_confirm(f, gui_state, &name); + }, + ); + } + // only draw charts if there are containers if has_containers { draw_blocks::chart(f, lower_main[1], app_data);