From fe71cbfb00f166b7c02a6e28e64650ed1b47d15d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 18 Jun 2024 06:53:32 +0000 Subject: [PATCH 01/31] chore: dependencies updated --- Cargo.lock | 399 +++++++++++++++++++++++++++++++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 310 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74df6d9..2640481 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", ] @@ -82,9 +82,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "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", @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-if" @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -266,11 +266,11 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -278,9 +278,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -363,6 +363,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.12.0" @@ -455,9 +466,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 +486,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" @@ -522,12 +527,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,9 +540,9 @@ 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 = "hyper" @@ -575,9 +580,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -632,13 +637,133 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.5.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -715,6 +840,12 @@ dependencies = [ "libc", ] +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -742,15 +873,15 @@ dependencies = [ [[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", ] @@ -804,9 +935,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -930,9 +1061,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -998,9 +1129,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags", ] @@ -1042,18 +1173,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1185,6 +1316,12 @@ dependencies = [ "syn", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1208,11 +1345,11 @@ dependencies = [ [[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", @@ -1230,6 +1367,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thiserror" version = "1.0.61" @@ -1292,25 +1440,20 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -1327,9 +1470,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -1448,27 +1591,12 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -1487,15 +1615,15 @@ dependencies = [ [[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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", "idna", @@ -1503,10 +1631,22 @@ dependencies = [ ] [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -1769,6 +1909,42 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.34" @@ -1788,3 +1964,46 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index f7e2d79..9ae7ac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ directories = "5.0" futures-util = "0.3" parking_lot = { version = "0.12" } ratatui = "0.26" -tokio = { version = "1.37", features = ["full"] } +tokio = { version = "1.38", features = ["full"] } tokio-util = "0.7" tracing = "0.1" tracing-subscriber = "0.3" From 51ceab3ebdb09356cd401d2f268840239255126f Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 18 Jun 2024 06:53:47 +0000 Subject: [PATCH 02/31] chore: docker-compose alpine verison bump --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 09f25f1..9ccaf49 100644 --- a/docker-compose.yml +++ b/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 From 197a031b8cf356f49f08e04472d0d1c489699415 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:53:33 +0000 Subject: [PATCH 03/31] fix: install.sh use curl --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 274e5b8c5a9e8adbc184f9810b8a3c0dfc714a68 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:37:16 +0000 Subject: [PATCH 04/31] fix: color match Correctly escape raw text, and correctly remove ANSI codes. Color matching white now becomes gray --- src/ui/color_match.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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"), From 1df4f78dc41013c33d901925933b1ccb29ad4bc8 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 10 Jul 2024 07:24:42 +0000 Subject: [PATCH 05/31] chore: dependencies updated --- Cargo.lock | 451 ++++++++++++++--------------------------------------- Cargo.toml | 4 +- 2 files changed, 116 insertions(+), 339 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2640481..4ddc596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,9 +134,9 @@ 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" @@ -208,18 +208,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.99" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" [[package]] name = "cfg-if" @@ -237,14 +237,14 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] name = "clap" -version = "4.5.7" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", @@ -363,22 +363,11 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[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" @@ -546,9 +535,9 @@ checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[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", @@ -580,9 +569,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", @@ -636,134 +625,14 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -796,9 +665,9 @@ checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[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", ] @@ -820,9 +689,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" @@ -840,12 +709,6 @@ dependencies = [ "libc", ] -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -858,9 +721,9 @@ 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" @@ -935,9 +798,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -1000,7 +863,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1061,9 +924,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1109,9 +972,9 @@ 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", @@ -1122,6 +985,7 @@ dependencies = [ "paste", "stability", "strum", + "strum_macros", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -1173,18 +1037,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -1193,9 +1057,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -1227,9 +1091,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" dependencies = [ "base64", "chrono", @@ -1308,20 +1172,14 @@ 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", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" @@ -1336,9 +1194,9 @@ 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", ] @@ -1358,26 +1216,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "1.0.61" @@ -1440,15 +1287,20 @@ dependencies = [ ] [[package]] -name = "tinystr" -version = "0.7.6" +name = "tinyvec" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ - "displaydoc", - "zerovec", + "tinyvec_macros", ] +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.38.0" @@ -1591,12 +1443,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -1605,11 +1472,12 @@ 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", ] @@ -1621,27 +1489,15 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -1650,9 +1506,9 @@ 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", @@ -1767,7 +1623,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]] @@ -1785,7 +1641,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]] @@ -1805,18 +1661,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]] @@ -1827,9 +1683,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" @@ -1839,9 +1695,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" @@ -1851,15 +1707,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" @@ -1869,9 +1725,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" @@ -1881,9 +1737,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" @@ -1893,9 +1749,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" @@ -1905,103 +1761,24 @@ 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" - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9ae7ac9..b48bb69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,12 +34,12 @@ crossterm = "0.27" directories = "5.0" futures-util = "0.3" parking_lot = { version = "0.12" } -ratatui = "0.26" +ratatui = "0.27" tokio = { version = "1.38", 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 From d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:43:42 +0000 Subject: [PATCH 06/31] feat: filter containers, closes #37 Enable filtering of containers, toggled by pressing `F1` or `/`, build on PR #38 from MohammadShabaniSBU --- README.md | 1 + src/app_data/mod.rs | 295 ++++++++++++++++++++++++++++++--------- src/docker_data/mod.rs | 31 ++-- src/input_handler/mod.rs | 165 ++++++++++++++-------- src/main.rs | 7 + src/ui/draw_blocks.rs | 196 ++++++++++++++++++++++++-- src/ui/gui_state.rs | 59 ++++---- src/ui/mod.rs | 20 ++- 8 files changed, 593 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index e492a62..f59bb97 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 ```( / )``` | Toggle 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.| diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index d6d8e92..d279384 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}, }; @@ -60,17 +61,21 @@ impl fmt::Display for Header { pub struct AppData { containers: StatefulList, error: Option, + filter_term: Option, sorted_by: Option<(Header, SortedOrder)>, + hidden_containers: Vec, pub args: CliArgs, } #[derive(Debug, Clone)] #[cfg(test)] pub struct AppData { + pub hidden_containers: Vec, + pub args: CliArgs, pub containers: StatefulList, pub error: Option, + pub filter_term: Option, pub sorted_by: Option<(Header, SortedOrder)>, - pub args: CliArgs, } impl AppData { @@ -79,8 +84,10 @@ impl AppData { Self { args, containers: StatefulList::new(vec![]), + hidden_containers: vec![], error: None, sorted_by: None, + filter_term: None, } } @@ -93,6 +100,90 @@ 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() + } + + /// Check the container name against the current filter + fn can_insert(&self, name: &str) -> bool { + self.filter_term.as_ref().map_or(true, |term| { + name.to_string() + .to_lowercase() + .contains(&term.to_lowercase()) + }) + } + + /// 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 occured + /// 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.name.get())); + + 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.name.get())); + + self.containers.items = new_items; + self.hidden_containers.extend(tmp_items); + + self.sort_containers(); + if self.get_container_len() != pre_len { + self.containers.start(); + } + } + + /// 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(); + } + + /// Remove the filter term completely, empty the "hidden" container vec + 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 @@ -206,7 +297,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() } @@ -260,35 +351,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 +398,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 +429,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 @@ -467,17 +564,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 +595,55 @@ impl AppData { /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly + /// Searches in both containes & 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)); + } } 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 +655,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(); } @@ -642,8 +750,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 +776,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 can_insert = self.can_insert(&name); let container = ContainerItem::new( created, id, image, is_oxker, name, ports, state, status, ); - self.containers.items.push(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(); @@ -1433,6 +1546,36 @@ mod tests { test_state(State::Unknown, &mut vec![DockerControls::Delete]); } + // ****** // + // Filter // + // ****** // + + #[test] + /// Data is filtered correctly + fn test_app_data_filter() { + 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("_2")); + assert!(!app_data.can_insert("_")); + assert!(!app_data.can_insert("_3")); + } + // **** // // Logs // // **** // @@ -1732,6 +1875,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, 11), + status: (Header::Status, 16), + cpu: (Header::Cpu, 7), + 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 // // ***** // diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 302f0d5..8508a28 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -303,13 +303,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 +324,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 +355,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 +383,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 +424,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/input_handler/mod.rs b/src/input_handler/mod.rs index 2cb4f8e..8df7342 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); } } @@ -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,7 +261,7 @@ 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); } } @@ -353,6 +354,98 @@ 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(&mut 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(&mut 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(&mut self, key_code: KeyCode) { + match key_code { + KeyCode::F(1) | KeyCode::Char('/') | KeyCode::Esc => { + self.app_data.lock().filter_term_clear(); + self.gui_state.lock().status_del(Status::Filter); + } + KeyCode::Enter => { + 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); + } + _ => (), + } + } + + /// 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,72 +458,26 @@ 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; } } } diff --git a/src/main.rs b/src/main.rs index f01e318..9ef3fee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,6 +167,11 @@ 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::{ @@ -209,8 +214,10 @@ mod tests { pub fn gen_appdata(containers: &[ContainerItem]) -> AppData { AppData { containers: StatefulList::new(containers.to_vec()), + hidden_containers: vec![], error: None, sorted_by: None, + filter_term: None, args: gen_args(), } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index f2d0b60..294463c 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -21,7 +21,7 @@ use crate::{ use super::{ gui_state::{BoxLocation, DeleteButton, Region}, - FrameData, + FrameData, Status, }; use super::{GuiState, SelectablePanel}; @@ -98,7 +98,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 @@ -233,7 +233,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); @@ -414,6 +422,32 @@ fn make_chart<'a, T: Stats + Display>( ) } +/// 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 line = Line::from(vec![ + Span::styled(" Enter ", style_but), + Span::styled(" done ", style_desc), + Span::styled(" Esc ", style_but), + Span::styled(" clear ", style_desc), + Span::styled( + "filter: ", + 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, area); +} + /// Draw heading bar at top of program, always visible /// TODO Should separate into loading icon/headers/help functions #[allow(clippy::too_many_lines)] @@ -521,6 +555,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,11 +579,11 @@ 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]); + // // 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() @@ -701,6 +740,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("toggle filter mode"), + ]), Line::from(vec![space(), button_item("0"), button_desc("stop sort")]), Line::from(vec![ space(), @@ -2461,7 +2507,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 @@ -2492,6 +2538,7 @@ mod tests { " │ ( 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(), + " │ ( F1 ) or ( / ) toggle filter mode │ ".to_owned(), " │ ( 0 ) stop sort │ ".to_owned(), " │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(), " │ ( esc ) close dialog │ ".to_owned(), @@ -2502,6 +2549,7 @@ mod tests { " │ │ ".to_owned(), " │ │ ".to_owned(), " ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(), + " ".to_owned(), ]; for (row_index, row) in expected.iter().enumerate() { @@ -3099,7 +3147,7 @@ mod tests { }); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + " 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 │", @@ -3148,6 +3196,134 @@ mod tests { } } + #[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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ 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(); + + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in result.chunks(usize::from(w)).enumerate() { + let expected_row = expected[row_index] + .chars() + .map(|i| i.to_string()) + .collect::>(); + for (cell_index, cell) in row.iter().enumerate() { + assert_eq!(cell.symbol(), expected_row[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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭────────────────────────── cpu 03.00% ───────────────────────────╮╭──────────────────────── memory 30.00 kB ────────────────────────╮╭──────── ports ─────────╮", + "│10.00%│ ••• ││100.00 kB│ ••• ││ ip private public│", + "│ │ •• • ││ │ •• • ││ 8001 │", + "│ │ ••• • • ││ │ ••• •• ││ │", + "│ │• •• ││ │• • ││ │", + "│ │ ││ │ ││ │", + "╰─────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────────────────────────────╯╰────────────────────────╯", + " Enter done Esc clear filter: r_1 ", + ]; + setup + .terminal + .draw(|f| { + draw_frame(f, &setup.app_data, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in result.chunks(usize::from(w)).enumerate() { + let expected_row = expected[row_index] + .chars() + .map(|i| i.to_string()) + .collect::>(); + for (cell_index, cell) in row.iter().enumerate() { + assert_eq!(cell.symbol(), expected_row[cell_index]); + } + } + } + #[test] /// Check that the whole layout is drawn correctly when have long container name and long image name fn test_draw_blocks_whole_layout_long_name() { diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index d93b1dc..354343a 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, @@ -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..9fca730 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -64,7 +64,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 +263,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 +311,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc Date: Fri, 12 Jul 2024 20:28:57 +0000 Subject: [PATCH 07/31] wip: test refactors --- src/ui/draw_blocks.rs | 452 ++++++++++++++++++++++-------------------- 1 file changed, 236 insertions(+), 216 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 294463c..c2fb200 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1128,6 +1128,26 @@ 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 // // ******************** // @@ -1154,14 +1174,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]); } } } @@ -1187,44 +1203,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 @@ -1248,37 +1255,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); - } } } } @@ -1305,13 +1303,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); } @@ -1328,25 +1323,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()); } } } @@ -1357,20 +1348,40 @@ mod tests { // *********************** // // Check that the correct solor is applied to the state/status/cpu/memory section + #[allow(unused)] 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]; + // 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); - } - } - } + // 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); + // } + // } + // } + + assert_eq!(1, 2); + + // let result = &setup.terminal.backend().buffer().content; + + // for (row_index, row) in result.chunks(usize::from(w)).enumerate() { + // let expected_row = expected_to_vec(&expected, row_index); + // for (cell_index, result_cell) in row.iter().enumerate() { + // assert_eq!(result_cell.symbol(), expected_row[cell_index]); + // if row_index == 0 || row_index == 5 || cell_index == 0 || cell_index == 11 { + // assert_eq!(result_cell.fg, Color::LightCyan); + // } + // if row_index == 1 && cell_index > 0 && cell_index < 11 { + // assert_eq!(result_cell.modifier, Modifier::BOLD); + // } else { + // assert!(result_cell.modifier.is_empty()); + // } + // } + // } } #[test] @@ -1399,13 +1410,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); } } @@ -1420,13 +1428,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); } @@ -1456,27 +1461,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(); + } } } @@ -1490,15 +1496,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); } @@ -1507,7 +1509,7 @@ 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); @@ -1529,71 +1531,40 @@ 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); + for (result_cell_index, result_cell) in result_row.iter().enumerate() { + assert_eq!(result_cell.symbol(), expected_row[result_cell_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::>(); - - 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..=14 | 78..=98) => { + assert_eq!(result_cell.fg, Color::Blue); } + // state, status, cpu, memory column + (1..=3, 15..=77) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 99..=108) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 109..=118) => { + 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); @@ -1620,17 +1591,12 @@ 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: … } #[test] @@ -2781,6 +2747,60 @@ mod tests { } } + // ********** // + // Filter Row // + // ********** // + + #[test] + /// Filter row is drawn correctly & colors are correct + fn test_draw_blocks_filter_row() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + + setup.app_data.lock().filter_term_push('c'); + 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 = [ + " Enter done Esc clear filter: c " + ]; + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in result.chunks(usize::from(w)).enumerate() { + let expected_row = expected_to_vec(&expected, row_index); + for (cell_index, cell) in row.iter().enumerate() { + match cell_index { + 0..=6 | 13..=17 => { + assert_eq!(cell.bg, Color::Magenta); + assert_eq!(cell.fg, Color::Black); + } + 7..=12 | 18..=24 | 33 => { + assert_eq!(cell.bg, Color::Reset); + assert_eq!(cell.fg, Color::Gray); + } + 25..=32 => { + assert_eq!(cell.bg, Color::Reset); + assert_eq!(cell.fg, Color::Magenta); + } + _ => { + assert_eq!(cell.bg, Color::Reset); + assert_eq!(cell.fg, Color::Reset); + } + } + assert_eq!(cell.symbol(), expected_row[cell_index]); + } + } + } + // *********** // // Error popup // // *********** // @@ -3273,37 +3293,37 @@ mod tests { 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", - "│⚪ 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│ line 1 │", - "│ line 2 │", - "│▶ line 3 │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", - "╭────────────────────────── cpu 03.00% ───────────────────────────╮╭──────────────────────── memory 30.00 kB ────────────────────────╮╭──────── ports ─────────╮", - "│10.00%│ ••• ││100.00 kB│ ••• ││ ip private public│", - "│ │ •• • ││ │ •• • ││ 8001 │", - "│ │ ••• • • ││ │ ••• •• ││ │", - "│ │• •• ││ │• • ││ │", - "│ │ ││ │ ││ │", - "╰─────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────────────────────────────╯╰────────────────────────╯", - " Enter done Esc clear filter: r_1 ", - ]; + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ ••• ││100.00 kB│ •• ││ ip private public│", + "│ │ •• • ││ │ •• • ││ 8001 │", + "│ │ ••• • • ││ │ ••• • • ││ │", + "│ │• •• ││ │• •• ││ │", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + " Enter done Esc clear filter: r_1 " + ]; setup .terminal .draw(|f| { From 94e4d693ab2ee78d840a63212d286b982e06ba36 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 12 Jul 2024 21:56:35 +0000 Subject: [PATCH 08/31] wip: test refactors --- src/ui/draw_blocks.rs | 103 ++++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 33 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index c2fb200..83d3a67 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1348,40 +1348,41 @@ mod tests { // *********************** // // Check that the correct solor is applied to the state/status/cpu/memory section - #[allow(unused)] 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]; + 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 (145..=207).contains(&index) { - // assert_eq!(result_cell.fg, color); - // assert_eq!(result_cell.modifier, Modifier::BOLD); - // } - // } - // } - - assert_eq!(1, 2); - - // let result = &setup.terminal.backend().buffer().content; - - // for (row_index, row) in result.chunks(usize::from(w)).enumerate() { - // let expected_row = expected_to_vec(&expected, row_index); - // for (cell_index, result_cell) in row.iter().enumerate() { - // assert_eq!(result_cell.symbol(), expected_row[cell_index]); - // if row_index == 0 || row_index == 5 || cell_index == 0 || cell_index == 11 { - // assert_eq!(result_cell.fg, Color::LightCyan); - // } - // if row_index == 1 && cell_index > 0 && cell_index < 11 { - // assert_eq!(result_cell.modifier, Modifier::BOLD); - // } else { - // assert!(result_cell.modifier.is_empty()); - // } - // } - // } + 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..=14 | 78..=98) => { + assert_eq!(result_cell.fg, Color::Blue); + } + // state, status, cpu, memory column of the first row + (1, 15..=77) => { + assert_eq!(result_cell.fg, color); + } + // state, status, cpu, memory column + (2..=3, 15..=77) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 99..=108) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 109..=118) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + _ => assert_eq!(result_cell.fg, Color::Reset), + } + } + } } #[test] @@ -1704,6 +1705,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() { @@ -1727,9 +1729,44 @@ mod tests { 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]); - check_expected(expected, w, h, &setup, Color::LightGreen); + 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..=14 | 79..=99) => { + assert_eq!(result_cell.fg, Color::Blue); + } + // state, status, cpu, memory column of the first row + (1, 15..=78) => { + assert_eq!(result_cell.fg, Color::LightGreen); + } + // state, status, cpu, memory column + (2..=3, 15..=78) => { + assert_eq!(result_cell.fg, Color::Green); + } + // rx column + (1..=3, 100..=109) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + // tx column + (1..=3, 110..=119) => { + 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() { From a04ef1b70178921d0ad260349b2304695e04737a Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 13 Jul 2024 22:08:42 +0000 Subject: [PATCH 09/31] tests: refactor all draw tests --- src/ui/draw_blocks.rs | 1204 ++++++++++++++++++++--------------------- 1 file changed, 589 insertions(+), 615 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 83d3a67..6d9419a 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1821,13 +1821,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); } } @@ -1844,13 +1841,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); } @@ -1885,20 +1880,13 @@ 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); @@ -1921,7 +1909,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] @@ -1932,28 +1926,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 @@ -1969,11 +1941,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 @@ -1990,7 +1975,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] @@ -2022,14 +2019,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]); } } } @@ -2038,28 +2031,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( @@ -2082,8 +2055,62 @@ mod tests { ); } } + + /// CPU and Memroy 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); @@ -2108,26 +2135,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()); @@ -2152,35 +2173,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()); @@ -2206,29 +2220,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()); @@ -2253,30 +2262,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()); @@ -2299,7 +2302,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 @@ -2308,17 +2311,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| { @@ -2326,13 +2329,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); + } } } @@ -2343,7 +2346,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| { @@ -2351,19 +2354,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 { + (2..=122) => Color::Black, + _ => Color::White, + } + ); + } } } @@ -2375,7 +2378,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| { @@ -2383,19 +2386,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 { + (2..=62) => Color::Black, + _ => Color::White, + } + ); + } } } @@ -2405,68 +2408,72 @@ 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(); + 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 | 1 => Color::White, + // given range | help section + x if range.contains(&x) || (123..=139).contains(&x) => Color::White, + _ => 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..=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)); // 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 "], 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)); // 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 "], 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)); // 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)); + 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)); + 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 "], 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)); // 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 "], 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)); // 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 "], 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)); // 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 "], 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] @@ -2485,21 +2492,21 @@ mod tests { }) .unwrap(); - 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 "]; - 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 { + (2..=122) => Color::Black, + _ => Color::White, + } + ); + } } } @@ -2519,87 +2526,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(), - " │ ( F1 ) or ( / ) toggle filter mode │ ".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(), - " ".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 ( / ) toggle 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 } } } @@ -2634,38 +2637,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); + } } } } @@ -2699,37 +2689,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); + } } } } @@ -2764,22 +2742,17 @@ 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); } } } @@ -2794,11 +2767,47 @@ mod tests { let (w, h) = (140, 1); let mut setup = test_setup(w, h, true, true); - setup.app_data.lock().filter_term_push('c'); 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 = [ + " Enter done Esc clear filter: " + ]; + + 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() { + match result_cell_index { + 0..=6 | 13..=17 => { + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + 7..=12 | 18..=24 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Gray); + } + 25..=32 => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Magenta); + } + _ => { + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); + } + } + assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); + } + } + + setup.app_data.lock().filter_term_push('c'); setup .terminal @@ -2808,32 +2817,31 @@ mod tests { .unwrap(); let expected = [ - " Enter done Esc clear filter: c " - ]; - let result = &setup.terminal.backend().buffer().content; + " Enter done Esc clear filter: c " + ]; - for (row_index, row) in result.chunks(usize::from(w)).enumerate() { + for (row_index, result_row) in get_result(&setup, w) { let expected_row = expected_to_vec(&expected, row_index); - for (cell_index, cell) in row.iter().enumerate() { - match cell_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..=6 | 13..=17 => { - assert_eq!(cell.bg, Color::Magenta); - assert_eq!(cell.fg, Color::Black); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); } 7..=12 | 18..=24 | 33 => { - assert_eq!(cell.bg, Color::Reset); - assert_eq!(cell.fg, Color::Gray); + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Gray); } 25..=32 => { - assert_eq!(cell.bg, Color::Reset); - assert_eq!(cell.fg, Color::Magenta); + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Magenta); } _ => { - assert_eq!(cell.bg, Color::Reset); - assert_eq!(cell.fg, Color::Reset); + assert_eq!(result_cell.bg, Color::Reset); + assert_eq!(result_cell.fg, Color::Reset); } } - assert_eq!(cell.symbol(), expected_row[cell_index]); } } } @@ -2855,39 +2863,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); + } } } } @@ -2919,27 +2921,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); + } } } } @@ -2971,23 +2966,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 @@ -3008,18 +3011,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()); } } } @@ -3065,31 +3067,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()); + } } } } @@ -3102,6 +3103,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 @@ -3110,39 +3141,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| { @@ -3150,35 +3161,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); } } } @@ -3242,13 +3232,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]); } } } @@ -3309,15 +3296,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - - for (row_index, row) in result.chunks(usize::from(w)).enumerate() { - let expected_row = expected[row_index] - .chars() - .map(|i| i.to_string()) - .collect::>(); - for (cell_index, cell) in row.iter().enumerate() { - assert_eq!(cell.symbol(), expected_row[cell_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]); } } @@ -3330,37 +3312,37 @@ mod tests { 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", - "│⚪ 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", - "│ line 1 │", - "│ line 2 │", - "│▶ line 3 │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "│ │", - "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", - "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", - "│10.00%│ ••• ││100.00 kB│ •• ││ ip private public│", - "│ │ •• • ││ │ •• • ││ 8001 │", - "│ │ ••• • • ││ │ ••• • • ││ │", - "│ │• •• ││ │• •• ││ │", - "│ │ ││ │ ││ │", - "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", - " Enter done Esc clear filter: r_1 " - ]; + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ 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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮", + "│10.00%│ ••• ││100.00 kB│ •• ││ ip private public│", + "│ │ •• • ││ │ •• • ││ 8001 │", + "│ │ ••• • • ││ │ ••• • • ││ │", + "│ │• •• ││ │• •• ││ │", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + " Enter done Esc clear filter: r_1 " + ]; setup .terminal .draw(|f| { @@ -3368,15 +3350,10 @@ mod tests { }) .unwrap(); - let result = &setup.terminal.backend().buffer().content; - - for (row_index, row) in result.chunks(usize::from(w)).enumerate() { - let expected_row = expected[row_index] - .chars() - .map(|i| i.to_string()) - .collect::>(); - for (cell_index, cell) in row.iter().enumerate() { - assert_eq!(cell.symbol(), expected_row[cell_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]); } } } @@ -3441,13 +3418,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]); } } } From 91b451c6a3404977fab59c8b9619ab37c5d98d10 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:09:56 +0000 Subject: [PATCH 10/31] refactor: filter mode displayed updated --- README.md | 2 +- src/input_handler/mod.rs | 4 ++-- src/ui/draw_blocks.rs | 29 +++++++++++++++-------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f59bb97..216972e 100644 --- a/README.md +++ b/README.md @@ -101,7 +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 ```( / )``` | Toggle filter mode. | +| ```( 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.| diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 8df7342..edf2c4c 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -388,11 +388,11 @@ impl InputHandler { /// Actions to take when Filter status active fn handle_filter(&mut self, key_code: KeyCode) { match key_code { - KeyCode::F(1) | KeyCode::Char('/') | KeyCode::Esc => { + KeyCode::Esc => { self.app_data.lock().filter_term_clear(); self.gui_state.lock().status_del(Status::Filter); } - KeyCode::Enter => { + KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('/') => { self.gui_state.lock().status_del(Status::Filter); } KeyCode::Backspace => { diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 6d9419a..4472f09 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -427,8 +427,8 @@ 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 line = Line::from(vec![ - Span::styled(" Enter ", style_but), - Span::styled(" done ", style_desc), + // Span::styled(" Enter ", style_but), + // Span::styled(" done ", style_desc), Span::styled(" Esc ", style_but), Span::styled(" clear ", style_desc), Span::styled( @@ -745,7 +745,7 @@ impl HelpInfo { button_item("F1"), or(), button_item("/"), - button_desc("toggle filter mode"), + button_desc("enter filter mode"), ]), Line::from(vec![space(), button_item("0"), button_desc("stop sort")]), Line::from(vec![ @@ -2549,7 +2549,7 @@ mod tests { " │ ( 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 ( / ) toggle filter mode │ ", + " │ ( F1 ) or ( / ) enter filter mode │ ", " │ ( 0 ) stop sort │ ", " │ ( 1 - 9 ) sort by header - or click header │ ", " │ ( esc ) close dialog │ ", @@ -2779,22 +2779,23 @@ mod tests { .unwrap(); let expected = [ - " Enter done Esc clear filter: " + " Esc clear filter: " ]; 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..=6 | 13..=17 => { + 0..=4 => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); } - 7..=12 | 18..=24 => { + 5..=11 => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Gray); } - 25..=32 => { + 12..=19 => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Magenta); } @@ -2803,7 +2804,6 @@ mod tests { assert_eq!(result_cell.fg, Color::Reset); } } - assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); } } @@ -2817,23 +2817,24 @@ mod tests { .unwrap(); let expected = [ - " Enter done Esc clear filter: c " + " Esc clear filter: c " ]; 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..=6 | 13..=17 => { + 0..=4 => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); } - 7..=12 | 18..=24 | 33 => { + 5..=11 | 20 => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Gray); } - 25..=32 => { + 12..=19 => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Magenta); } @@ -3341,7 +3342,7 @@ mod tests { "│ │• •• ││ │• •• ││ │", "│ │ ││ │ ││ │", "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", - " Enter done Esc clear filter: r_1 " + " Esc clear filter: r_1 " ]; setup .terminal From 4ba27f126ca49f731caa57801a2cf39ca6ee3f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Wed, 24 Jul 2024 00:53:58 +0200 Subject: [PATCH 11/31] feat: left align text fieds of container block --- src/ui/draw_blocks.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index f2d0b60..bc4eb21 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -146,7 +146,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}{:>width$}", + "{MARGIN}{:(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:>width$}", + "{MARGIN}{: Date: Wed, 24 Jul 2024 14:20:29 +0000 Subject: [PATCH 12/31] feat: advanced filtering Allow filtering by name, image name, status, or a combination of all of the three --- src/app_data/container_state.rs | 4 + src/app_data/mod.rs | 288 ++++++++++++++++++++++++++++---- src/input_handler/mod.rs | 6 + src/main.rs | 6 +- src/ui/draw_blocks.rs | 141 +++++++++++++--- 5 files changed, 390 insertions(+), 55 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 0dcee3e..ad834ee 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -75,6 +75,10 @@ macro_rules! unit_struct { pub fn set(&mut self, value: String) { self.0 = value; } + + pub fn contains(&self, term: &str) -> bool { + self.0.to_lowercase().contains(term) + } } impl std::fmt::Display for $name { diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index d279384..2fa3bb7 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -55,26 +55,85 @@ 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_term: Option, - sorted_by: Option<(Header, SortedOrder)>, + filter: Filter, hidden_containers: Vec, + sorted_by: Option<(Header, SortedOrder)>, pub args: CliArgs, } #[derive(Debug, Clone)] #[cfg(test)] pub struct AppData { - pub hidden_containers: Vec, pub args: CliArgs, pub containers: StatefulList, pub error: Option, - pub filter_term: Option, + pub filter: Filter, + pub hidden_containers: Vec, pub sorted_by: Option<(Header, SortedOrder)>, } @@ -87,7 +146,7 @@ impl AppData { hidden_containers: vec![], error: None, sorted_by: None, - filter_term: None, + filter: Filter::new(), } } @@ -104,20 +163,33 @@ impl AppData { /// Get the current filter term pub const fn get_filter_term(&self) -> Option<&String> { - self.filter_term.as_ref() + self.filter.term.as_ref() } - /// Check the container name against the current filter - fn can_insert(&self, name: &str) -> bool { - self.filter_term.as_ref().map_or(true, |term| { - name.to_string() - .to_lowercase() - .contains(&term.to_lowercase()) + /// 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.to_lowercase().contains(&term) + } + FilterBy::Image => container.image.contains(&term), + FilterBy::Name => container.name.contains(&term), + FilterBy::Status => container.status.to_lowercase().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 occured + /// 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(); @@ -127,7 +199,7 @@ impl AppData { .hidden_containers .iter() .cloned() - .partition(|item| self.can_insert(item.name.get())); + .partition(|item| self.can_insert(item)); while let Some(x) = new_items.pop() { self.containers.items.push(x); @@ -140,7 +212,7 @@ impl AppData { .items .iter() .cloned() - .partition(|item| self.can_insert(item.name.get())); + .partition(|item| self.can_insert(item)); self.containers.items = new_items; self.hidden_containers.extend(tmp_items); @@ -151,31 +223,54 @@ impl AppData { } } + /// 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() { + if let Some(term) = self.filter.term.as_mut() { term.push(c); } else { - self.filter_term = Some(format!("{c}")); + 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() { + 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.term = None; } } self.filter_containers(); } - /// Remove the filter term completely, empty the "hidden" container vec + // 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; + 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); @@ -307,9 +402,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 @@ -595,7 +695,7 @@ impl AppData { /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly - /// Searches in both containes & hidden_containers + /// 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); @@ -777,10 +877,10 @@ impl AppData { }; } else { // container not known, so make new ContainerItem and push into containers Ve - let can_insert = self.can_insert(&name); let container = ContainerItem::new( created, id, image, is_oxker, name, ports, state, status, ); + let can_insert = self.can_insert(&container); if can_insert { self.containers.items.push(container); } else { @@ -1551,8 +1651,8 @@ mod tests { // ****** // #[test] - /// Data is filtered correctly - fn test_app_data_filter() { + /// Data is filtered correctly by name + fn test_app_data_filter_by_name() { let (_, containers) = gen_containers(); let mut app_data = gen_appdata(&containers); @@ -1571,9 +1671,137 @@ mod tests { assert_eq!(post_len, 1); // Can insert checks against the current filter term - assert!(app_data.can_insert("_2")); - assert!(!app_data.can_insert("_")); - assert!(!app_data.can_insert("_3")); + // todo!("fix me"); + 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(); + "Exited".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(); + "Exited".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(); + "Exited".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])); } // **** // diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index edf2c4c..85578a2 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -401,6 +401,12 @@ impl InputHandler { 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(); + } _ => (), } } diff --git a/src/main.rs b/src/main.rs index 9ef3fee..aeff8a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -175,7 +175,9 @@ mod tests { use bollard::service::{ContainerSummary, Port}; use crate::{ - app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList}, + app_data::{ + AppData, ContainerId, ContainerItem, ContainerPorts, Filter, State, StatefulList, + }, parse_args::CliArgs, }; @@ -217,7 +219,7 @@ mod tests { hidden_containers: vec![], error: None, sorted_by: None, - filter_term: None, + filter: Filter::new(), args: gen_args(), } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 4472f09..9299ab8 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, @@ -422,17 +422,59 @@ 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 line = Line::from(vec![ - // Span::styled(" Enter ", style_but), - // Span::styled(" done ", style_desc), + + 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( - "filter: ", + " term: ", Style::default() .fg(Color::Magenta) .add_modifier(Modifier::BOLD), @@ -445,7 +487,7 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) Style::default().fg(Color::Gray), ), ]); - frame.render_widget(line, area); + frame.render_widget(Line::from(line), area); } /// Draw heading bar at top of program, always visible @@ -579,12 +621,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) @@ -998,7 +1034,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("") @@ -1148,6 +1184,7 @@ mod tests { .chunks(usize::from(w)) .enumerate() } + // ******************** // // DockerControls panel // // ******************** // @@ -2056,7 +2093,7 @@ mod tests { } } - /// CPU and Memroy charts used in multiple tests, based on data from above insert_chart_data() + /// 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│ •• │", @@ -2762,7 +2799,9 @@ mod tests { // ********** // #[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); @@ -2779,7 +2818,7 @@ mod tests { .unwrap(); let expected = [ - " Esc clear filter: " + " Esc clear ← by → Name Image Status All term: " ]; for (row_index, result_row) in get_result(&setup, w) { @@ -2787,7 +2826,7 @@ mod tests { 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 => { + 0..=4 | 12..=19 => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); } @@ -2795,9 +2834,14 @@ mod tests { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Gray); } - 12..=19 => { + 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); @@ -2807,7 +2851,9 @@ mod tests { } } + // 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 @@ -2817,7 +2863,7 @@ mod tests { .unwrap(); let expected = [ - " Esc clear filter: c " + " Esc clear ← by → Name Image Status All term: cd " ]; for (row_index, result_row) in get_result(&setup, w) { @@ -2826,17 +2872,66 @@ mod tests { assert_eq!(result_cell.symbol(), expected_row[result_cell_index]); match result_cell_index { - 0..=4 => { + 0..=4 | 12..=19 => { assert_eq!(result_cell.bg, Color::Magenta); assert_eq!(result_cell.fg, Color::Black); } - 5..=11 | 20 => { + 5..=11 | 54..=55 => { assert_eq!(result_cell.bg, Color::Reset); assert_eq!(result_cell.fg, Color::Gray); } - 12..=19 => { + 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); @@ -3314,7 +3409,7 @@ mod tests { let expected = [ " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", - "╭ Containers 1/1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "╭ 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 │", @@ -3342,7 +3437,7 @@ mod tests { "│ │• •• ││ │• •• ││ │", "│ │ ││ │ ││ │", "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", - " Esc clear filter: r_1 " + " Esc clear ← by → Name Image Status All term: r_1 " ]; setup .terminal From 8daa4f5b8a08fcc9335677aeddcdc71692c67e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alja=C5=BE=20Mur=20Er=C5=BEen?= Date: Wed, 24 Jul 2024 19:03:58 +0200 Subject: [PATCH 13/31] fix tests --- src/ui/draw_blocks.rs | 66 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index bc4eb21..8b0a92d 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1396,9 +1396,9 @@ mod tests { 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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1468,9 +1468,9 @@ mod tests { 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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1558,9 +1558,9 @@ mod tests { 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 │", + "│⚪ 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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1595,9 +1595,9 @@ 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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1622,9 +1622,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1648,9 +1648,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 +1674,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1700,9 +1700,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1726,9 +1726,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -3101,9 +3101,9 @@ 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 │", + "│⚪ 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 │", "│ ││ │", "│ ││ │", @@ -3172,9 +3172,9 @@ mod tests { 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 │", + "│⚪ 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 │", "│ ││ │", "│ ││ │", From 0288cbc8146cde1dd40ceaec9550198b635bb8f5 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:19:14 +0000 Subject: [PATCH 14/31] chore: .devcontainer extensions updated --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 5ae253b8734ba0495e4e8149b17d5228b3d86f8d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:19:20 +0000 Subject: [PATCH 15/31] chore: dependencies updated --- Cargo.lock | 83 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ddc596..ae37528 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,9 +190,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cansi" @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.0" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" [[package]] name = "cfg-if" @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.9" +version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" dependencies = [ "anstream", "anstyle", @@ -316,7 +316,7 @@ dependencies = [ "bitflags", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -506,9 +506,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", @@ -761,6 +761,18 @@ 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", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -786,21 +798,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.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" dependencies = [ "memchr", ] @@ -993,9 +995,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags", ] @@ -1091,9 +1093,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.3" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64", "chrono", @@ -1132,7 +1134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -1216,9 +1218,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.70" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -1227,18 +1229,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", @@ -1303,28 +1305,27 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" 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.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index b48bb69..8bb5e06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ directories = "5.0" futures-util = "0.3" parking_lot = { version = "0.12" } ratatui = "0.27" -tokio = { version = "1.38", features = ["full"] } +tokio = { version = "1.39", features = ["full"] } tokio-util = "0.7" tracing = "0.1" tracing-subscriber = "0.3" From 9e5cf68b8f064f72ec09ef0e8f1c206e801ef9a1 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:02:56 +0000 Subject: [PATCH 16/31] fix: colum width minimums --- src/app_data/container_state.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 0dcee3e..565e13a 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -665,14 +665,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), } } } From d472d074f6357f4c482c2f24d53660f4c3fa772d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 24 Jul 2024 21:07:25 +0000 Subject: [PATCH 17/31] fix: left align headers, Remove pointless match statements --- src/ui/draw_blocks.rs | 71 ++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 48 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 8b0a92d..7fc7595 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -146,7 +146,7 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { Line::from(vec![ Span::styled( format!( - "{:(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{:(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{}{:>width$}", - MARGIN, + "{:>width$}{MARGIN}", i.cpu_stats.back().unwrap_or(&CpuStats::default()), width = &widths.cpu.1.into() ), @@ -179,7 +178,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 +188,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 +196,18 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!( - "{MARGIN}{: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)), ), ]) @@ -430,54 +428,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::Max(4), Constraint::Min(column_width.try_into().unwrap_or_default()), Constraint::Max(info_width.try_into().unwrap_or_default()), ] @@ -541,7 +516,7 @@ 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)) + let loading_paragraph = Paragraph::new(format!(" {:<3}", data.loading_icon)) .block(block(Color::White)) .alignment(Alignment::Center); frame.render_widget(loading_paragraph, split_bar[0]); From 93e1279b1fc77019442a385e2e36be2fe438e828 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:30:24 +0000 Subject: [PATCH 18/31] chore: Rust 1.80 linting --- src/docker_data/mod.rs | 4 ++-- src/input_handler/mod.rs | 26 +++++++++++++------------- src/ui/gui_state.rs | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 8508a28..be3e05f 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -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:: { @@ -271,7 +271,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); diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 85578a2..3110596 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -178,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>, @@ -266,7 +266,7 @@ impl InputHandler { } /// 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 { @@ -307,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 { @@ -321,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 @@ -333,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 { @@ -344,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 { @@ -366,7 +366,7 @@ impl InputHandler { } /// Actions to take when Error status active - fn handle_error(&mut self, key_code: KeyCode) { + fn handle_error(&self, key_code: KeyCode) { match key_code { KeyCode::Esc | KeyCode::Char('c' | 'C') => { self.app_data.lock().remove_error(); @@ -377,7 +377,7 @@ impl InputHandler { } /// Actions to take when Delete status active - async fn handle_delete(&mut self, key_code: KeyCode) { + 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(), @@ -386,7 +386,7 @@ impl InputHandler { } /// Actions to take when Filter status active - fn handle_filter(&mut self, key_code: KeyCode) { + fn handle_filter(&self, key_code: KeyCode) { match key_code { KeyCode::Esc => { self.app_data.lock().filter_term_clear(); @@ -489,7 +489,7 @@ impl InputHandler { } /// Check if a button press interacts with either the yes or no buttons in the delete container confirm window - async fn button_intersect(&mut self, mouse_event: MouseEvent) { + 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, @@ -508,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(), @@ -534,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 { @@ -545,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/ui/gui_state.rs b/src/ui/gui_state.rs index 354343a..ce007e8 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -209,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)) @@ -219,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)) @@ -295,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() } From 2cc2a65d57725ce6a877c4f4342c7a3193f2c570 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:47:19 +0000 Subject: [PATCH 19/31] tests: fix layout tests with new left alignment --- src/app_data/mod.rs | 12 +- src/ui/draw_blocks.rs | 433 +++++++++++++++++++++--------------------- 2 files changed, 221 insertions(+), 224 deletions(-) diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 2fa3bb7..002a09b 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -2091,9 +2091,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), @@ -2113,9 +2113,9 @@ mod tests { let result = app_data.get_width(); let expected = Columns { name: (Header::Name, 28), - 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), diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index a641b15..5eef8c3 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1359,44 +1359,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) { - 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..=14 | 78..=98) => { - assert_eq!(result_cell.fg, Color::Blue); - } - // state, status, cpu, memory column of the first row - (1, 15..=77) => { - assert_eq!(result_cell.fg, color); - } - // state, status, cpu, memory column - (2..=3, 15..=77) => { - assert_eq!(result_cell.fg, Color::Green); - } - // rx column - (1..=3, 99..=108) => { - assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); - } - // tx column - (1..=3, 109..=118) => { - assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); - } - _ => assert_eq!(result_cell.fg, Color::Reset), - } - } - } - } - #[test] /// No containers, panel unselected, then selected, border color changes correctly fn test_draw_blocks_containers_none() { @@ -1454,18 +1416,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 @@ -1529,11 +1491,11 @@ mod tests { 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())); @@ -1546,6 +1508,7 @@ mod tests { 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]); @@ -1555,19 +1518,19 @@ mod tests { assert_eq!(result_cell.fg, Color::LightCyan); } // name, id, image column - (1..=3, 4..=14 | 78..=98) => { + (1..=3, 4..=17 | 71..=91) => { assert_eq!(result_cell.fg, Color::Blue); } // state, status, cpu, memory column - (1..=3, 15..=77) => { + (1..=3, 18..=70) => { assert_eq!(result_cell.fg, Color::Green); } // rx column - (1..=3, 99..=108) => { + (1..=3, 92..=101) => { assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); } // tx column - (1..=3, 109..=118) => { + (1..=3, 102..=111) => { assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); } _ => assert_eq!(result_cell.fg, Color::Reset), @@ -1587,12 +1550,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; @@ -1612,6 +1575,44 @@ mod tests { } } + // 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] /// When container is paused, correct colors displayed fn test_draw_blocks_containers_paused() { @@ -1620,11 +1621,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; @@ -1646,12 +1647,12 @@ mod tests { let mut setup = test_setup(w, h, true, true); 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 │", - "│ │", - "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", +"╭ 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 │", +"│ │", +"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Dead; let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); @@ -1662,6 +1663,7 @@ mod tests { super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); }) .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); } @@ -1673,9 +1675,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1699,9 +1701,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1726,9 +1728,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1741,6 +1743,7 @@ mod tests { 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() { @@ -1752,23 +1755,23 @@ mod tests { assert_eq!(result_cell.fg, Color::LightCyan); } // name, id, image column - (1..=3, 4..=14 | 79..=99) => { + (1..=3, 4..=17 | 74..=94) => { assert_eq!(result_cell.fg, Color::Blue); } // state, status, cpu, memory column of the first row - (1, 15..=78) => { + (1, 18..=73) => { assert_eq!(result_cell.fg, Color::LightGreen); } // state, status, cpu, memory column - (2..=3, 15..=78) => { + (2..=3, 18..=73) => { assert_eq!(result_cell.fg, Color::Green); } // rx column - (1..=3, 100..=109) => { + (1..=3, 95..=104) => { assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); } // tx column - (1..=3, 110..=119) => { + (1..=3, 105..=114) => { assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); } _ => { @@ -1787,9 +1790,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 │", "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; @@ -1802,8 +1805,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 // // ********** // @@ -2358,7 +2363,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| { @@ -2374,7 +2379,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - (2..=122) => Color::Black, + (4..=124) => Color::Black, _ => Color::White, } ); @@ -2390,7 +2395,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| { @@ -2406,7 +2411,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - (2..=62) => Color::Black, + (4..=64) => Color::Black, _ => Color::White, } ); @@ -2441,9 +2446,9 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - 0 | 1 => Color::White, + 0..=3 | 125..=139 => Color::White, // given range | help section - x if range.contains(&x) || (123..=139).contains(&x) => Color::White, + x if range.contains(&x) => Color::Gray, _ => Color::Black, } ); @@ -2452,40 +2457,32 @@ mod tests { }; // 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] @@ -2497,6 +2494,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| { @@ -2504,8 +2503,6 @@ mod tests { }) .unwrap(); - let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "]; - 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() { @@ -2514,7 +2511,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - (2..=122) => Color::Black, + (4..=124) => Color::Black, _ => Color::White, } ); @@ -3265,36 +3262,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 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ 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 @@ -3329,36 +3326,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 │", - "│ │•• ••• ││ │•• ••• ││ │", - "│ │ ││ │ ││ │", - "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", + " 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 │", + "│ │•• ••• ││ │•• ••• ││ │", + "│ │ ││ │ ││ │", + "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", ]; setup .terminal @@ -3383,9 +3380,9 @@ mod tests { setup.app_data.lock().filter_term_push('1'); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + " 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 │", + "│⚪ 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 │", @@ -3412,7 +3409,7 @@ mod tests { "│ │• •• ││ │• •• ││ │", "│ │ ││ │ ││ │", "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", - " Esc clear ← by → Name Image Status All term: r_1 " + " Esc clear ← by → Name Image Status All term: r_1 ", ]; setup .terminal @@ -3451,36 +3448,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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ 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 From 51e9b1da2add864441c76c82fda0e7dec852ba76 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 27 Jul 2024 09:14:24 +0000 Subject: [PATCH 20/31] refactor: formatting --- src/ui/draw_blocks.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 5eef8c3..bdf71b9 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1647,12 +1647,12 @@ mod tests { let mut setup = test_setup(w, h, true, true); 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 │", -"│ │", -"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭ 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 │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Dead; let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); From 068e4025a5d6049a9a6951a0480a6bdef7379f88 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 27 Jul 2024 09:55:57 +0000 Subject: [PATCH 21/31] fix: Dockerfile command case --- containerised/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 0e927aae178c1d8f60561b93607a26d45a1d9331 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 27 Jul 2024 11:07:28 +0000 Subject: [PATCH 22/31] fix: heading section help margin --- src/ui/draw_blocks.rs | 67 ++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index bdf71b9..e193375 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -552,7 +552,7 @@ pub fn heading_bar( // Need to add widths to this let suffix = if data.help_visible { "exit" } else { "show" }; - let info_text = format!("( h ) {suffix} help {MARGIN}",); + let info_text = format!("( h ) {suffix} help{MARGIN}",); let info_width = info_text.chars().count(); let column_width = usize::from(area.width).saturating_sub(info_width); @@ -560,7 +560,7 @@ pub fn heading_bar( let splits = if data.has_containers { vec![ Constraint::Max(4), - Constraint::Min(column_width.try_into().unwrap_or_default()), + Constraint::Max(column_width.try_into().unwrap_or_default()), Constraint::Max(info_width.try_into().unwrap_or_default()), ] } else { @@ -2319,7 +2319,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 @@ -2338,7 +2338,7 @@ mod tests { } fd.help_visible = true; - let expected = [" ( h ) exit help "]; + let expected = [" ( h ) exit help "]; setup .terminal .draw(|f| { @@ -2363,7 +2363,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| { @@ -2379,7 +2379,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - (4..=124) => Color::Black, + (4..=121) => Color::Black, _ => Color::White, } ); @@ -2395,7 +2395,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| { @@ -2411,7 +2411,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - (4..=64) => Color::Black, + (4..=61) => Color::Black, _ => Color::White, } ); @@ -2426,6 +2426,7 @@ mod tests { let mut setup = test_setup(w, h, true, true); let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + // Actual test, used for each header and sorted type let mut test = |expected: &[&str], range: RangeInclusive, x: (Header, SortedOrder)| { fd.sorted_by = Some(x); @@ -2446,7 +2447,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - 0..=3 | 125..=139 => Color::White, + 0..=3 | 122..=139 => Color::White, // given range | help section x if range.contains(&x) => Color::Gray, _ => Color::Black, @@ -2457,32 +2458,32 @@ mod tests { }; // Name - 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)); + 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"],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)); + 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"], 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)); + 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"],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)); + 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)); + 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)); + 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"], 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)); + 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"], 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)); + 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"], 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(&[" 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] @@ -2494,7 +2495,7 @@ 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"]; + let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "]; setup .terminal @@ -2511,7 +2512,7 @@ mod tests { assert_eq!( result_cell.fg, match result_cell_index { - (4..=124) => Color::Black, + (4..=121) => Color::Black, _ => Color::White, } ); @@ -3262,7 +3263,7 @@ mod tests { }); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help", + " 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 │", @@ -3326,7 +3327,7 @@ mod tests { }); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help", + " 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 │", @@ -3380,7 +3381,7 @@ mod tests { setup.app_data.lock().filter_term_push('1'); let expected = [ - " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help", + " 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 │", @@ -3448,7 +3449,7 @@ 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", + " 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 │", From 0e90f4eb55ac5fb5d45e7d212c3686027dd3913e Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 27 Jul 2024 11:07:53 +0000 Subject: [PATCH 23/31] chore: dependencies updated --- Cargo.lock | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae37528..80c9586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +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", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.10" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6b81fb3c84f5563d509c59b5a48d935f689e993afa90fe39047f05adef9142" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.10" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca6706fd5224857d9ac5eb9355f6683563cc0541c7cd9d014043b57cbec78ac" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" dependencies = [ "anstream", "anstyle", @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ "heck", "proc-macro2", @@ -278,15 +278,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +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" @@ -659,9 +659,9 @@ 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" @@ -1305,9 +1305,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", @@ -1523,9 +1523,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" From f408acfe9a9f5a976735b8a8a51500fd7b865daf Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 27 Jul 2024 11:24:37 +0000 Subject: [PATCH 24/31] chore: create_release v0.5.6 --- create_release.sh | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) 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 From de8768181631c6d961ce0e4dacb50c2ed02abc36 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:46:58 +0000 Subject: [PATCH 25/31] feat: unhealthy status, closes #43 Highlight an unhealthy container in Orange, and display "! running" as the state, refactor: Move dev Docker files to docker directory --- .github/screenshot_01.png | Bin 57914 -> 42007 bytes README.md | 4 +- docker/Dockerfile.unhealthy | 17 ++ .../docker-compose.yml | 15 ++ src/app_data/container_state.rs | 169 ++++++++++++++---- src/app_data/mod.rs | 61 ++++--- src/docker_data/mod.rs | 12 +- src/exec.rs | 9 +- src/main.rs | 7 +- src/ui/draw_blocks.rs | 75 +++++++- src/ui/mod.rs | 2 + 11 files changed, 291 insertions(+), 80 deletions(-) create mode 100644 docker/Dockerfile.unhealthy rename docker-compose.yml => docker/docker-compose.yml (73%) diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 8355605508df97f9122917033a25fb140f0089b8..0a199b6dd9f58341200623a966c67f36e0037167 100644 GIT binary patch literal 42007 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 RGc#6qDnHG&F8uA>X>O#P}!}rRA_rR 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 NQC3Z+MC!HQ{{Suplpp{A literal 57914 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%0Dy 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 diff --git a/README.md b/README.md index 216972e..cefc768 100644 --- a/README.md +++ b/README.md @@ -168,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/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 73% rename from docker-compose.yml rename to docker/docker-compose.yml index 9ccaf49..2b4c1ea 100644 --- a/docker-compose.yml +++ b/docker/docker-compose.yml @@ -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/src/app_data/container_state.rs b/src/app_data/container_state.rs index 600f152..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() } @@ -75,26 +80,20 @@ macro_rules! unit_struct { pub fn set(&mut self, value: String) { self.0 = value; } + } - pub fn contains(&self, term: &str) -> bool { - self.0.to_lowercase().contains(term) + 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) } } } @@ -211,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))) } } @@ -278,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}") @@ -314,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], } } @@ -543,7 +589,7 @@ pub struct ContainerItem { pub ports: Vec, pub rx: ByteStats, pub state: State, - pub status: String, + pub status: ContainerStatus, pub tx: ByteStats, } @@ -572,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(); @@ -686,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 @@ -774,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 002a09b..51027b3 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -179,11 +179,11 @@ impl AppData { FilterBy::All => { container.name.contains(&term) || container.image.contains(&term) - || container.status.to_lowercase().contains(&term) + || container.status.contains(&term) } FilterBy::Image => container.image.contains(&term), FilterBy::Name => container.name.contains(&term), - FilterBy::Status => container.status.to_lowercase().contains(&term), + FilterBy::Status => container.status.contains(&term), } }) } @@ -335,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 @@ -727,7 +728,7 @@ impl AppData { 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.status.1 = columns.status.1.max(count(container.status.get())); } } columns @@ -836,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() @@ -983,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; @@ -1017,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 @@ -1342,7 +1345,7 @@ mod tests { result, Some(( ContainerId::from("1"), - State::Running, + State::Running(RunningState::Healthy), "container_1".to_owned() )) ); @@ -1356,7 +1359,7 @@ mod tests { result, Some(( ContainerId::from("1"), - State::Running, + State::Running(RunningState::Healthy), "container_1".to_owned() )) ); @@ -1384,7 +1387,7 @@ mod tests { result, Some(( ContainerId::from("2"), - State::Running, + State::Running(RunningState::Healthy), "container_2".to_owned() )) ); @@ -1409,7 +1412,7 @@ mod tests { result, Some(( ContainerId::from("3"), - State::Running, + State::Running(RunningState::Healthy), "container_3".to_owned() )) ); @@ -1423,7 +1426,7 @@ mod tests { result, Some(( ContainerId::from("3"), - State::Running, + State::Running(RunningState::Healthy), "container_3".to_owned() )) ); @@ -1504,7 +1507,7 @@ mod tests { result, Some(( ContainerId::from("3"), - State::Running, + State::Running(RunningState::Healthy), "container_3".to_owned() )) ); @@ -1594,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)]); @@ -1635,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, @@ -1671,7 +1674,6 @@ mod tests { assert_eq!(post_len, 1); // Can insert checks against the current filter term - // todo!("fix me"); assert!(app_data.can_insert(&containers[1])); assert!(!app_data.can_insert(&containers[0])); assert!(!app_data.can_insert(&containers[2])); @@ -1710,7 +1712,7 @@ mod tests { /// Data is filtered correctly by status fn test_app_data_filter_by_status() { let (_, mut containers) = gen_containers(); - "Exited".clone_into(&mut containers[0].status); + 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()); @@ -1738,7 +1740,7 @@ mod tests { /// Data is filtered correctly by all fn test_app_data_filter_by_all() { let (_, mut containers) = gen_containers(); - "Exited".clone_into(&mut containers[0].status); + 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()); @@ -1767,7 +1769,7 @@ mod tests { /// Data is filtered correctly after various next() and previous() commands fn test_app_data_filter_prev() { let (_, mut containers) = gen_containers(); - "Exited".clone_into(&mut containers[0].status); + 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()); @@ -2067,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), ) )) ); @@ -2188,7 +2190,7 @@ mod tests { public: None } ], - State::Running + State::Running(RunningState::Healthy), )) ); @@ -2197,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 be3e05f..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}, @@ -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::>() } 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/main.rs b/src/main.rs index aeff8a4..aa5c2cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -176,7 +176,8 @@ mod tests { use crate::{ app_data::{ - AppData, ContainerId, ContainerItem, ContainerPorts, Filter, State, StatefulList, + AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter, + RunningState, State, StatefulList, }, parse_args::CliArgs, }; @@ -208,8 +209,8 @@ 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")), ) } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index e193375..d98e5ee 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -21,7 +21,7 @@ use crate::{ use super::{ gui_state::{BoxLocation, DeleteButton, Region}, - FrameData, Status, + 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 = "⚪ "; @@ -163,7 +162,7 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { Span::styled( format!( "{: "no ports", + State::Running(_) | State::Paused | State::Restarting => "no ports", _ => "", }; let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD)) @@ -383,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) @@ -1081,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}, @@ -1782,6 +1781,68 @@ mod tests { } } + #[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() { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9fca730..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>, From 7a517db9f7c14c35e56ff70cf76ffb608fd30e17 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:22:55 +0000 Subject: [PATCH 26/31] chore: dependencies updated --- Cargo.lock | 102 ++++++++++++++++++++++++++++++++++++----------------- Cargo.toml | 2 +- 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 80c9586..c1d19f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -140,9 +140,9 @@ 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.1" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca2be1d5c43812bae364ee3f30b3afcb7877cf59f4aeb94c66f313a41d2fac9" [[package]] name = "cansi" @@ -217,9 +223,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" @@ -242,9 +248,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", "clap_derive", @@ -252,9 +258,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstream", "anstyle", @@ -266,9 +272,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", @@ -533,6 +539,12 @@ version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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.4.1" @@ -545,6 +557,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -588,10 +601,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", @@ -648,9 +661,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", @@ -727,9 +740,9 @@ 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", ] @@ -920,9 +933,12 @@ 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" @@ -1059,11 +1075,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -1101,7 +1118,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.3.0", "serde", "serde_derive", "serde_json", @@ -1129,9 +1146,9 @@ 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 0.8.11", @@ -1766,13 +1783,34 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "zerocopy" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +dependencies = [ + "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", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy-derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8bb5e06..fcc6106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ 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" From 12f24357a68abe871f44d871d95b6e2ef062181e Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:24:42 +0000 Subject: [PATCH 27/31] feat: place image name in logs panel title --- .github/screenshot_01.png | Bin 42007 -> 42391 bytes src/app_data/mod.rs | 44 +++++++++++++++++++------------------- src/ui/draw_blocks.rs | 38 ++++++++++++++++---------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 0a199b6dd9f58341200623a966c67f36e0037167..352bd7c6e6eb4518e278e27a303bf4f8f78d5610 100644 GIT binary patch literal 42391 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 RGc#6qDnHG&F8uA>X>O#P}!}rRA_rR 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 NQC3Z+MC!HQ{{Suplpp{A diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 51027b3..64a4075 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -590,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() @@ -602,7 +602,7 @@ impl AppData { } else { format!("{logs_len} ") }; - format!("{}- {}", prefix, ci.name.get()) + format!("{}- {} - {}", prefix, ci.name.get(), ci.image.get()) }) } @@ -1824,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] @@ -1851,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] @@ -1895,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] @@ -1915,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] @@ -1935,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(); @@ -1944,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] @@ -1965,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(); @@ -1975,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(); @@ -1984,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(); @@ -1993,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] @@ -2014,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(); @@ -2023,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(); @@ -2031,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(); @@ -2039,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"); } // ********** // diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index d98e5ee..f37c7aa 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1934,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())); @@ -1969,13 +1969,13 @@ mod tests { // animation moved by one frame setup.gui_state.lock().next_loading(uuid); - let expected = [ - "╭ Logs - container_1 ───╮", - "│ parsing logs ⠹ │", - "│ │", - "│ │", - "│ │", - "╰───────────────────────╯", + let expected = [ + "╭ Logs - container_1 - image_1 ╮", + "│ parsing logs ⠹ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────╯", ]; let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); @@ -2081,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 │", @@ -3333,7 +3333,7 @@ mod tests { "│ ││ │", "│ ││ │", "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", - "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", "│ line 1 │", "│ line 2 │", "│▶ line 3 │", @@ -3397,7 +3397,7 @@ mod tests { "│ ││ │", "│ ││ │", "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", - "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", "│ line 1 │", "│ line 2 │", "│▶ line 3 │", @@ -3449,7 +3449,7 @@ mod tests { "│ ││ stop │", "│ ││ delete │", "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", - "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "╭ Logs 3/3 - container_1 - image_1 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", "│ line 1 │", "│ line 2 │", "│▶ line 3 │", @@ -3519,7 +3519,7 @@ mod tests { "│ ││ │", "│ ││ │", "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰─────────────────╯", - "╭ 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_name_for_the_purposes_of_this_test ──────────────────────────────────────────────────────────────────────────╮", "│ line 1 │", "│ line 2 │", "│▶ line 3 │", From 6975ebe70f7058229c232e4a56b090f55247d2a2 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:20:22 +0000 Subject: [PATCH 28/31] docs: screenshot updated --- .github/screenshot_01.png | Bin 42391 -> 42351 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/screenshot_01.png b/.github/screenshot_01.png index 352bd7c6e6eb4518e278e27a303bf4f8f78d5610..c9f45726d0d48e27006f9a510c25eaeee316c367 100644 GIT binary patch literal 42351 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 literal 42391 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 Date: Thu, 1 Aug 2024 20:47:23 +0000 Subject: [PATCH 29/31] chore: dependencies updated --- Cargo.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 +- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1d19f7..3775299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,22 @@ dependencies = [ "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" @@ -381,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" @@ -722,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" @@ -782,6 +814,7 @@ checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -846,7 +879,7 @@ dependencies = [ "bollard", "cansi", "clap", - "crossterm", + "crossterm 0.28.1", "directories", "futures-util", "parking_lot", @@ -997,7 +1030,7 @@ dependencies = [ "bitflags", "cassowary", "compact_str", - "crossterm", + "crossterm 0.27.0", "itertools", "lru", "paste", @@ -1035,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" @@ -1152,6 +1198,7 @@ checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio 0.8.11", + "mio 1.0.1", "signal-hook", ] diff --git a/Cargo.toml b/Cargo.toml index fcc6106..904c0c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ anyhow = "1.0" 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" } From a4240677d231ae9f906e644b47f99bc5acd791e5 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:52:30 +0000 Subject: [PATCH 30/31] docs: changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index def4abd..4532b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +### Chores ++ .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 ++ 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 ++ 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] + # v0.6.4 ### 2024-05-25 From ffac05a9cd18c9fd86abf80815924db55f659af0 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Thu, 1 Aug 2024 21:59:45 +0000 Subject: [PATCH 31/31] chore: release v0.7.0 --- .github/release-body.md | 25 +++++++++++++++++++++---- CHANGELOG.md | 35 +++++++++++++++++++---------------- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app_data/mod.rs | 2 +- src/ui/draw_blocks.rs | 2 +- 6 files changed, 44 insertions(+), 24 deletions(-) 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/CHANGELOG.md b/CHANGELOG.md index 4532b65..5075d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,31 @@ +# v0.7.0 +### 2024-08-01 + ### Chores -+ .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] ++ .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, [6975ebe70f7058229c232e4a56b090f55247d2a2] ++ screenshot updated, [6975ebe7](https://github.com/mrjackwills/oxker/commit/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] ++ 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, [0dc98dfc8113869b81be9d697ca77418c919e4bf] -+ Dockerfile command use uppercase, [068e4025a5d6049a9a6951a0480a6bdef7379f88] -+ heading section help margin, [0e927aae178c1d8f60561b93607a26d45a1d9331] -+ install.sh use curl, [197a031b8cf356f49f08e04472d0d1c489699415] ++ 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, [dfced564278eafdbb8a5b95badbae3a7c4bf87b3] ++ 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 3775299..7beacf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -873,7 +873,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.6.4" +version = "0.7.0" dependencies = [ "anyhow", "bollard", diff --git a/Cargo.toml b/Cargo.toml index 904c0c4..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" diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 64a4075..e57f79c 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -1867,7 +1867,7 @@ mod tests { // 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 - image_2" ); + assert_eq!(result, " 2/3 - container_2 - image_2"); } #[test] diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index f37c7aa..c3e5693 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -1969,7 +1969,7 @@ mod tests { // animation moved by one frame setup.gui_state.lock().next_loading(uuid); - let expected = [ + let expected = [ "╭ Logs - container_1 - image_1 ╮", "│ parsing logs ⠹ │", "│ │",