diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6d78016..ab43ec0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -25,11 +25,11 @@ "extensions": [ "bmuskalla.vscode-tldr", "christian-kohler.path-intellisense", + "fill-labs.dependi", "foxundermoon.shell-format", "mutantdino.resourcemonitor", "redhat.vscode-yaml", "rust-lang.rust-analyzer", - "serayuzgur.crates", "tamasfe.even-better-toml", "timonwong.shellcheck", "vadimcn.vscode-lldb" diff --git a/.github/release-body.md b/.github/release-body.md index 839a5e7..963245d 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,12 +1,29 @@ -### 2024-05-25 +### 2024-08-01 ### Chores -+ Dependencies updated, [51fdd26be5b3166bcff5c26ece6d6ec0d893381e], [c1be658b8cc4786a9a7f2e0a88568019b3995c14] ++ .devcontainer extensions updated, [0288cbc8146cde1dd40ceaec9550198b635bb8f5] ++ dependencies updated, [1df4f78dc41013c33d901925933b1ccb29ad4bc8], [5ae253b8734ba0495e4e8149b17d5228b3d86f8d], [7a517db9f7c14c35e56ff70cf76ffb608fd30e17], [9c291cd9c81b6d9a02085878588ed3b845fd0046], [0e90f4eb55ac5fb5d45e7d212c3686027dd3913e], [fe71cbfb00f166b7c02a6e28e64650ed1b47d15d] ++ docker-compose alpine version bump, [51ceab3ebdb09356cd401d2f268840239255126f] ++ Rust 1.80 linting, [93e1279b1fc77019442a385e2e36be2fe438e828] ++ create_release v0.5.6, [f408acfe9a9f5a976735b8a8a51500fd7b865daf] ### Docs -+ exec mode "not available on Windows", in both README.md and help panel, [df449a85376bbeec87215952d6a9196721f7132e] ++ screenshot updated, [6975ebe70f7058229c232e4a56b090f55247d2a2] + +### Features ++ left align all text, [e0d421c4918a17c9e0e21fd214edb99d71281c9d] ++ place image name in logs panel title, [12f24357a68abe871f44d871d95b6e2ef062181e] ++ distinguish between unhealthy & healthy running containers, closes #43, [de8768181631c6d961ce0e4dacb50c2ed02abc36] ++ filter containers, use `F1` or `/` to enter filter mode, closes #37, thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e], [[7ee1f06f804683e3395953a02138d4e9da115ea9]] ++ place image name in logs panel title, [ef19b9cf89a881d0a7ac818885317ce2bd683dfc] ### Fixes -+ closes #36 Double key strokes on Windows, [9b7d575a76398cbe19e17f6494baf802dbb512b9] ++ log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc8113869b81be9d697ca77418c919e4bf] ++ Dockerfile command use uppercase, [068e4025a5d6049a9a6951a0480a6bdef7379f88] ++ heading section help margin, [0e927aae178c1d8f60561b93607a26d45a1d9331] ++ install.sh use curl, [197a031b8cf356f49f08e04472d0d1c489699415] + +### Tests ++ fix layout tests with new left alignment, [dfced564278eafdbb8a5b95badbae3a7c4bf87b3] see CHANGELOG.md for more details diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 8355605..c9f4572 100644 Binary files a/.github/screenshot_01.png and b/.github/screenshot_01.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index def4abd..5075d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +# v0.7.0 +### 2024-08-01 + +### Chores ++ .devcontainer extensions updated, [0288cbc8](https://github.com/mrjackwills/oxker/commit/0288cbc8146cde1dd40ceaec9550198b635bb8f5) ++ dependencies updated, [1df4f78d](https://github.com/mrjackwills/oxker/commit/1df4f78dc41013c33d901925933b1ccb29ad4bc8), [5ae253b8](https://github.com/mrjackwills/oxker/commit/5ae253b8734ba0495e4e8149b17d5228b3d86f8d), [7a517db9](https://github.com/mrjackwills/oxker/commit/7a517db9f7c14c35e56ff70cf76ffb608fd30e17), [9c291cd9](https://github.com/mrjackwills/oxker/commit/9c291cd9c81b6d9a02085878588ed3b845fd0046), [0e90f4eb](https://github.com/mrjackwills/oxker/commit/0e90f4eb55ac5fb5d45e7d212c3686027dd3913e), [fe71cbfb](https://github.com/mrjackwills/oxker/commit/fe71cbfb00f166b7c02a6e28e64650ed1b47d15d) ++ docker-compose alpine version bump, [51ceab3e](https://github.com/mrjackwills/oxker/commit/51ceab3ebdb09356cd401d2f268840239255126f) ++ Rust 1.80 linting, [93e1279b](https://github.com/mrjackwills/oxker/commit/93e1279b1fc77019442a385e2e36be2fe438e828) ++ create_release v0.5.6, [f408acfe](https://github.com/mrjackwills/oxker/commit/f408acfe9a9f5a976735b8a8a51500fd7b865daf) + +### Docs ++ screenshot updated, [6975ebe7](https://github.com/mrjackwills/oxker/commit/6975ebe70f7058229c232e4a56b090f55247d2a2) + +### Features ++ left align all text, [e0d421c4](https://github.com/mrjackwills/oxker/commit/e0d421c4918a17c9e0e21fd214edb99d71281c9d) ++ place image name in logs panel title, [12f24357](https://github.com/mrjackwills/oxker/commit/12f24357a68abe871f44d871d95b6e2ef062181e) ++ distinguish between unhealthy & healthy running containers, closes [#43](https://github.com/mrjackwills/oxker/issues/43), [de876818](https://github.com/mrjackwills/oxker/commit/de8768181631c6d961ce0e4dacb50c2ed02abc36) ++ filter containers, use `F1` or `/` to enter filter mode, closes [#37](https://github.com/mrjackwills/oxker/issues/37), thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0db](https://github.com/mrjackwills/oxker/commit/d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e), [[7ee1f06f804683e3395953a02138d4e9da115ea9]] ++ place image name in logs panel title, [ef19b9cf](https://github.com/mrjackwills/oxker/commit/ef19b9cf89a881d0a7ac818885317ce2bd683dfc) + +### Fixes ++ log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc](https://github.com/mrjackwills/oxker/commit/0dc98dfc8113869b81be9d697ca77418c919e4bf) ++ Dockerfile command use uppercase, [068e4025](https://github.com/mrjackwills/oxker/commit/068e4025a5d6049a9a6951a0480a6bdef7379f88) ++ heading section help margin, [0e927aae](https://github.com/mrjackwills/oxker/commit/0e927aae178c1d8f60561b93607a26d45a1d9331) ++ install.sh use curl, [197a031b](https://github.com/mrjackwills/oxker/commit/197a031b8cf356f49f08e04472d0d1c489699415) + +### Tests ++ fix layout tests with new left alignment, [dfced564](https://github.com/mrjackwills/oxker/commit/dfced564278eafdbb8a5b95badbae3a7c4bf87b3) + # v0.6.4 ### 2024-05-25 diff --git a/Cargo.lock b/Cargo.lock index 74df6d9..7beacf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -26,7 +26,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -52,9 +52,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -67,33 +67,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -113,9 +113,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -134,15 +134,15 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bollard" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" +checksum = "4a063d51a634c7137ecd9f6390ec78e1c512e84c9ded80198ec7df3339a16a33" dependencies = [ "base64", "bollard-stubs", @@ -155,7 +155,7 @@ dependencies = [ "hyper", "hyper-named-pipe", "hyper-util", - "hyperlocal-next", + "hyperlocal", "log", "pin-project-lite", "serde", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.44.0-rc.2" +version = "1.45.0-rc.26.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" +checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" dependencies = [ "serde", "serde_repr", @@ -189,10 +189,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] -name = "bytes" -version = "1.6.0" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" [[package]] name = "cansi" @@ -208,18 +214,18 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] [[package]] name = "cc" -version = "1.0.98" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" @@ -237,14 +243,14 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] name = "clap" -version = "4.5.4" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +258,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstream", "anstyle", @@ -266,11 +272,11 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -278,15 +284,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "compact_str" @@ -316,13 +322,29 @@ dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio 1.0.1", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -365,9 +387,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" @@ -375,6 +397,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -455,9 +487,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hashbrown" @@ -475,12 +507,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -512,9 +538,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -522,12 +548,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -535,15 +561,21 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -551,6 +583,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -575,9 +608,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", @@ -594,10 +627,10 @@ dependencies = [ ] [[package]] -name = "hyperlocal-next" -version = "0.9.0" +name = "hyperlocal" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", @@ -654,9 +687,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -665,15 +698,15 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -695,9 +728,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -715,6 +748,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.12" @@ -727,30 +766,30 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ "hashbrown 0.14.5", ] [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -767,6 +806,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -792,21 +844,11 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" dependencies = [ "memchr", ] @@ -831,13 +873,13 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.6.4" +version = "0.7.0" dependencies = [ "anyhow", "bollard", "cansi", "clap", - "crossterm", + "crossterm 0.28.1", "directories", "futures-util", "parking_lot", @@ -869,7 +911,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -924,15 +966,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "dee4364d9f3b902ef14fab8a1ddffb783a1cb6b4bba3bfc1fa3922732c7de97f" +dependencies = [ + "zerocopy 0.6.6", +] [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -978,19 +1023,20 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.3" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" dependencies = [ "bitflags", "cassowary", "compact_str", - "crossterm", + "crossterm 0.27.0", "itertools", "lru", "paste", "stability", "strum", + "strum_macros", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -998,9 +1044,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] @@ -1022,6 +1068,19 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1042,18 +1101,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -1062,11 +1121,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -1096,15 +1156,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_derive", "serde_json", @@ -1132,12 +1192,13 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.1", "signal-hook", ] @@ -1177,9 +1238,9 @@ dependencies = [ [[package]] name = "stability" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", "syn", @@ -1199,20 +1260,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", @@ -1221,9 +1282,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -1232,18 +1293,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -1293,9 +1354,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -1308,28 +1369,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.1", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -1477,25 +1537,26 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-truncate" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools", + "unicode-segmentation", "unicode-width", ] [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -1504,15 +1565,15 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "rand", @@ -1526,9 +1587,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -1627,7 +1688,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1645,7 +1706,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1665,18 +1726,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1687,9 +1748,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1699,9 +1760,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1711,15 +1772,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1729,9 +1790,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1741,9 +1802,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1753,9 +1814,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1765,24 +1826,45 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" dependencies = [ - "zerocopy-derive", + "byteorder", + "zerocopy-derive 0.6.6", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f7e2d79..4255dc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.6.4" +version = "0.7.0" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" @@ -27,19 +27,19 @@ similar_names = "allow" [dependencies] anyhow = "1.0" -bollard = "0.16" +bollard = "0.17" cansi = "2.2" clap = { version = "4.5", features = ["color", "derive", "unicode"] } -crossterm = "0.27" +crossterm = "0.28" directories = "5.0" futures-util = "0.3" parking_lot = { version = "0.12" } -ratatui = "0.26" -tokio = { version = "1.37", features = ["full"] } +ratatui = "0.27" +tokio = { version = "1.39", features = ["full"] } tokio-util = "0.7" tracing = "0.1" tracing-subscriber = "0.3" -uuid = { version = "1.8", features = ["fast-rng", "v4"] } +uuid = { version = "1.10", features = ["fast-rng", "v4"] } [profile.release] lto = true diff --git a/README.md b/README.md index e492a62..cefc768 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ In application controls | ```( enter )```| Run selected docker command.| | ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. | | ```( 0 )``` | Stop sorting.| +| ```( F1 )``` or ```( / )``` | Enter filter mode. | | ```( e )``` | Exec into the selected container - not available on Windows.| | ```( h )``` | Toggle help menu.| | ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| @@ -167,10 +168,10 @@ cargo test Run some example docker images -using docker-compose.yml; +using docker/docker-compose.yml; ```shell -docker compose -f docker-compose.yml up -d +docker compose -f ./docker/docker-compose.yml up -d ``` or individually diff --git a/containerised/Dockerfile b/containerised/Dockerfile index 5cceeca..3c2c7b8 100644 --- a/containerised/Dockerfile +++ b/containerised/Dockerfile @@ -2,7 +2,7 @@ ## Builder ## ############# -FROM --platform=linux/amd64 rust:slim as BUILDER +FROM --platform=linux/amd64 rust:slim AS builder ARG TARGETARCH @@ -49,12 +49,12 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker / ## Runtime ## ############# -FROM scratch as RUNTIME +FROM scratch # Set an ENV to indicate that we're running in a container ENV OXKER_RUNTIME=container -COPY --from=BUILDER /oxker /app/ +COPY --from=builder /oxker /app/ # Run the application # this is used in the application itself so DO NOT EDIT diff --git a/create_release.sh b/create_release.sh index 096d9e9..04d0f9d 100755 --- a/create_release.sh +++ b/create_release.sh @@ -1,6 +1,7 @@ #!/bin/bash -# rust create_release v0.5.5 +# rust create_release v0.5.6 +# 2024-07-27 STAR_LINE='****************************************' CWD=$(pwd) @@ -191,25 +192,25 @@ check_cross() { fi } -cargo_build_x86_linux() { +cross_build_x86_linux() { check_cross echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}" cross build --target x86_64-unknown-linux-musl --release } -cargo_build_aarch64_linux() { +cross_build_aarch64_linux() { check_cross echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}" cross build --target aarch64-unknown-linux-musl --release } -cargo_build_armv6_linux() { +cross_build_armv6_linux() { check_cross echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}" cross build --target arm-unknown-linux-musleabihf --release } -cargo_build_x86_windows() { +cross_build_x86_windows() { check_cross echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}" cross build --target x86_64-pc-windows-gnu --release @@ -217,15 +218,15 @@ cargo_build_x86_windows() { # Build all releases that GitHub workflow would # This will download GB's of docker images -cargo_build_all() { +cross_build_all() { cargo clean - cargo_build_armv6_linux + cross_build_armv6_linux ask_continue - cargo_build_aarch64_linux + cross_build_aarch64_linux ask_continue - cargo_build_x86_linux + cross_build_x86_linux ask_continue - cargo_build_x86_windows + cross_build_x86_windows ask_continue } @@ -264,7 +265,7 @@ release_flow() { get_git_remote_url cargo_test - cargo_build_all + cross_build_all cargo_publish cd "${CWD}" || error_close "Can't find ${CWD}" @@ -347,23 +348,23 @@ build_choice() { exit ;; 1) - cargo_build_x86_linux + cross_build_x86_linux exit ;; 2) - cargo_build_aarch64_linux + cross_build_aarch64_linux exit ;; 3) - cargo_build_armv6_linux + cross_build_armv6_linux exit ;; 4) - cargo_build_x86_windows + cross_build_x86_windows exit ;; 5) - cargo_build_all + cross_build_all exit ;; esac diff --git a/docker/Dockerfile.unhealthy b/docker/Dockerfile.unhealthy new file mode 100644 index 0000000..34835d0 --- /dev/null +++ b/docker/Dockerfile.unhealthy @@ -0,0 +1,17 @@ +# Use an official lightweight image as a base +FROM alpine:latest + +# Install a simple utility (e.g., curl) to run as a health check +RUN apk --no-cache add curl + +# Create a dummy file that we will use in our health check +RUN touch /tmp/healthy + +# Define a simple health check +HEALTHCHECK --interval=5s --timeout=3s --retries=3 \ + CMD [ ! -f /tmp/healthy ] || exit 1 + +# Start a basic loop that keeps the container running +CMD ["sh", "-c", "while :; do echo 'Container is running but will be unhealthy'; sleep 30; done"] + +# docker build -t unhealthy-container . -f Dockerfile.unhealthy; docker run -d --name unhealthy unhealthy-container \ No newline at end of file diff --git a/docker-compose.yml b/docker/docker-compose.yml similarity index 68% rename from docker-compose.yml rename to docker/docker-compose.yml index 09f25f1..2b4c1ea 100644 --- a/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,7 +4,7 @@ networks: name: oxker-examaple-net services: postgres: - image: postgres:alpine3.19 + image: postgres:alpine3.20 container_name: postgres environment: - POSTGRES_PASSWORD=never_use_this_password_in_production @@ -18,7 +18,7 @@ services: limits: memory: 1024M redis: - image: redis:alpine3.19 + image: redis:alpine3.20 container_name: redis ipc: private restart: always @@ -39,5 +39,20 @@ services: resources: limits: memory: 512M + some_container: + container_name: some_container + image: some_container + build: + context: . + dockerfile: Dockerfile.unhealthy + ipc: private + restart: always + networks: + - oxker-example-net + deploy: + resources: + limits: + memory: 128M + diff --git a/install.sh b/install.sh index f53c55e..394c113 100755 --- a/install.sh +++ b/install.sh @@ -9,7 +9,7 @@ esac if [ -n "$SUFFIX" ]; then OXKER_GZ="oxker_linux_${SUFFIX}.tar.gz" - wget "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}" + curl -L -O "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}" tar xzvf "${OXKER_GZ}" oxker install -Dm 755 oxker -t "${HOME}/.local/bin" rm "${OXKER_GZ}" oxker diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 0dcee3e..7eccb32 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -10,6 +10,8 @@ use ratatui::{ widgets::{ListItem, ListState}, }; +use crate::ui::ORANGE; + use super::Header; const ONE_KB: f64 = 1000.0; @@ -48,6 +50,9 @@ impl PartialOrd for ContainerId { } } +pub trait Contains { + fn contains(&self, input: &str) -> bool; +} /// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them macro_rules! unit_struct { ($name:ident) => { @@ -67,7 +72,7 @@ macro_rules! unit_struct { } } - impl$name { + impl $name { pub fn get(&self) -> &str { self.0.as_str() } @@ -77,20 +82,18 @@ macro_rules! unit_struct { } } + impl Contains for $name { + fn contains(&self, input: &str) -> bool { + self.0.to_lowercase().contains(input) + } + } + impl std::fmt::Display for $name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { if self.0.chars().count() >= 30 { - write!( - f, - "{}…", - self.0.chars().take(29).collect::() - ) + write!(f, "{}…", self.0.chars().take(29).collect::()) } else { - write!( - f, - "{}", - self.0 - ) + write!(f, "{}", self.0) } } } @@ -207,62 +210,108 @@ impl StatefulList { } } -/// States of the container +/// Store the containers status in a struct, so can then check for healthy/unhealthy status +/// It's usually something like "Up 1 hour", "Exited (0) 10 hours ago", "Up 10 minutes (unhealthy)" +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] +pub struct ContainerStatus(String); + +impl From for ContainerStatus { + fn from(value: String) -> Self { + Self(value) + } +} + +impl ContainerStatus { + /// Check if a container is unhealthy + pub fn unhealthy(&self) -> bool { + self.contains("(unhealthy)") + } + + /// Get a reference to the source string + pub const fn get(&self) -> &String { + &self.0 + } +} + +impl Contains for ContainerStatus { + /// Check if the state contains a specific string + fn contains(&self, item: &str) -> bool { + self.0.to_lowercase().contains(item) + } +} + +/// By default a container's running status will be healthy #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd)] +pub enum RunningState { + Healthy, + Unhealthy, +} +/// States of the container +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum State { Dead, Exited, Paused, Removing, Restarting, - Running, + Running(RunningState), Unknown, } impl State { pub const fn is_alive(self) -> bool { - matches!(self, Self::Running) + matches!(self, Self::Running(_)) } pub const fn get_color(self) -> Color { match self { Self::Paused => Color::Yellow, Self::Removing => Color::LightRed, Self::Restarting => Color::LightGreen, - Self::Running => Color::Green, + Self::Running(RunningState::Healthy) => Color::Green, + Self::Running(RunningState::Unhealthy) => ORANGE, _ => Color::Red, } } /// Dirty way to create order for the state, rather than impl Ord pub const fn order(self) -> u8 { match self { - Self::Running => 0, - Self::Paused => 1, - Self::Restarting => 2, - Self::Removing => 3, - Self::Exited => 4, - Self::Dead => 5, - Self::Unknown => 6, + Self::Running(RunningState::Healthy) => 0, + Self::Running(RunningState::Unhealthy) => 1, + Self::Paused => 2, + Self::Restarting => 3, + Self::Removing => 4, + Self::Exited => 5, + Self::Dead => 6, + Self::Unknown => 7, } } } -impl From<&str> for State { - fn from(input: &str) -> Self { +/// Need status, to check if container is unhealthy or not +impl From<(&str, &ContainerStatus)> for State { + fn from((input, status): (&str, &ContainerStatus)) -> Self { match input { "dead" => Self::Dead, "exited" => Self::Exited, "paused" => Self::Paused, "removing" => Self::Removing, "restarting" => Self::Restarting, - "running" => Self::Running, + "running" => { + if status.unhealthy() { + Self::Running(RunningState::Unhealthy) + } else { + Self::Running(RunningState::Healthy) + } + } _ => Self::Unknown, } } } -impl From> for State { - fn from(input: Option) -> Self { - input.map_or(Self::Unknown, |input| Self::from(input.as_str())) +/// Again, need status, to check if container is unhealthy or not +impl From<(Option, &ContainerStatus)> for State { + fn from((input, status): (Option, &ContainerStatus)) -> Self { + input.map_or(Self::Unknown, |input| Self::from((input.as_str(), status))) } } @@ -274,7 +323,8 @@ impl fmt::Display for State { Self::Paused => "॥ paused", Self::Removing => "removing", Self::Restarting => "↻ restarting", - Self::Running => "✓ running", + Self::Running(RunningState::Healthy) => "✓ running", + Self::Running(RunningState::Unhealthy) => "! running", Self::Unknown => "? unknown", }; write!(f, "{disp}") @@ -310,7 +360,7 @@ impl DockerControls { State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete], State::Paused => vec![Self::Resume, Self::Stop, Self::Delete], State::Restarting => vec![Self::Stop, Self::Delete], - State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete], + State::Running(_) => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete], _ => vec![Self::Delete], } } @@ -539,7 +589,7 @@ pub struct ContainerItem { pub ports: Vec, pub rx: ByteStats, pub state: State, - pub status: String, + pub status: ContainerStatus, pub tx: ByteStats, } @@ -568,7 +618,7 @@ impl ContainerItem { name: String, ports: Vec, state: State, - status: String, + status: ContainerStatus, ) -> Self { let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state)); docker_controls.start(); @@ -665,14 +715,14 @@ impl Columns { pub const fn new() -> Self { Self { name: (Header::Name, 4), - state: (Header::State, 11), - status: (Header::Status, 16), - cpu: (Header::Cpu, 7), + state: (Header::State, 5), + status: (Header::Status, 6), + cpu: (Header::Cpu, 3), mem: (Header::Memory, 7, 7), id: (Header::Id, 8), image: (Header::Image, 5), - net_rx: (Header::Rx, 7), - net_tx: (Header::Tx, 7), + net_rx: (Header::Rx, 4), + net_tx: (Header::Tx, 4), } } } @@ -682,11 +732,11 @@ mod tests { use ratatui::widgets::ListItem; use crate::{ - app_data::{ContainerImage, Logs}, + app_data::{ContainerImage, Logs, RunningState}, ui::log_sanitizer, }; - use super::{ByteStats, ContainerName, CpuStats, LogsTz}; + use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State}; #[test] /// Display CpuStats as a string @@ -770,4 +820,55 @@ mod tests { assert_eq!(logs.logs.items.len(), 2); } + + #[test] + /// check ContainerStatus unhealthy state + fn test_container_state_unhealthy() { + let input = ContainerStatus::from("Up 1 hour".to_owned()); + + assert!(!input.unhealthy()); + + let input = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); + + assert!(input.unhealthy()); + } + + #[test] + /// Generate container State from a &str and &ContainerStatus + fn test_container_status_unhealthy() { + let healthy = ContainerStatus::from("Up 1 hour".to_owned()); + let unhealthy = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); + + // Running and healthy + let input = State::from(("running", &healthy)); + assert_eq!(input, State::Running(RunningState::Healthy)); + + // Running and unhealthy + let input = State::from(("running", &unhealthy)); + assert_eq!(input, State::Running(RunningState::Unhealthy)); + + // Dead + let input = State::from(("dead", &healthy)); + assert_eq!(input, State::Dead); + + // Exited + let input = State::from(("exited", &healthy)); + assert_eq!(input, State::Exited); + + // Paused + let input = State::from(("paused", &healthy)); + assert_eq!(input, State::Paused); + + // Removing + let input = State::from(("removing", &healthy)); + assert_eq!(input, State::Removing); + + // Restarting + let input = State::from(("restarting", &healthy)); + assert_eq!(input, State::Restarting); + + // Unknown + let input = State::from(("oxker", &healthy)); + assert_eq!(input, State::Unknown); + } } diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index d6d8e92..e57f79c 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -3,6 +3,7 @@ use core::fmt; use parking_lot::Mutex; use ratatui::widgets::{ListItem, ListState}; use std::{ + hash::Hash, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -54,12 +55,73 @@ impl fmt::Display for Header { } } +#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum FilterBy { + #[default] + Name, + Image, + Status, + All, +} + +/// Convert errors into strings to display +impl fmt::Display for FilterBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Name => "Name", + Self::Image => "Image", + Self::Status => "Status", + Self::All => "All", + } + ) + } +} + +impl FilterBy { + const fn next(self) -> Option { + match self { + Self::Name => Some(Self::Image), + Self::Image => Some(Self::Status), + Self::Status => Some(Self::All), + Self::All => None, + } + } + + const fn prev(self) -> Option { + match self { + Self::Name => None, + Self::Image => Some(Self::Name), + Self::Status => Some(Self::Image), + Self::All => Some(Self::Status), + } + } +} + +#[derive(Debug, Clone)] +pub struct Filter { + pub term: Option, + pub by: FilterBy, +} +impl Filter { + pub fn new() -> Self { + Self { + term: None, + by: FilterBy::default(), + } + } +} + /// Global app_state, stored in an Arc #[derive(Debug, Clone)] #[cfg(not(test))] pub struct AppData { containers: StatefulList, error: Option, + filter: Filter, + hidden_containers: Vec, sorted_by: Option<(Header, SortedOrder)>, pub args: CliArgs, } @@ -67,10 +129,12 @@ pub struct AppData { #[derive(Debug, Clone)] #[cfg(test)] pub struct AppData { + pub args: CliArgs, pub containers: StatefulList, pub error: Option, + pub filter: Filter, + pub hidden_containers: Vec, pub sorted_by: Option<(Header, SortedOrder)>, - pub args: CliArgs, } impl AppData { @@ -79,8 +143,10 @@ impl AppData { Self { args, containers: StatefulList::new(vec![]), + hidden_containers: vec![], error: None, sorted_by: None, + filter: Filter::new(), } } @@ -93,6 +159,126 @@ impl AppData { .as_secs() } + /// Filter related methods + + /// Get the current filter term + pub const fn get_filter_term(&self) -> Option<&String> { + self.filter.term.as_ref() + } + + /// Get the current filter by choice + pub const fn get_filter_by(&self) -> FilterBy { + self.filter.by + } + + /// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by + fn can_insert(&self, container: &ContainerItem) -> bool { + self.filter.term.as_ref().map_or(true, |term| { + let term = term.to_lowercase(); + match self.filter.by { + FilterBy::All => { + container.name.contains(&term) + || container.image.contains(&term) + || container.status.contains(&term) + } + FilterBy::Image => container.image.contains(&term), + FilterBy::Name => container.name.contains(&term), + FilterBy::Status => container.status.contains(&term), + } + }) + } + + /// Remove items from the containers list based on the filter term, and insert into a "hidden" vec + /// sets the state to start if any filtering has occurred + /// Also search in the "hidden" vec for items and insert back into the main containers vec + fn filter_containers(&mut self) { + let pre_len = self.get_container_len(); + + if !self.hidden_containers.is_empty() { + let (mut new_items, tmp_items): (Vec<_>, Vec<_>) = self + .hidden_containers + .iter() + .cloned() + .partition(|item| self.can_insert(item)); + + while let Some(x) = new_items.pop() { + self.containers.items.push(x); + } + self.hidden_containers = tmp_items; + } + + let (new_items, tmp_items) = self + .containers + .items + .iter() + .cloned() + .partition(|item| self.can_insert(item)); + + self.containers.items = new_items; + self.hidden_containers.extend(tmp_items); + + self.sort_containers(); + if self.get_container_len() != pre_len { + self.containers.start(); + } + } + + /// Re-filter the containers, used after the filter.by has been changed + fn re_filter(&mut self) { + self.containers.items.append(&mut self.hidden_containers); + self.hidden_containers = vec![]; + self.filter_containers(); + } + + /// Set a single char into the filter term + pub fn filter_term_push(&mut self, c: char) { + if let Some(term) = self.filter.term.as_mut() { + term.push(c); + } else { + self.filter.term = Some(format!("{c}")); + }; + self.filter_containers(); + } + + /// Delete the final char of the filter term + pub fn filter_term_pop(&mut self) { + if let Some(term) = self.filter.term.as_mut() { + // should now search for items in the tmp vec, and insert into containers if found + term.pop(); + if term.is_empty() { + self.filter.term = None; + } + } + self.filter_containers(); + } + + // change the filter_by option + pub fn filter_by_next(&mut self) { + if let Some(by) = self.filter.by.next() { + self.filter.by = by; + self.re_filter(); + } + } + + // change the filter_by option + pub fn filter_by_prev(&mut self) { + if let Some(by) = self.filter.by.prev() { + self.filter.by = by; + self.re_filter(); + } + } + + /// Remove the filter completely + pub fn filter_term_clear(&mut self) { + self.filter.term = None; + while let Some(i) = self.hidden_containers.pop() { + if self.get_container_by_id(&i.id).is_none() { + self.containers.items.push(i); + }; + } + self.sort_containers(); + } + /// Container sort related methods /// Change the sorted order, also set the selected container state to match new order @@ -149,7 +335,8 @@ impl AppData { Header::Status => item_ord .0 .status - .cmp(&item_ord.1.status) + .get() + .cmp(item_ord.1.status.get()) .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), Header::Cpu => item_ord .0 @@ -206,7 +393,7 @@ impl AppData { /// Container state methods - /// Just get the total number of containers + /// Get the total number of none "hidden" containers pub fn get_container_len(&self) -> usize { self.containers.items.len() } @@ -216,9 +403,14 @@ impl AppData { &self.containers.items } - /// Get title for containers section + /// Get title for containers section, add a suffix indicating if the containers are currently under filter pub fn container_title(&self) -> String { - self.containers.get_state_title() + let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() { + " - filtered" + } else { + "" + }; + format!("{}{}", self.containers.get_state_title(), suffix) } /// Select the first container @@ -260,35 +452,35 @@ impl AppData { let mut longest_private = 10; let mut longest_public = 9; - for item in &self.containers.items { - // if let Some(ports) = item.ports.as_ref() { - longest_ip = longest_ip.max( - item.ports - .iter() - .map(ContainerPorts::len_ip) - .max() - .unwrap_or(3), - ); - longest_private = longest_private.max( - item.ports - .iter() - .map(ContainerPorts::len_private) - .max() - .unwrap_or(8), - ); - longest_public = longest_public.max( - item.ports - .iter() - .map(ContainerPorts::len_public) - .max() - .unwrap_or(6), - ); + for item in [&self.containers.items, &self.hidden_containers] { + for item in item { + longest_ip = longest_ip.max( + item.ports + .iter() + .map(ContainerPorts::len_ip) + .max() + .unwrap_or(3), + ); + longest_private = longest_private.max( + item.ports + .iter() + .map(ContainerPorts::len_private) + .max() + .unwrap_or(8), + ); + longest_public = longest_public.max( + item.ports + .iter() + .map(ContainerPorts::len_public) + .max() + .unwrap_or(6), + ); + } } - // } (longest_ip, longest_private, longest_public) - // ) } + /// Get Option of the current selected container's ports, sorted by private port pub fn get_selected_ports(&mut self) -> Option<(Vec, State)> { if let Some(item) = self.get_mut_selected_container() { @@ -307,11 +499,16 @@ impl AppData { .and_then(|i| self.containers.items.get_mut(i)) } - /// return a mutable container by given id + /// Get a mutable container by given id fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { self.containers.items.iter_mut().find(|i| &i.id == id) } + /// Get a mutable container by given id in the tmp_container vec + fn get_hidden_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { + self.hidden_containers.iter_mut().find(|i| &i.id == id) + } + /// Get the ContainerName of by ID pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { self.containers @@ -333,6 +530,7 @@ impl AppData { self.get_selected_container() .map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) } + /// Selected DockerCommand methods /// Get the current selected docker command @@ -392,8 +590,8 @@ impl AppData { /// Logs related methods /// Get the title for log panel for selected container, will be either - /// 1) "logs x/x - container_name" where container_name is 32 chars max - /// 2) "logs - container_name" when no logs found, again 32 chars max + /// 1) "logs x/x - container_name - container_image" + /// 2) "logs - container_name - container_image" when no logs found /// 3) "" no container currently selected - aka no containers on system pub fn get_log_title(&self) -> String { self.get_selected_container() @@ -404,7 +602,7 @@ impl AppData { } else { format!("{logs_len} ") }; - format!("{}- {}", prefix, ci.name.get()) + format!("{}- {} - {}", prefix, ci.name.get(), ci.image.get()) }) } @@ -467,17 +665,17 @@ impl AppData { /// Error related methods - /// return single app_state error + /// Get single app_state error pub const fn get_error(&self) -> Option { self.error } - /// remove single app_state error + /// Remove single app_state error pub fn remove_error(&mut self) { self.error = None; } - /// insert single app_state error + /// Insert single app_state error pub fn set_error(&mut self, error: AppError, gui_state: &Arc>, status: Status) { gui_state.lock().status_push(status); self.error = Some(error); @@ -498,44 +696,55 @@ impl AppData { /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly + /// Searches in both contains & hidden_containers pub fn get_width(&self) -> Columns { let mut columns = Columns::new(); let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12); // Should probably find a refactor here somewhere - for container in &self.containers.items { - let cpu_count = count( - &container - .cpu_stats - .back() - .unwrap_or(&CpuStats::default()) - .to_string(), - ); + for container in [&self.containers.items, &self.hidden_containers] { + for container in container { + let cpu_count = count( + &container + .cpu_stats + .back() + .unwrap_or(&CpuStats::default()) + .to_string(), + ); - let mem_current_count = count( - &container - .mem_stats - .back() - .unwrap_or(&ByteStats::default()) - .to_string(), - ); + let mem_current_count = count( + &container + .mem_stats + .back() + .unwrap_or(&ByteStats::default()) + .to_string(), + ); - // Issue here! - columns.cpu.1 = columns.cpu.1.max(cpu_count); - columns.image.1 = columns.image.1.max(count(&container.image.to_string())); - columns.mem.1 = columns.mem.1.max(mem_current_count); - columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string())); - columns.name.1 = columns.name.1.max(count(&container.name.to_string())); - columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string())); - columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string())); - columns.state.1 = columns.state.1.max(count(&container.state.to_string())); - columns.status.1 = columns.status.1.max(count(&container.status)); + columns.cpu.1 = columns.cpu.1.max(cpu_count); + columns.image.1 = columns.image.1.max(count(&container.image.to_string())); + columns.mem.1 = columns.mem.1.max(mem_current_count); + columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string())); + columns.name.1 = columns.name.1.max(count(&container.name.to_string())); + columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string())); + columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string())); + columns.state.1 = columns.state.1.max(count(&container.state.to_string())); + columns.status.1 = columns.status.1.max(count(container.status.get())); + } } columns } /// Update related methods + /// Get mutable reference to a container in the containers vec & the hidden_containers vec + fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { + if self.get_hidden_container_by_id(id).is_some() { + self.get_hidden_container_by_id(id) + } else { + self.get_container_by_id(id) + } + } + /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once /// Will also, if a sort is set, sort the containers pub fn update_stats_by_id( @@ -547,7 +756,7 @@ impl AppData { rx: u64, tx: u64, ) { - if let Some(container) = self.get_container_by_id(id) { + if let Some(container) = self.get_any_container_by_id(id) { if container.cpu_stats.len() >= 60 { container.cpu_stats.pop_front(); } @@ -628,12 +837,13 @@ impl AppData { .as_ref() .map_or(false, |i| i.starts_with(ENTRY_POINT)); - let state = State::from(i.state.as_ref().map_or("dead", |z| z)); - let status = i - .status - .as_ref() - .map_or(String::new(), std::clone::Clone::clone); + let status = ContainerStatus::from( + i.status + .as_ref() + .map_or(String::new(), std::clone::Clone::clone), + ); + let state = State::from((i.state.as_ref().map_or("dead", |z| z), &status)); let image = i .image .as_ref() @@ -642,8 +852,8 @@ impl AppData { let created = i .created .map_or(0, |i| u64::try_from(i).unwrap_or_default()); - // If container info already in containers Vec, then just update details - if let Some(item) = self.get_container_by_id(&id) { + + if let Some(item) = self.get_any_container_by_id(&id) { if item.name.get() != name { item.name.set(name); }; @@ -668,24 +878,29 @@ impl AppData { item.image.set(image); }; } else { - // container not known, so make new ContainerItem and push into containers Vec + // container not known, so make new ContainerItem and push into containers Ve let container = ContainerItem::new( created, id, image, is_oxker, name, ports, state, status, ); - self.containers.items.push(container); + let can_insert = self.can_insert(&container); + if can_insert { + self.containers.items.push(container); + } else { + self.hidden_containers.push(container); + } } } } } - /// update logs of a given container, based on id + /// Update logs of a given container, based on id pub fn update_log_by_id(&mut self, logs: Vec, id: &ContainerId) { let color = self.args.color; let raw = self.args.raw; let timestamp = self.args.timestamp; - if let Some(container) = self.get_container_by_id(id) { + if let Some(container) = self.get_any_container_by_id(id) { if !container.is_oxker { container.last_updated = Self::get_systemtime(); let current_len = container.logs.len(); @@ -770,7 +985,7 @@ mod tests { i.state = State::Exited; } if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - i.state = State::Running; + i.state = State::Running(RunningState::Healthy); } if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { i.state = State::Paused; @@ -804,11 +1019,12 @@ mod tests { assert_eq!(result, &containers); if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { - "Exited (0) 10 minutes ago".clone_into(&mut i.status); + ContainerStatus::from("Exited (0) 10 minutes ago".to_owned()).clone_into(&mut i.status); } if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { - "Up 2 hours (Paused)".clone_into(&mut i.status); + // "Up 2 hours (Paused)".clone_into(&mut i.status); + ContainerStatus::from("Up 2 hours (Paused)".to_owned()).clone_into(&mut i.status); } // Sort by status @@ -1129,7 +1345,7 @@ mod tests { result, Some(( ContainerId::from("1"), - State::Running, + State::Running(RunningState::Healthy), "container_1".to_owned() )) ); @@ -1143,7 +1359,7 @@ mod tests { result, Some(( ContainerId::from("1"), - State::Running, + State::Running(RunningState::Healthy), "container_1".to_owned() )) ); @@ -1171,7 +1387,7 @@ mod tests { result, Some(( ContainerId::from("2"), - State::Running, + State::Running(RunningState::Healthy), "container_2".to_owned() )) ); @@ -1196,7 +1412,7 @@ mod tests { result, Some(( ContainerId::from("3"), - State::Running, + State::Running(RunningState::Healthy), "container_3".to_owned() )) ); @@ -1210,7 +1426,7 @@ mod tests { result, Some(( ContainerId::from("3"), - State::Running, + State::Running(RunningState::Healthy), "container_3".to_owned() )) ); @@ -1291,7 +1507,7 @@ mod tests { result, Some(( ContainerId::from("3"), - State::Running, + State::Running(RunningState::Healthy), "container_3".to_owned() )) ); @@ -1381,7 +1597,7 @@ mod tests { "container_1".to_owned(), vec![], state, - "Up 1 hour".to_owned(), + ContainerStatus::from("Up 1 hour".to_owned()), ) }; let mut app_data = gen_appdata(&[gen_item_state(state)]); @@ -1422,7 +1638,7 @@ mod tests { &mut vec![DockerControls::Stop, DockerControls::Delete], ); test_state( - State::Running, + State::Running(RunningState::Healthy), &mut vec![ DockerControls::Pause, DockerControls::Restart, @@ -1433,6 +1649,163 @@ mod tests { test_state(State::Unknown, &mut vec![DockerControls::Delete]); } + // ****** // + // Filter // + // ****** // + + #[test] + /// Data is filtered correctly by name + fn test_app_data_filter_by_name() { + let (_, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + assert!(app_data.get_filter_term().is_none()); + + let pre_len = app_data.containers.items.len(); + app_data.filter_term_push('_'); + app_data.filter_term_push('2'); + + assert_eq!(app_data.get_filter_term(), Some(&"_2".to_string())); + + app_data.filter_containers(); + let post_len = app_data.containers.items.len(); + assert!(pre_len != post_len); + assert_eq!(post_len, 1); + + // Can insert checks against the current filter term + assert!(app_data.can_insert(&containers[1])); + assert!(!app_data.can_insert(&containers[0])); + assert!(!app_data.can_insert(&containers[2])); + } + + #[test] + /// Data is filtered correctly by image + fn test_app_data_filter_by_image() { + let (_, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + assert!(app_data.get_filter_term().is_none()); + + let pre_len = app_data.containers.items.len(); + for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] { + app_data.filter_term_push(c); + } + // app_data.filter_term_push('2'); + app_data.filter_by_next(); + + assert_eq!(app_data.get_filter_by(), FilterBy::Image); + assert_eq!(app_data.get_filter_term(), Some(&"image_2".to_string())); + + app_data.filter_containers(); + let post_len = app_data.containers.items.len(); + assert!(pre_len != post_len); + assert_eq!(post_len, 1); + + assert!(!app_data.can_insert(&containers[0])); + assert!(app_data.can_insert(&containers[1])); + assert!(!app_data.can_insert(&containers[2])); + } + + #[test] + /// Data is filtered correctly by status + fn test_app_data_filter_by_status() { + let (_, mut containers) = gen_containers(); + ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status); + let mut app_data = gen_appdata(&containers); + + assert!(app_data.get_filter_term().is_none()); + + let pre_len = app_data.containers.items.len(); + app_data.filter_term_push('x'); + + app_data.filter_by_next(); + app_data.filter_by_next(); + + assert_eq!(app_data.get_filter_by(), FilterBy::Status); + assert_eq!(app_data.get_filter_term(), Some(&"x".to_string())); + + app_data.filter_containers(); + let post_len = app_data.containers.items.len(); + assert!(pre_len != post_len); + assert_eq!(post_len, 1); + + assert!(app_data.can_insert(&containers[0])); + assert!(!app_data.can_insert(&containers[1])); + assert!(!app_data.can_insert(&containers[2])); + } + + #[test] + /// Data is filtered correctly by all + fn test_app_data_filter_by_all() { + let (_, mut containers) = gen_containers(); + ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status); + let mut app_data = gen_appdata(&containers); + + assert!(app_data.get_filter_term().is_none()); + + let pre_len = app_data.containers.items.len(); + app_data.filter_term_push('x'); + + app_data.filter_by_next(); + app_data.filter_by_next(); + app_data.filter_by_next(); + + assert_eq!(app_data.get_filter_by(), FilterBy::All); + assert_eq!(app_data.get_filter_term(), Some(&"x".to_string())); + + app_data.filter_containers(); + let post_len = app_data.containers.items.len(); + assert!(pre_len != post_len); + assert_eq!(post_len, 1); + + assert!(app_data.can_insert(&containers[0])); + assert!(!app_data.can_insert(&containers[1])); + assert!(!app_data.can_insert(&containers[2])); + } + + #[test] + /// Data is filtered correctly after various next() and previous() commands + fn test_app_data_filter_prev() { + let (_, mut containers) = gen_containers(); + ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status); + let mut app_data = gen_appdata(&containers); + + assert!(app_data.get_filter_term().is_none()); + + let pre_len = app_data.containers.items.len(); + app_data.filter_term_push('x'); + + app_data.filter_by_next(); + app_data.filter_by_next(); + + assert_eq!(app_data.get_filter_by(), FilterBy::Status); + assert_eq!(app_data.get_filter_term(), Some(&"x".to_string())); + + app_data.filter_containers(); + let post_len = app_data.containers.items.len(); + assert!(pre_len != post_len); + assert_eq!(post_len, 1); + + assert!(app_data.can_insert(&containers[0])); + assert!(!app_data.can_insert(&containers[1])); + assert!(!app_data.can_insert(&containers[2])); + + app_data.filter_by_prev(); + assert_eq!(app_data.get_filter_by(), FilterBy::Image); + assert_eq!(app_data.get_filter_term(), Some(&"x".to_string())); + + app_data.filter_containers(); + let post_len = app_data.containers.items.len(); + assert!(pre_len != post_len); + assert_eq!(post_len, 0); + + assert!(!app_data.can_insert(&containers[0])); + assert!(!app_data.can_insert(&containers[1])); + assert!(!app_data.can_insert(&containers[2])); + } + // **** // // Logs // // **** // @@ -1451,18 +1824,18 @@ mod tests { // No logs app_data.containers.start(); let result = app_data.get_log_title(); - assert_eq!(result, " - container_1"); + assert_eq!(result, " - container_1 - image_1"); // On last line of logs let logs = (1..=3).map(|i| format!("{i}")).collect::>(); app_data.update_log_by_id(logs, &ids[0]); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1"); + assert_eq!(result, " 3/3 - container_1 - image_1"); // Change log state to no longer be at the end app_data.log_previous(); let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_1"); + assert_eq!(result, " 2/3 - container_1 - image_1"); } #[test] @@ -1478,23 +1851,23 @@ mod tests { app_data.containers_start(); let result = app_data.get_log_title(); - assert_eq!(result, " - container_1"); + assert_eq!(result, " - container_1 - image_1"); // change container app_data.containers_next(); let result = app_data.get_log_title(); - assert_eq!(result, " - container_2"); + assert_eq!(result, " - container_2 - image_2"); // On last line of logs let logs = (1..=3).map(|i| format!("{i}")).collect::>(); app_data.update_log_by_id(logs, &ids[1]); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_2"); + assert_eq!(result, " 3/3 - container_2 - image_2"); // Change log state to no longer be at the end app_data.log_previous(); let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_2"); + assert_eq!(result, " 2/3 - container_2 - image_2"); } #[test] @@ -1522,7 +1895,7 @@ mod tests { assert_eq!(result.len(), 3); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1"); + assert_eq!(result, " 3/3 - container_1 - image_1"); } #[test] @@ -1542,7 +1915,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1"); + assert_eq!(result, " 1/3 - container_1 - image_1"); } #[test] @@ -1562,7 +1935,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1"); + assert_eq!(result, " 1/3 - container_1 - image_1"); app_data.log_end(); let result = app_data.get_log_state(); @@ -1571,7 +1944,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1"); + assert_eq!(result, " 3/3 - container_1 - image_1"); } #[test] @@ -1592,7 +1965,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1"); + assert_eq!(result, " 1/3 - container_1 - image_1"); app_data.log_next(); @@ -1602,7 +1975,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_1"); + assert_eq!(result, " 2/3 - container_1 - image_1"); app_data.log_next(); let result = app_data.get_log_state(); @@ -1611,7 +1984,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1"); + assert_eq!(result, " 3/3 - container_1 - image_1"); app_data.log_next(); let result = app_data.get_log_state(); @@ -1620,7 +1993,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1"); + assert_eq!(result, " 3/3 - container_1 - image_1"); } #[test] @@ -1641,7 +2014,7 @@ mod tests { assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 3/3 - container_1"); + assert_eq!(result, " 3/3 - container_1 - image_1"); app_data.log_previous(); @@ -1650,7 +2023,7 @@ mod tests { assert_eq!(result.as_ref().unwrap().selected(), Some(1)); assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 2/3 - container_1"); + assert_eq!(result, " 2/3 - container_1 - image_1"); app_data.log_previous(); let result = app_data.get_log_state(); @@ -1658,7 +2031,7 @@ mod tests { assert_eq!(result.as_ref().unwrap().selected(), Some(0)); assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1"); + assert_eq!(result, " 1/3 - container_1 - image_1"); app_data.log_previous(); let result = app_data.get_log_state(); @@ -1666,7 +2039,7 @@ mod tests { assert_eq!(result.as_ref().unwrap().selected(), Some(0)); assert_eq!(result.unwrap().offset(), 0); let result = app_data.get_log_title(); - assert_eq!(result, " 1/3 - container_1"); + assert_eq!(result, " 1/3 - container_1 - image_1"); } // ********** // @@ -1696,12 +2069,12 @@ mod tests { ( vec![(0.0, 1.1), (1.0, 1.2)], CpuStats::new(1.2), - State::Running + State::Running(RunningState::Healthy), ), ( vec![(0.0, 1.0), (1.0, 2.0)], ByteStats::new(2), - State::Running + State::Running(RunningState::Healthy), ) )) ); @@ -1720,9 +2093,9 @@ mod tests { let result = app_data.get_width(); let expected = Columns { name: (Header::Name, 11), - state: (Header::State, 11), - status: (Header::Status, 16), - cpu: (Header::Cpu, 7), + state: (Header::State, 9), + status: (Header::Status, 9), + cpu: (Header::Cpu, 6), mem: (Header::Memory, 7, 7), id: (Header::Id, 8), image: (Header::Image, 7), @@ -1732,6 +2105,32 @@ mod tests { assert_eq!(result, expected); } + #[test] + /// Header widths return correctly when some containers hidden + fn test_app_data_get_width_filtered() { + let (_ids, mut containers) = gen_containers(); + containers[0].name = ContainerName::from("some_longer_name_with_filter"); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_width(); + let expected = Columns { + name: (Header::Name, 28), + state: (Header::State, 9), + status: (Header::Status, 9), + cpu: (Header::Cpu, 6), + mem: (Header::Memory, 7, 7), + id: (Header::Id, 8), + image: (Header::Image, 7), + net_rx: (Header::Rx, 7), + net_tx: (Header::Tx, 7), + }; + + assert_eq!(result, expected); + app_data.filter_term_push('c'); + app_data.filter_containers(); + assert_eq!(result, expected); + } + // ***** // // Ports // // ***** // @@ -1791,7 +2190,7 @@ mod tests { public: None } ], - State::Running + State::Running(RunningState::Healthy), )) ); @@ -1800,7 +2199,10 @@ mod tests { app_data.containers.items[0].ports = vec![]; let result = app_data.get_selected_ports(); - assert_eq!(result, Some((vec![], State::Running))); + assert_eq!( + result, + Some((vec![], State::Running(RunningState::Healthy))) + ); } // ************** // diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 302f0d5..307d058 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -22,7 +22,7 @@ use tokio::{ use uuid::Uuid; use crate::{ - app_data::{AppData, ContainerId, DockerControls, State}, + app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State}, app_error::AppError, parse_args::CliArgs, ui::{GuiState, Status}, @@ -201,7 +201,7 @@ impl DockerData { /// Get all current containers, handle into ContainerItem in the app_data struct rather than here /// Just make sure that items sent are guaranteed to have an id /// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set - pub async fn update_all_containers(&mut self) -> Vec<(State, ContainerId)> { + pub async fn update_all_containers(&self) -> Vec<(State, ContainerId)> { let containers = self .docker .list_containers(Some(ListContainersOptions:: { @@ -236,7 +236,15 @@ impl DockerData { output .into_iter() .filter_map(|i| { - i.id.map(|id| (State::from(i.state), ContainerId::from(id.as_str()))) + i.id.map(|id| { + ( + State::from(( + i.state, + &ContainerStatus::from(i.status.map_or_else(String::new, |i| i)), + )), + ContainerId::from(id.as_str()), + ) + }) }) .collect::>() } @@ -271,7 +279,7 @@ impl DockerData { } /// Update all logs, spawn each container into own tokio::spawn thread - fn init_all_logs(&mut self, all_ids: &[(State, ContainerId)]) { + fn init_all_logs(&self, all_ids: &[(State, ContainerId)]) { for (_, id) in all_ids { let docker = Arc::clone(&self.docker); let app_data = Arc::clone(&self.app_data); @@ -303,13 +311,14 @@ impl DockerData { }; self.update_all_container_stats(&all_ids); self.app_data.lock().sort_containers(); + self.gui_state.lock().stop_loading_animation(Uuid::nil()); } /// Initialize docker container data, before any messages are received async fn initialise_container_data(&mut self) { self.gui_state.lock().status_push(Status::Init); let loading_uuid = Uuid::new_v4(); - let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid); + GuiState::start_loading_animation(&self.gui_state, loading_uuid); let all_ids = self.update_all_containers().await; self.update_all_container_stats(&all_ids); @@ -323,9 +332,7 @@ impl DockerData { self.init = None; } } - self.gui_state - .lock() - .stop_loading_animation(&loading_handle, loading_uuid); + self.gui_state.lock().stop_loading_animation(loading_uuid); self.gui_state.lock().status_del(Status::Init); } @@ -356,27 +363,27 @@ impl DockerData { } DockerMessage::Pause(id) => { tokio::spawn(async move { - let handle = GuiState::start_loading_animation(&gui_state, uuid); + GuiState::start_loading_animation(&gui_state, uuid); if docker.pause_container(id.get()).await.is_err() { Self::set_error(&app_data, DockerControls::Pause, &gui_state); } - gui_state.lock().stop_loading_animation(&handle, uuid); + gui_state.lock().stop_loading_animation(uuid); }); self.update_everything().await; } DockerMessage::Restart(id) => { tokio::spawn(async move { - let handle = GuiState::start_loading_animation(&gui_state, uuid); + GuiState::start_loading_animation(&gui_state, uuid); if docker.restart_container(id.get(), None).await.is_err() { Self::set_error(&app_data, DockerControls::Restart, &gui_state); } - gui_state.lock().stop_loading_animation(&handle, uuid); + gui_state.lock().stop_loading_animation(uuid); }); self.update_everything().await; } DockerMessage::Start(id) => { tokio::spawn(async move { - let handle = GuiState::start_loading_animation(&gui_state, uuid); + GuiState::start_loading_animation(&gui_state, uuid); if docker .start_container(id.get(), None::>) .await @@ -384,33 +391,33 @@ impl DockerData { { Self::set_error(&app_data, DockerControls::Start, &gui_state); } - gui_state.lock().stop_loading_animation(&handle, uuid); + gui_state.lock().stop_loading_animation(uuid); }); self.update_everything().await; } DockerMessage::Stop(id) => { tokio::spawn(async move { - let handle = GuiState::start_loading_animation(&gui_state, uuid); + GuiState::start_loading_animation(&gui_state, uuid); if docker.stop_container(id.get(), None).await.is_err() { Self::set_error(&app_data, DockerControls::Stop, &gui_state); } - gui_state.lock().stop_loading_animation(&handle, uuid); + gui_state.lock().stop_loading_animation(uuid); }); self.update_everything().await; } DockerMessage::Resume(id) => { tokio::spawn(async move { - let handle = GuiState::start_loading_animation(&gui_state, uuid); + GuiState::start_loading_animation(&gui_state, uuid); if docker.unpause_container(id.get()).await.is_err() { Self::set_error(&app_data, DockerControls::Resume, &gui_state); } - gui_state.lock().stop_loading_animation(&handle, uuid); + gui_state.lock().stop_loading_animation(uuid); }); self.update_everything().await; } DockerMessage::Delete(id) => { tokio::spawn(async move { - let handle = GuiState::start_loading_animation(&gui_state, uuid); + GuiState::start_loading_animation(&gui_state, uuid); if docker .remove_container( id.get(), @@ -425,7 +432,7 @@ impl DockerData { { Self::set_error(&app_data, DockerControls::Stop, &gui_state); } - gui_state.lock().stop_loading_animation(&handle, uuid); + gui_state.lock().stop_loading_animation(uuid); }); self.update_everything().await; self.gui_state.lock().set_delete_container(None); diff --git a/src/exec.rs b/src/exec.rs index 2f3a9d2..1c3468e 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -18,7 +18,7 @@ use tokio::{ use tokio_util::sync::CancellationToken; use crate::{ - app_data::{AppData, ContainerId, State}, + app_data::{AppData, ContainerId, RunningState, State}, app_error::AppError, }; @@ -162,7 +162,12 @@ impl ExecMode { let container = app_data.lock().get_selected_container_id_state_name(); if let Some((id, state, _)) = container { - if state == State::Running { + if [ + State::Running(RunningState::Healthy), + State::Running(RunningState::Unhealthy), + ] + .contains(&state) + { if tty_readable() && !use_cli { if let Ok(exec) = docker .create_exec( diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 2cb4f8e..3110596 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -71,6 +71,7 @@ impl InputHandler { Status::Error, Status::Help, Status::DeleteConfirm, + Status::Filter, ]) { self.mouse_press(mouse_event); } @@ -125,7 +126,7 @@ impl InputHandler { 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); + GuiState::start_loading_animation(&self.gui_state, uuid); let (sx, rx) = tokio::sync::oneshot::channel::>(); self.docker_tx.send(DockerMessage::Exec(sx)).await.ok(); @@ -143,7 +144,7 @@ impl InputHandler { }, ); } - self.gui_state.lock().stop_loading_animation(&handle, uuid); + self.gui_state.lock().stop_loading_animation(uuid); } } @@ -177,7 +178,7 @@ impl InputHandler { } /// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file - async fn s_key(&mut self) { + async fn s_key(&self) { /// This is the inner workings, *inlined* here to return a Result async fn save_logs( app_data: &Arc>, @@ -248,7 +249,7 @@ impl InputHandler { self.gui_state.lock().status_push(log_status); let uuid = Uuid::new_v4(); - let handle = GuiState::start_loading_animation(&self.gui_state, uuid); + GuiState::start_loading_animation(&self.gui_state, uuid); if save_logs(&self.app_data, &self.gui_state, &self.docker_tx) .await .is_err() @@ -260,12 +261,12 @@ impl InputHandler { ); } self.gui_state.lock().status_del(log_status); - self.gui_state.lock().stop_loading_animation(&handle, uuid); + self.gui_state.lock().stop_loading_animation(uuid); } } /// Send docker command, if the Commands panel is selected - async fn enter_key(&mut self) { + async fn enter_key(&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 { @@ -306,7 +307,7 @@ impl InputHandler { } /// Change the the "next" selectable panel - fn tab_key(&mut self) { + fn tab_key(&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 { @@ -320,7 +321,7 @@ impl InputHandler { } /// Change to previously selected panel - fn back_tab_key(&mut self) { + fn back_tab_key(&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 @@ -332,7 +333,7 @@ impl InputHandler { } } - fn home_key(&mut self) { + fn home_key(&self) { let mut locked_data = self.app_data.lock(); let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { @@ -343,7 +344,7 @@ impl InputHandler { } /// Go to end of the list of the currently selected panel - fn end_key(&mut self) { + fn end_key(&self) { let mut locked_data = self.app_data.lock(); let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { @@ -353,6 +354,104 @@ impl InputHandler { } } + /// Actions to take when in Help status active + fn handle_help(&mut self, key_code: KeyCode) { + match key_code { + KeyCode::Esc | KeyCode::Char('h' | 'H') => { + self.gui_state.lock().status_del(Status::Help); + } + KeyCode::Char('m' | 'M') => self.m_key(), + _ => (), + } + } + + /// Actions to take when Error status active + fn handle_error(&self, key_code: KeyCode) { + match key_code { + KeyCode::Esc | KeyCode::Char('c' | 'C') => { + self.app_data.lock().remove_error(); + self.gui_state.lock().status_del(Status::Error); + } + _ => (), + } + } + + /// Actions to take when Delete status active + async fn handle_delete(&self, key_code: KeyCode) { + match key_code { + KeyCode::Char('y' | 'Y') => self.confirm_delete().await, + KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(), + _ => (), + } + } + + /// Actions to take when Filter status active + fn handle_filter(&self, key_code: KeyCode) { + match key_code { + KeyCode::Esc => { + self.app_data.lock().filter_term_clear(); + self.gui_state.lock().status_del(Status::Filter); + } + KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('/') => { + self.gui_state.lock().status_del(Status::Filter); + } + KeyCode::Backspace => { + self.app_data.lock().filter_term_pop(); + } + KeyCode::Char(x) => { + self.app_data.lock().filter_term_push(x); + } + KeyCode::Right => { + self.app_data.lock().filter_by_next(); + } + KeyCode::Left => { + self.app_data.lock().filter_by_prev(); + } + _ => (), + } + } + + /// Handle button presses in all other scenarios + async fn handle_others(&mut self, key_code: KeyCode) { + match key_code { + KeyCode::Char('0') => self.app_data.lock().reset_sorted(), + KeyCode::Char('1') => self.sort(Header::Name), + KeyCode::Char('2') => self.sort(Header::State), + KeyCode::Char('3') => self.sort(Header::Status), + KeyCode::Char('4') => self.sort(Header::Cpu), + KeyCode::Char('5') => self.sort(Header::Memory), + KeyCode::Char('6') => self.sort(Header::Id), + KeyCode::Char('7') => self.sort(Header::Image), + KeyCode::Char('8') => self.sort(Header::Rx), + KeyCode::Char('9') => self.sort(Header::Tx), + KeyCode::Char('e' | 'E') => self.e_key().await, + KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), + KeyCode::Char('m' | 'M') => self.m_key(), + KeyCode::Char('s' | 'S') => self.s_key().await, + KeyCode::Tab => self.tab_key(), + KeyCode::BackTab => self.back_tab_key(), + KeyCode::Home => self.home_key(), + KeyCode::End => self.end_key(), + KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), + KeyCode::PageUp => { + for _ in 0..=6 { + self.previous(); + } + } + KeyCode::F(1) | KeyCode::Char('/') => { + self.gui_state.lock().status_push(Status::Filter); + self.docker_tx.send(DockerMessage::Update).await.ok(); + } + KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), + KeyCode::PageDown => { + for _ in 0..=6 { + self.next(); + } + } + KeyCode::Enter => self.enter_key().await, + _ => (), + } + } /// Handle keyboard button events async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) { let contains_delete = self @@ -365,78 +464,32 @@ impl InputHandler { let contains_error = contains(Status::Error); let contains_help = contains(Status::Help); let contains_exec = contains(Status::Exec); + let contains_filter: bool = contains(Status::Filter); if !contains_exec { - // Always just quit on Ctrl + c/C or q/Q let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); - if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() { + if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() && !contains_filter { + // Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active self.quit().await; } if contains_error { - match key_code { - KeyCode::Esc | KeyCode::Char('c' | 'C') => { - self.app_data.lock().remove_error(); - self.gui_state.lock().status_del(Status::Error); - } - _ => (), - } + self.handle_error(key_code); } else if contains_help { - match key_code { - KeyCode::Esc | KeyCode::Char('h' | 'H') => { - self.gui_state.lock().status_del(Status::Help); - } - KeyCode::Char('m' | 'M') => self.m_key(), - _ => (), - } + self.handle_help(key_code); + } else if contains_filter { + self.handle_filter(key_code); } else if contains_delete { - match key_code { - KeyCode::Char('y' | 'Y') => self.confirm_delete().await, - KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(), - _ => (), - } + self.handle_delete(key_code).await; } else { - match key_code { - KeyCode::Char('0') => self.app_data.lock().reset_sorted(), - KeyCode::Char('1') => self.sort(Header::Name), - KeyCode::Char('2') => self.sort(Header::State), - KeyCode::Char('3') => self.sort(Header::Status), - KeyCode::Char('4') => self.sort(Header::Cpu), - KeyCode::Char('5') => self.sort(Header::Memory), - KeyCode::Char('6') => self.sort(Header::Id), - KeyCode::Char('7') => self.sort(Header::Image), - KeyCode::Char('8') => self.sort(Header::Rx), - KeyCode::Char('9') => self.sort(Header::Tx), - KeyCode::Char('e' | 'E') => self.e_key().await, - KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), - KeyCode::Char('m' | 'M') => self.m_key(), - KeyCode::Char('s' | 'S') => self.s_key().await, - KeyCode::Tab => self.tab_key(), - KeyCode::BackTab => self.back_tab_key(), - KeyCode::Home => self.home_key(), - KeyCode::End => self.end_key(), - KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), - KeyCode::PageUp => { - for _ in 0..=6 { - self.previous(); - } - } - KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), - KeyCode::PageDown => { - for _ in 0..=6 { - self.next(); - } - } - KeyCode::Enter => self.enter_key().await, - _ => (), - } + self.handle_others(key_code).await; } } } /// Check if a button press interacts with either the yes or no buttons in the delete container confirm window - async fn button_intersect(&mut self, mouse_event: MouseEvent) { + async fn button_intersect(&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, @@ -455,7 +508,7 @@ impl InputHandler { } /// Handle mouse button events - fn mouse_press(&mut self, mouse_event: MouseEvent) { + fn mouse_press(&self, mouse_event: MouseEvent) { match mouse_event.kind { MouseEventKind::ScrollUp => self.previous(), MouseEventKind::ScrollDown => self.next(), @@ -481,7 +534,7 @@ impl InputHandler { } /// Change state to next, depending which panel is currently in focus - fn next(&mut self) { + fn next(&self) { let mut locked_data = self.app_data.lock(); let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { @@ -492,7 +545,7 @@ impl InputHandler { } /// Change state to previous, depending which panel is currently in focus - fn previous(&mut self) { + fn previous(&self) { let mut locked_data = self.app_data.lock(); let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { diff --git a/src/main.rs b/src/main.rs index f01e318..aa5c2cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,10 +167,18 @@ async fn main() { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] mod tests { + use std::{ + collections::{HashSet, VecDeque}, + vec, + }; + use bollard::service::{ContainerSummary, Port}; use crate::{ - app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList}, + app_data::{ + AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter, + RunningState, State, StatefulList, + }, parse_args::CliArgs, }; @@ -201,16 +209,18 @@ mod tests { private: u16::try_from(index).unwrap_or(1) + 8000, public: None, }], - State::Running, - format!("Up {index} hour"), + State::Running(RunningState::Healthy), + ContainerStatus::from(format!("Up {index} hour")), ) } pub fn gen_appdata(containers: &[ContainerItem]) -> AppData { AppData { containers: StatefulList::new(containers.to_vec()), + hidden_containers: vec![], error: None, sorted_by: None, + filter: Filter::new(), args: gen_args(), } } diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs index 4760db3..9643b43 100644 --- a/src/ui/color_match.rs +++ b/src/ui/color_match.rs @@ -41,15 +41,19 @@ pub mod log_sanitizer { /// Remove all ansi formatting from a given string and create ratatui Lines pub fn remove_ansi<'a>(input: &str) -> Vec> { - raw(&categorise_text(input) - .into_iter() - .map(|i| i.text) - .collect::()) + vec![Line::from( + categorise_text(input) + .into_iter() + .map(|i| i.text) + .collect::() + .trim() + .to_owned(), + )] } /// create ratatui Lines that exactly match the given strings pub fn raw<'a>(input: &str) -> Vec> { - vec![Line::from(Span::raw(input.to_owned()))] + vec![Line::from(input.escape_debug().collect::())] } /// Change from ansi to tui colors @@ -62,7 +66,7 @@ pub mod log_sanitizer { CansiColor::Blue => Color::Blue, CansiColor::Magenta => Color::Magenta, CansiColor::Cyan => Color::Cyan, - CansiColor::White | CansiColor::BrightWhite => Color::White, + CansiColor::White | CansiColor::BrightWhite => Color::Gray, CansiColor::BrightRed => Color::LightRed, CansiColor::BrightGreen => Color::LightGreen, CansiColor::BrightYellow => Color::LightYellow, @@ -92,7 +96,7 @@ mod tests { let expected = vec![Line { spans: [Span { content: std::borrow::Cow::Borrowed( - "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m", + "\\u{1b}[31;47mo\\u{1b}[32;40mx\\u{1b}[33;41mk\\u{1b}[34;42me\\u{1b}[35;43mr\\u{1b}[0m", ), style: Style::default(), }] @@ -111,7 +115,7 @@ mod tests { spans: vec![ Span { content: std::borrow::Cow::Borrowed("o"), - style: Style::default().fg(Color::Red).bg(Color::White), + style: Style::default().fg(Color::Red).bg(Color::Gray), }, Span { content: std::borrow::Cow::Borrowed("x"), diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index f2d0b60..c3e5693 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -13,7 +13,7 @@ use ratatui::{ use std::{default::Default, time::Instant}; use std::{fmt::Display, sync::Arc}; -use crate::app_data::{ContainerItem, ContainerName, Header, SortedOrder}; +use crate::app_data::{ContainerItem, ContainerName, FilterBy, Header, SortedOrder}; use crate::{ app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats}, app_error::AppError, @@ -21,7 +21,7 @@ use crate::{ use super::{ gui_state::{BoxLocation, DeleteButton, Region}, - FrameData, + FrameData, Status, ORANGE, }; use super::{GuiState, SelectablePanel}; @@ -39,7 +39,6 @@ const NAME: &str = env!("CARGO_PKG_NAME"); const VERSION: &str = env!("CARGO_PKG_VERSION"); const REPO: &str = env!("CARGO_PKG_REPOSITORY"); const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); -const ORANGE: Color = Color::Rgb(255, 178, 36); const MARGIN: &str = " "; const RIGHT_ARROW: &str = "▶ "; const CIRCLE: &str = "⚪ "; @@ -98,7 +97,7 @@ fn generate_block<'a>( .borders(Borders::ALL) .border_type(BorderType::Rounded) .title(title); - if fd.selected_panel == panel { + if fd.selected_panel == panel && !gui_state.lock().status_contains(&[Status::Filter]) { block = block.border_style(Style::default().fg(Color::LightCyan)); } block @@ -146,7 +145,7 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { Line::from(vec![ Span::styled( format!( - "{:>width$}", + "{:(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:>width$}", - i.status, + "{:width$}", - MARGIN, + "{:>width$}{MARGIN}", i.cpu_stats.back().unwrap_or(&CpuStats::default()), width = &widths.cpu.1.into() ), @@ -179,7 +177,7 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:>width_current$} / {:>width_limit$}", + "{:>width_current$} / {:>width_limit$}{MARGIN}", i.mem_stats.back().unwrap_or(&ByteStats::default()), i.mem_limit, width_current = &widths.mem.1.into(), @@ -189,8 +187,7 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{}{:>width$}", - MARGIN, + "{:>width$}{MARGIN}", i.id.get_short(), width = &widths.id.1.into() ), @@ -198,18 +195,18 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:>width$}", + "{:width$}", i.rx, width = widths.net_rx.1.into()), + format!("{:>width$}{MARGIN}", i.rx, width = widths.net_rx.1.into()), Style::default().fg(Color::Rgb(255, 233, 193)), ), Span::styled( - format!("{MARGIN}{:>width$}", i.tx, width = widths.net_tx.1.into()), + format!("{:>width$}{MARGIN}", i.tx, width = widths.net_tx.1.into()), Style::default().fg(Color::Rgb(205, 140, 140)), ), ]) @@ -233,7 +230,15 @@ pub fn containers( .collect::>(); if items.is_empty() { - let paragraph = Paragraph::new("no containers running") + let text = if app_data.lock().get_filter_term().is_some() { + "no containers match filter" + } else if gui_state.lock().is_loading() { + &format!("loading {}", fd.loading_icon) + } else { + "no containers running" + }; + + let paragraph = Paragraph::new(text) .block(block) .alignment(Alignment::Center); f.render_widget(paragraph, area); @@ -305,7 +310,7 @@ pub fn ports( if ports.0.is_empty() { let text = match ports.1 { - State::Running | State::Paused | State::Restarting => "no ports", + State::Running(_) | State::Paused | State::Restarting => "no ports", _ => "", }; let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD)) @@ -377,7 +382,7 @@ fn make_chart<'a, T: Stats + Display>( ) -> Chart<'a> { let title_color = state.get_color(); let label_color = match state { - State::Running => ORANGE, + State::Running(_) => ORANGE, _ => state.get_color(), }; Chart::new(dataset) @@ -414,6 +419,74 @@ fn make_chart<'a, T: Stats + Display>( ) } +/// Create the filter_by by spans, coloured dependant on which one is selected +fn filter_by_spans(app_data: &Arc>) -> [Span; 4] { + let filter_by = app_data.lock().get_filter_by(); + + let selected = Style::default().bg(Color::Gray).fg(Color::Black); + let not_selected = Style::default().bg(Color::Reset).fg(Color::Reset); + + // This should be refactored somehow + let name = [" Name ", " Image ", " Status ", " All "]; + + match filter_by { + FilterBy::Name => [ + Span::styled(name[0], selected), + Span::styled(name[1], not_selected), + Span::styled(name[2], not_selected), + Span::styled(name[3], not_selected), + ], + FilterBy::Image => [ + Span::styled(name[0], not_selected), + Span::styled(name[1], selected), + Span::styled(name[2], not_selected), + Span::styled(name[3], not_selected), + ], + FilterBy::Status => [ + Span::styled(name[0], not_selected), + Span::styled(name[1], not_selected), + Span::styled(name[2], selected), + Span::styled(name[3], not_selected), + ], + FilterBy::All => [ + Span::styled(name[0], not_selected), + Span::styled(name[1], not_selected), + Span::styled(name[2], not_selected), + Span::styled(name[3], selected), + ], + } +} + +/// Draw the filter bar +pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) { + let style_but = Style::default().fg(Color::Black).bg(Color::Magenta); + let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset); + + let mut line = vec![ + Span::styled(" Esc ", style_but), + Span::styled(" clear ", style_desc), + Span::styled(" ← by → ", style_but), + Span::from(" "), + ]; + line.extend_from_slice(&filter_by_spans(app_data)); + line.extend_from_slice(&[ + Span::styled( + " term: ", + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ), + Span::styled( + app_data + .lock() + .get_filter_term() + .map_or(String::new(), std::borrow::ToOwned::to_owned), + Style::default().fg(Color::Gray), + ), + ]); + frame.render_widget(Line::from(line), area); +} + /// Draw heading bar at top of program, always visible /// TODO Should separate into loading icon/headers/help functions #[allow(clippy::too_many_lines)] @@ -430,54 +503,31 @@ pub fn heading_bar( // Generate a block for the header, if the header is currently being used to sort a column, then highlight it white let header_block = |x: &Header| { let mut color = Color::Black; - let mut prefix = ""; - let mut prefix_margin = 0; + let mut suffix = ""; if let Some((a, b)) = &data.sorted_by { if x == a { match b { - SortedOrder::Asc => prefix = "▲ ", - SortedOrder::Desc => prefix = "▼ ", + SortedOrder::Asc => suffix = " ▲", + SortedOrder::Desc => suffix = " ▼", } - prefix_margin = 2; - color = Color::White; + color = Color::Gray; }; }; - ( - Block::default().style(Style::default().bg(Color::Magenta).fg(color)), - prefix, - prefix_margin, - ) + + (Block::default().style(Style::default().fg(color)), suffix) }; // Generate block for the headers, state and status has a specific layout, others all equal // width is dependant on it that column is selected to sort - or not let gen_header = |header: &Header, width: usize| { let block = header_block(header); + // Yes this is a mess, needs documenting correctly - let text = match header { - Header::State => format!( - " {x:>width$}", - x = format!("{ic}{header}", ic = block.1), - width = width - ), - Header::Name => format!( - " {x:>width$}", - x = format!("{ic}{header}", ic = block.1), - width = width - ), - Header::Status => format!( - "{} {x:>width$}", - MARGIN, - x = format!("{ic}{header}", ic = block.1), - width = width - ), - _ => format!( - "{}{x:>width$}", - MARGIN, - x = format!("{ic}{header}", ic = block.1), - width = width - ), - }; + + let text = format!( + "{x: 0 { column_width } else { 1 }; let splits = if data.has_containers { vec![ - Constraint::Max(2), - Constraint::Min(column_width.try_into().unwrap_or_default()), + Constraint::Max(4), + Constraint::Max(column_width.try_into().unwrap_or_default()), Constraint::Max(info_width.try_into().unwrap_or_default()), ] } else { @@ -521,6 +571,11 @@ pub fn heading_bar( .constraints(splits) .split(area); + // Draw loading icon, or not, and a prefix with a single space + let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon)) + .block(block(Color::White)) + .alignment(Alignment::Left); + frame.render_widget(loading_paragraph, split_bar[0]); if data.has_containers { let header_section_width = split_bar[1].width; @@ -540,12 +595,6 @@ pub fn heading_bar( }) .collect::>(); - // Draw loading icon, or not, and a prefix with a single space - let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon)) - .block(block(Color::White)) - .alignment(Alignment::Center); - frame.render_widget(loading_paragraph, split_bar[0]); - let container_splits = header_data.iter().map(|i| i.2).collect::>(); let headers_section = Layout::default() .direction(Direction::Horizontal) @@ -701,6 +750,13 @@ impl HelpInfo { "toggle mouse capture - if disabled, text on screen can be selected & copied", ), ]), + Line::from(vec![ + space(), + button_item("F1"), + or(), + button_item("/"), + button_desc("enter filter mode"), + ]), Line::from(vec![space(), button_item("0"), button_desc("stop sort")]), Line::from(vec![ space(), @@ -952,7 +1008,7 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { } /// Draw info box in one of the 9 BoxLocations -// TODO is this broken? +// TODO is this broken - I don't think so pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc>) { let block = Block::default() .title("") @@ -1024,8 +1080,8 @@ mod tests { use crate::{ app_data::{ - AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, Header, - SortedOrder, State, StatefulList, + AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, ContainerStatus, + Header, SortedOrder, State, StatefulList, }, app_error::AppError, tests::{gen_appdata, gen_container_summary, gen_containers}, @@ -1082,6 +1138,27 @@ mod tests { setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]); } + /// Get a single row of String's from the expected data + fn expected_to_vec(expected: &[&str], row_index: usize) -> Vec { + expected[row_index] + .chars() + .map(|i| i.to_string()) + .collect::>() + } + + fn get_result( + setup: &TuiTestSetup, + w: u16, + ) -> std::iter::Enumerate> { + setup + .terminal + .backend() + .buffer() + .content + .chunks(usize::from(w)) + .enumerate() + } + // ******************** // // DockerControls panel // // ******************** // @@ -1108,14 +1185,10 @@ mod tests { "╰──────────╯", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.fg, Color::Reset); + for (row_index, row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (cell_index, cell) in row.iter().enumerate() { + assert_eq!(cell.symbol(), expected_row[cell_index]); } } } @@ -1141,44 +1214,35 @@ mod tests { "│ delete │", "╰──────────╯", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - // Check the text color is correct - match index { + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); + match (row_index, result_cell_index) { // pause - 15..=19 => { + (1, 3..=7) => { assert_eq!(result_cell.fg, Color::Yellow); } // restart - 27..=33 => { + (2, 3..=9) => { assert_eq!(result_cell.fg, Color::Magenta); } // stop - 39..=42 => { + (3, 3..=6) => { assert_eq!(result_cell.fg, Color::Red); } // delete - 51..=56 => { + (4, 3..=8) => { assert_eq!(result_cell.fg, Color::Gray); } - // no text _ => { assert_eq!(result_cell.fg, Color::Reset); } } - if result_cell.symbol().starts_with('▶') { - assert_eq!(result_cell.fg, Color::Reset); - } } } - // Change the controls state setup .app_data @@ -1202,37 +1266,28 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - // Chceck the text color is correct - match index { + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); + match (row_index, result_cell_index) { // resume - 15..=20 => { + (1, 3..=8) => { assert_eq!(result_cell.fg, Color::Blue); } // stop - 27..=30 => { + (2, 3..=6) => { assert_eq!(result_cell.fg, Color::Red); } // delete - 39..=44 => { + (3, 3..=8) => { assert_eq!(result_cell.fg, Color::Gray); } - // no text _ => { assert_eq!(result_cell.fg, Color::Reset); } } - if result_cell.symbol().starts_with('▶') { - assert_eq!(result_cell.fg, Color::Reset); - } } } } @@ -1259,13 +1314,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::Reset); } @@ -1282,25 +1334,21 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - if BORDER_CHARS.contains(&result_cell.symbol()) { + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + if row_index == 0 + || row_index == 5 + || result_cell_index == 0 + || result_cell_index == 11 + { assert_eq!(result_cell.fg, Color::LightCyan); } - // Make sure that the selected line has bold text - match index { - // pause - 13..=22 => { - assert_eq!(result_cell.modifier, Modifier::BOLD); - } - _ => { - assert!(result_cell.modifier.is_empty()); - } + if row_index == 1 && result_cell_index > 0 && result_cell_index < 11 { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert!(result_cell.modifier.is_empty()); } } } @@ -1310,23 +1358,6 @@ mod tests { // Container summary panel // // *********************** // - // Check that the correct solor is applied to the state/status/cpu/memory section - fn check_expected(expected: [&str; 6], w: u16, _h: u16, setup: &TuiTestSetup, color: Color) { - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - if (145..=207).contains(&index) { - assert_eq!(result_cell.fg, color); - assert_eq!(result_cell.modifier, Modifier::BOLD); - } - } - } - } - #[test] /// No containers, panel unselected, then selected, border color changes correctly fn test_draw_blocks_containers_none() { @@ -1353,13 +1384,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); assert_eq!(result_cell.fg, Color::Reset); } } @@ -1374,13 +1402,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::LightCyan); } @@ -1390,18 +1415,18 @@ mod tests { #[test] /// Containers panel drawn, selected line is bold, border is blue - fn test_draw_blocks_containers_some() { + fn test_draw_blocks_containers_selected_bold() { let (w, h) = (130, 6); let mut setup = test_setup(w, h, true, true); let expected = [ - "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", - "│ │", - "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", - ]; + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; setup .terminal @@ -1410,27 +1435,28 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - // result matches expected - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - // Selected container is bold - match index { - 131 | 133..=258 => assert_eq!(result_cell.modifier, Modifier::BOLD), - _ => { - assert!(result_cell.modifier.is_empty()); - } - } - - // Border is blue if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::LightCyan); } + + let not_bold = || assert!(result_cell.modifier.is_empty()); + if row_index == 1 { + match result_cell_index { + 0 | 2 | 129 => { + not_bold(); + } + _ => { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + } + } else { + not_bold(); + } } } @@ -1444,15 +1470,11 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - // Border is gray if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::Reset); } @@ -1461,18 +1483,18 @@ mod tests { } #[test] - /// ALl columns on all rows are coloured correctly + /// Columns on all rows are coloured correctly fn test_draw_blocks_containers_colors() { let (w, h) = (130, 6); let mut setup = test_setup(w, h, true, true); let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", "│ │", - "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" ]; let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); @@ -1483,71 +1505,41 @@ mod tests { }) .unwrap(); - let index_blue = [ - 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 208, 209, 210, 211, 212, 213, - 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, - ]; - let index_blue = index_blue - .iter() - .flat_map(|&x| vec![x, x + 130, x + 260]) - .collect::>(); - let index_green = [ - 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, - 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, - 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, - 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, - ]; - let index_green = index_green - .iter() - .flat_map(|&x| vec![x, x + 130, x + 260]) - .collect::>(); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); - let index_rx = [229, 230, 231, 232, 233, 234, 235, 236, 237, 238]; - let index_rx = index_rx - .iter() - .flat_map(|&x| vec![x, x + 130, x + 260]) - .collect::>(); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - let index_tx = [239, 240, 241, 242, 243, 244, 245, 246, 247, 248]; - let index_tx = index_tx - .iter() - .flat_map(|&x| vec![x, x + 130, x + 260]) - .collect::>(); - - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - - let result_cell = &result[index]; - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - match index { - _x if index_blue.contains(&index) => { - assert_eq!(result_cell.fg, Color::Blue); - } - _x if index_green.contains(&index) => { - assert_eq!(result_cell.fg, Color::Green); - } - _x if index_rx.contains(&index) => { - assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); - } - _x if index_tx.contains(&index) => { - assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); - } - (0..=130) | (259..=260) | (389..=390) | (519..=520) | (649..=779) => { + match (row_index, result_cell_index) { + //border + (0 | 5, _) | (1..=4, 0 | 129) => { assert_eq!(result_cell.fg, Color::LightCyan); } - _ => { - assert_eq!(result_cell.fg, Color::Reset); + // name, id, image column + (1..=3, 4..=17 | 71..=91) => { + assert_eq!(result_cell.fg, Color::Blue); } + // state, status, cpu, memory column + (1..=3, 18..=70) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 92..=101) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 102..=111) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + _ => assert_eq!(result_cell.fg, Color::Reset), } } } } #[test] - /// When long container/image name, it is truncated correctly + /// Long container + image name is truncated correctly fn test_draw_blocks_containers_long_name_image() { let (w, h) = (170, 6); let mut setup = test_setup(w, h, true, true); @@ -1557,12 +1549,12 @@ mod tests { ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); let expected = [ - "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", - "│ │", - "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); setup.app_data.lock().containers.items[0].state = State::Paused; @@ -1574,17 +1566,50 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); } } + } - // THis char: … + // Check that the correct colour is applied to the state/status/cpu/memory section + fn check_expected(expected: [&str; 6], w: u16, _h: u16, setup: &TuiTestSetup, color: Color) { + for (row_index, result_row) in get_result(setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + + match (row_index, result_cell_index) { + // border + (0 | 5, _) | (1..=4, 0 | 129) => { + assert_eq!(result_cell.fg, Color::LightCyan); + } + // name, id, image column + (1..=3, 4..=17 | 71..=91) => { + assert_eq!(result_cell.fg, Color::Blue); + } + // state, status, cpu, memory column of the first row + (1, 18..=70) => { + assert_eq!(result_cell.fg, color); + } + // state, status, cpu, memory column + (2..=3, 4..=77) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 92..=101) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 102..=111) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + _ => assert_eq!(result_cell.fg, Color::Reset), + } + } + } } #[test] @@ -1595,11 +1620,11 @@ mod tests { let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", - "│ │", - "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "│⚪ container_1 ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); setup.app_data.lock().containers.items[0].state = State::Paused; @@ -1622,9 +1647,9 @@ mod tests { let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ✖ dead Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│⚪ container_1 ✖ dead Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1637,6 +1662,7 @@ mod tests { super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); }) .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); } @@ -1648,9 +1674,9 @@ mod tests { let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ✖ exited Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│⚪ container_1 ✖ exited Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1674,9 +1700,9 @@ mod tests { let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 removing Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│⚪ container_1 removing Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1692,6 +1718,7 @@ mod tests { check_expected(expected, w, h, &setup, Color::LightRed); } + #[test] /// When container state is restarting, correct colors displayed fn test_draw_blocks_containers_restarting() { @@ -1700,9 +1727,9 @@ mod tests { let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ↻ restarting Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│⚪ container_1 ↻ restarting Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1716,8 +1743,106 @@ mod tests { }) .unwrap(); - check_expected(expected, w, h, &setup, Color::LightGreen); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + + match (row_index, result_cell_index) { + // border + (0 | 5, _) | (1..=4, 0 | 129) => { + assert_eq!(result_cell.fg, Color::LightCyan); + } + // name, id, image column + (1..=3, 4..=17 | 74..=94) => { + assert_eq!(result_cell.fg, Color::Blue); + } + // state, status, cpu, memory column of the first row + (1, 18..=73) => { + assert_eq!(result_cell.fg, Color::LightGreen); + } + // state, status, cpu, memory column + (2..=3, 18..=73) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 95..=104) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 105..=114) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } } + + #[test] + /// When container state is unknown, correct colors displayed + fn test_draw_blocks_containers_unhealthy() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let status = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned()); + setup.app_data.lock().containers.items[0].state = State::from(("running", &status)); + setup.app_data.lock().containers.items[0].status = status; + + let expected= [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ! running Up 1 hour (unhealthy) 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + match (row_index, result_cell_index) { + // border + (0 | 5, _) | (1..=4, 0 | 129) => { + assert_eq!(result_cell.fg, Color::LightCyan); + } + // name, id, image column + (1..=3, 4..=17 | 83..=103) => { + assert_eq!(result_cell.fg, Color::Blue); + } + // state, status, cpu, memory column of the first row + (1, 18..=82) => { + assert_eq!(result_cell.fg, ORANGE); + } + // state, status, cpu, memory column + (2..=3, 18..=82) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 104..=113) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 114..=123) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + _ => assert_eq!(result_cell.fg, Color::Reset), + } + } + } + } + #[test] /// When container state is unknown, correct colors displayed fn test_draw_blocks_containers_unknown() { @@ -1726,9 +1851,9 @@ mod tests { let expected = [ "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│⚪ container_1 ? unknown Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│⚪ container_1 ? unknown Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1741,8 +1866,10 @@ mod tests { super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); }) .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); } + // ********** // // Logs panel // // ********** // @@ -1772,13 +1899,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); assert_eq!(result_cell.fg, Color::Reset); } } @@ -1795,13 +1919,11 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); if BORDER_CHARS.contains(&result_cell.symbol()) { assert_eq!(result_cell.fg, Color::LightCyan); } @@ -1812,18 +1934,18 @@ mod tests { #[test] /// Parsing logs, spinner visible, and then animates by one frame fn test_draw_blocks_logs_parsing() { - let (w, h) = (25, 6); + let (w, h) = (32, 6); let mut setup = test_setup(w, h, true, true); let uuid = Uuid::new_v4(); setup.gui_state.lock().next_loading(uuid); let expected = [ - "╭ Logs - container_1 ───╮", - "│ parsing logs ⠙ │", - "│ │", - "│ │", - "│ │", - "╰───────────────────────╯", + "╭ Logs - container_1 - image_1 ╮", + "│ parsing logs ⠙ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯", ]; let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); @@ -1836,31 +1958,24 @@ mod tests { }) .unwrap(); - let test = |terminal: &Terminal, expected: [&str; 6]| { - let result = &terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.fg, Color::Reset); - } + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.fg, Color::Reset); } - }; - - test(&setup.terminal, expected); + } // animation moved by one frame setup.gui_state.lock().next_loading(uuid); let expected = [ - "╭ Logs - container_1 ───╮", - "│ parsing logs ⠹ │", - "│ │", - "│ │", - "│ │", - "╰───────────────────────╯", + "╭ Logs - container_1 - image_1 ╮", + "│ parsing logs ⠹ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯", ]; let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); @@ -1872,7 +1987,13 @@ mod tests { }) .unwrap(); - test(&setup.terminal, expected); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.fg, Color::Reset); + } + } } #[test] @@ -1883,28 +2004,6 @@ mod tests { insert_logs(&setup); - let test = |terminal: &Terminal, - expected: [&str; 6], - range: RangeInclusive| { - let result = &terminal.backend().buffer().content; - - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.fg, Color::Reset); - - if range.contains(&index) { - assert_eq!(result_cell.modifier, Modifier::BOLD); - } else { - assert!(result_cell.modifier.is_empty()); - } - } - } - }; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); setup .terminal @@ -1920,11 +2019,24 @@ mod tests { "│ │", "╰───────────────────────╯", ]; - test(&setup.terminal, expected, 76..=98); + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.fg, Color::Reset); + + if row_index == 3 && (1..=23).contains(&result_cell_index) { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert!(result_cell.modifier.is_empty()); + } + } + } // Change selected log line setup.app_data.lock().log_previous(); - let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + _ = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); setup .terminal @@ -1941,7 +2053,19 @@ mod tests { "│ │", "╰───────────────────────╯", ]; - test(&setup.terminal, expected, 51..=73); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.fg, Color::Reset); + + if row_index == 2 && (1..=23).contains(&result_cell_index) { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert!(result_cell.modifier.is_empty()); + } + } + } } #[test] @@ -1957,7 +2081,7 @@ mod tests { insert_logs(&setup); let expected = [ - "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test ──────────────╮", + "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image╮", "│ line 1 │", "│ line 2 │", "│▶ line 3 │", @@ -1973,14 +2097,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); } } } @@ -1989,28 +2109,8 @@ mod tests { // Charts panel // // ************ // - const EXPECTED: [&str; 10] = [ - "╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮", - "│10.00%│ • ││100.00 kB│ •• │", - "│ │ •• ││ │ •• │", - "│ │ ••• ││ │ • • │", - "│ │ • • ││ │ • • │", - "│ │ • •• ││ │•• •• │", - "│ │• • ││ │• • │", - "│ │• • ││ │• • │", - "│ │ ││ │ │", - "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", - ]; - const MEMORY_INDEX: [usize; 16] = [ - 134, 135, 214, 215, 293, 295, 372, 375, 451, 452, 455, 456, 531, 535, 611, 615, - ]; - - const CPU_INDEX: [usize; 15] = [ - 92, 171, 172, 250, 251, 252, 330, 332, 409, 413, 414, 488, 493, 568, 573, - ]; - #[allow(clippy::cast_precision_loss)] - // Add fixed data to the cpu & mem vecdeques, that match the above data + // Add fixed data to the cpu & mem vecdeques fn insert_chart_data(setup: &TuiTestSetup) { for i in 1..=10 { setup.app_data.lock().update_stats_by_id( @@ -2033,8 +2133,62 @@ mod tests { ); } } + + /// CPU and Memory charts used in multiple tests, based on data from above insert_chart_data() + const EXPECTED: [&str; 10] = [ + "╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮", + "│10.00%│ • ││100.00 kB│ •• │", + "│ │ •• ││ │ •• │", + "│ │ ••• ││ │ • • │", + "│ │ • • ││ │ • • │", + "│ │ • •• ││ │•• •• │", + "│ │• • ││ │• • │", + "│ │• • ││ │• • │", + "│ │ ││ │ │", + "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", + ]; + + // co-ordinates of the dots from the cpu chart + const CPU_XY: [(usize, usize); 15] = [ + (1, 12), + (2, 11), + (2, 12), + (3, 10), + (3, 11), + (3, 12), + (4, 10), + (4, 12), + (5, 9), + (5, 13), + (5, 14), + (6, 8), + (6, 13), + (7, 8), + (7, 13), + ]; + + // co-ordinates of the dots from the memory chart + const MEM_XY: [(usize, usize); 16] = [ + (1, 54), + (1, 55), + (2, 54), + (2, 55), + (3, 53), + (3, 55), + (4, 52), + (4, 55), + (5, 51), + (5, 52), + (5, 55), + (5, 56), + (6, 51), + (6, 55), + (7, 51), + (7, 55), + ]; + #[test] - /// When status is Running, but not data, charts drawn without dots etc + /// When status is Running, but not data, charts drawn without dots etc, colours correct fn test_draw_blocks_charts_running_none() { let (w, h) = (80, 10); let mut setup = test_setup(w, h, true, true); @@ -2059,26 +2213,20 @@ mod tests { "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - match index { - // chart tiles - cpu 03.00% && memory 30.00 kB - are green - 14..=25 | 52..=67 => { + match (row_index, result_cell_index) { + (0, 14..=25 | 52..=67) => { assert_eq!(result_cell.fg, Color::Green); assert_eq!(result_cell.modifier, Modifier::BOLD); } - // Cpu & Memory max are orange and bold - 81..=86 | 121..=127 => { + (1, 1..=6 | 41..=47) => { assert_eq!(result_cell.fg, ORANGE); assert_eq!(result_cell.modifier, Modifier::BOLD); } - // All others _ => { assert_eq!(result_cell.fg, Color::Reset); assert!(result_cell.modifier.is_empty()); @@ -2103,35 +2251,28 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in EXPECTED.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&EXPECTED, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - match index { - // chart tiles - cpu 03.00% && memory 30.00 kB - are green - 14..=25 | 51..=67 => { + match (row_index, result_cell_index) { + (0, 14..=25 | 51..=67) => { assert_eq!(result_cell.fg, Color::Green); assert_eq!(result_cell.modifier, Modifier::BOLD); } - // Cpu & Memory max are orange and bold - 81..=86 | 121..=129 => { + (1, 1..=6 | 41..=49) => { assert_eq!(result_cell.fg, ORANGE); assert_eq!(result_cell.modifier, Modifier::BOLD); } - // cpu dots are magenta - _x if CPU_INDEX.contains(&index) => { + xy if CPU_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Magenta); assert!(result_cell.modifier.is_empty()); } - // memory dots are cyan - _x if MEMORY_INDEX.contains(&index) => { + xy if MEM_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Cyan); assert!(result_cell.modifier.is_empty()); } - // All others _ => { assert_eq!(result_cell.fg, Color::Reset); assert!(result_cell.modifier.is_empty()); @@ -2157,29 +2298,24 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in EXPECTED.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&EXPECTED, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - match index { - // Titles and y axis are yellow - 14..=25 | 51..=67 | 81..=86 | 121..=129 => { + match (row_index, result_cell_index) { + (0, 14..=25 | 51..=67) | (1, 1..=6 | 41..=49) => { assert_eq!(result_cell.fg, Color::Yellow); assert_eq!(result_cell.modifier, Modifier::BOLD); } - _x if CPU_INDEX.contains(&index) => { + xy if CPU_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Magenta); assert!(result_cell.modifier.is_empty()); } - // memory dots are cyan - _x if MEMORY_INDEX.contains(&index) => { + xy if MEM_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Cyan); assert!(result_cell.modifier.is_empty()); } - // All others _ => { assert_eq!(result_cell.fg, Color::Reset); assert!(result_cell.modifier.is_empty()); @@ -2204,30 +2340,24 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in EXPECTED.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&EXPECTED, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - match index { - // Titles and y axis are red - 14..=25 | 51..=67 | 81..=86 | 121..=129 => { + match (row_index, result_cell_index) { + (0, 14..=25 | 51..=67) | (1, 1..=6 | 41..=49) => { assert_eq!(result_cell.fg, Color::Red); assert_eq!(result_cell.modifier, Modifier::BOLD); } - // cpu dots are magenta - _x if CPU_INDEX.contains(&index) => { + xy if CPU_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Magenta); assert!(result_cell.modifier.is_empty()); } - // memory dots are cyan - _x if MEMORY_INDEX.contains(&index) => { + xy if MEM_XY.contains(&xy) => { assert_eq!(result_cell.fg, Color::Cyan); assert!(result_cell.modifier.is_empty()); } - // All others _ => { assert_eq!(result_cell.fg, Color::Reset); assert!(result_cell.modifier.is_empty()); @@ -2250,7 +2380,7 @@ mod tests { let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); - let expected = " ( h ) show help "; + let expected = [" ( h ) show help "]; setup .terminal @@ -2259,17 +2389,17 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (index, expected_char) in expected.chars().enumerate() { - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.bg, Color::Magenta); - assert_eq!(result_cell.fg, Color::White); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::White); + } } fd.help_visible = true; - let expected = " ( h ) exit help "; + let expected = [" ( h ) exit help "]; setup .terminal .draw(|f| { @@ -2277,13 +2407,13 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (index, expected_char) in expected.chars().enumerate() { - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.bg, Color::Magenta); - assert_eq!(result_cell.fg, Color::Black); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } } } @@ -2294,7 +2424,7 @@ mod tests { let mut setup = test_setup(w, h, true, true); let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); - let expected = " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "; + let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "]; setup .terminal .draw(|f| { @@ -2302,19 +2432,19 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (index, expected_char) in expected.chars().enumerate() { - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.bg, Color::Magenta); - assert_eq!( - result_cell.fg, - match index { - (2..=122) => Color::Black, - _ => Color::White, - } - ); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match result_cell_index { + (4..=121) => Color::Black, + _ => Color::White, + } + ); + } } } @@ -2326,7 +2456,7 @@ mod tests { let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); let expected = - " name state status cpu ( h ) show help "; + [" name state status cpu ( h ) show help "]; setup .terminal .draw(|f| { @@ -2334,19 +2464,19 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (index, expected_char) in expected.chars().enumerate() { - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.bg, Color::Magenta); - assert_eq!( - result_cell.fg, - match index { - (2..=62) => Color::Black, - _ => Color::White, - } - ); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match result_cell_index { + (4..=61) => Color::Black, + _ => Color::White, + } + ); + } } } @@ -2356,68 +2486,65 @@ mod tests { let (w, h) = (140, 1); let mut setup = test_setup(w, h, true, true); let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); - let mut test = |expected: &str, range: RangeInclusive, x: (Header, SortedOrder)| { - fd.sorted_by = Some(x); - setup - .terminal - .draw(|f| { - super::heading_bar(setup.area, f, &fd, &setup.gui_state); - }) - .unwrap(); + // Actual test, used for each header and sorted type + let mut test = + |expected: &[&str], range: RangeInclusive, x: (Header, SortedOrder)| { + fd.sorted_by = Some(x); - let result = &setup.terminal.backend().buffer().content; - for (index, expected_char) in expected.chars().enumerate() { - let result_cell = &result[index]; - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.bg, Color::Magenta); - assert_eq!( - result_cell.fg, - match index { - 0 | 1 => Color::White, - // given range | help section - x if range.contains(&x) || (123..=139).contains(&x) => Color::White, - _ => Color::Black, + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match result_cell_index { + 0..=3 | 122..=139 => Color::White, + // given range | help section + x if range.contains(&x) => Color::Gray, + _ => Color::Black, + } + ); } - ); - } - }; + } + }; // Name - test(" ▲ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Asc)); - test(" ▼ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Desc)); - + test(&[" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Asc)); + test(&[" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Desc)); // state - test(" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Asc)); - test(" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Desc)); - + test(&[" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "],18..=29, (Header::State, SortedOrder::Asc)); + test(&[" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 18..=29, (Header::State, SortedOrder::Desc)); // status - test(" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Asc)); - test(" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Desc)); - + test(&[" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Asc)); + test(&[" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Desc)); // cpu - test(" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Asc)); - test(" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Desc)); - - // mem - test(" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Asc)); - test(" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Desc)); - - // id - test(" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Asc)); - test(" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Desc)); - + test(&[" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Asc)); + test(&[" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Desc)); + // memory + test(&[" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Asc)); + test(&[" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Desc)); + //id + test(&[" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Asc)); + test(&[" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Desc)); // image - test(" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Asc)); - test(" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Desc)); - + test(&[" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Asc)); + test(&[" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Desc)); // rx - test(" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Asc)); - test(" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Desc)); - + test(&[" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Asc)); + test(&[" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Desc)); // tx - test(" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help ", 109..=118, (Header::Tx, SortedOrder::Asc)); - test(" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help ", 109..=118, (Header::Tx, SortedOrder::Desc)); + test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▲ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Asc)); + test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▼ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Desc)); } #[test] @@ -2429,6 +2556,8 @@ mod tests { setup.gui_state.lock().next_loading(uuid); let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "]; + setup .terminal .draw(|f| { @@ -2436,21 +2565,19 @@ mod tests { }) .unwrap(); - let expected = " ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "; - - let result = &setup.terminal.backend().buffer().content; - for (index, expected_char) in expected.chars().enumerate() { - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - assert_eq!(result_cell.bg, Color::Magenta); - assert_eq!( - result_cell.fg, - match index { - (2..=122) => Color::Black, - _ => Color::White, - } - ); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match result_cell_index { + (4..=121) => Color::Black, + _ => Color::White, + } + ); + } } } @@ -2461,7 +2588,7 @@ mod tests { /// This will cause issues once the version has more than the current 5 chars (0.5.0) // Help popup is drawn correctly fn test_draw_blocks_help() { - let (w, h) = (87, 32); + let (w, h) = (87, 33); let mut setup = test_setup(w, h, true, true); setup @@ -2470,85 +2597,83 @@ mod tests { super::help_box(f); }) .unwrap(); + let version_row = format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "); let expected = [ - " ".to_owned(), - format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "), - " │ │ ".to_owned(), - " │ 88 │ ".to_owned(), - " │ 88 │ ".to_owned(), - " │ 88 │ ".to_owned(), - " │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ".to_owned(), - r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#.to_owned(), - r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#.to_owned(), - r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#.to_owned(), - r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#.to_owned(), - " │ │ ".to_owned(), - " │ A simple tui to view & control docker containers │ ".to_owned(), - " │ │ ".to_owned(), - " │ ( tab ) or ( shift+tab ) change panels │ ".to_owned(), - " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ".to_owned(), - " │ ( enter ) send docker container command │ ".to_owned(), - " │ ( e ) exec into a container │ ".to_owned(), - " │ ( h ) toggle this help information │ ".to_owned(), - " │ ( s ) save logs to file │ ".to_owned(), - " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ".to_owned(), - " │ ( 0 ) stop sort │ ".to_owned(), - " │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(), - " │ ( esc ) close dialog │ ".to_owned(), - " │ ( q ) quit at any time │ ".to_owned(), - " │ │ ".to_owned(), - " │ currently an early work in progress, all and any input appreciated │ ".to_owned(), - " │ https://github.com/mrjackwills/oxker │ ".to_owned(), - " │ │ ".to_owned(), - " │ │ ".to_owned(), - " ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(), + " ", + version_row.as_str(), + " │ │ ", + " │ 88 │ ", + " │ 88 │ ", + " │ 88 │ ", + " │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ", + r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#, + r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#, + r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#, + r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#, + " │ │ ", + " │ A simple tui to view & control docker containers │ ", + " │ │ ", + " │ ( tab ) or ( shift+tab ) change panels │ ", + " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ", + " │ ( enter ) send docker container command │ ", + " │ ( e ) exec into a container │ ", + " │ ( h ) toggle this help information │ ", + " │ ( s ) save logs to file │ ", + " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ", + " │ ( F1 ) or ( / ) enter filter mode │ ", + " │ ( 0 ) stop sort │ ", + " │ ( 1 - 9 ) sort by header - or click header │ ", + " │ ( esc ) close dialog │ ", + " │ ( q ) quit at any time │ ", + " │ │ ", + " │ currently an early work in progress, all and any input appreciated │ ", + " │ https://github.com/mrjackwills/oxker │ ", + " │ │ ", + " │ │ ", + " ╰───────────────────────────────────────────────────────────────────────────────────╯ ", + " " ]; - for (row_index, row) in expected.iter().enumerate() { - let mut bracket_key = vec![]; - let mut push_bracket_key = false; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - let result = &setup.terminal.backend().buffer().content; - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - let result_str = result_cell.symbol(); - - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - // First and last row, and first char and last char in each row, is empty - if row_index == 0 - || row_index == usize::from(h - 1) - || char_index == 0 - || char_index == usize::from(w - 1) - { - assert_eq!(result_cell.fg, Color::Reset); - assert_eq!(result_cell.bg, Color::Reset); - // Borders - } else if BORDER_CHARS.contains(&result_str) { - assert_eq!(result_cell.fg, Color::Black); - assert_eq!(result_cell.bg, Color::Magenta); - // everything else has a magenta background - } else { - assert_eq!(result_cell.bg, Color::Magenta); - } - - // check that ( [key] ) is white - if result_str == "(" { - push_bracket_key = true; - bracket_key.push(result_cell); - } - if push_bracket_key { - bracket_key.push(result_cell); - if result_str == ")" { - push_bracket_key = false; - for i in &bracket_key { - assert_eq!(i.fg, Color::White); - } - bracket_key.clear(); + match (row_index, result_cell_index) { + // first & last row, and first & last char on each row, is reset/reset, making sure that the help info is centered in the given area + (0 | 32, _) | (0..=33, 0 | 86) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + // border is black on magenta + (1 | 31, _) | (1..=31, 1 | 85) => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + // oxker logo && description + (2..=10, 2..=85) | (12, 19..=66) + // button in the brackets + | (14, 2..=10 | 13..=27) + | (15, 2..=10 | 13..=21 | 24..=40 | 43..=56) + | (16 | 23, 2..=12) + | (17..=20 | 22 | 25, 2..=8) + | (21, 2..=9 | 12..=18) + | (24, 2..=10) => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::White); + } + // The URL is white and underlined + (28, 25..=60) => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::White); + assert_eq!(result_cell.modifier, Modifier::UNDERLINED); + } + // The rest is black on magenta + _ => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); } } - // TODO should really be testing every color of every str here } } } @@ -2583,38 +2708,25 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - if row_index == 0 - || row_index == usize::from(h - 1) - || char_index < 8 - || char_index > usize::from(w - 9) - { - assert_eq!(result_cell.fg, Color::Reset); - assert_eq!(result_cell.bg, Color::Reset); - } else { - assert_eq!(result_cell.bg, Color::White); - } - - // Borders are black - if BORDER_CHARS.contains(&result_cell.symbol()) { - assert_eq!(result_cell.fg, Color::Black); - // Container name is red - } else if row_index == 3 && (57..=67).contains(&char_index) { - assert_eq!(result_cell.fg, Color::Red); - // All other text is black - } else if !row_index == 0 - && !row_index == usize::from(h - 1) - && !char_index < 8 - && !char_index > usize::from(w - 9) - { - assert_eq!(result_cell.fg, Color::Black); + match (row_index, result_cell_index) { + (0 | 9, _) | (1..=8, 0..=7 | 74..=81) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + (3, 57..=67) => { + assert_eq!(result_cell.bg, Color::White); + assert_eq!(result_cell.fg, Color::Red); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert_eq!(result_cell.bg, Color::White); + assert_eq!(result_cell.fg, Color::Black); + } } } } @@ -2648,37 +2760,25 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - assert_eq!(result_cell.symbol(), expected_char.to_string()); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - if row_index == 0 - || row_index == usize::from(h - 1) - || char_index < 8 - || char_index > usize::from(w - 9) - { - assert_eq!(result_cell.fg, Color::Reset); - assert_eq!(result_cell.bg, Color::Reset); - } else { - assert_eq!(result_cell.bg, Color::White); - } - - // Borders are black - if BORDER_CHARS.contains(&result_cell.symbol()) { - assert_eq!(result_cell.fg, Color::Black); - // Container name is red - } else if row_index == 3 && (57..=82).contains(&char_index) { - assert_eq!(result_cell.fg, Color::Red); - // All other text is black - } else if !row_index == 0 - && !row_index == usize::from(h - 1) - && !char_index < 8 - && !char_index > usize::from(w - 9) - { - assert_eq!(result_cell.fg, Color::Black); + match (row_index, result_cell_index) { + (0 | 9, _) | (1..=8, 0..=7 | 98..=106) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + (3, 57..=91) => { + assert_eq!(result_cell.bg, Color::White); + assert_eq!(result_cell.fg, Color::Red); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert_eq!(result_cell.bg, Color::White); + assert_eq!(result_cell.fg, Color::Black); + } } } } @@ -2713,22 +2813,165 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(expected_char.to_string(), result_cell.symbol()); - let (fg, bg) = if row_index >= 6 && char_index >= 32 { - (Color::White, Color::Blue) - } else { - (Color::Reset, Color::Reset) + let (bg, fg) = match (row_index, result_cell_index) { + (6..=8, 32..=44) => (Color::Blue, Color::White), + _ => (Color::Reset, Color::Reset), }; - - assert_eq!(result_cell.fg, fg); assert_eq!(result_cell.bg, bg); + assert_eq!(result_cell.fg, fg); + } + } + } + + // ********** // + // Filter Row // + // ********** // + + #[test] + #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] + /// Filter row is drawn correctly & colors are correct + /// Colours change when filter_by option is changed + fn test_draw_blocks_filter_row() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + + setup + .gui_state + .lock() + .status_push(crate::ui::Status::Filter); + setup + .terminal + .draw(|f| { + super::filter_bar(setup.area, f, &setup.app_data); + }) + .unwrap(); + + let expected = [ + " Esc clear ← by → Name Image Status All term: " + ]; + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + match result_cell_index { + 0..=4 | 12..=19 => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + 5..=11 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Gray); + } + 21..=26 => { + assert_eq!(result_cell.bg, Color::Gray); + assert_eq!(result_cell.fg, Color::Black); + } + 47..=53 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Magenta); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + // Test when char added to search term + setup.app_data.lock().filter_term_push('c'); + setup.app_data.lock().filter_term_push('d'); + + setup + .terminal + .draw(|f| { + super::filter_bar(setup.area, f, &setup.app_data); + }) + .unwrap(); + + let expected = [ + " Esc clear ← by → Name Image Status All term: cd " + ]; + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + + match result_cell_index { + 0..=4 | 12..=19 => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + 5..=11 | 54..=55 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Gray); + } + 21..=26 => { + assert_eq!(result_cell.bg, Color::Gray); + assert_eq!(result_cell.fg, Color::Black); + } + 47..=53 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Magenta); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + // Test when filter_by chances + setup.app_data.lock().filter_by_next(); + setup + .terminal + .draw(|f| { + super::filter_bar(setup.area, f, &setup.app_data); + }) + .unwrap(); + + let expected = [ + " Esc clear ← by → Name Image Status All term: cd " + ]; + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + + match result_cell_index { + 0..=4 | 12..=19 => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + 5..=11 | 54..=55 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Gray); + } + 27..=33 => { + assert_eq!(result_cell.bg, Color::Gray); + assert_eq!(result_cell.fg, Color::Black); + } + 47..=53 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Magenta); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + } } } } @@ -2750,39 +2993,33 @@ mod tests { }) .unwrap(); - let expected = vec![ - " ".to_owned(), - " ╭───────────────── Error ──────────────────╮ ".to_owned(), - " │ │ ".to_owned(), - " │ Unable to access docker daemon │ ".to_owned(), - " │ │ ".to_owned(), - format!(" │ oxker::v{VERSION} closing in 04 seconds │ "), - " │ │ ".to_owned(), - " ╰──────────────────────────────────────────╯ ".to_owned(), - " ".to_owned(), + let version_row = format!(" │ oxker::v{VERSION} closing in 04 seconds │ "); + let expected = [ + " ", + " ╭───────────────── Error ──────────────────╮ ", + " │ │ ", + " │ Unable to access docker daemon │ ", + " │ │ ", + version_row.as_str(), + " │ │ ", + " ╰──────────────────────────────────────────╯ ", + " ", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - - if (1..=usize::from(h) - 2).contains(&row_index) - && (1..=usize::from(w) - 2).contains(&char_index) - { - assert_eq!(result_cell.bg, Color::Red); - } - if result_cell - .symbol() - .chars() - .next() - .unwrap() - .is_alphanumeric() - { - assert_eq!(result_cell.fg, Color::White); + match (row_index, result_cell_index) { + (0 | 8, _) | (1..=7, 0 | 45) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + _ => { + assert_eq!(result_cell.bg, Color::Red); + assert_eq!(result_cell.fg, Color::White); + } } } } @@ -2814,27 +3051,20 @@ mod tests { " ", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); - assert_eq!(result_cell.symbol(), expected_char.to_string()); - if (1..=usize::from(h) - 2).contains(&row_index) - && (1..=usize::from(w) - 2).contains(&char_index) - { - assert_eq!(result_cell.bg, Color::Red); - } - if result_cell - .symbol() - .chars() - .next() - .unwrap() - .is_alphanumeric() - || ["(", ")"].contains(&result_cell.symbol()) - { - assert_eq!(result_cell.fg, Color::White); + match (row_index, result_cell_index) { + (0 | 9, _) | (1..=8, 0 | 38) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + _ => { + assert_eq!(result_cell.bg, Color::Red); + assert_eq!(result_cell.fg, Color::White); + } } } } @@ -2866,23 +3096,31 @@ mod tests { "╰────────────────────────────╯", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(expected_char.to_string(), result_cell.symbol()); - if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) { - assert_eq!(result_cell.fg, Color::Green); - assert_eq!(result_cell.modifier, Modifier::BOLD); - } else { - assert_eq!(result_cell.fg, Color::Reset); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + match (row_index, result_cell_index) { + (0, 11..=17) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + (1, 11..=18) => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } } } } - // when state is "State::Running | State::Paused | State::Restarting, won't show "no ports" + // When state is "State::Running | State::Paused | State::Restarting, won't show "no ports" setup.app_data.lock().containers.items[0].state = State::Dead; let max_lens = setup.app_data.lock().get_longest_port(); setup @@ -2903,18 +3141,17 @@ mod tests { "╰────────────────────────────╯", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(expected_char.to_string(), result_cell.symbol()); - if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) { + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); + if let (0, 11..=17) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Red); assert_eq!(result_cell.modifier, Modifier::BOLD); } else { assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); } } } @@ -2960,31 +3197,30 @@ mod tests { "╰──────────────────────────────╯", ]; - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); - assert_eq!(expected_char.to_string(), result_cell.symbol()); + match (row_index, result_cell_index) { + (0, 12..=18) => { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + (1, 1..=28) => { + assert_eq!(result_cell.fg, Color::Yellow); + assert!(result_cell.modifier.is_empty()); + } + (2..=4, 1..=28) => { + assert_eq!(result_cell.fg, Color::White); + assert!(result_cell.modifier.is_empty()); + } - let result_cell_as_char = result_cell - .symbol() - .chars() - .next() - .unwrap() - .is_ascii_alphanumeric(); - if row_index == 0 && result_cell_as_char { - assert_eq!(result_cell.fg, Color::Green); - } - if row_index == 1 && result_cell_as_char { - assert_eq!(result_cell.fg, Color::Yellow); - } - if (2..=3).contains(&row_index) && result_cell_as_char { - assert_eq!(result_cell.fg, Color::White); - } - if row_index == 4 && result_cell_as_char { - assert_eq!(result_cell.fg, Color::White); + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } } } } @@ -2997,6 +3233,36 @@ mod tests { let mut setup = test_setup(w, h, true, true); let max_lens = setup.app_data.lock().get_longest_port(); + setup + .terminal + .draw(|f| { + super::ports(f, setup.area, &setup.app_data, max_lens); + }) + .unwrap(); + + let expected = [ + "╭─────────── ports ────────────╮", + "│ ip private public │", + "│ 8001 │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯", + ]; + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); + if let (0, 12..=18) = (row_index, result_cell_index) { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + } + } + setup.app_data.lock().containers.items[0].state = State::Paused; setup .terminal @@ -3005,39 +3271,19 @@ mod tests { }) .unwrap(); - let expected = [ - "╭─────────── ports ────────────╮", - "│ ip private public │", - "│ 8001 │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────╯", - ]; - - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(expected_char.to_string(), result_cell.symbol()); - - if row_index == 0 - && result_cell - .symbol() - .chars() - .next() - .unwrap() - .is_ascii_alphanumeric() - { + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); + if let (0, 12..=18) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Yellow); + assert_eq!(result_cell.modifier, Modifier::BOLD); } } } - setup.app_data.lock().containers.items[0].state = State::Dead; + setup.app_data.lock().containers.items[0].state = State::Exited; setup .terminal .draw(|f| { @@ -3045,35 +3291,14 @@ mod tests { }) .unwrap(); - // This is wrong - why? - let expected = [ - "╭─────────── ports ────────────╮", - "│ ip private public │", - "│ 8001 │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────╯", - ]; - - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(expected_char.to_string(), result_cell.symbol()); - - if row_index == 0 - && result_cell - .symbol() - .chars() - .next() - .unwrap() - .is_ascii_alphanumeric() - { + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + assert_eq!(result_cell.bg, Color::Reset); + if let (0, 12..=18) = (row_index, result_cell_index) { assert_eq!(result_cell.fg, Color::Red); + assert_eq!(result_cell.modifier, Modifier::BOLD); } } } @@ -3099,36 +3324,36 @@ mod tests { }); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", - "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", - "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", - "│ ││ delete │", - "│ ││ │", - "│ ││ │", - "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", - "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│ line 1 │", - "│ line 2 │", - "│▶ line 3 │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", - "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", - "│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│", - "│ │ ••• • ││ │ ••• • ││ 8001 │", - "│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003│", - "│ │ ││ │ ││ │", - "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", + "│ ││ delete │", + "│ ││ │", + "│ ││ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", + "╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│", + "│ │ ••• • ││ │ ••• • ││ 8001 │", + "│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003│", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", ]; setup .terminal @@ -3137,13 +3362,128 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + } + } + } - assert_eq!(result_cell.symbol(), expected_char.to_string(),); + #[test] + #[allow(clippy::too_many_lines)] + /// Check that the whole layout is drawn correctly + fn test_draw_blocks_whole_layout_with_filter() { + let (w, h) = (160, 30); + let mut setup = test_setup(w, h, true, true); + insert_chart_data(&setup); + insert_logs(&setup); + + setup.app_data.lock().containers.items[1] + .ports + .push(ContainerPorts { + ip: Some("127.0.0.1".to_owned()), + private: 8003, + public: Some(8003), + }); + + let expected = [ + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", + "│ ││ delete │", + "│ ││ │", + "│ ││ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", + "╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│", + "│ │ ••• • ││ │ ••• • ││ 8001 │", + "│ │•• ••• ││ │•• ••• ││ │", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + ]; + setup + .terminal + .draw(|f| { + draw_frame(f, &setup.app_data, &setup.gui_state); + }) + .unwrap(); + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + } + } + + setup + .gui_state + .lock() + .status_push(crate::ui::Status::Filter); + setup.app_data.lock().filter_term_push('r'); + setup.app_data.lock().filter_term_push('_'); + setup.app_data.lock().filter_term_push('1'); + + let expected = [ + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/1 - filtered ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", + "│ ││ restart │", + "│ ││ stop │", + "│ ││ delete │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", + "╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ ••• ││100.00 kB│ •• ││ ip private public│", + "│ │ •• • ││ │ •• • ││ 8001 │", + "│ │ ••• • • ││ │ ••• • • ││ │", + "│ │• •• ││ │• •• ││ │", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + " Esc clear ← by → Name Image Status All term: r_1 ", + ]; + setup + .terminal + .draw(|f| { + draw_frame(f, &setup.app_data, &setup.gui_state); + }) + .unwrap(); + + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); } } } @@ -3170,36 +3510,36 @@ mod tests { ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", - "╭ Containers 1/3 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭─────────────────╮", - "│⚪ a_long_container_name_for_the… ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB ││▶ pause │", - "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", - "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", - "│ ││ delete │", - "│ ││ │", - "│ ││ │", - "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰─────────────────╯", - "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│ line 1 │", - "│ line 2 │", - "│▶ line 3 │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", - "╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮╭────────── ports ───────────╮", - "│10.00%│ •••• ││100.00 kB│ ••••• ││ ip private public│", - "│ │ •••• • ││ │ ••• • ││ 8001 │", - "│ │••• •••• ││ │••• ••• ││127.0.0.1 8003 8003│", - "│ │ ││ │ ││ │", - "╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/3 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭─────────────────╮", + "│⚪ a_long_container_name_for_the… ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB ││▶ pause │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", + "│ ││ delete │", + "│ ││ │", + "│ ││ │", + "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰─────────────────╯", + "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test - a_long_image_name_for_the_purposes_of_this_test ──────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ •••• ││100.00 kB│ ••••• ││ ip private public│", + "│ │ •••• • ││ │ ••• • ││ 8001 │", + "│ │••• •••• ││ │••• ••• ││127.0.0.1 8003 8003│", + "│ │ ││ │ ││ │", + "╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯", ]; setup .terminal @@ -3208,13 +3548,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - for (row_index, row) in expected.iter().enumerate() { - for (char_index, expected_char) in row.chars().enumerate() { - let index = row_index * usize::from(w) + char_index; - let result_cell = &result[index]; - - assert_eq!(result_cell.symbol(), expected_char.to_string(),); + for (row_index, result_row) in get_result(&setup, w) { + let expected_row = expected_to_vec(&expected, row_index); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); } } } diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index d93b1dc..ce007e8 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -163,19 +163,21 @@ pub enum Status { DockerConnect, Error, Exec, + Filter, Help, Init, Logs, } /// Global gui_state, stored in an Arc -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default)] pub struct GuiState { delete_container: Option, delete_map: HashMap, heading_map: HashMap, - is_loading: HashSet, + loading_handle: Option>, loading_index: u8, + loading_set: HashSet, panel_map: HashMap, selected_panel: SelectablePanel, status: HashSet, @@ -207,7 +209,7 @@ 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 { + pub fn button_intersect(&self, rect: Rect) -> Option { self.delete_map .iter() .filter(|i| i.1.intersects(rect)) @@ -217,7 +219,7 @@ impl GuiState { } /// Check if a given Rect (a clicked area of 1x1), interacts with any known panels - pub fn header_intersect(&mut self, rect: Rect) -> Option
{ + pub fn header_intersect(&self, rect: Rect) -> Option
{ self.heading_map .iter() .filter(|i| i.1.intersects(rect)) @@ -293,7 +295,7 @@ impl GuiState { self.status.insert(Status::Exec); } - pub fn get_exec_mode(&mut self) -> Option { + pub fn get_exec_mode(&self) -> Option { self.exec_mode.clone() } @@ -325,45 +327,46 @@ impl GuiState { } else { self.loading_index += 1; } - self.is_loading.insert(uuid); + self.loading_set.insert(uuid); } + pub fn is_loading(&self) -> bool { + !self.loading_set.is_empty() + } /// 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(&self) -> char { - if self.is_loading.is_empty() { - ' ' - } else { + if self.is_loading() { FRAMES[usize::from(self.loading_index)] - } - } - - /// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0 - fn remove_loading(&mut self, uuid: Uuid) { - self.is_loading.remove(&uuid); - if self.is_loading.is_empty() { - self.loading_index = 0; + } else { + ' ' } } /// Animate the loading icon in its own Tokio thread - pub fn start_loading_animation( - gui_state: &Arc>, - loading_uuid: Uuid, - ) -> JoinHandle<()> { + /// This should only be able to executed once, rather than multiple spawns + pub fn start_loading_animation(gui_state: &Arc>, loading_uuid: Uuid) { + if !gui_state.lock().is_loading() { + let inner_state = Arc::clone(gui_state); + gui_state.lock().loading_handle = Some(tokio::spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + inner_state.lock().next_loading(loading_uuid); + } + })); + } 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); + pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) { + self.loading_set.remove(&loading_uuid); + if self.loading_set.is_empty() { + self.loading_index = 0; + if let Some(h) = &self.loading_handle { + h.abort(); + } + self.loading_handle = None; + } } /// Set info box content diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c970bfc..99023f8 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -32,6 +32,8 @@ use crate::{ input_handler::InputMessages, }; +pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36); + pub struct Ui { app_data: Arc>, gui_state: Arc>, @@ -64,7 +66,6 @@ impl Ui { is_running: Arc, ) { if let Ok(mut terminal) = Self::setup_terminal() { - // let args = app_data.lock().args.clone(); let cursor_position = terminal.get_cursor().unwrap_or_default(); let mut ui = Self { app_data, @@ -264,14 +265,20 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData { /// Draw the main ui to a frame of the terminal fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) { let fd = FrameData::from((app_data.lock(), gui_state.lock())); + let contains_filter = gui_state.lock().status_contains(&[Status::Filter]); + + let whole_constraints = if contains_filter { + vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)] + } else { + vec![Constraint::Max(1), Constraint::Min(1)] + }; let whole_layout = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Max(1), Constraint::Min(1)].as_ref()) + .constraints(whole_constraints) .split(f.size()); // Split into 3, containers+controls, logs, then graphs - // This one is the issue! let upper_main = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref()) @@ -306,6 +313,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc