diff --git a/.github/release-body.md b/.github/release-body.md index 679805e..166376e 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,14 +1,42 @@ -### 2024-10-22 +### 2024-12-05 ### Chores -+ dependencies updated, [ea877d23711b98ffd1108a74206d93d43482d44d], [af609c0dbf0caab4a073f822166de34999afb41b] -+ .devcontainer updated, [a9844436d003b84a3e9d8b600ea029b232566f3a] -+ create_release.sh updated, [c4943370f4a67f6c01c75a8a7f825912427666a2], [1389d8adbba75fef480eb1de09337eb7beb10ba3] ++ dependencies updated, [b78713579c4706d605e5b35fcd832610a0152294], [c6200e8f77f8bb1f0152cb9374029d15cc45df9d] ++ Rust 1.83 linting, [751d997a3dac823e144ae62e6c1455676e50ddb8] ### Features -+ Add Stderr output to logs, thanks [vincentmasse](https://github.com/vincentmasse), closes #48, merges #49, [b95c9311416cd0dbcfa5de90c23f3065bc2d6b17], [9936ad45e186ee431aade920674a2dc283937355], [289ede3f2531feeec56094a76bf34f4c69431bbe] ++ `--no-stderr` cli arg, removes Standard error output from logs, closes #52, [c739637b91c8fa742a69f4d888678d7b3964678c] ++ ContainerPorts use ipaddr, [1b26997d25f748e0d452f41fe41791533046ecdf] + +### Fixes ++ update containerised Dockerfile, [0c6f53228f01196e352c2069383ba1e7a10950a8] ++ calculate_usage overflow, [5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e] ++ DockerData spawns insertion error, [d4906d33c26b75d92e7d80040c488faa90a257c6] ### Refactors -+ Rust 1.82 linting, [c058c5a301cfd4e8d7a0079c4c3f8fdeae2803e5] ++ speed up docker logs init process, [8b9fe4246865441704ae12dff0938868a4fe6f81] ++ remove docker sleep, [f1562d1084336fe5be39894c93cb49107f0a4a6d] ++ dead code removed, [5ee48d5708fa6de0206c021db0bb611196e66fba], [ba6a95241389f99d504ee4bf3e87e19006f12e49], [f0b1145651625ad4e577d79baaf902d4d3bc0579] ++ input_handler, [7f4238349525c01ae9fb8b1f6c0946e5364dd55e] ++ statefulList get_state_title, [2d540b0e2210cc04d73035ec59211ffc739174f6] ++ statefulList next/previous, [7bb2bef28d90ebc58da86a0365a1904a0c32dffe] ++ help_box closure fn, [2860426d57a4458fcee49a2fd20e8e7bb9e71fb5] ++ use check_sub for sleep calculations, [fe3696e5576739d8b033d9e748b5ea696c4b4e4f] ++ rename scheduler to heartbeat, [68a6551ed038a36330b2f098112829465a1c3c7a] ++ remove unnecessary is_running load, [76ccf7c00691f815c3ab0bede838c99252ba84f0] ++ execute_command(), [2a834d6c2fa4a15124d24ddbd12f667829e148ad] ++ Remove numerous clones(), [e5927f781a7e9517b9fa00a2d1a835d2774a9d26] ++ remove app_data param from generate_lock(), [1a8dab654a1fdbf351a72dc54fe3d1943355bba6] ++ combine get_filter methods, [356ea5549bb4877e9893fe0e1053e73c5a62e806] ++ FrameData refactors, [57781701ff14c553dfbafb965ee8a33ab44dd36f], [6e2f82db81caaa98ce4781fa15928eb9e246ace6] ++ update_container_stat combine is_alive(), [55cc746736f6863aedc5ad838744a983796244d8] ++ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b700b9fde5578ae204992a67986d456ab] ++ pass `&FrameDate` into `draw_frame()`, [35aec5060fdbe606267be26656b4aeee43d50c02] ++ dead code removed, [caf23be4a7faff99aaca80b081a02e4e0a372009] ++ input_handler, [9c4f8910381b90b563da12eaba4b79cb60c40129] ++ draw_block, [de76bc22936b124dcb9646f302f6cc14691dbb63] + +### Tests ++ fix logs tests, [9b22f5da18e4bf92766a68a7f4cd61ad72724cfd] see CHANGELOG.md for more details diff --git a/CHANGELOG.md b/CHANGELOG.md index 858c2c6..05e5d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +# v0.9.0 +### 2024-12-05 + +### Chores ++ dependencies updated, [b7871357](https://github.com/mrjackwills/oxker/commit/b78713579c4706d605e5b35fcd832610a0152294), [c6200e8f](https://github.com/mrjackwills/oxker/commit/c6200e8f77f8bb1f0152cb9374029d15cc45df9d) ++ Rust 1.83 linting, [751d997a](https://github.com/mrjackwills/oxker/commit/751d997a3dac823e144ae62e6c1455676e50ddb8) + +### Features ++ `--no-stderr` cli arg, removes Standard error output from logs, closes [#52](https://github.com/mrjackwills/oxker/issues/52), [c739637b](https://github.com/mrjackwills/oxker/commit/c739637b91c8fa742a69f4d888678d7b3964678c) ++ ContainerPorts use ipaddr, [1b26997d](https://github.com/mrjackwills/oxker/commit/1b26997d25f748e0d452f41fe41791533046ecdf) + +### Fixes ++ update containerised Dockerfile, [0c6f5322](https://github.com/mrjackwills/oxker/commit/0c6f53228f01196e352c2069383ba1e7a10950a8) ++ calculate_usage overflow, [5106a01f](https://github.com/mrjackwills/oxker/commit/5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e) ++ DockerData spawns insertion error, [d4906d33](https://github.com/mrjackwills/oxker/commit/d4906d33c26b75d92e7d80040c488faa90a257c6) + +### Refactors ++ speed up docker logs init process, [8b9fe424](https://github.com/mrjackwills/oxker/commit/8b9fe4246865441704ae12dff0938868a4fe6f81) ++ remove docker sleep, [f1562d10](https://github.com/mrjackwills/oxker/commit/f1562d1084336fe5be39894c93cb49107f0a4a6d) ++ dead code removed, [5ee48d57](https://github.com/mrjackwills/oxker/commit/5ee48d5708fa6de0206c021db0bb611196e66fba), [ba6a9524](https://github.com/mrjackwills/oxker/commit/ba6a95241389f99d504ee4bf3e87e19006f12e49), [f0b11456](https://github.com/mrjackwills/oxker/commit/f0b1145651625ad4e577d79baaf902d4d3bc0579) ++ input_handler, [7f423834](https://github.com/mrjackwills/oxker/commit/7f4238349525c01ae9fb8b1f6c0946e5364dd55e) ++ statefulList get_state_title, [2d540b0e](https://github.com/mrjackwills/oxker/commit/2d540b0e2210cc04d73035ec59211ffc739174f6) ++ statefulList next/previous, [7bb2bef2](https://github.com/mrjackwills/oxker/commit/7bb2bef28d90ebc58da86a0365a1904a0c32dffe) ++ help_box closure fn, [2860426d](https://github.com/mrjackwills/oxker/commit/2860426d57a4458fcee49a2fd20e8e7bb9e71fb5) ++ use check_sub for sleep calculations, [fe3696e5](https://github.com/mrjackwills/oxker/commit/fe3696e5576739d8b033d9e748b5ea696c4b4e4f) ++ rename scheduler to heartbeat, [68a6551e](https://github.com/mrjackwills/oxker/commit/68a6551ed038a36330b2f098112829465a1c3c7a) ++ remove unnecessary is_running load, [76ccf7c0](https://github.com/mrjackwills/oxker/commit/76ccf7c00691f815c3ab0bede838c99252ba84f0) ++ execute_command(), [2a834d6c](https://github.com/mrjackwills/oxker/commit/2a834d6c2fa4a15124d24ddbd12f667829e148ad) ++ Remove numerous clones(), [e5927f78](https://github.com/mrjackwills/oxker/commit/e5927f781a7e9517b9fa00a2d1a835d2774a9d26) ++ remove app_data param from generate_lock(), [1a8dab65](https://github.com/mrjackwills/oxker/commit/1a8dab654a1fdbf351a72dc54fe3d1943355bba6) ++ combine get_filter methods, [356ea554](https://github.com/mrjackwills/oxker/commit/356ea5549bb4877e9893fe0e1053e73c5a62e806) ++ FrameData refactors, [57781701](https://github.com/mrjackwills/oxker/commit/57781701ff14c553dfbafb965ee8a33ab44dd36f), [6e2f82db](https://github.com/mrjackwills/oxker/commit/6e2f82db81caaa98ce4781fa15928eb9e246ace6) ++ update_container_stat combine is_alive(), [55cc7467](https://github.com/mrjackwills/oxker/commit/55cc746736f6863aedc5ad838744a983796244d8) ++ remove `input_poll_rate` from `Ui`, instead use const `POLL_RATE`, [69f6c96b](https://github.com/mrjackwills/oxker/commit/69f6c96b700b9fde5578ae204992a67986d456ab) ++ pass `&FrameDate` into `draw_frame()`, [35aec506](https://github.com/mrjackwills/oxker/commit/35aec5060fdbe606267be26656b4aeee43d50c02) ++ dead code removed, [caf23be4](https://github.com/mrjackwills/oxker/commit/caf23be4a7faff99aaca80b081a02e4e0a372009) ++ input_handler, [9c4f8910](https://github.com/mrjackwills/oxker/commit/9c4f8910381b90b563da12eaba4b79cb60c40129) ++ draw_block, [de76bc22](https://github.com/mrjackwills/oxker/commit/de76bc22936b124dcb9646f302f6cc14691dbb63) + +### Tests ++ fix logs tests, [9b22f5da](https://github.com/mrjackwills/oxker/commit/9b22f5da18e4bf92766a68a7f4cd61ad72724cfd) + # v0.8.0 ### 2024-10-22 diff --git a/Cargo.lock b/Cargo.lock index 0bfe203..1da5da6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -19,9 +19,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -55,43 +55,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.90" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "autocfg" @@ -128,9 +128,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bollard" -version = "0.17.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" dependencies = [ "base64", "bollard-stubs", @@ -151,7 +151,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror", + "thiserror 2.0.4", "tokio", "tokio-util", "tower-service", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.45.0-rc.26.0.1" +version = "1.47.1-rc.27.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" dependencies = [ "serde", "serde_repr", @@ -184,9 +184,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cansi" @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.31" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ "shlex", ] @@ -239,9 +239,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" dependencies = [ "clap_builder", "clap_derive", @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" dependencies = [ "anstream", "anstyle", @@ -275,15 +275,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -330,6 +330,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -340,6 +375,12 @@ dependencies = [ "serde", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "directories" version = "5.0.1" @@ -361,6 +402,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" @@ -375,12 +427,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -481,9 +533,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", @@ -496,12 +548,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hex" version = "0.4.3" @@ -510,9 +556,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -556,9 +602,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -591,9 +637,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -647,13 +693,148 @@ 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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -669,12 +850,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "serde", ] @@ -686,10 +867,14 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "instability" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", "quote", "syn", ] @@ -711,16 +896,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -732,9 +918,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libredox" @@ -752,6 +938,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -774,7 +966,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.0", + "hashbrown 0.15.2", ] [[package]] @@ -794,11 +986,10 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "log", "wasi", @@ -859,7 +1050,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.8.0" +version = "0.9.0" dependencies = [ "anyhow", "bollard", @@ -914,9 +1105,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -940,10 +1131,20 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.88" +name = "pretty_assertions" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1025,7 +1226,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1036,9 +1237,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags", "errno", @@ -1067,18 +1268,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.211" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.211" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -1087,9 +1288,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -1130,7 +1331,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.6.0", + "indexmap 2.7.0", "serde", "serde_derive", "serde_json", @@ -1199,14 +1400,20 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[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" @@ -1243,9 +1450,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.82" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1253,19 +1460,50 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.64" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" +dependencies = [ + "thiserror-impl 2.0.4", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" dependencies = [ "proc-macro2", "quote", @@ -1284,9 +1522,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -1305,34 +1543,29 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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.41.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", @@ -1359,9 +1592,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -1378,9 +1611,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1389,9 +1622,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -1400,9 +1633,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -1421,9 +1654,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -1445,26 +1678,11 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" @@ -1497,15 +1715,27 @@ checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 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" @@ -1545,9 +1775,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" dependencies = [ "cfg-if", "once_cell", @@ -1556,9 +1786,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" dependencies = [ "bumpalo", "log", @@ -1571,9 +1801,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1581,9 +1811,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" dependencies = [ "proc-macro2", "quote", @@ -1594,9 +1824,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] name = "winapi" @@ -1647,6 +1877,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1768,6 +2007,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[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 = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -1788,3 +2069,46 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index dec1d8e..e9cc624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.8.0" +version = "0.9.0" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" @@ -27,7 +27,7 @@ similar_names = "allow" [dependencies] anyhow = "1.0" -bollard = "0.17" +bollard = "0.18" cansi = "2.2" clap = { version = "4.5", features = ["color", "derive", "unicode"] } crossterm = "0.28" @@ -35,7 +35,7 @@ directories = "5.0" futures-util = "0.3" parking_lot = { version = "0.12" } ratatui = "0.29" -tokio = { version = "1.41", features = ["full"] } +tokio = { version = "1.42", features = ["full"] } tokio-util = "0.7" tracing = "0.1" tracing-subscriber = "0.3" diff --git a/README.md b/README.md index 49c2938..b239e57 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Available command line arguments |```-s```| If running via Docker, will display the oxker container.| |```-g```| No TUI, essentially a debugging mode with limited functionality, for now.| |```--host [string]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.| +|```--no-stderr```| Do not include stderr output in logs.| |```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.| |```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.| diff --git a/containerised/Dockerfile b/containerised/Dockerfile index 3c2c7b8..e05760a 100644 --- a/containerised/Dockerfile +++ b/containerised/Dockerfile @@ -2,7 +2,7 @@ ## Builder ## ############# -FROM --platform=linux/amd64 rust:slim AS builder +FROM --platform=$BUILDPLATFORM rust:slim AS builder ARG TARGETARCH @@ -11,9 +11,9 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="aarch64-linux-gnu-gcc" ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-C target-feature=+crt-static -C link-arg=-lgcc" ENV CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER="arm-linux-gnueabihf-ld" -COPY ./containerised/platform.sh . +COPY ./containerised/target.sh . -RUN chmod +x ./platform.sh && ./platform.sh +RUN chmod +x ./target.sh && ./target.sh RUN apt-get update && apt-get install $(cat /.compiler) -y @@ -29,10 +29,10 @@ COPY Cargo.* /usr/src/oxker/ WORKDIR /usr/src/oxker # Install target platform (Cross-Compilation) -RUN rustup target add $(cat /.platform) +RUN rustup target add $(cat /.target) # This is a dummy build to get the dependencies cached - probably not needed - as run via a github action -RUN cargo build --target $(cat /.platform) --release +RUN cargo build --target $(cat /.target) --release # Now copy in the rest of the sources COPY src /usr/src/oxker/src/ @@ -41,9 +41,9 @@ COPY src /usr/src/oxker/src/ RUN touch /usr/src/oxker/src/main.rs # This is the actual application build -RUN cargo build --release --target $(cat /.platform) +RUN cargo build --release --target $(cat /.target) -RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker / +RUN cp /usr/src/oxker/target/$(cat /.target)/release/oxker / ############# ## Runtime ## diff --git a/containerised/platform.sh b/containerised/target.sh similarity index 62% rename from containerised/platform.sh rename to containerised/target.sh index 9ada7c8..40ba3d6 100644 --- a/containerised/platform.sh +++ b/containerised/target.sh @@ -4,15 +4,15 @@ case $TARGETARCH in "amd64") - echo "x86_64-unknown-linux-musl" >/.platform + echo "x86_64-unknown-linux-musl" >/.target echo "" >/.compiler ;; "arm64") - echo "aarch64-unknown-linux-musl" >/.platform + echo "aarch64-unknown-linux-musl" >/.target echo "gcc-aarch64-linux-gnu" >/.compiler ;; "arm") - echo "arm-unknown-linux-musleabihf" >/.platform + echo "arm-unknown-linux-musleabihf" >/.target echo "gcc-arm-linux-gnueabihf" >/.compiler ;; esac diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 1dd717a..69b5c5e 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -2,6 +2,7 @@ use std::{ cmp::Ordering, collections::{HashSet, VecDeque}, fmt, + net::IpAddr, }; use bollard::service::Port; @@ -103,17 +104,17 @@ macro_rules! unit_struct { unit_struct!(ContainerName); unit_struct!(ContainerImage); -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ContainerPorts { - pub ip: Option, + pub ip: Option, pub private: u16, pub public: Option, } -impl From<&Port> for ContainerPorts { - fn from(value: &Port) -> Self { +impl From for ContainerPorts { + fn from(value: Port) -> Self { Self { - ip: value.ip.clone(), + ip: value.ip.and_then(|i| i.parse::().ok()), private: value.private_port, public: value.public_port, } @@ -122,7 +123,9 @@ impl From<&Port> for ContainerPorts { impl ContainerPorts { pub fn len_ip(&self) -> usize { - self.ip.as_ref().unwrap_or(&String::new()).chars().count() + self.ip + .as_ref() + .map_or(0, |i| i.to_string().chars().count()) } pub fn len_private(&self) -> usize { format!("{}", self.private).chars().count() @@ -133,11 +136,12 @@ impl ContainerPorts { .count() } - pub fn print(&self) -> (String, String, String) { + /// Return as tuple of Strings, ip address, private port, and public port + pub fn get_all(&self) -> (String, String, String) { ( self.ip .as_ref() - .map_or(String::new(), std::borrow::ToOwned::to_owned), + .map_or(String::new(), std::string::ToString::to_string), format!("{}", self.private), self.public.map_or(String::new(), |s| s.to_string()), ) @@ -171,27 +175,25 @@ impl StatefulList { pub fn next(&mut self) { if !self.items.is_empty() { - let i = match self.state.selected() { - Some(i) => { - if i < self.items.len() - 1 { - i + 1 - } else { - i - } + self.state.select(Some(self.state.selected().map_or(0, |i| { + if i < self.items.len() - 1 { + i + 1 + } else { + i } - None => 0, - }; - self.state.select(Some(i)); + }))); } } pub fn previous(&mut self) { if !self.items.is_empty() { - let i = self - .state - .selected() - .map_or(0, |i| if i == 0 { 0 } else { i - 1 }); - self.state.select(Some(i)); + self.state.select(Some(self.state.selected().map_or(0, |i| { + if i == 0 { + 0 + } else { + i - 1 + } + }))); } } @@ -201,11 +203,11 @@ impl StatefulList { String::new() } else { let len = self.items.len(); - let c = self + let count = self .state .selected() .map_or(0, |value| if len > 0 { value + 1 } else { value }); - format!(" {c}/{}", self.items.len()) + format!(" {count}/{len}") } } } @@ -259,9 +261,12 @@ pub enum State { } impl State { + /// The container is alive if the start is Running, either healthy or unhealthy pub const fn is_alive(self) -> bool { matches!(self, Self::Running(_)) } + /// Color of the state for the containers section + /// TODO allow usable editable colours pub const fn get_color(self) -> Color { match self { Self::Paused => Color::Yellow, @@ -333,7 +338,7 @@ impl fmt::Display for State { /// Items for the container control list #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DockerControls { +pub enum DockerCommand { Pause, Restart, Start, @@ -342,7 +347,7 @@ pub enum DockerControls { Delete, } -impl DockerControls { +impl DockerCommand { pub const fn get_color(self) -> Color { match self { Self::Pause => Color::Yellow, @@ -366,7 +371,7 @@ impl DockerControls { } } -impl fmt::Display for DockerControls { +impl fmt::Display for DockerCommand { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let disp = match self { Self::Pause => "pause", @@ -577,7 +582,7 @@ impl Logs { pub struct ContainerItem { pub cpu_stats: VecDeque, pub created: u64, - pub docker_controls: StatefulList, + pub docker_controls: StatefulList, pub id: ContainerId, pub image: ContainerImage, pub is_oxker: bool, @@ -620,7 +625,7 @@ impl ContainerItem { state: State, status: ContainerStatus, ) -> Self { - let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state)); + let mut docker_controls = StatefulList::new(DockerCommand::gen_vec(state)); docker_controls.start(); Self { diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index d21c232..f355f2b 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -143,10 +143,10 @@ impl AppData { Self { args, containers: StatefulList::new(vec![]), - hidden_containers: vec![], error: None, - sorted_by: None, filter: Filter::new(), + hidden_containers: vec![], + sorted_by: None, } } @@ -160,15 +160,9 @@ impl AppData { } /// Filter related methods - - /// Get the current filter term - pub const fn get_filter_term(&self) -> Option<&String> { - self.filter.term.as_ref() - } - - /// Get the current filter by choice - pub const fn get_filter_by(&self) -> FilterBy { - self.filter.by + /// Get the filterby and filter_term + pub const fn get_filter(&self) -> (FilterBy, Option<&String>) { + (self.filter.by, self.filter.term.as_ref()) } /// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by @@ -252,7 +246,7 @@ impl AppData { self.filter_containers(); } - // change the filter_by option + /// change the filter_by option pub fn filter_by_next(&mut self) { if let Some(by) = self.filter.by.next() { self.filter.by = by; @@ -260,7 +254,7 @@ impl AppData { } } - // change the filter_by option + /// change the filter_by option pub fn filter_by_prev(&mut self) { if let Some(by) = self.filter.by.prev() { self.filter.by = by; @@ -280,7 +274,6 @@ impl AppData { } /// Container sort related methods - /// Change the sorted order, also set the selected container state to match new order fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { self.sorted_by = x; @@ -350,7 +343,6 @@ impl AppData { .back() .cmp(&item_ord.1.mem_stats.back()) .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Id => item_ord .0 .id @@ -372,7 +364,6 @@ impl AppData { .tx .cmp(&item_ord.1.tx) .then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())), - Header::Name => item_ord .0 .name @@ -392,19 +383,26 @@ impl AppData { } /// Container state methods - /// Get the total number of none "hidden" containers pub fn get_container_len(&self) -> usize { self.containers.items.len() } + pub fn get_all_id_state(&self) -> Vec<(State, ContainerId)> { + self.containers + .items + .iter() + .map(|i| (i.state, i.id.clone())) + .collect::>() + } + /// Get all the ContainerItems pub fn get_container_items(&self) -> &[ContainerItem] { &self.containers.items } /// Get title for containers section, add a suffix indicating if the containers are currently under filter - pub fn container_title(&self) -> String { + pub fn get_container_title(&self) -> String { let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() { " - filtered" } else { @@ -447,43 +445,41 @@ impl AppData { } /// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public) + ///display like this: "│ ip, private, public│", so (5,10,9) are the minimum lengths required pub fn get_longest_port(&self) -> (usize, usize, usize) { - let mut longest_ip = 5; - let mut longest_private = 10; - let mut longest_public = 9; + let mut output = (5, 10, 9); for item in [&self.containers.items, &self.hidden_containers] { for item in item { - longest_ip = longest_ip.max( + output.0 = output.0.max( item.ports .iter() .map(ContainerPorts::len_ip) .max() - .unwrap_or(3), + .unwrap_or(output.0), ); - longest_private = longest_private.max( + output.1 = output.1.max( item.ports .iter() .map(ContainerPorts::len_private) .max() - .unwrap_or(8), + .unwrap_or(output.1), ); - longest_public = longest_public.max( + output.2 = output.2.max( item.ports .iter() .map(ContainerPorts::len_public) .max() - .unwrap_or(6), + .unwrap_or(output.2), ); } } - - (longest_ip, longest_private, longest_public) + output } /// 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() { + pub fn get_selected_ports(&self) -> Option<(Vec, State)> { + if let Some(item) = self.get_selected_container() { let mut ports = item.ports.clone(); ports.sort_by(|a, b| a.private.cmp(&b.private)); return Some((ports, item.state)); @@ -510,12 +506,12 @@ impl AppData { } /// Get the ContainerName of by ID - pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { + pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<&ContainerName> { self.containers .items .iter_mut() .find(|i| &i.id == id) - .map(|i| i.name.clone()) + .map(|i| &i.name) } /// Find the id of the currently selected container. @@ -532,10 +528,9 @@ impl AppData { } /// Selected DockerCommand methods - /// Get the current selected docker command /// So know which command to execute - pub fn selected_docker_controls(&self) -> Option { + pub fn selected_docker_controls(&self) -> Option { self.get_selected_container().and_then(|i| { i.docker_controls.state.selected().and_then(|x| { i.docker_controls @@ -574,21 +569,19 @@ impl AppData { } } - /// Get mutable Option of the currently selected container DockerControls state + /// Get mutable Option of the currently selected container DockerCommand state pub fn get_control_state(&mut self) -> Option<&mut ListState> { self.get_mut_selected_container() .map(|i| &mut i.docker_controls.state) } - /// Get mutable Option of the currently selected container DockerControls items - /// TODO command or control, need a uniform name across the application - pub fn get_control_items(&mut self) -> Option<&mut Vec> { + /// Get mutable Option of the currently selected container DockerConmand items + pub fn get_control_items(&mut self) -> Option<&mut Vec> { self.get_mut_selected_container() .map(|i| &mut i.docker_controls.items) } /// Logs related methods - /// Get the title for log panel for selected container, will be either /// 1) "logs x/x - container_name - container_image" /// 2) "logs - container_name - container_image" when no logs found @@ -635,11 +628,11 @@ impl AppData { } /// Get mutable Vec of current containers logs - pub fn get_logs(&mut self) -> Vec> { + pub fn get_logs(&self) -> Vec> { self.containers .state .selected() - .and_then(|i| self.containers.items.get_mut(i)) + .and_then(|i| self.containers.items.get(i)) .map_or(vec![], |i| i.logs.to_vec()) } @@ -653,18 +646,16 @@ impl AppData { } /// Chart data related methods - /// Get mutable Option of the currently selected container chart data - pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> { + pub fn get_chart_data(&self) -> Option<(CpuTuple, MemTuple)> { self.containers .state .selected() - .and_then(|i| self.containers.items.get_mut(i)) - .map(|i| i.get_chart_data()) + .and_then(|i| self.containers.items.get(i)) + .map(container_state::ContainerItem::get_chart_data) } /// Error related methods - /// Get single app_state error pub const fn get_error(&self) -> Option { self.error @@ -701,7 +692,6 @@ impl AppData { 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, &self.hidden_containers] { for container in container { let cpu_count = container.cpu_stats.back().map_or_else( @@ -729,7 +719,6 @@ impl AppData { } /// 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() { @@ -769,12 +758,11 @@ impl AppData { container.tx.update(tx); container.mem_limit.update(mem_limit); } - // need to benchmark this? self.sort_containers(); } /// Update, or insert, containers - pub fn update_containers(&mut self, all_containers: &mut [ContainerSummary]) { + pub fn update_containers(&mut self, mut all_containers: Vec) { let all_ids = self .containers .items @@ -809,7 +797,7 @@ impl AppData { } } - for i in all_containers { + for mut i in all_containers { if let Some(id) = i.id.as_ref() { let name = i.names.as_mut().map_or(String::new(), |names| { names.first_mut().map_or(String::new(), |f| { @@ -820,8 +808,8 @@ impl AppData { }) }); - let ports = i.ports.as_ref().map_or(vec![], |i| { - i.iter().map(ContainerPorts::from).collect::>() + let ports = i.ports.map_or(vec![], |i| { + i.into_iter().map(ContainerPorts::from).collect::>() }); let id = ContainerId::from(id.as_str()); @@ -855,7 +843,7 @@ impl AppData { item.status = status; }; if item.state != state { - item.docker_controls.items = DockerControls::gen_vec(state); + item.docker_controls.items = DockerCommand::gen_vec(state); // Update the list state, needs to be None if the gen_vec returns an empty vec match state { State::Removing | State::Restarting | State::Unknown => { @@ -1476,7 +1464,7 @@ mod tests { let mut app_data = gen_appdata(&containers); let result = app_data.get_container_name_by_id(&ContainerId::from("2")); - assert_eq!(result, Some(ContainerName::from("container_2"))); + assert_eq!(result, Some(&ContainerName::from("container_2"))); } #[test] @@ -1526,7 +1514,7 @@ mod tests { app_data.docker_controls_start(); let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerControls::Pause)); + assert_eq!(result, Some(DockerCommand::Pause)); } #[test] @@ -1539,7 +1527,7 @@ mod tests { app_data.docker_controls_next(); let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerControls::Restart)); + assert_eq!(result, Some(DockerCommand::Restart)); } #[test] @@ -1551,12 +1539,12 @@ mod tests { app_data.docker_controls_end(); let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerControls::Delete)); + assert_eq!(result, Some(DockerCommand::Delete)); // Next has no effect when at end app_data.docker_controls_next(); let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerControls::Delete)); + assert_eq!(result, Some(DockerCommand::Delete)); } #[test] @@ -1569,19 +1557,19 @@ mod tests { app_data.docker_controls_previous(); let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerControls::Stop)); + assert_eq!(result, Some(DockerCommand::Stop)); // previous has no effect when at start app_data.docker_controls_start(); app_data.docker_controls_previous(); let result = app_data.selected_docker_controls(); - assert_eq!(result, Some(DockerControls::Pause)); + assert_eq!(result, Some(DockerCommand::Pause)); } #[test] /// DockerCommands get correct controls dependant on container state fn test_app_data_get_control_items() { - let test_state = |state: State, expected: &mut Vec| { + let test_state = |state: State, expected: &mut Vec| { let gen_item_state = |state: State| { ContainerItem::new( 1, @@ -1605,42 +1593,42 @@ mod tests { test_state( State::Dead, &mut vec![ - DockerControls::Start, - DockerControls::Restart, - DockerControls::Delete, + DockerCommand::Start, + DockerCommand::Restart, + DockerCommand::Delete, ], ); test_state( State::Exited, &mut vec![ - DockerControls::Start, - DockerControls::Restart, - DockerControls::Delete, + DockerCommand::Start, + DockerCommand::Restart, + DockerCommand::Delete, ], ); test_state( State::Paused, &mut vec![ - DockerControls::Resume, - DockerControls::Stop, - DockerControls::Delete, + DockerCommand::Resume, + DockerCommand::Stop, + DockerCommand::Delete, ], ); - test_state(State::Removing, &mut vec![DockerControls::Delete]); + test_state(State::Removing, &mut vec![DockerCommand::Delete]); test_state( State::Restarting, - &mut vec![DockerControls::Stop, DockerControls::Delete], + &mut vec![DockerCommand::Stop, DockerCommand::Delete], ); test_state( State::Running(RunningState::Healthy), &mut vec![ - DockerControls::Pause, - DockerControls::Restart, - DockerControls::Stop, - DockerControls::Delete, + DockerCommand::Pause, + DockerCommand::Restart, + DockerCommand::Stop, + DockerCommand::Delete, ], ); - test_state(State::Unknown, &mut vec![DockerControls::Delete]); + test_state(State::Unknown, &mut vec![DockerCommand::Delete]); } // ****** // @@ -1654,13 +1642,13 @@ mod tests { let mut app_data = gen_appdata(&containers); - assert!(app_data.get_filter_term().is_none()); + assert!(app_data.get_filter().1.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())); + assert_eq!(app_data.get_filter().1, Some(&"_2".to_string())); app_data.filter_containers(); let post_len = app_data.containers.items.len(); @@ -1680,7 +1668,7 @@ mod tests { let mut app_data = gen_appdata(&containers); - assert!(app_data.get_filter_term().is_none()); + assert!(app_data.get_filter().1.is_none()); let pre_len = app_data.containers.items.len(); for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] { @@ -1689,8 +1677,10 @@ mod tests { // 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())); + assert_eq!( + app_data.get_filter(), + (FilterBy::Image, Some(&"image_2".to_string())) + ); app_data.filter_containers(); let post_len = app_data.containers.items.len(); @@ -1709,7 +1699,7 @@ mod tests { 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()); + assert!(app_data.get_filter().1.is_none()); let pre_len = app_data.containers.items.len(); app_data.filter_term_push('x'); @@ -1717,8 +1707,10 @@ mod tests { 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())); + assert_eq!( + app_data.get_filter(), + (FilterBy::Status, Some(&"x".to_string())) + ); app_data.filter_containers(); let post_len = app_data.containers.items.len(); @@ -1737,7 +1729,7 @@ mod tests { 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()); + assert!(app_data.get_filter().1.is_none()); let pre_len = app_data.containers.items.len(); app_data.filter_term_push('x'); @@ -1746,8 +1738,10 @@ mod tests { 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())); + assert_eq!( + app_data.get_filter(), + (FilterBy::All, Some(&"x".to_string())) + ); app_data.filter_containers(); let post_len = app_data.containers.items.len(); @@ -1766,7 +1760,7 @@ mod tests { 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()); + assert!(app_data.get_filter().1.is_none()); let pre_len = app_data.containers.items.len(); app_data.filter_term_push('x'); @@ -1774,8 +1768,10 @@ mod tests { 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())); + assert_eq!( + app_data.get_filter(), + (FilterBy::Status, Some(&"x".to_string())) + ); app_data.filter_containers(); let post_len = app_data.containers.items.len(); @@ -1787,8 +1783,10 @@ mod tests { 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())); + assert_eq!( + app_data.get_filter(), + (FilterBy::Image, Some(&"x".to_string())) + ); app_data.filter_containers(); let post_len = app_data.containers.items.len(); @@ -2230,12 +2228,12 @@ mod tests { let (_ids, containers) = gen_containers(); let mut app_data = gen_appdata(&containers); let result_pre = app_data.get_container_items().to_owned(); - let mut input = [ + let input = vec![ gen_container_summary(1, "paused"), gen_container_summary(2, "dead"), ]; - app_data.update_containers(&mut input); + app_data.update_containers(input); let result_post = app_data.get_container_items().to_owned(); assert_ne!(result_pre, result_post); assert_eq!(result_post[0].state, State::Paused); diff --git a/src/app_error.rs b/src/app_error.rs index ba0d66f..e192ad8 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -1,16 +1,13 @@ -use crate::app_data::DockerControls; +use crate::app_data::DockerCommand; use std::fmt; /// app errors to set in global state -#[allow(unused)] #[derive(Debug, Clone, Copy)] pub enum AppError { - DockerCommand(DockerControls), + DockerCommand(DockerCommand), DockerExec, DockerLogs, DockerConnect, - DockerInterval, - InputPoll, MouseCapture(bool), Terminal, } @@ -23,8 +20,6 @@ impl fmt::Display for AppError { Self::DockerExec => write!(f, "Unable to exec into container"), Self::DockerLogs => write!(f, "Unable to save logs"), Self::DockerConnect => write!(f, "Unable to access docker daemon"), - Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"), - Self::InputPoll => write!(f, "Unable to poll user input"), Self::MouseCapture(x) => { let reason = if *x { "en" } else { "dis" }; write!(f, "Unable to {reason}able mouse capture") diff --git a/src/docker_data/message.rs b/src/docker_data/message.rs index 866aada..b0af01a 100644 --- a/src/docker_data/message.rs +++ b/src/docker_data/message.rs @@ -1,19 +1,13 @@ use std::sync::Arc; -use crate::app_data::ContainerId; +use crate::app_data::{ContainerId, DockerCommand}; use bollard::Docker; use tokio::sync::oneshot::Sender; #[derive(Debug)] pub enum DockerMessage { ConfirmDelete(ContainerId), - Delete(ContainerId), + Control((DockerCommand, ContainerId)), Exec(Sender>), - Pause(ContainerId), - Quit, - Restart(ContainerId), - Start(ContainerId), - Stop(ContainerId), - Resume(ContainerId), Update, } diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index b97102c..b288af3 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -10,10 +10,7 @@ use futures_util::StreamExt; use parking_lot::Mutex; use std::{ collections::HashMap, - sync::{ - atomic::{AtomicBool, AtomicUsize}, - Arc, - }, + sync::{atomic::AtomicUsize, Arc}, }; use tokio::{ sync::mpsc::{Receiver, Sender}, @@ -22,7 +19,7 @@ use tokio::{ use uuid::Uuid; use crate::{ - app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State}, + app_data::{AppData, ContainerId, DockerCommand, State}, app_error::AppError, parse_args::CliArgs, ui::{GuiState, Status}, @@ -37,6 +34,15 @@ enum SpawnId { Log(ContainerId), } +impl SpawnId { + /// Extract the &ContainerId out of self + const fn get_id(&self) -> &ContainerId { + match self { + Self::Log(id) | Self::Stats((id, _)) => id, + } + } +} + /// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time /// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap /// Binate value is toggled when all handles have been spawned off @@ -62,8 +68,6 @@ pub struct DockerData { binate: Binate, docker: Arc, gui_state: Arc>, - is_running: Arc, - init: Option>, receiver: Receiver, spawns: Arc>>>, } @@ -71,24 +75,30 @@ pub struct DockerData { impl DockerData { /// Use docker stats to calculate current cpu usage #[allow(clippy::cast_precision_loss)] - // TODO FIX: this can overflow fn calculate_usage(stats: &Stats) -> f64 { let mut cpu_percentage = 0.0; - let previous_cpu = stats.precpu_stats.cpu_usage.total_usage; - let cpu_delta = stats.cpu_stats.cpu_usage.total_usage as f64 - previous_cpu as f64; + let cpu_delta = stats + .cpu_stats + .cpu_usage + .total_usage + .saturating_sub(stats.precpu_stats.cpu_usage.total_usage) + as f64; if let (Some(cpu_stats_usage), Some(precpu_stats_usage)) = ( stats.cpu_stats.system_cpu_usage, stats.precpu_stats.system_cpu_usage, ) { - let system_delta = (cpu_stats_usage - precpu_stats_usage) as f64; + let system_delta = cpu_stats_usage.saturating_sub(precpu_stats_usage) as f64; let online_cpus = stats.cpu_stats.online_cpus.unwrap_or_else(|| { - stats - .cpu_stats - .cpu_usage - .percpu_usage - .as_ref() - .map_or(0, std::vec::Vec::len) as u64 + u64::try_from( + stats + .cpu_stats + .cpu_usage + .percpu_usage + .as_ref() + .map_or(0, std::vec::Vec::len), + ) + .unwrap_or_default() }) as f64; if system_delta > 0.0 && cpu_delta > 0.0 { cpu_percentage = (cpu_delta / system_delta) * online_cpus * 100.0; @@ -103,97 +113,86 @@ impl DockerData { async fn update_container_stat( app_data: Arc>, docker: Arc, - id: ContainerId, - init: Option<(Arc, usize)>, state: State, spawn_id: SpawnId, spawns: Arc>>>, ) { - if state.is_alive() || init.is_some() { - let mut stream = docker - .stats( - id.get(), - Some(StatsOptions { - stream: false, - one_shot: false, - }), - ) - .take(1); + let id = spawn_id.get_id(); + let mut stream = docker + .stats( + id.get(), + Some(StatsOptions { + stream: false, + one_shot: false, + }), + ) + .take(1); - while let Some(Ok(stats)) = stream.next().await { - // Memory stats are only collected if the container is alive - is this the behaviour we want? - let mem_stat = if state.is_alive() { - let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i { - MemoryStatsStats::V1(x) => x.inactive_file, - MemoryStatsStats::V2(x) => x.inactive_file, - }); + while let Some(Ok(stats)) = stream.next().await { + // Memory stats are only collected if the container is alive - is this the behaviour we want? + let (mem_stat, cpu_stats) = if state.is_alive() { + let mem_cache = stats.memory_stats.stats.map_or(0, |i| match i { + MemoryStatsStats::V1(x) => x.inactive_file, + MemoryStatsStats::V2(x) => x.inactive_file, + }); + ( Some( stats .memory_stats .usage .unwrap_or_default() .saturating_sub(mem_cache), - ) - } else { - None - }; + ), + Some(Self::calculate_usage(&stats)), + ) + } else { + (None, None) + }; - let mem_limit = stats.memory_stats.limit.unwrap_or_default(); + let op_key = stats + .networks + .as_ref() + .and_then(|networks| networks.keys().next().cloned()); - let op_key = stats + let (rx, tx) = if let Some(key) = op_key { + stats .networks - .as_ref() - .and_then(|networks| networks.keys().next().cloned()); + .unwrap_or_default() + .get(&key) + .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes)) + } else { + (0, 0) + }; - let cpu_stats = if state.is_alive() { - Some(Self::calculate_usage(&stats)) - } else { - None - }; - let (rx, tx) = if let Some(key) = op_key { - stats - .networks - .unwrap_or_default() - .get(&key) - .map_or((0, 0), |f| (f.rx_bytes, f.tx_bytes)) - } else { - (0, 0) - }; - - app_data - .lock() - .update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx); - } + app_data.lock().update_stats_by_id( + id, + cpu_stats, + mem_stat, + stats.memory_stats.limit.unwrap_or_default(), + rx, + tx, + ); } spawns.lock().remove(&spawn_id); - if let Some((target, _)) = init { - target.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } } /// Update all stats, spawn each container into own tokio::spawn thread - fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { + fn update_all_container_stats(&mut self) { + let all_ids = self.app_data.lock().get_all_id_state(); for (state, id) in all_ids { - let docker = Arc::clone(&self.docker); - let app_data = Arc::clone(&self.app_data); - let spawns = Arc::clone(&self.spawns); - let spawn_id = SpawnId::Stats((id.clone(), self.binate)); + let spawn_id = SpawnId::Stats((id, self.binate)); - let init = self.init.as_ref().map(|i| (Arc::clone(i), all_ids.len())); - self.spawns - .lock() - .entry(spawn_id.clone()) - .or_insert_with(|| { - tokio::spawn(Self::update_container_stat( - app_data, - docker, - id.clone(), - init, - *state, - spawn_id, - spawns, - )) - }); + if let std::collections::hash_map::Entry::Vacant(spawns) = + self.spawns.lock().entry(spawn_id.clone()) + { + spawns.insert(tokio::spawn(Self::update_container_stat( + Arc::clone(&self.app_data), + Arc::clone(&self.docker), + state, + spawn_id, + Arc::clone(&self.spawns), + ))); + } } self.binate = self.binate.toggle(); } @@ -201,7 +200,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(&self) -> Vec<(State, ContainerId)> { + async fn update_all_containers(&self) { let containers = self .docker .list_containers(Some(ListContainersOptions:: { @@ -211,7 +210,7 @@ impl DockerData { .await .unwrap_or_default(); - let mut output = containers + let output = containers .into_iter() .filter_map(|f| match f.id { Some(_) => { @@ -230,23 +229,7 @@ impl DockerData { }) .collect::>(); - self.app_data.lock().update_containers(&mut output); - - // Just get the containers that are currently running, or being restarted, no point updating info on paused or dead containers - output - .into_iter() - .filter_map(|i| { - 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::>() + self.app_data.lock().update_containers(output); } /// Update single container logs @@ -257,10 +240,11 @@ impl DockerData { id: ContainerId, since: u64, spawns: Arc>>>, + stderr: bool, ) { let options = Some(LogsOptions:: { stdout: true, - stderr: true, + stderr, timestamps: true, since: i64::try_from(since).unwrap_or_default(), ..Default::default() @@ -275,44 +259,29 @@ impl DockerData { output.push(data); } } - spawns.lock().remove(&SpawnId::Log(id.clone())); app_data.lock().update_log_by_id(output, &id); + spawns.lock().remove(&SpawnId::Log(id)); } /// Update all logs, spawn each container into own tokio::spawn thread - fn init_all_logs(&self, all_ids: &[(State, ContainerId)]) { + fn init_all_logs(&self, all_ids: Vec<(State, ContainerId)>) -> Arc { + let init = Arc::new(AtomicUsize::new(0)); for (_, id) in all_ids { + let app_data: Arc> = + Arc::clone(&self.app_data); let docker = Arc::clone(&self.docker); - let app_data = Arc::clone(&self.app_data); let spawns = Arc::clone(&self.spawns); - let key = SpawnId::Log(id.clone()); + let std_err = self.args.std_err; + let init = Arc::clone(&init); self.spawns.lock().insert( - key, - tokio::spawn(Self::update_log(app_data, docker, id.clone(), 0, spawns)), + SpawnId::Log(id.clone()), + tokio::spawn(async move { + Self::update_log(app_data, docker, id, 0, spawns, std_err).await; + init.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + }), ); } - } - - /// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed) - async fn update_everything(&mut self) { - let all_ids = self.update_all_containers().await; - if let Some(container) = self.app_data.lock().get_selected_container() { - let last_updated = container.last_updated; - self.spawns - .lock() - .entry(SpawnId::Log(container.id.clone())) - .or_insert_with(|| { - // MAYBE make a struct that can create this data? - let app_data = Arc::clone(&self.app_data); - let docker = Arc::clone(&self.docker); - let id = container.id.clone(); - let spawns = Arc::clone(&self.spawns); - tokio::spawn(Self::update_log(app_data, docker, id, last_updated, spawns)) - }); - }; - self.update_all_container_stats(&all_ids); - self.app_data.lock().sort_containers(); - self.gui_state.lock().stop_loading_animation(Uuid::nil()); + init } /// Initialize docker container data, before any messages are received @@ -320,27 +289,48 @@ impl DockerData { self.gui_state.lock().status_push(Status::Init); let loading_uuid = Uuid::new_v4(); GuiState::start_loading_animation(&self.gui_state, loading_uuid); - let all_ids = self.update_all_containers().await; + self.update_all_containers().await; + let all_ids = self.app_data.lock().get_all_id_state(); + let all_ids_len = all_ids.len(); + let init = self.init_all_logs(all_ids); + self.update_all_container_stats(); - self.update_all_container_stats(&all_ids); - - self.init_all_logs(&all_ids); - - while let Some(x) = self.init.as_ref() { + while init.load(std::sync::atomic::Ordering::SeqCst) != all_ids_len { self.app_data.lock().sort_containers(); - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - if x.load(std::sync::atomic::Ordering::SeqCst) == all_ids.len() { - self.init = None; - } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; } self.gui_state.lock().stop_loading_animation(loading_uuid); self.gui_state.lock().status_del(Status::Init); } + /// Update all cpu_mem, and selected container log (if a log update join_handle isn't currently being executed) + async fn update_everything(&mut self) { + self.update_all_containers().await; + if let Some(container) = self.app_data.lock().get_selected_container() { + let last_updated = container.last_updated; + let spawn_id = SpawnId::Log(container.id.clone()); + // Only spawn if not already spawned with a given id/binate pair + if let std::collections::hash_map::Entry::Vacant(spawns) = + self.spawns.lock().entry(spawn_id) + { + spawns.insert(tokio::spawn(Self::update_log( + Arc::clone(&self.app_data), + Arc::clone(&self.docker), + container.id.clone(), + last_updated, + Arc::clone(&self.spawns), + self.args.std_err, + ))); + } + }; + self.update_all_container_stats(); + self.app_data.lock().sort_containers(); + } + /// Set the global error as the docker error, and set gui_state to error fn set_error( app_data: &Arc>, - error: DockerControls, + error: DockerCommand, gui_state: &Arc>, ) { app_data @@ -348,150 +338,102 @@ impl DockerData { .set_error(AppError::DockerCommand(error), gui_state, Status::Error); } + /// Execute docker commands (start, stop etc) on it's own tokio thread + async fn execute_command(&mut self, control: DockerCommand, id: ContainerId) { + let (app_data, docker, gui_state) = ( + Arc::clone(&self.app_data), + Arc::clone(&self.docker), + Arc::clone(&self.gui_state), + ); + tokio::spawn(async move { + let uuid = Uuid::new_v4(); + GuiState::start_loading_animation(&gui_state, uuid); + if match control { + DockerCommand::Delete => { + docker + .remove_container( + id.get(), + Some(RemoveContainerOptions { + v: false, + force: true, + link: false, + }), + ) + .await + } + DockerCommand::Pause => docker.pause_container(id.get()).await, + DockerCommand::Restart => docker.restart_container(id.get(), None).await, + DockerCommand::Resume => docker.unpause_container(id.get()).await, + DockerCommand::Start => { + docker + .start_container(id.get(), None::>) + .await + } + DockerCommand::Stop => docker.stop_container(id.get(), None).await, + } + .is_err() + { + Self::set_error(&app_data, control, &gui_state); + } + gui_state.lock().stop_loading_animation(uuid); + }); + + self.update_everything().await; + } + /// Handle incoming messages, container controls & all container information update /// Spawn Docker commands off into own thread - #[allow(clippy::too_many_lines)] async fn message_handler(&mut self) { while let Some(message) = self.receiver.recv().await { - let docker = Arc::clone(&self.docker); - let gui_state = Arc::clone(&self.gui_state); - let app_data = Arc::clone(&self.app_data); - let uuid = Uuid::new_v4(); - // TODO need to refactor these match message { - DockerMessage::Exec(docker_tx) => { - docker_tx.send(Arc::clone(&self.docker)).ok(); - } - DockerMessage::Pause(id) => { - tokio::spawn(async move { - 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(uuid); - }); - self.update_everything().await; - } - DockerMessage::Restart(id) => { - tokio::spawn(async move { - 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(uuid); - }); - self.update_everything().await; - } - DockerMessage::Start(id) => { - tokio::spawn(async move { - GuiState::start_loading_animation(&gui_state, uuid); - if docker - .start_container(id.get(), None::>) - .await - .is_err() - { - Self::set_error(&app_data, DockerControls::Start, &gui_state); - } - gui_state.lock().stop_loading_animation(uuid); - }); - self.update_everything().await; - } - DockerMessage::Stop(id) => { - tokio::spawn(async move { - 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(uuid); - }); - self.update_everything().await; - } - DockerMessage::Resume(id) => { - tokio::spawn(async move { - 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(uuid); - }); - self.update_everything().await; - } - DockerMessage::Delete(id) => { - tokio::spawn(async move { - GuiState::start_loading_animation(&gui_state, uuid); - if docker - .remove_container( - id.get(), - Some(RemoveContainerOptions { - v: false, - force: true, - link: false, - }), - ) - .await - .is_err() - { - Self::set_error(&app_data, DockerControls::Stop, &gui_state); - } - gui_state.lock().stop_loading_animation(uuid); - }); - self.update_everything().await; - self.gui_state.lock().set_delete_container(None); - } DockerMessage::ConfirmDelete(id) => { self.gui_state.lock().set_delete_container(Some(id)); } - DockerMessage::Update => self.update_everything().await, - DockerMessage::Quit => { - self.spawns - .lock() - .values() - .for_each(tokio::task::JoinHandle::abort); - self.is_running - .store(false, std::sync::atomic::Ordering::SeqCst); + DockerMessage::Control((command, id)) => self.execute_command(command, id).await, + DockerMessage::Exec(docker_tx) => { + docker_tx.send(Arc::clone(&self.docker)).ok(); } + DockerMessage::Update => self.update_everything().await, } } } /// Send an update message every x ms, where x is the args.docker_interval - fn scheduler(args: &CliArgs, docker_tx: Sender) { + fn heartbeat(args: &CliArgs, docker_tx: Sender) { let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval)); let mut now = std::time::Instant::now(); tokio::spawn(async move { loop { - let to_sleep = update_duration.saturating_sub(now.elapsed()); - tokio::time::sleep(to_sleep).await; docker_tx.send(DockerMessage::Update).await.ok(); + if let Some(to_sleep) = update_duration.checked_sub(now.elapsed()) { + tokio::time::sleep(to_sleep).await; + } now = std::time::Instant::now(); } }); } /// Initialise self, and start the message receiving loop - pub async fn init( + pub async fn start( app_data: Arc>, docker: Docker, docker_rx: Receiver, docker_tx: Sender, gui_state: Arc>, - is_running: Arc, ) { let args = app_data.lock().args.clone(); if app_data.lock().get_error().is_none() { let mut inner = Self { app_data, - args: args.clone(), + args, binate: Binate::One, docker: Arc::new(docker), gui_state, - init: Some(Arc::new(AtomicUsize::new(0))), - is_running, receiver: docker_rx, spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; - Self::scheduler(&args, docker_tx); + Self::heartbeat(&inner.args, docker_tx); inner.message_handler().await; } } @@ -499,19 +441,19 @@ impl DockerData { // tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers #[cfg(test)] +#[allow(clippy::float_cmp)] mod tests { use bollard::container::{ - BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData, + BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, Stats, StorageStats, ThrottlingData, }; use super::*; - #[allow(clippy::too_many_lines)] - fn gen_stats(x: u64, y: u64) -> Stats { + fn gen_stats() -> Stats { Stats { read: String::new(), preread: String::new(), - num_procs: 0, + num_procs: 1, pids_stats: PidsStats { current: None, limit: None, @@ -542,33 +484,12 @@ mod tests { }, cpu_stats: CPUStats { cpu_usage: CPUUsage { - percpu_usage: Some(vec![ - 291_593_800, - 182_192_900, - 195_048_700, - 23_032_300, - 132_928_700, - 235_555_600, - 120_225_700, - 175_752_000, - 213_060_300, - 95_321_600, - 226_821_000, - 0, - 109_151_300, - 0, - 86_240_200, - 1_884_400, - 59_077_300, - 23_224_900, - 95_386_300, - 144_987_400, - ]), - total_usage: 250_000_000, - usage_in_usermode: 1_020_000_000, - usage_in_kernelmode: 1_030_000_000, + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, }, - system_cpu_usage: Some(x), + system_cpu_usage: Some(400), online_cpus: Some(1), throttling_data: ThrottlingData { periods: 0, @@ -578,33 +499,12 @@ mod tests { }, precpu_stats: CPUStats { cpu_usage: CPUUsage { - percpu_usage: Some(vec![ - 291_593_800, - 182_192_900, - 195_048_700, - 23_032_300, - 132_928_700, - 235_555_600, - 120_225_700, - 175_752_000, - 213_060_300, - 95_321_600, - 226_821_000, - 0, - 109_151_300, - 0, - 86_240_200, - 1_884_400, - 59_077_300, - 23_224_900, - 93_831_100, - 144_987_400, - ]), - total_usage: 200_000_000, - usage_in_usermode: 1_020_000_000, - usage_in_kernelmode: 1_020_000_000, + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, }, - system_cpu_usage: Some(y), + system_cpu_usage: Some(400), online_cpus: Some(1), throttling_data: ThrottlingData { periods: 0, @@ -618,25 +518,198 @@ mod tests { write_count_normalized: None, write_size_bytes: None, }, - name: "/container_1".to_owned(), - id: "1".to_owned(), + name: String::new(), + id: String::new(), } } #[test] - #[allow(clippy::float_cmp)] - /// Test the stats calculator, had to cheat here to get round input/outputs - fn test_calculate_usage_no_previous_cpu() { - let stats = gen_stats(1_000_000_000, 900_000_000); - let result = DockerData::calculate_usage(&stats); - assert_eq!(result, 50.0); + fn test_calculate_usage_50() { + let mut stats = gen_stats(); + stats.precpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, + }, + system_cpu_usage: Some(400), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + stats.cpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![150]), + usage_in_usermode: 20, + total_usage: 150, + usage_in_kernelmode: 30, + }, + system_cpu_usage: Some(500), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + let cpu_percentage = DockerData::calculate_usage(&stats); + assert_eq!(50.0, cpu_percentage); + } - let stats = gen_stats(1_000_000_000, 800_000_000); - let result = DockerData::calculate_usage(&stats); - assert_eq!(result, 25.0); + #[test] + fn test_calculate_usage_25() { + let mut stats = gen_stats(); + stats.precpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, + }, + system_cpu_usage: Some(400), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + stats.cpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![75]), + usage_in_usermode: 20, + total_usage: 125, + usage_in_kernelmode: 30, + }, + system_cpu_usage: Some(500), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; - let stats = gen_stats(1_000_000_000, 750_000_000); - let result = DockerData::calculate_usage(&stats); - assert_eq!(result, 20.00); + let cpu_percentage = DockerData::calculate_usage(&stats); + assert_eq!(25.0, cpu_percentage); + } + + #[test] + fn test_calculate_usage_75() { + let mut stats = gen_stats(); + stats.precpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, + }, + system_cpu_usage: Some(400), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + + stats.cpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![175]), + usage_in_usermode: 20, + total_usage: 175, + usage_in_kernelmode: 30, + }, + system_cpu_usage: Some(500), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + + let cpu_percentage = DockerData::calculate_usage(&stats); + assert_eq!(75.0, cpu_percentage); + } + + #[test] + fn test_calculate_usage_100() { + let mut stats = gen_stats(); + stats.precpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, + }, + system_cpu_usage: Some(400), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + stats.cpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![200]), + usage_in_usermode: 20, + total_usage: 200, + usage_in_kernelmode: 30, + }, + system_cpu_usage: Some(500), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + let cpu_percentage = DockerData::calculate_usage(&stats); + assert_eq!(100.0, cpu_percentage); + } + + #[test] + fn test_calculate_usage_175() { + let mut stats = gen_stats(); + stats.precpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![50]), + usage_in_usermode: 10, + total_usage: 100, + usage_in_kernelmode: 20, + }, + system_cpu_usage: Some(400), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + + stats.cpu_stats = CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![275]), + usage_in_usermode: 20, + total_usage: 275, + usage_in_kernelmode: 30, + }, + system_cpu_usage: Some(500), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }; + + let cpu_percentage = DockerData::calculate_usage(&stats); + assert_eq!(175.0, cpu_percentage); } } diff --git a/src/exec.rs b/src/exec.rs index 283a945..ba77737 100644 --- a/src/exec.rs +++ b/src/exec.rs @@ -144,9 +144,9 @@ impl TerminalSize { #[derive(Debug, Clone)] pub enum ExecMode { // use Bollard Rust library - Internal((ContainerId, Arc)), + Internal((Arc, Arc)), // use the external `docker-cli` - External(ContainerId), + External(Arc), } impl ExecMode { @@ -186,7 +186,10 @@ impl ExecMode { { if let Some(Ok(msg)) = output.next().await { if !msg.to_string().starts_with(OCI_ERROR) { - return Some(Self::Internal((id.clone(), Arc::clone(docker)))); + return Some(Self::Internal(( + Arc::new(id), + Arc::clone(docker), + ))); } } } @@ -199,7 +202,7 @@ impl ExecMode { { if let Ok(output) = String::from_utf8(output.stdout) { if !output.starts_with(OCI_ERROR) { - return Some(Self::External(id.clone())); + return Some(Self::External(Arc::new(id))); } } } @@ -302,9 +305,9 @@ impl ExecMode { Ok(()) } - // This is the fix for key pressed not being handled correctly on quit - // It writes a special message to the stdout, and then listens out for a valid response - // afterwhich it's assumes that we're completely done with TTY + /// This is the fix for key pressed not being handled correctly on quit + /// It writes a special message to the stdout, and then listens out for a valid response + /// afterwhich it's assumes that we're completely done with TTY fn internal_cleanup(&self) -> Result<(), AppError> { match self { Self::External(_) => Ok(()), diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 8228f59..1670410 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -1,14 +1,11 @@ use std::{ fs::OpenOptions, io::{BufWriter, Write}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::{atomic::AtomicBool, Arc}, time::SystemTime, }; -use bollard::{container::LogsOptions, Docker}; +use bollard::container::LogsOptions; use cansi::v3::categorise_text; use crossterm::{ event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, @@ -22,7 +19,7 @@ use uuid::Uuid; mod message; use crate::{ - app_data::{AppData, DockerControls, Header}, + app_data::{AppData, DockerCommand, Header}, app_error::AppError, docker_data::DockerMessage, exec::{tty_readable, ExecMode}, @@ -43,7 +40,7 @@ pub struct InputHandler { impl InputHandler { /// Initialize self, and running the message handling loop - pub async fn init( + pub async fn start( app_data: Arc>, rec: Receiver, docker_tx: Sender, @@ -58,35 +55,30 @@ impl InputHandler { rec, mouse_capture: true, }; - inner.start().await; + inner.message_handler().await; } /// check for incoming messages - async fn start(&mut self) { + async fn message_handler(&mut self) { while let Some(message) = self.rec.recv().await { match message { InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await, InputMessages::MouseEvent(mouse_event) => { - if !self.gui_state.lock().status_contains(&[ - Status::Error, - Status::Help, - Status::DeleteConfirm, - Status::Filter, - ]) { + let status = self.gui_state.lock().get_status(); + let contains = |s: Status| status.contains(&s); + + if !contains(Status::Error) + | !contains(Status::Help) + | !contains(Status::DeleteConfirm) + | !contains(Status::Filter) + { self.mouse_press(mouse_event); } - let delete_confirm = self - .gui_state - .lock() - .status_contains(&[Status::DeleteConfirm]); - if delete_confirm { + if contains(Status::DeleteConfirm) { self.button_intersect(mouse_event).await; } } } - if !self.is_running.load(Ordering::SeqCst) { - break; - } } } @@ -97,12 +89,10 @@ impl InputHandler { /// Send a quit message to docker, to abort all spawns, if an error is returned, set is_running to false here instead /// If gui_status is Error or Init, then just set the is_running to false immediately, for a quicker exit - async fn quit(&self) { - let error_init = self - .gui_state - .lock() - .status_contains(&[Status::Error, Status::Init]); - if error_init || self.docker_tx.send(DockerMessage::Quit).await.is_err() { + fn quit(&self) { + let status = self.gui_state.lock().get_status(); + let contains = |s: Status| status.contains(&s); + if !contains(Status::Error) | !contains(Status::Init) { self.is_running .store(false, std::sync::atomic::Ordering::SeqCst); } @@ -112,7 +102,10 @@ impl InputHandler { async fn confirm_delete(&self) { let id = self.gui_state.lock().get_delete_container(); if let Some(id) = id { - self.docker_tx.send(DockerMessage::Delete(id)).await.ok(); + self.docker_tx + .send(DockerMessage::Control((DockerCommand::Delete, id))) + .await + .ok(); } } @@ -127,7 +120,7 @@ impl InputHandler { if !is_oxker && tty_readable() { let uuid = Uuid::new_v4(); GuiState::start_loading_animation(&self.gui_state, uuid); - let (sx, rx) = tokio::sync::oneshot::channel::>(); + let (sx, rx) = tokio::sync::oneshot::channel(); self.docker_tx.send(DockerMessage::Exec(sx)).await.ok(); if let Ok(docker) = rx.await { @@ -150,118 +143,109 @@ impl InputHandler { /// Toggle the mouse capture (via input of the 'm' key) fn m_key(&mut self) { + let err = || { + self.app_data.lock().set_error( + AppError::MouseCapture(!self.mouse_capture), + &self.gui_state, + Status::Error, + ); + }; if self.mouse_capture { if execute!(std::io::stdout(), DisableMouseCapture).is_ok() { self.gui_state .lock() .set_info_box("✖ mouse capture disabled"); } else { - self.app_data.lock().set_error( - AppError::MouseCapture(false), - &self.gui_state, - Status::Error, - ); + err(); } } else if Ui::enable_mouse_capture().is_ok() { self.gui_state .lock() .set_info_box("✓ mouse capture enabled"); } else { - self.app_data.lock().set_error( - AppError::MouseCapture(true), - &self.gui_state, - Status::Error, - ); + err(); }; self.mouse_capture = !self.mouse_capture; } /// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file - async fn s_key(&self) { - /// This is the inner workings, *inlined* here to return a Result - async fn save_logs( - app_data: &Arc>, - gui_state: &Arc>, - docker_tx: &Sender, - ) -> Result<(), Box> { - let args = app_data.lock().args.clone(); - let container = app_data.lock().get_selected_container_id_state_name(); - if let Some((id, _, name)) = container { - if let Some(log_path) = args.save_dir { - let (sx, rx) = tokio::sync::oneshot::channel::>(); - docker_tx.send(DockerMessage::Exec(sx)).await?; + async fn save_logs(&self) -> Result<(), Box> { + let args = self.app_data.lock().args.clone(); + let container = self.app_data.lock().get_selected_container_id_state_name(); + if let Some((id, _, name)) = container { + if let Some(log_path) = args.save_dir { + let (sx, rx) = tokio::sync::oneshot::channel(); + self.docker_tx.send(DockerMessage::Exec(sx)).await?; - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .map_or(0, |i| i.as_secs()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_or(0, |i| i.as_secs()); - let path = log_path.join(format!("{name}_{now}.log")); + let path = log_path.join(format!("{name}_{now}.log")); - let docker = rx.await?; - let options = Some(LogsOptions:: { - stderr: true, - stdout: true, - timestamps: args.timestamp, - since: 0, - ..Default::default() - }); - let mut logs = docker.logs(id.get(), options); - let mut output = vec![]; + let options = Some(LogsOptions:: { + stderr: true, + stdout: true, + timestamps: args.timestamp, + since: 0, + ..Default::default() + }); + let mut logs = rx.await?.logs(id.get(), options); + let mut output = vec![]; - while let Some(Ok(value)) = logs.next().await { - let data = value.to_string(); - if !data.trim().is_empty() { - output.push( - categorise_text(&data) - .into_iter() - .map(|i| i.text) - .collect::(), - ); - } - } - if !output.is_empty() { - let mut stream = BufWriter::new( - OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(&path)?, + while let Some(Ok(value)) = logs.next().await { + let data = value.to_string(); + if !data.trim().is_empty() { + output.push( + categorise_text(&data) + .into_iter() + .map(|i| i.text) + .collect::(), ); - - for line in &output { - stream.write_all(line.as_bytes())?; - } - stream.flush()?; - - gui_state - .lock() - .set_info_box(&format!("saved to {}", path.display())); } } + if !output.is_empty() { + let mut stream = BufWriter::new( + OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&path)?, + ); + + for line in &output { + stream.write_all(line.as_bytes())?; + } + stream.flush()?; + + self.gui_state + .lock() + .set_info_box(&format!("saved to {}", path.display())); + } } - Ok(()) } + Ok(()) + } - let log_status = Status::Logs; - let status = self.gui_state.lock().status_contains(&[log_status]); - if !status { - self.gui_state.lock().status_push(log_status); + /// Attempt to save the currently selected container logs to a file + async fn s_key(&self) { + let status = self.gui_state.lock().get_status(); + let contains = |s: Status| status.contains(&s); + if !contains(Status::Logs) { + self.gui_state.lock().status_push(Status::Logs); let uuid = Uuid::new_v4(); GuiState::start_loading_animation(&self.gui_state, uuid); - if save_logs(&self.app_data, &self.gui_state, &self.docker_tx) - .await - .is_err() - { + if self.save_logs().await.is_err() { self.app_data.lock().set_error( AppError::DockerLogs, &self.gui_state, Status::Error, ); } - self.gui_state.lock().status_del(log_status); + self.gui_state.lock().status_del(Status::Logs); self.gui_state.lock().stop_loading_animation(uuid); } } @@ -281,26 +265,17 @@ impl InputHandler { let option_id = self.app_data.lock().get_selected_container_id(); if let Some(id) = option_id { match command { - DockerControls::Delete => self + DockerCommand::Delete => self .docker_tx .send(DockerMessage::ConfirmDelete(id)) .await .ok(), - DockerControls::Pause => { - self.docker_tx.send(DockerMessage::Pause(id)).await.ok() - } - DockerControls::Resume => { - self.docker_tx.send(DockerMessage::Resume(id)).await.ok() - } - DockerControls::Start => { - self.docker_tx.send(DockerMessage::Start(id)).await.ok() - } - DockerControls::Stop => { - self.docker_tx.send(DockerMessage::Stop(id)).await.ok() - } - DockerControls::Restart => { - self.docker_tx.send(DockerMessage::Restart(id)).await.ok() - } + + _ => self + .docker_tx + .send(DockerMessage::Control((command, id))) + .await + .ok(), }; } } @@ -308,50 +283,43 @@ impl InputHandler { } /// Change the the "next" selectable panel + /// If no containers, and on Commands panel, skip to next panel, as Commands panel isn't visible in this state 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 { - 2 - } else { - 1 - }; - for _ in 0..count { + self.gui_state.lock().next_panel(); + if self.app_data.lock().get_container_len() == 0 + && self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands + { self.gui_state.lock().next_panel(); } } /// Change to previously selected panel + /// Need to skip the commands planel if there no are current containers running 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 - } else { - 1 - }; - for _ in 0..count { + self.gui_state.lock().previous_panel(); + if self.app_data.lock().get_container_len() == 0 + && self.gui_state.lock().get_selected_panel() == SelectablePanel::Commands + { self.gui_state.lock().previous_panel(); } } 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 { - SelectablePanel::Containers => locked_data.containers_start(), - SelectablePanel::Logs => locked_data.log_start(), - SelectablePanel::Commands => locked_data.docker_controls_start(), + SelectablePanel::Containers => self.app_data.lock().containers_start(), + SelectablePanel::Logs => self.app_data.lock().log_start(), + SelectablePanel::Commands => self.app_data.lock().docker_controls_start(), } } /// Go to end of the list of the currently selected panel 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 { - SelectablePanel::Containers => locked_data.containers_end(), - SelectablePanel::Logs => locked_data.log_end(), - SelectablePanel::Commands => locked_data.docker_controls_end(), + SelectablePanel::Containers => self.app_data.lock().containers_end(), + SelectablePanel::Logs => self.app_data.lock().log_end(), + SelectablePanel::Commands => self.app_data.lock().docker_controls_end(), } } @@ -455,24 +423,21 @@ impl InputHandler { } /// Handle keyboard button events async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) { - let contains_delete = self - .gui_state - .lock() - .status_contains(&[Status::DeleteConfirm]); - - let contains = |s: Status| self.gui_state.lock().status_contains(&[s]); + let status = self.gui_state.lock().get_status(); + let contains = |s: Status| status.contains(&s); 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); + let contains_filter = contains(Status::Filter); + let contains_delete = contains(Status::DeleteConfirm); if !contains_exec { 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() && !contains_filter { - // Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active - self.quit().await; + // Always just quit on Ctrl + c/C or q/Q, unless in Filter status active + self.quit(); } if contains_error { @@ -514,22 +479,13 @@ impl InputHandler { MouseEventKind::ScrollUp => self.previous(), MouseEventKind::ScrollDown => self.next(), MouseEventKind::Down(MouseButton::Left) => { - let header = self.gui_state.lock().header_intersect(Rect::new( - mouse_event.column, - mouse_event.row, - 1, - 1, - )); + let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1); + let header = self.gui_state.lock().header_intersect(mouse_point); if let Some(header) = header { self.sort(header); } - self.gui_state.lock().panel_intersect(Rect::new( - mouse_event.column, - mouse_event.row, - 1, - 1, - )); + self.gui_state.lock().panel_intersect(mouse_point); } _ => (), } @@ -537,23 +493,21 @@ impl InputHandler { /// Change state to next, depending which panel is currently in focus fn next(&self) { - let mut locked_data = self.app_data.lock(); let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { - SelectablePanel::Containers => locked_data.containers_next(), - SelectablePanel::Logs => locked_data.log_next(), - SelectablePanel::Commands => locked_data.docker_controls_next(), + SelectablePanel::Containers => self.app_data.lock().containers_next(), + SelectablePanel::Logs => self.app_data.lock().log_next(), + SelectablePanel::Commands => self.app_data.lock().docker_controls_next(), }; } /// Change state to previous, depending which panel is currently in focus fn previous(&self) { - let mut locked_data = self.app_data.lock(); let selected_panel = self.gui_state.lock().get_selected_panel(); match selected_panel { - SelectablePanel::Containers => locked_data.containers_previous(), - SelectablePanel::Logs => locked_data.log_previous(), - SelectablePanel::Commands => locked_data.docker_controls_previous(), + SelectablePanel::Containers => self.app_data.lock().containers_previous(), + SelectablePanel::Logs => self.app_data.lock().log_previous(), + SelectablePanel::Commands => self.app_data.lock().docker_controls_previous(), } } } diff --git a/src/main.rs b/src/main.rs index 81a0639..3ad0af4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,32 +52,28 @@ async fn docker_init( docker_rx: Receiver, docker_tx: Sender, gui_state: &Arc>, - is_running: &Arc, - host: Option, ) { + let host = read_docker_host(&app_data.lock().args); + let connection = host.map_or_else(Docker::connect_with_socket_defaults, |host| { Docker::connect_with_socket(&host, 120, API_DEFAULT_VERSION) }); if let Ok(docker) = connection { if docker.ping().await.is_ok() { - let app_data = Arc::clone(app_data); - let gui_state = Arc::clone(gui_state); - let is_running = Arc::clone(is_running); - - tokio::spawn(DockerData::init( - app_data, docker, docker_rx, docker_tx, gui_state, is_running, + tokio::spawn(DockerData::start( + Arc::clone(app_data), + docker, + docker_rx, + docker_tx, + Arc::clone(gui_state), )); - } else { - app_data - .lock() - .set_error(AppError::DockerConnect, gui_state, Status::DockerConnect); + return; } - } else { - app_data - .lock() - .set_error(AppError::DockerConnect, gui_state, Status::DockerConnect); } + app_data + .lock() + .set_error(AppError::DockerConnect, gui_state, Status::DockerConnect); } /// Create data for, and then spawn a tokio thread, for the input handler @@ -88,15 +84,12 @@ fn handler_init( input_rx: Receiver, is_running: &Arc, ) { - let app_data = Arc::clone(app_data); - let gui_state = Arc::clone(gui_state); - let is_running = Arc::clone(is_running); - tokio::spawn(input_handler::InputHandler::init( - app_data, + tokio::spawn(input_handler::InputHandler::start( + Arc::clone(app_data), input_rx, docker_sx.clone(), - gui_state, - is_running, + Arc::clone(gui_state), + Arc::clone(is_running), )); } @@ -106,34 +99,20 @@ async fn main() { let args = CliArgs::new(); - // If running via Docker image, need to sleep else program will just quit straight away, no real idea why - // So just sleep for small while - if args.in_container { - std::thread::sleep(std::time::Duration::from_millis(250)); - } - let host = read_docker_host(&args); - let app_data = Arc::new(Mutex::new(AppData::default(args.clone()))); let gui_state = Arc::new(Mutex::new(GuiState::default())); let is_running = Arc::new(AtomicBool::new(true)); let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32); - docker_init( - &app_data, - docker_rx, - docker_tx.clone(), - &gui_state, - &is_running, - host, - ) - .await; + docker_init(&app_data, docker_rx, docker_tx.clone(), &gui_state).await; if args.gui { let (input_tx, input_rx) = tokio::sync::mpsc::channel(32); handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); - Ui::create(app_data, gui_state, input_tx, is_running).await; + Ui::start(app_data, gui_state, input_tx, is_running).await; } else { info!("in debug mode\n"); + let mut now = std::time::Instant::now(); // Debug mode for testing, less pointless now, will display some basic information while is_running.load(Ordering::SeqCst) { let err = app_data.lock().get_error(); @@ -141,10 +120,12 @@ async fn main() { error!("{}", err); process::exit(1); } - tokio::time::sleep(std::time::Duration::from_millis(u64::from( - args.docker_interval, - ))) - .await; + if let Some(Ok(to_sleep)) = u128::from(args.docker_interval) + .checked_sub(now.elapsed().as_millis()) + .map(u64::try_from) + { + tokio::time::sleep(std::time::Duration::from_millis(to_sleep)).await; + } let containers = app_data .lock() .get_container_items() @@ -158,6 +139,7 @@ async fn main() { } println!(); } + now = std::time::Instant::now(); } } } @@ -182,6 +164,7 @@ mod tests { docker_interval: 1000, gui: true, host: None, + std_err: false, in_container: false, save_dir: None, raw: false, diff --git a/src/parse_args.rs b/src/parse_args.rs index e29d84c..911ba20 100644 --- a/src/parse_args.rs +++ b/src/parse_args.rs @@ -37,13 +37,17 @@ pub struct Args { #[clap(long, short = None)] pub host: Option, - /// Force use of docker cli when execing into containers - #[clap(long="use-cli", short = None)] - pub use_cli: bool, + /// Do not include stderr output in logs + #[clap(long = "no-stderr")] + pub no_std_err: bool, /// Directory for saving exported logs, defaults to `$HOME` #[clap(long="save-dir", short = None)] pub save_dir: Option, + + /// Force use of docker cli when execing into containers + #[clap(long="use-cli", short = None)] + pub use_cli: bool, } #[derive(Debug, Clone)] @@ -58,6 +62,7 @@ pub struct CliArgs { pub raw: bool, pub show_self: bool, pub timestamp: bool, + pub std_err: bool, pub use_cli: bool, } @@ -65,12 +70,7 @@ impl CliArgs { /// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately /// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container fn check_if_in_container() -> bool { - if let Ok(value) = std::env::var(ENV_KEY) { - if value == ENV_VALUE { - return true; - } - } - false + std::env::var(ENV_KEY).map_or(false, |i| i == ENV_VALUE) } /// Parse cli arguments @@ -97,6 +97,7 @@ impl CliArgs { in_container: Self::check_if_in_container(), save_dir: logs_dir, raw: args.raw, + std_err: !args.no_std_err, show_self: !args.show_self, timestamp: !args.timestamp, } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 7423418..d939255 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -43,6 +43,9 @@ const MARGIN: &str = " "; const RIGHT_ARROW: &str = "▶ "; const CIRCLE: &str = "⚪ "; +const COLOR_RX: Color = Color::Rgb(255, 233, 193); +const COLOR_TX: Color = Color::Rgb(205, 140, 140); + const CONSTRAINT_50_50: [Constraint; 2] = [Constraint::Percentage(50), Constraint::Percentage(50)]; const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)]; const CONSTRAINT_POPUP: [Constraint; 5] = [ @@ -72,7 +75,6 @@ fn max_line_width(text: &str) -> usize { /// Generate block, add a border if is the selected panel, /// add custom title based on state of each panel fn generate_block<'a>( - app_data: &Arc>, area: Rect, fd: &FrameData, gui_state: &Arc>, @@ -81,12 +83,13 @@ fn generate_block<'a>( gui_state .lock() .update_region_map(Region::Panel(panel), area); + let mut title = match panel { SelectablePanel::Containers => { - format!("{}{}", panel.title(), app_data.lock().container_title()) + format!("{}{}", panel.title(), fd.container_title) } SelectablePanel::Logs => { - format!("{}{}", panel.title(), app_data.lock().get_log_title()) + format!("{}{}", panel.title(), fd.log_title) } SelectablePanel::Commands => String::new(), }; @@ -97,7 +100,7 @@ fn generate_block<'a>( .borders(Borders::ALL) .border_type(BorderType::Rounded) .title(title); - if fd.selected_panel == panel && !gui_state.lock().status_contains(&[Status::Filter]) { + if fd.selected_panel == panel && !fd.status.contains(&Status::Filter) { block = block.border_style(Style::default().fg(Color::LightCyan)); } block @@ -111,7 +114,7 @@ pub fn commands( fd: &FrameData, gui_state: &Arc>, ) { - let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands); + let block = generate_block(area, fd, gui_state, SelectablePanel::Commands); let items = app_data.lock().get_control_items().map_or(vec![], |i| { i.iter() .map(|c| { @@ -141,7 +144,6 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { let state_style = Style::default().fg(i.state.get_color()); let blue = Style::default().fg(Color::Blue); - // Truncate? Line::from(vec![ Span::styled( format!( @@ -203,11 +205,11 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> { ), Span::styled( format!("{:>width$}{MARGIN}", i.rx, width = widths.net_rx.1.into()), - Style::default().fg(Color::Rgb(255, 233, 193)), + Style::default().fg(COLOR_RX), ), Span::styled( format!("{:>width$}{MARGIN}", i.tx, width = widths.net_tx.1.into()), - Style::default().fg(Color::Rgb(205, 140, 140)), + Style::default().fg(COLOR_TX), ), ]) } @@ -220,7 +222,7 @@ pub fn containers( fd: &FrameData, gui_state: &Arc>, ) { - let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers); + let block = generate_block(area, fd, gui_state, SelectablePanel::Containers); let items = app_data .lock() @@ -230,9 +232,9 @@ pub fn containers( .collect::>(); if items.is_empty() { - let text = if app_data.lock().get_filter_term().is_some() { + let text = if fd.filter_term.is_some() { "no containers match filter" - } else if gui_state.lock().is_loading() { + } else if fd.is_loading { &format!("loading {}", fd.loading_icon) } else { "no containers running" @@ -259,8 +261,8 @@ pub fn logs( fd: &FrameData, gui_state: &Arc>, ) { - let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Logs); - if fd.init { + let block = generate_block(area, fd, gui_state, SelectablePanel::Logs); + if fd.status.contains(&Status::Init) { let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon)) .style(Style::default()) .block(block) @@ -268,7 +270,6 @@ pub fn logs( f.render_widget(paragraph, area); } else { let logs = app_data.lock().get_logs(); - if logs.is_empty() { let paragraph = Paragraph::new("no logs found") .block(block) @@ -287,15 +288,9 @@ pub fn logs( } } -// Display the ports in a formatted list -pub fn ports( - f: &mut Frame, - area: Rect, - app_data: &Arc>, - max_lens: (usize, usize, usize), -) { - let ports = app_data.lock().get_selected_ports(); - if let Some(ports) = ports { +/// Display the ports in a formatted list +pub fn ports(f: &mut Frame, area: Rect, fd: &FrameData) { + if let Some(ports) = fd.ports.as_ref() { let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -307,7 +302,7 @@ pub fn ports( .add_modifier(Modifier::BOLD), )); - let (ip, private, public) = max_lens; + let (ip, private, public) = fd.port_max_lens; if ports.0.is_empty() { let text = match ports.1 { @@ -328,7 +323,7 @@ pub fn ports( )]; for item in &ports.0 { let fg = Color::White; - let strings = item.print(); + let strings = item.get_all(); let line = vec![ Span::from(format!("{:>ip$}", strings.0)).fg(fg), @@ -344,9 +339,8 @@ pub fn ports( } /// Draw the cpu + mem charts -pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc>) { - let cpu_mem = app_data.lock().get_chart_data(); - if let Some((cpu, mem)) = cpu_mem { +pub fn chart(f: &mut Frame, area: Rect, fd: &FrameData) { + if let Some((cpu, mem)) = fd.chart_data.as_ref() { let area = Layout::default() .direction(Direction::Horizontal) .constraints(CONSTRAINT_50_50) @@ -422,45 +416,30 @@ 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(); - +fn filter_by_spans(fd: &FrameData) -> [Span; 4] { 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), - ], + let mut filter_spans = [ + Span::styled(name[0], not_selected), + Span::styled(name[1], not_selected), + Span::styled(name[2], not_selected), + Span::styled(name[3], not_selected), + ]; + + match fd.filter_by { + FilterBy::Name => filter_spans[0] = Span::styled(name[0], selected), + FilterBy::Image => filter_spans[1] = Span::styled(name[1], selected), + FilterBy::Status => filter_spans[2] = Span::styled(name[2], selected), + FilterBy::All => filter_spans[3] = Span::styled(name[3], selected), } + filter_spans } /// Draw the filter bar -pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) { +pub fn filter_bar(area: Rect, frame: &mut Frame, fd: &FrameData) { let style_but = Style::default().fg(Color::Black).bg(Color::Magenta); let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset); @@ -470,7 +449,7 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) Span::styled(" ← by → ", style_but), Span::from(" "), ]; - line.extend_from_slice(&filter_by_spans(app_data)); + line.extend_from_slice(&filter_by_spans(fd)); line.extend_from_slice(&[ Span::styled( " term: ", @@ -479,10 +458,9 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) .add_modifier(Modifier::BOLD), ), Span::styled( - app_data - .lock() - .get_filter_term() - .map_or(String::new(), std::borrow::ToOwned::to_owned), + fd.filter_term + .as_ref() + .map_or(String::new(), std::clone::Clone::clone), Style::default().fg(Color::Gray), ), ]); @@ -491,10 +469,11 @@ pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc>) /// Draw heading bar at top of program, always visible /// TODO Should separate into loading icon/headers/help functions +#[allow(clippy::too_many_lines)] pub fn heading_bar( area: Rect, frame: &mut Frame, - data: &FrameData, + fd: &FrameData, gui_state: &Arc>, ) { let block = |fg: Color| Block::default().style(Style::default().bg(Color::Magenta).fg(fg)); @@ -505,7 +484,7 @@ pub fn heading_bar( let header_block = |x: &Header| { let mut color = Color::Black; let mut suffix = ""; - if let Some((a, b)) = &data.sorted_by { + if let Some((a, b)) = &fd.sorted_by { if x == a { match b { SortedOrder::Asc => suffix = " ▲", @@ -523,6 +502,7 @@ pub fn heading_bar( let gen_header = |header: &Header, width: usize| { let block = header_block(header); + // TODO // Yes this is a mess, needs documenting correctly let text = format!( @@ -538,26 +518,30 @@ pub fn heading_bar( // Meta data to iterate over to create blocks with correct widths let header_meta = [ - (Header::Name, data.columns.name.1), - (Header::State, data.columns.state.1), - (Header::Status, data.columns.status.1), - (Header::Cpu, data.columns.cpu.1), - (Header::Memory, data.columns.mem.1 + data.columns.mem.2 + 3), - (Header::Id, data.columns.id.1), - (Header::Image, data.columns.image.1), - (Header::Rx, data.columns.net_rx.1), - (Header::Tx, data.columns.net_tx.1), + (Header::Name, fd.columns.name.1), + (Header::State, fd.columns.state.1), + (Header::Status, fd.columns.status.1), + (Header::Cpu, fd.columns.cpu.1), + (Header::Memory, fd.columns.mem.1 + fd.columns.mem.2 + 3), + (Header::Id, fd.columns.id.1), + (Header::Image, fd.columns.image.1), + (Header::Rx, fd.columns.net_rx.1), + (Header::Tx, fd.columns.net_tx.1), ]; // Need to add widths to this - let suffix = if data.help_visible { "exit" } else { "show" }; + let suffix = if fd.status.contains(&Status::Help) { + "exit" + } else { + "show" + }; let info_text = format!("( h ) {suffix} help{MARGIN}",); let info_width = info_text.chars().count(); let column_width = usize::from(area.width).saturating_sub(info_width); let column_width = if column_width > 0 { column_width } else { 1 }; - let splits = if data.has_containers { + let splits = if fd.has_containers { vec![ Constraint::Max(4), Constraint::Max(column_width.try_into().unwrap_or_default()), @@ -573,11 +557,11 @@ pub fn heading_bar( .split(area); // 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!("{:>2}", fd.loading_icon)) .block(block(Color::White)) .alignment(Alignment::Left); frame.render_widget(loading_paragraph, split_bar[0]); - if data.has_containers { + if fd.has_containers { let header_section_width = split_bar[1].width; let mut counter = 0; @@ -612,7 +596,7 @@ pub fn heading_bar( } // show/hide help - let color = if data.help_visible { + let color = if fd.status.contains(&Status::Help) { Color::Black } else { Color::White @@ -622,7 +606,7 @@ pub fn heading_bar( .alignment(Alignment::Right); // If no containers, don't display the headers, could maybe do this first? - let help_index = if data.has_containers { 2 } else { 0 }; + let help_index = if fd.has_containers { 2 } else { 0 }; frame.render_widget(help_paragraph, split_bar[help_index]); } @@ -851,22 +835,19 @@ pub fn help_box(f: &mut Frame) { let name_paragraph = Paragraph::new(name_info.lines) .style(Style::default().bg(Color::Magenta).fg(Color::White)) - .block(Block::default()) .alignment(Alignment::Center); + let style = || Style::default().bg(Color::Magenta).fg(Color::Black); let description_paragraph = Paragraph::new(description_info.lines) - .style(Style::default().bg(Color::Magenta).fg(Color::Black)) - .block(Block::default()) + .style(style()) .alignment(Alignment::Center); let help_paragraph = Paragraph::new(button_info.lines) - .style(Style::default().bg(Color::Magenta).fg(Color::Black)) - .block(Block::default()) + .style(style()) .alignment(Alignment::Left); let final_paragraph = Paragraph::new(final_info.lines) - .style(Style::default().bg(Color::Magenta).fg(Color::Black)) - .block(Block::default()) + .style(style()) .alignment(Alignment::Center); let block = Block::default() @@ -1003,20 +984,19 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { let area = popup(lines, max_line_width, f.area(), BoxLocation::MiddleCentre); - // let (paragraph, area) = gen_error(f, error, seconds); f.render_widget(Clear, area); f.render_widget(paragraph, area); } /// Draw info box in one of the 9 BoxLocations // TODO is this broken - I don't think so -pub fn info(f: &mut Frame, text: &str, instant: Instant, gui_state: &Arc>) { +pub fn info(f: &mut Frame, text: String, instant: &Instant, gui_state: &Arc>) { let block = Block::default() .title("") .title_alignment(Alignment::Center) .borders(Borders::NONE); - let mut max_line_width = max_line_width(text); + let mut max_line_width = max_line_width(&text); let mut lines = text.lines().count(); // Add some horizontal & vertical margins @@ -1068,7 +1048,11 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio #[allow(clippy::unwrap_used)] mod tests { - use std::{ops::RangeInclusive, sync::Arc}; + use std::{ + net::{IpAddr, Ipv4Addr}, + ops::RangeInclusive, + sync::Arc, + }; use parking_lot::Mutex; use ratatui::{ @@ -1086,7 +1070,10 @@ mod tests { }, app_error::AppError, tests::{gen_appdata, gen_container_summary, gen_containers}, - ui::{draw_frame, GuiState}, + ui::{ + draw_blocks::{COLOR_RX, COLOR_TX}, + draw_frame, GuiState, Status, + }, }; use super::{FrameData, ORANGE, VERSION}; @@ -1102,6 +1089,42 @@ mod tests { const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"]; + impl From<(&Arc>, &Arc>)> for FrameData { + fn from(data: (&Arc>, &Arc>)) -> Self { + let (app_data, gui_data) = (data.0.lock(), data.1.lock()); + + // set max height for container section, needs +5 to deal with docker commands list and borders + let height = app_data.get_container_len(); + let height = if height < 12 { + u16::try_from(height + 5).unwrap_or_default() + } else { + 12 + }; + + let (filter_by, filter_term) = app_data.get_filter(); + Self { + chart_data: app_data.get_chart_data(), + columns: app_data.get_width(), + container_title: app_data.get_container_title(), + delete_confirm: gui_data.get_delete_container(), + filter_by, + filter_term: filter_term.cloned(), + has_containers: app_data.get_container_len() > 0, + has_error: app_data.get_error(), + height, + ports: app_data.get_selected_ports(), + port_max_lens: app_data.get_longest_port(), + info_text: gui_data.info_box_text.clone(), + is_loading: gui_data.is_loading(), + loading_icon: gui_data.get_loading().to_string(), + log_title: app_data.get_log_title(), + selected_panel: gui_data.get_selected_panel(), + sorted_by: app_data.get_sorted(), + status: gui_data.get_status(), + } + } + } + /// Generate state to be used in *most* gui tests fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup { let backend = TestBackend::new(w, h); @@ -1120,8 +1143,7 @@ mod tests { let app_data = Arc::new(Mutex::new(app_data)); let gui_state = Arc::new(Mutex::new(gui_state)); - - let fd = FrameData::from((app_data.lock(), gui_state.lock())); + let fd = FrameData::from((&app_data, &gui_state)); let area = Rect::new(0, 0, w, h); TuiTestSetup { app_data, @@ -1248,7 +1270,7 @@ mod tests { setup .app_data .lock() - .update_containers(&mut vec![gen_container_summary(1, "paused")]); + .update_containers(vec![gen_container_summary(1, "paused")]); setup.app_data.lock().docker_controls_next(); let expected = [ @@ -1327,7 +1349,7 @@ mod tests { // Control panel now selected, should have a blue border setup.gui_state.lock().next_panel(); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { @@ -1376,7 +1398,7 @@ mod tests { ]; setup.gui_state.lock().next_panel(); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1394,7 +1416,7 @@ mod tests { } setup.gui_state.lock().previous_panel(); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1463,7 +1485,7 @@ mod tests { // Change selected panel, border is now no longer blue setup.gui_state.lock().next_panel(); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { @@ -1497,7 +1519,7 @@ mod tests { "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" ]; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1527,11 +1549,11 @@ mod tests { } // rx column (1..=3, 92..=101) => { - assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 102..=111) => { - assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + assert_eq!(result_cell.fg, COLOR_TX); } _ => assert_eq!(result_cell.fg, Color::Reset), } @@ -1557,7 +1579,7 @@ mod tests { "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup.app_data.lock().containers.items[0].state = State::Paused; setup @@ -1601,11 +1623,11 @@ mod tests { } // rx column (1..=3, 92..=101) => { - assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 102..=111) => { - assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + assert_eq!(result_cell.fg, COLOR_TX); } _ => assert_eq!(result_cell.fg, Color::Reset), } @@ -1627,7 +1649,7 @@ mod tests { "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup.app_data.lock().containers.items[0].state = State::Paused; setup @@ -1655,7 +1677,7 @@ mod tests { "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Dead; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1682,7 +1704,7 @@ mod tests { "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Exited; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1708,7 +1730,7 @@ mod tests { "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Removing; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1735,7 +1757,7 @@ mod tests { "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Restarting; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1768,11 +1790,11 @@ mod tests { } // rx column (1..=3, 95..=104) => { - assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 105..=114) => { - assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + assert_eq!(result_cell.fg, COLOR_TX); } _ => { assert_eq!(result_cell.fg, Color::Reset); @@ -1800,7 +1822,7 @@ mod tests { "│ │", "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯" ]; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1832,11 +1854,11 @@ mod tests { } // rx column (1..=3, 104..=113) => { - assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + assert_eq!(result_cell.fg, COLOR_RX); } // tx column (1..=3, 114..=123) => { - assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + assert_eq!(result_cell.fg, COLOR_TX); } _ => assert_eq!(result_cell.fg, Color::Reset), } @@ -1859,7 +1881,7 @@ mod tests { "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", ]; setup.app_data.lock().containers.items[0].state = State::Unknown; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal @@ -1878,21 +1900,18 @@ mod tests { #[test] /// No logs, panel unselected, then selected, border color changes correctly fn test_draw_blocks_logs_none() { - let (w, h) = (25, 6); + let (w, h) = (35, 6); let mut setup = test_setup(w, h, true, true); - setup.app_data.lock().containers = StatefulList::new(vec![]); let expected = [ - "╭ Logs ─────────────────╮", - "│ no logs found │", - "│ │", - "│ │", - "│ │", - "╰───────────────────────╯", + "╭ Logs - container_1 - image_1 ───╮", + "│ no logs found │", + "│ │", + "│ │", + "│ │", + "╰─────────────────────────────────╯", ]; - let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); - setup .terminal .draw(|f| { @@ -1910,7 +1929,7 @@ mod tests { setup.gui_state.lock().next_panel(); setup.gui_state.lock().next_panel(); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); // When selected, has a blue border setup @@ -1924,7 +1943,6 @@ mod tests { 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); } @@ -1949,8 +1967,8 @@ mod tests { "╰──────────────────────────────╯", ]; - let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); - fd.init = true; + let mut fd = FrameData::from((&setup.app_data, &setup.gui_state)); + fd.status.insert(Status::Init); setup .terminal @@ -1979,8 +1997,8 @@ mod tests { "╰──────────────────────────────╯", ]; - let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); - fd.init = true; + let mut fd = FrameData::from((&setup.app_data, &setup.gui_state)); + fd.status.insert(Status::Init); setup .terminal .draw(|f| { @@ -2000,12 +2018,12 @@ mod tests { #[test] /// Logs correct displayed, changing log state also draws correctly fn test_draw_blocks_logs_some() { - let (w, h) = (25, 6); + let (w, h) = (36, 6); let mut setup = test_setup(w, h, true, true); insert_logs(&setup); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { @@ -2013,12 +2031,12 @@ mod tests { }) .unwrap(); let expected = [ - "╭ Logs 3/3 - container_1╮", - "│ line 1 │", - "│ line 2 │", - "│▶ line 3 │", - "│ │", - "╰───────────────────────╯", + "╭ Logs 3/3 - container_1 - image_1 ╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "╰──────────────────────────────────╯", ]; for (row_index, result_row) in get_result(&setup, w) { @@ -2027,7 +2045,7 @@ mod tests { 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) { + if row_index == 3 && (1..=34).contains(&result_cell_index) { assert_eq!(result_cell.modifier, Modifier::BOLD); } else { assert!(result_cell.modifier.is_empty()); @@ -2037,22 +2055,22 @@ mod tests { // Change selected log line setup.app_data.lock().log_previous(); - _ = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); }) .unwrap(); let expected = [ - "╭ Logs 2/3 - container_1╮", - "│ line 1 │", - "│▶ line 2 │", - "│ line 3 │", - "│ │", - "╰───────────────────────╯", + "╭ Logs 2/3 - container_1 - image_1 ╮", + "│ line 1 │", + "│▶ line 2 │", + "│ line 3 │", + "│ │", + "╰──────────────────────────────────╯", ]; for (row_index, result_row) in get_result(&setup, w) { let expected_row = expected_to_vec(&expected, row_index); @@ -2060,7 +2078,7 @@ mod tests { 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) { + if row_index == 2 && (1..=34).contains(&result_cell_index) { assert_eq!(result_cell.modifier, Modifier::BOLD); } else { assert!(result_cell.modifier.is_empty()); @@ -2090,7 +2108,7 @@ mod tests { "╰──────────────────────────────────────────────────────────────────────────────╯", ]; - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { @@ -2194,10 +2212,11 @@ mod tests { let (w, h) = (80, 10); let mut setup = test_setup(w, h, true, true); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::chart(f, setup.area, &setup.app_data); + super::chart(f, setup.area, &fd); }) .unwrap(); @@ -2244,11 +2263,12 @@ mod tests { let mut setup = test_setup(w, h, true, true); insert_chart_data(&setup); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::chart(f, setup.area, &setup.app_data); + super::chart(f, setup.area, &fd); }) .unwrap(); @@ -2291,11 +2311,12 @@ mod tests { insert_chart_data(&setup); setup.app_data.lock().containers.items[0].state = State::Paused; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::chart(f, setup.area, &setup.app_data); + super::chart(f, setup.area, &fd); }) .unwrap(); @@ -2333,11 +2354,12 @@ mod tests { let mut setup = test_setup(w, h, true, true); insert_chart_data(&setup); setup.app_data.lock().containers.items[0].state = State::Dead; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::chart(f, setup.area, &setup.app_data); + super::chart(f, setup.area, &fd); }) .unwrap(); @@ -2379,7 +2401,7 @@ mod tests { let mut setup = test_setup(w, h, true, true); setup.app_data.lock().containers = StatefulList::new(vec![]); - let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let mut fd = FrameData::from((&setup.app_data, &setup.gui_state)); let expected = [" ( h ) show help "]; @@ -2399,7 +2421,7 @@ mod tests { } } - fd.help_visible = true; + fd.status.insert(Status::Help); let expected = [" ( h ) exit help "]; setup .terminal @@ -2423,7 +2445,7 @@ mod tests { fn test_draw_blocks_headers_some_containers() { let (w, h) = (140, 1); let mut setup = test_setup(w, h, true, true); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "]; setup @@ -2454,7 +2476,7 @@ mod tests { fn test_draw_blocks_headers_some_containers_reduced_width() { let (w, h) = (80, 1); let mut setup = test_setup(w, h, true, true); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let expected = [" name state status cpu ( h ) show help "]; @@ -2486,7 +2508,7 @@ mod tests { fn test_draw_blocks_headers_sort_containers() { 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 fd = FrameData::from((&setup.app_data, &setup.gui_state)); // Actual test, used for each header and sorted type let mut test = @@ -2555,7 +2577,7 @@ mod tests { let mut setup = test_setup(w, h, true, true); let uuid = Uuid::new_v4(); setup.gui_state.lock().next_loading(uuid); - let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "]; @@ -2810,7 +2832,12 @@ mod tests { setup .terminal .draw(|f| { - super::info(f, "test", std::time::Instant::now(), &setup.gui_state); + super::info( + f, + "test".to_owned(), + &std::time::Instant::now(), + &setup.gui_state, + ); }) .unwrap(); @@ -2848,7 +2875,7 @@ mod tests { setup .terminal .draw(|f| { - super::filter_bar(setup.area, f, &setup.app_data); + super::filter_bar(setup.area, f, &setup.fd); }) .unwrap(); @@ -2889,11 +2916,12 @@ 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'); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::filter_bar(setup.area, f, &setup.app_data); + super::filter_bar(setup.area, f, &fd); }) .unwrap(); @@ -2934,10 +2962,11 @@ mod tests { // Test when filter_by chances setup.app_data.lock().filter_by_next(); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::filter_bar(setup.area, f, &setup.app_data); + super::filter_bar(setup.area, f, &fd); }) .unwrap(); @@ -3078,11 +3107,11 @@ mod tests { let mut setup = test_setup(w, h, true, true); setup.app_data.lock().containers.items[0].ports = vec![]; - let max_lens = setup.app_data.lock().get_longest_port(); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::ports(f, setup.area, &setup.app_data, max_lens); + super::ports(f, setup.area, &fd); }) .unwrap(); @@ -3123,11 +3152,12 @@ mod tests { // 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(); + + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::ports(f, setup.area, &setup.app_data, max_lens); + super::ports(f, setup.area, &fd); }) .unwrap(); @@ -3173,17 +3203,16 @@ mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some("127.0.0.1".to_owned()), + ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), private: 8003, public: Some(8003), }); - let max_lens = setup.app_data.lock().get_longest_port(); - + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::ports(f, setup.area, &setup.app_data, max_lens); + super::ports(f, setup.area, &fd); }) .unwrap(); @@ -3232,12 +3261,12 @@ mod tests { fn test_draw_blocks_ports_container_state() { let (w, h) = (32, 8); let mut setup = test_setup(w, h, true, true); - let max_lens = setup.app_data.lock().get_longest_port(); + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::ports(f, setup.area, &setup.app_data, max_lens); + super::ports(f, setup.area, &fd); }) .unwrap(); @@ -3265,10 +3294,11 @@ mod tests { } setup.app_data.lock().containers.items[0].state = State::Paused; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::ports(f, setup.area, &setup.app_data, max_lens); + super::ports(f, setup.area, &fd); }) .unwrap(); @@ -3285,10 +3315,11 @@ mod tests { } setup.app_data.lock().containers.items[0].state = State::Exited; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - super::ports(f, setup.area, &setup.app_data, max_lens); + super::ports(f, setup.area, &fd); }) .unwrap(); @@ -3319,7 +3350,7 @@ mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some("127.0.0.1".to_owned()), + ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), private: 8003, public: Some(8003), }); @@ -3356,10 +3387,12 @@ mod tests { "│ │ ││ │ ││ │", "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", ]; + + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - draw_frame(f, &setup.app_data, &setup.gui_state); + draw_frame(f, &setup.app_data, &setup.gui_state, &fd); }) .unwrap(); @@ -3383,7 +3416,7 @@ mod tests { setup.app_data.lock().containers.items[1] .ports .push(ContainerPorts { - ip: Some("127.0.0.1".to_owned()), + ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), private: 8003, public: Some(8003), }); @@ -3420,10 +3453,11 @@ mod tests { "│ │ ││ │ ││ │", "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", ]; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - draw_frame(f, &setup.app_data, &setup.gui_state); + draw_frame(f, &setup.app_data, &setup.gui_state, &fd); }) .unwrap(); @@ -3474,10 +3508,11 @@ mod tests { "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯", " Esc clear ← by → Name Image Status All term: r_1 ", ]; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - draw_frame(f, &setup.app_data, &setup.gui_state); + draw_frame(f, &setup.app_data, &setup.gui_state, &fd); }) .unwrap(); @@ -3500,7 +3535,7 @@ mod tests { setup.app_data.lock().containers.items[0] .ports .push(ContainerPorts { - ip: Some("127.0.0.1".to_owned()), + ip: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), private: 8003, public: Some(8003), }); @@ -3542,10 +3577,11 @@ mod tests { "│ │ ││ │ ││ │", "╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯╰────────────────────────────╯", ]; + let fd = FrameData::from((&setup.app_data, &setup.gui_state)); setup .terminal .draw(|f| { - draw_frame(f, &setup.app_data, &setup.gui_state); + draw_frame(f, &setup.app_data, &setup.gui_state, &fd); }) .unwrap(); diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index ce007e8..b7b1850 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -266,10 +266,9 @@ impl GuiState { self.delete_container = id; } - /// Check if the current gui_status contains any of the given status' - /// Don't really like this methodology for gui state, needs a re-think - pub fn status_contains(&self, status: &[Status]) -> bool { - status.iter().any(|i| self.status.contains(i)) + /// Return a copy of the Status HashSet + pub fn get_status(&self) -> HashSet { + self.status.clone() } /// Remove a gui_status into the current gui_status HashSet diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2158fef..fc3244c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,13 +4,14 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use parking_lot::{Mutex, MutexGuard}; +use parking_lot::Mutex; use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Position}, Frame, Terminal, }; use std::{ + collections::HashSet, io::{self, Stdout, Write}, sync::{atomic::Ordering, Arc}, time::Duration, @@ -26,18 +27,21 @@ mod gui_state; pub use self::color_match::*; pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ - app_data::{AppData, Columns, ContainerId, Header, SortedOrder}, + app_data::{ + AppData, Columns, ContainerId, ContainerPorts, CpuTuple, FilterBy, Header, MemTuple, + SortedOrder, State, + }, app_error::AppError, exec::TerminalSize, input_handler::InputMessages, }; pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36); +const POLL_RATE: Duration = std::time::Duration::from_millis(100); pub struct Ui { app_data: Arc>, gui_state: Arc>, - input_poll_rate: Duration, input_tx: Sender, is_running: Arc, now: Instant, @@ -59,7 +63,7 @@ impl Ui { } /// Create a new Ui struct, and execute the drawing loop - pub async fn create( + pub async fn start( app_data: Arc>, gui_state: Arc>, input_tx: Sender, @@ -71,7 +75,6 @@ impl Ui { app_data, cursor_position, gui_state, - input_poll_rate: std::time::Duration::from_millis(100), input_tx, is_running, now: Instant::now(), @@ -141,7 +144,7 @@ impl Ui { Ok(()) } - /// Use exeternal docker cli to exec into a container + /// Use external docker cli to exec into a container async fn exec(&mut self) { let exec_mode = self.gui_state.lock().get_exec_mode(); @@ -163,20 +166,21 @@ impl Ui { /// The loop for drawing the main UI to the terminal async fn gui_loop(&mut self) -> Result<(), AppError> { while self.is_running.load(Ordering::SeqCst) { - let exec = self.gui_state.lock().status_contains(&[Status::Exec]); + let fd = FrameData::from(&*self); + let exec = fd.status.contains(&Status::Exec); if exec { self.exec().await; } if self .terminal - .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state)) + .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state, &fd)) .is_err() { return Err(AppError::Terminal); } - if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { + if crossterm::event::poll(POLL_RATE).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { if key.kind == event::KeyEventKind::Press { @@ -206,11 +210,8 @@ impl Ui { /// Draw either the Error, or main oxker ui, to the terminal async fn draw_ui(&mut self) -> Result<(), AppError> { - let status_dockerconnect = self - .gui_state - .lock() - .status_contains(&[Status::DockerConnect]); - if status_dockerconnect { + let status = self.gui_state.lock().get_status(); + if status.contains(&Status::DockerConnect) { self.err_loop()?; } else { self.gui_loop().await?; @@ -219,54 +220,73 @@ impl Ui { } } -/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here -#[derive(Debug)] +/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here +#[derive(Debug, Clone)] pub struct FrameData { + chart_data: Option<(CpuTuple, MemTuple)>, columns: Columns, + container_title: String, delete_confirm: Option, + filter_by: FilterBy, + filter_term: Option, has_containers: bool, has_error: Option, height: u16, - help_visible: bool, - init: bool, info_text: Option<(String, Instant)>, + is_loading: bool, loading_icon: String, + log_title: String, + port_max_lens: (usize, usize, usize), + ports: Option<(Vec, State)>, selected_panel: SelectablePanel, sorted_by: Option<(Header, SortedOrder)>, + status: HashSet, } -impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData { - fn from(data: (MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)) -> Self { +impl From<&Ui> for FrameData { + fn from(ui: &Ui) -> Self { + let (app_data, gui_data) = (ui.app_data.lock(), ui.gui_state.lock()); + // set max height for container section, needs +5 to deal with docker commands list and borders - let height = data.0.get_container_len(); + let height = app_data.get_container_len(); let height = if height < 12 { u16::try_from(height + 5).unwrap_or_default() } else { 12 }; + let (filter_by, filter_term) = app_data.get_filter(); Self { - columns: data.0.get_width(), - delete_confirm: data.1.get_delete_container(), - has_containers: data.0.get_container_len() > 0, - has_error: data.0.get_error(), + chart_data: app_data.get_chart_data(), + columns: app_data.get_width(), + container_title: app_data.get_container_title(), + delete_confirm: gui_data.get_delete_container(), + filter_by, + filter_term: filter_term.cloned(), + has_containers: app_data.get_container_len() > 0, + has_error: app_data.get_error(), height, - help_visible: data.1.status_contains(&[Status::Help]), - init: data.1.status_contains(&[Status::Init]), - info_text: data.1.info_box_text.clone(), - loading_icon: data.1.get_loading().to_string(), - selected_panel: data.1.get_selected_panel(), - sorted_by: data.0.get_sorted(), + info_text: gui_data.info_box_text.clone(), + is_loading: gui_data.is_loading(), + loading_icon: gui_data.get_loading().to_string(), + log_title: app_data.get_log_title(), + port_max_lens: app_data.get_longest_port(), + ports: app_data.get_selected_ports(), + selected_panel: gui_data.get_selected_panel(), + sorted_by: app_data.get_sorted(), + status: gui_data.get_status(), } } } /// 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 { +fn draw_frame( + f: &mut Frame, + app_data: &Arc>, + gui_state: &Arc>, + fd: &FrameData, +) { + let whole_constraints = if fd.status.contains(&Status::Filter) { vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)] } else { vec![Constraint::Max(1), Constraint::Min(1)] @@ -300,21 +320,21 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>, gui_state: &Arc