diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5dca4c6..0cdbe41 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,7 +17,7 @@ "seccomp=unconfined" ], - "postCreateCommand": "cargo install cross typos-cli", + "postCreateCommand": "rustup target add x86_64-unknown-linux-musl && cargo install cross typos-cli", "mounts": [ "source=/etc/timezone,target=/etc/timezone,type=bind,readonly" diff --git a/.github/release-body.md b/.github/release-body.md index d57c0a8..b482219 100644 --- a/.github/release-body.md +++ b/.github/release-body.md @@ -1,7 +1,27 @@ -### 2023-02-04 +### 2023-03-02 + +### Chores ++ dependencies updated, [aac3ef2b1def3345d749d813d9b76020d6b5e5ca], [4723be7fb2eb101024bb9d5a514e2c6cc51eb6f6], [c69ab4f7c3b873f25ea46958add37be78d23e9cf], [ba6437862dae0f422660a602aeabd6217d023fac], [2bb4c338903e09856053894d9646307e31d32f1c] ++ dev container install x86 musl toolchain, [e650034d50f01a7598876d4f2887df691700e06a] + +### Docs ++ typos removed, [23ad9a5fb3cacf3fb8cb70c65ca9133ed9949e45], [cebb975cb82f653407ec801fd8c726ca6ed68289], [fdc67c9249a239bac97a78b20c9378472865209c] ++ comments improved, [ec962295a8789ff8010604e974969bf618ea7108] + +### Features ++ mouse capture is now more specific, should have substantial performance impact, 10x reduction in cpu usage when mouse is moved observed, as well as fixing intermittent mouse events output bug, [0a1b53111627206cc7436589e5b7212e1b72edb8], [93f7c07f708885f8870da5dfb6d57c62f93c9c78], [c74f6c1179b5f62989eb74f395a56b43a8781b03] ++ improve the styling of the help information popup, [28de74b866f07c8543e46be3cab929eff28953fd] ++ use checked_sub & checked_div for bounds checks, [72279e26ae996353c95a75527f704bac1e4bcf4d] ### Fixes -+ Container runner `FROM scratch` (missing from v0.2.2 D'oh), this now should actually reduce Docker image size by ~60%, [0bd317b7ce6f9f42a614c488099b5fc7a14d91c7] ++ correctly set gui error, [340893a860e99ec4029d12613f2a6de3cb7b47e2] + +### Refactors ++ dead code removed, [b8f5792d1865d3a398cd7f23aa9473a55dc6ea44] ++ improve the get_width function, [04c26fe8fc7c79506921b9cff42825b1ee132737] ++ place ui methods into a Ui struct, [3437df59884f084624031fceb34ea3012a8e2251] ++ get_horizotal/vertical constraints into single method, [e8f5cf9c6f8cd5f807a05fb61e31d7cd1426486f] ++ docker update_everything variables, [074cb957f274675a468f08fecb1c43ff7453217d] see CHANGELOG.md for more details diff --git a/.github/workflows/create_release_and_build.yml b/.github/workflows/create_release_and_build.yml index 118944d..3becdbc 100644 --- a/.github/workflows/create_release_and_build.yml +++ b/.github/workflows/create_release_and_build.yml @@ -38,6 +38,7 @@ jobs: run: cargo install cross --git https://github.com/cross-rs/cross # Build for linux x86_64 + # Should this actually build for musl? - name: build release linux_x86_64 run: cargo build --release # Compress output into tar diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6618f..e081eed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +# v0.2.4 +### 2023-03-02 + +### Chores ++ dependencies updated, [aac3ef2b](https://github.com/mrjackwills/oxker/commit/aac3ef2b1def3345d749d813d9b76020d6b5e5ca), [4723be7f](https://github.com/mrjackwills/oxker/commit/4723be7fb2eb101024bb9d5a514e2c6cc51eb6f6), [c69ab4f7](https://github.com/mrjackwills/oxker/commit/c69ab4f7c3b873f25ea46958add37be78d23e9cf), [ba643786](https://github.com/mrjackwills/oxker/commit/ba6437862dae0f422660a602aeabd6217d023fac), [2bb4c338](https://github.com/mrjackwills/oxker/commit/2bb4c338903e09856053894d9646307e31d32f1c) ++ dev container install x86 musl toolchain, [e650034d](https://github.com/mrjackwills/oxker/commit/e650034d50f01a7598876d4f2887df691700e06a) + +### Docs ++ typos removed, [23ad9a5f](https://github.com/mrjackwills/oxker/commit/23ad9a5fb3cacf3fb8cb70c65ca9133ed9949e45), [cebb975c](https://github.com/mrjackwills/oxker/commit/cebb975cb82f653407ec801fd8c726ca6ed68289), [fdc67c92](https://github.com/mrjackwills/oxker/commit/fdc67c9249a239bac97a78b20c9378472865209c) ++ comments improved, [ec962295](https://github.com/mrjackwills/oxker/commit/ec962295a8789ff8010604e974969bf618ea7108) + +### Features ++ mouse capture is now more specific, should have substantial performance impact, 10x reduction in cpu usage when mouse is moved observed, as well as fixing intermittent mouse events output bug, [0a1b5311](https://github.com/mrjackwills/oxker/commit/0a1b53111627206cc7436589e5b7212e1b72edb8), [93f7c07f](https://github.com/mrjackwills/oxker/commit/93f7c07f708885f8870da5dfb6d57c62f93c9c78), [c74f6c11](https://github.com/mrjackwills/oxker/commit/c74f6c1179b5f62989eb74f395a56b43a8781b03) ++ improve the styling of the help information popup, [28de74b8](https://github.com/mrjackwills/oxker/commit/28de74b866f07c8543e46be3cab929eff28953fd) ++ use checked_sub & checked_div for bounds checks, [72279e26](https://github.com/mrjackwills/oxker/commit/72279e26ae996353c95a75527f704bac1e4bcf4d) + +### Fixes ++ correctly set gui error, [340893a8](https://github.com/mrjackwills/oxker/commit/340893a860e99ec4029d12613f2a6de3cb7b47e2) + +### Refactors ++ dead code removed, [b8f5792d](https://github.com/mrjackwills/oxker/commit/b8f5792d1865d3a398cd7f23aa9473a55dc6ea44) ++ improve the get_width function, [04c26fe8](https://github.com/mrjackwills/oxker/commit/04c26fe8fc7c79506921b9cff42825b1ee132737) ++ place ui methods into a Ui struct, [3437df59](https://github.com/mrjackwills/oxker/commit/3437df59884f084624031fceb34ea3012a8e2251) ++ get_horizotal/vertical constraints into single method, [e8f5cf9c](https://github.com/mrjackwills/oxker/commit/e8f5cf9c6f8cd5f807a05fb61e31d7cd1426486f) ++ docker update_everything variables, [074cb957](https://github.com/mrjackwills/oxker/commit/074cb957f274675a468f08fecb1c43ff7453217d) + # v0.2.3 ### 2023-02-04 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afcfe0e..1005181 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ oxker encourages any, and all, suggestions, bug reports, pull requests, and/or f ## Submitting Issues -Please use the oxker [issue templates for](https://github.com/mrjackwills/oxker/issues/new/choose) any Bug Report, Feature Suggestions, +Please use the oxker [issue templates](https://github.com/mrjackwills/oxker/issues/new/choose) for any Bug Report, Feature Suggestions, Refactor Idea, or Security Vulnerabilities. Don't hesitate to submit any issues or pull requests, regardless of size. diff --git a/Cargo.lock b/Cargo.lock index 837bd00..31770af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] name = "autocfg" @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.4" +version = "4.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" dependencies = [ "bitflags", "clap_derive", @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.1.0" +version = "4.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" dependencies = [ "heck", "proc-macro-error", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" dependencies = [ "os_str_bytes", ] @@ -202,9 +202,9 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f67c7faacd4db07a939f55d66a983a5355358a1f17d32cc9a8d01d1266b9ce" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags", "crossterm_winapi", @@ -227,9 +227,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" dependencies = [ "cc", "cxxbridge-flags", @@ -239,9 +239,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" dependencies = [ "cc", "codespan-reporting", @@ -254,15 +254,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" [[package]] name = "cxxbridge-macro" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" dependencies = [ "proc-macro2", "quote", @@ -370,9 +370,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" dependencies = [ "bytes", "fnv", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856b5cb0902c2b6d65d5fd97dfa30f9b70c7538e770b98eab5ed52d8db923e01" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] name = "hex" @@ -422,9 +422,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -543,19 +543,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "is-terminal" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" dependencies = [ - "hermit-abi 0.3.0", + "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -627,14 +627,14 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "os_str_bytes" @@ -696,13 +696,13 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "oxker" -version = "0.2.3" +version = "0.2.4" dependencies = [ "anyhow", "bollard", "cansi", "clap", - "crossterm 0.26.0", + "crossterm 0.26.1", "futures-util", "parking_lot", "tokio", @@ -732,7 +732,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" dependencies = [ "unicode-ident", ] @@ -871,7 +871,7 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] @@ -914,9 +914,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" dependencies = [ "itoa", "ryu", @@ -972,9 +972,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", @@ -993,18 +993,18 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -1033,9 +1033,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1073,18 +1073,19 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.3.17" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", "serde", @@ -1100,9 +1101,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" dependencies = [ "time-core", ] @@ -1124,9 +1125,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.25.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" dependencies = [ "autocfg", "bytes", @@ -1139,7 +1140,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys", ] [[package]] @@ -1155,9 +1156,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" dependencies = [ "bytes", "futures-core", @@ -1426,21 +1427,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 28cbe37..205a921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxker" -version = "0.2.3" +version = "0.2.4" edition = "2021" authors = ["Jack Wills "] description = "A simple tui to view & control docker containers" @@ -19,7 +19,7 @@ clap={version="4.1", features = ["derive", "unicode", "color"] } crossterm = "0.26" futures-util = "0.3" parking_lot = {version= "0.12"} -tokio = {version = "1.25", features=["full"]} +tokio = {version = "1.26", features=["full"]} tracing = "0.1" tracing-subscriber = "0.3" tui = "0.19" diff --git a/README.md b/README.md index 07fd330..aa4ee99 100644 --- a/README.md +++ b/README.md @@ -21,34 +21,45 @@ ### Cargo Published on crates.io, so if you have cargo installed, simply run -```cargo install oxker``` + +```shell +cargo install oxker +``` ### Docker Published on Docker Hub, with images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6` -`docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker` +```shell +docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker +``` ### Nix Using nix flakes, oxker can be ran directly with -```nix run nixpkgs#oxker``` +```shell +nix run nixpkgs#oxker +``` Without flakes, you can build a shell that contains oxker using -```nix-shell -p oxker``` +```shell +nix-shell -p oxker +``` ### AUR oxker can be installed from the [AUR](https://aur.archlinux.org/packages/oxker) with using an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers): -```paru -S oxker``` +```shell +paru -S oxker +``` ### Pre-Built See the pre-built binaries or, download & install (x86_64 one liner) -```bash +```shell wget https://www.github.com/mrjackwills/oxker/releases/latest/download/oxker_linux_x86_64.tar.gz && tar xzvf oxker_linux_x86_64.tar.gz oxker && install -Dm 755 oxker -t "${HOME}/.local/bin" && @@ -59,19 +70,21 @@ or, for automatic platform selection, download, and installation (to `$HOME/.loc *One should always verify script content before running in a shell* -```bash +```shell curl https://raw.githubusercontent.com/mrjackwills/oxker/main/install.sh | bash ``` ## Run -```oxker``` +```shell +oxker +``` In application controls | button| result| |--|--| | ```( tab )``` or ```( shift+tab )``` | change panel, clicking on a panel also changes the selected panel| -| ```( ↑ ↓ )``` or ```( j k )``` or ```(PgUp PgDown)``` or ```(Home End)```| change selected line in selected panel, mouse scroll also changes selected line | +| ```( ↑ ↓ )``` or ```( j k )``` or ```( PgUp PgDown )``` or ```( Home End )```| change selected line in selected panel, mouse scroll also changes selected line | | ```( enter )```| execute selected docker command| | ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | | ```( 0 )``` | stop sorting | @@ -94,7 +107,9 @@ Available command line arguments ### x86_64 -```cargo build --release``` +```shell +cargo build --release +``` ### Raspberry pi @@ -102,13 +117,17 @@ requires docker & Color::Red, } } - // Dirty way to create order for the state, rather than impl Ord + /// Dirty way to create order for the state, rather than impl Ord pub const fn order(self) -> u8 { match self { Self::Running => 0, diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 441c23a..3169b1e 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -203,7 +203,7 @@ impl AppData { self.containers.start(); } - // select the last container + /// select the last container pub fn containers_end(&mut self) { self.containers.end(); } @@ -213,7 +213,7 @@ impl AppData { self.containers.next(); } - // select the previous container + /// select the previous container pub fn containers_previous(&mut self) { self.containers.previous(); } @@ -428,12 +428,6 @@ impl AppData { .to_string(), ); - let rx_count = count(&container.rx.to_string()); - let tx_count = count(&container.tx.to_string()); - let image_count = count(&container.image); - let name_count = count(&container.name); - let state_count = count(&container.state.to_string()); - let status_count = count(&container.status); let mem_current_count = count( &container .mem_stats @@ -441,35 +435,16 @@ impl AppData { .unwrap_or(&ByteStats::default()) .to_string(), ); - let mem_limit_count = count(&container.mem_limit.to_string()); - if cpu_count > columns.cpu.1 { - columns.cpu.1 = cpu_count; - }; - if image_count > columns.image.1 { - columns.image.1 = image_count; - }; - if mem_current_count > columns.mem.1 { - columns.mem.1 = mem_current_count; - }; - if mem_limit_count > columns.mem.2 { - columns.mem.2 = mem_limit_count; - }; - if name_count > columns.name.1 { - columns.name.1 = name_count; - }; - if state_count > columns.state.1 { - columns.state.1 = state_count; - }; - if status_count > columns.status.1 { - columns.status.1 = status_count; - }; - if rx_count > columns.net_rx.1 { - columns.net_rx.1 = rx_count; - }; - if tx_count > columns.net_tx.1 { - columns.net_tx.1 = tx_count; - }; + columns.cpu.1 = columns.cpu.1.max(cpu_count); + columns.image.1 = columns.image.1.max(count(&container.image)); + columns.mem.1 = columns.mem.1.max(mem_current_count); + columns.mem.2 = columns.mem.2.max(count(&container.mem_limit.to_string())); + columns.name.1 = columns.name.1.max(count(&container.name)); + columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string())); + columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string())); + columns.state.1 = columns.state.1.max(count(&container.state.to_string())); + columns.status.1 = columns.status.1.max(count(&container.status)); } columns } @@ -519,9 +494,7 @@ impl AppData { container.mem_limit.update(mem_limit); } // need to benchmark this? - // if self.get_sorted().is_some() { self.sort_containers(); - // } } /// Update, or insert, containers diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index ad0c500..51b955e 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -7,10 +7,7 @@ use futures_util::StreamExt; use parking_lot::Mutex; use std::{ collections::HashMap, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::{atomic::AtomicBool, Arc}, }; use tokio::{sync::mpsc::Receiver, task::JoinHandle}; use uuid::Uuid; @@ -34,7 +31,7 @@ enum SpawnId { /// 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 -/// Also effectively means that if the docker_update interval minimum will be 1000ms +/// Also effectively means that the docker_update interval minimum will be 1000ms #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] enum Binate { One, @@ -176,7 +173,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 q`./app/oxker` as an entry point, unless the `-s` flag is set + /// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set pub async fn update_all_containers(&mut self) -> Vec<(bool, ContainerId)> { let containers = self .docker @@ -270,14 +267,14 @@ impl DockerData { 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 id = container.id.clone(); let last_updated = container.last_updated; self.spawns .lock() - .entry(SpawnId::Log(id.clone())) + .entry(SpawnId::Log(container.id.clone())) .or_insert_with(|| { - let docker = Arc::clone(&self.docker); 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)) }); @@ -407,7 +404,8 @@ impl DockerData { .values() .into_iter() .for_each(tokio::task::JoinHandle::abort); - self.is_running.store(false, Ordering::SeqCst); + self.is_running + .store(false, std::sync::atomic::Ordering::SeqCst); } } } @@ -436,7 +434,6 @@ impl DockerData { spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; - inner.message_handler().await; } } diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 7e1f649..1d05087 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -4,9 +4,7 @@ use std::sync::{ }; use crossterm::{ - event::{ - DisableMouseCapture, EnableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind, - }, + event::{DisableMouseCapture, KeyCode, MouseButton, MouseEvent, MouseEventKind}, execute, }; use parking_lot::Mutex; @@ -21,7 +19,7 @@ use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{GuiState, SelectablePanel, Status}, + ui::{GuiState, SelectablePanel, Status, Ui}, }; pub use message::InputMessages; @@ -82,27 +80,23 @@ impl InputHandler { /// Toggle the mouse capture (via input of the 'm' key) fn m_key(&mut self) { if self.mouse_capture { - match execute!(std::io::stdout(), DisableMouseCapture) { - Ok(_) => self - .gui_state + if execute!(std::io::stdout(), DisableMouseCapture).is_ok() { + self.gui_state .lock() - .set_info_box("✖ mouse capture disabled".to_owned()), - Err(_) => { - self.app_data - .lock() - .set_error(AppError::MouseCapture(false)); - } + .set_info_box("✖ mouse capture disabled".to_owned()); + } else { + self.app_data + .lock() + .set_error(AppError::MouseCapture(false)); + self.gui_state.lock().status_push(Status::Error); } + } else if Ui::enable_mouse_capture().is_ok() { + self.gui_state + .lock() + .set_info_box("✓ mouse capture enabled".to_owned()); } else { - match execute!(std::io::stdout(), EnableMouseCapture) { - Ok(_) => self - .gui_state - .lock() - .set_info_box("✓ mouse capture enabled".to_owned()), - Err(_) => { - self.app_data.lock().set_error(AppError::MouseCapture(true)); - } - } + self.app_data.lock().set_error(AppError::MouseCapture(true)); + self.gui_state.lock().status_push(Status::Error); }; // If the info box sleep handle is currently being executed, as in 'm' is pressed twice within a 4000ms window @@ -134,7 +128,8 @@ impl InputHandler { .lock() .status_contains(&[Status::Error, Status::Init]); if error_init || self.docker_sender.send(DockerMessage::Quit).await.is_err() { - self.is_running.store(false, Ordering::SeqCst); + self.is_running + .store(false, std::sync::atomic::Ordering::SeqCst); } } diff --git a/src/main.rs b/src/main.rs index 56b198f..ec9eef4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ #![forbid(unsafe_code)] #![warn( + clippy::nursery, + clippy::pedantic, clippy::expect_used, clippy::todo, clippy::unused_async, clippy::unwrap_used )] // Warning - These are indeed pedantic -#![warn(clippy::pedantic)] -#![warn(clippy::nursery)] #![allow( clippy::module_name_repetitions, clippy::doc_markdown, @@ -34,12 +34,12 @@ mod input_handler; mod parse_args; mod ui; -use ui::{create_ui, GuiState, Status}; +use ui::{GuiState, Status, Ui}; use crate::docker_data::DockerMessage; // this is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container -const ENTRY_POINT: &str = "./app/oxker"; +const ENTRY_POINT: &str = "/app/oxker"; const ENV_KEY: &str = "OXKER_RUNTIME"; const ENV_VALUE: &str = "container"; @@ -132,9 +132,7 @@ async fn main() { handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running); if args.gui { - create_ui(app_data, docker_sx, gui_state, is_running, input_sx) - .await - .unwrap_or(()); + Ui::create(app_data, docker_sx, gui_state, is_running, input_sx).await; } else { // Debug mode for testing, mostly pointless, doesn't take terminal info!("in debug mode"); diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 88010c3..f03a68d 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -481,93 +481,253 @@ pub fn heading_bar( /// From a given &str, return the maximum number of chars on a single line fn max_line_width(text: &str) -> usize { - let mut max_line_width = 0; - text.lines().into_iter().for_each(|line| { - let width = line.chars().count(); - if width > max_line_width { - max_line_width = width; + text.lines() + .into_iter() + .map(|i| i.chars().count()) + .max() + .unwrap_or_default() +} + +/// Help popup box needs these three pieces of information +struct HelpInfo { + spans: Vec>, + width: usize, + height: usize, +} + +impl HelpInfo { + /// Find the max width of a Span in &[Spans], although it isn't calculating it correctly + fn calc_width(spans: &[Spans]) -> usize { + spans + .iter() + .flat_map(|x| x.0.iter()) + .map(tui::text::Span::width) + .max() + .unwrap_or(1) + } + + /// Just an empty span, i.e. a new line + fn empty_span<'a>() -> Spans<'a> { + Spans::from(String::new()) + } + + /// generate a span, of given &str and given color + fn span<'a>(input: &str, color: Color) -> Span<'a> { + Span::styled(input.to_owned(), Style::default().fg(color)) + } + + /// Span to black text span + fn black_span<'a>(input: &str) -> Span<'a> { + Self::span(input, Color::Black) + } + + /// Span to white text span + fn white_span<'a>(input: &str) -> Span<'a> { + Self::span(input, Color::White) + } + + /// Generate the `oxker` name span + metadata + fn gen_name() -> Self { + let mut spans = NAME_TEXT + .lines() + .into_iter() + .map(|i| Spans::from(Self::white_span(i))) + .collect::>(); + spans.insert(0, Self::empty_span()); + let width = Self::calc_width(&spans); + let height = spans.len(); + Self { + spans, + width, + height, } - }); - max_line_width + } + + /// Generate the description span + metadata + fn gen_description() -> Self { + let spans = [ + Self::empty_span(), + Spans::from(Self::white_span(DESCRIPTION)), + Self::empty_span(), + ]; + let width = Self::calc_width(&spans); + let height = spans.len(); + Self { + spans: spans.to_vec(), + width, + height, + } + } + + /// Generate the button information span + metadata + fn gen_button() -> Self { + let button_item = |x: &str| Self::white_span(&format!(" {x} ")); + let button_desc = |x: &str| Self::black_span(x); + let or = || button_desc("or"); + let space = || button_desc(" "); + + let spans = [ + Spans::from(vec![ + space(), + button_item("( tab )"), + or(), + button_item("( shift+tab )"), + button_desc("to change panels"), + ]), + Spans::from(vec![ + space(), + button_item("( ↑ ↓ )"), + or(), + button_item("( j k )"), + or(), + button_item("( PgUp PgDown )"), + or(), + button_item("( Home End )"), + button_desc("to change selected line"), + ]), + Spans::from(vec![ + space(), + button_item("( enter )"), + button_desc("to send docker container command"), + ]), + Spans::from(vec![ + space(), + button_item("( h )"), + button_desc("to toggle this help information"), + ]), + Spans::from(vec![ + space(), + button_item("( 0 )"), + button_desc("to stop sort"), + ]), + Spans::from(vec![ + space(), + button_item("( 1 - 9 )"), + button_desc("sort by header - or click header"), + ]), + Spans::from(vec![ + space(), + button_item("( m )"), + button_desc( + "to toggle mouse capture - if disabled, text on screen can be selected & copied", + ), + ]), + Spans::from(vec![ + space(), + button_item("( q )"), + button_desc("to quit at any time"), + ]), + ]; + + let height = spans.len(); + let width = Self::calc_width(&spans); + Self { + spans: spans.to_vec(), + width, + height, + } + } + + /// Generate the final lines, GitHub link etc, + metadata + fn gen_final() -> Self { + let spans = [ + Self::empty_span(), + Spans::from(vec![Self::black_span( + "currently an early work in progress, all and any input appreciated", + )]), + Spans::from(vec![Span::styled( + REPO.to_owned(), + Style::default() + .bg(Color::Magenta) + .fg(Color::Black) + .add_modifier(Modifier::UNDERLINED), + )]), + ]; + let height = spans.len(); + let width = Self::calc_width(&spans); + Self { + spans: spans.to_vec(), + width, + height, + } + } } /// Draw the help box in the centre of the screen -/// TODO should make every line it's own renderable span pub fn help_box(f: &mut Frame<'_, B>) { let title = format!(" {VERSION} "); - let description_text = format!("\n{DESCRIPTION}"); + let name_info = HelpInfo::gen_name(); + let description_info = HelpInfo::gen_description(); + let button_info = HelpInfo::gen_button(); + let final_info = HelpInfo::gen_final(); - let mut help_text = String::from("\n ( tab ) or ( shift+tab ) to change panels"); - help_text - .push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line"); - help_text.push_str("\n ( enter ) to send docker container commands"); - help_text.push_str("\n ( h ) to toggle this help information"); - help_text.push_str("\n ( 0 ) stop sort"); - help_text.push_str("\n ( 1 - 9 ) sort by header - or click header"); - help_text.push_str( - "\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied", + // have to add 10, but shouldn't need to, is an error somewhere + let max_line_width = [ + name_info.width, + description_info.width, + button_info.width, + final_info.width, + ] + .into_iter() + .max() + .unwrap_or_default() + + 10; + let max_height = + name_info.height + description_info.height + button_info.height + final_info.height + 2; + + let area = popup( + max_height, + max_line_width, + f.size(), + BoxLocation::MiddleCentre, ); - help_text.push_str("\n ( q ) to quit at any time"); - help_text.push_str("\n mouse scrolling & clicking also available"); - help_text.push_str("\n\n currently an early work in progress, all and any input appreciated"); - help_text.push_str(format!("\n {}", REPO.trim()).as_str()); - // Find the maximum line widths & height - let all_text = format!("{NAME_TEXT}{description_text}{help_text}"); - let mut max_line_width = max_line_width(&all_text); - let mut lines = all_text.lines().count(); + let split_popup = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Max(name_info.height.try_into().unwrap_or_default()), + Constraint::Max(description_info.height.try_into().unwrap_or_default()), + Constraint::Max(button_info.height.try_into().unwrap_or_default()), + Constraint::Max(final_info.height.try_into().unwrap_or_default()), + ] + .as_ref(), + ) + .split(area); - // Add some vertical and horizontal padding to the info box - lines += 3; - max_line_width += 4; - - let name_paragraph = Paragraph::new(NAME_TEXT) + let name_paragraph = Paragraph::new(name_info.spans) .style(Style::default().bg(Color::Magenta).fg(Color::White)) .block(Block::default()) .alignment(Alignment::Center); - let description_paragraph = Paragraph::new(description_text.as_str()) + let description_paragraph = Paragraph::new(description_info.spans) .style(Style::default().bg(Color::Magenta).fg(Color::Black)) .block(Block::default()) .alignment(Alignment::Center); - let help_paragraph = Paragraph::new(help_text.as_str()) + let help_paragraph = Paragraph::new(button_info.spans) .style(Style::default().bg(Color::Magenta).fg(Color::Black)) .block(Block::default()) .alignment(Alignment::Left); + let final_paragraph = Paragraph::new(final_info.spans) + .style(Style::default().bg(Color::Magenta).fg(Color::Black)) + .block(Block::default()) + .alignment(Alignment::Center); + let block = Block::default() .title(title) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(Style::default().fg(Color::Black)); - let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre); - - let split_popup = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Max(NAME_TEXT.lines().count().try_into().unwrap_or_default()), - Constraint::Max( - description_text - .lines() - .count() - .try_into() - .unwrap_or_default(), - ), - Constraint::Max(help_text.lines().count().try_into().unwrap_or_default()), - ] - .as_ref(), - ) - .split(area); - // Order is important here f.render_widget(Clear, area); f.render_widget(name_paragraph, split_popup[0]); f.render_widget(description_paragraph, split_popup[1]); f.render_widget(help_paragraph, split_popup[2]); + f.render_widget(final_paragraph, split_popup[3]); f.render_widget(block, area); } @@ -640,23 +800,19 @@ pub fn info(f: &mut Frame<'_, B>, text: String) { /// draw a box in the one of the BoxLocations, based on max line width + number of lines fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocation) -> Rect { // Make sure blank_space can't be an negative, as will crash - let blank_vertical = if usize::from(r.height) > text_lines { - (usize::from(r.height) - text_lines) / 2 - } else { - 1 - }; - let blank_horizontal = if usize::from(r.width) > text_width { - (usize::from(r.width) - text_width) / 2 - } else { - 1 + let calc = |x: u16, y: usize| { + (usize::from(x).checked_sub(y).map_or(1usize, |f| f)) + .checked_div(2) + .map_or(1usize, |f| f) }; - let v_constraints = box_location.get_vertical_constraints( + let blank_vertical = calc(r.height, text_lines); + let blank_horizontal = calc(r.width, text_width); + + let (h_constraints, v_constraints) = box_location.get_constraints( + blank_horizontal.try_into().unwrap_or_default(), blank_vertical.try_into().unwrap_or_default(), text_lines.try_into().unwrap_or_default(), - ); - let h_constraints = box_location.get_horizontal_constraints( - blank_horizontal.try_into().unwrap_or_default(), text_width.try_into().unwrap_or_default(), ); @@ -672,3 +828,15 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio .constraints(h_constraints) .split(popup_layout[indexes.0])[indexes.1] } + +// Draw nothing, as in a blank screen +// pub fn nothing(f: &mut Frame<'_, B>) { +// let whole_layout = Layout::default() +// .direction(Direction::Vertical) +// .constraints([Constraint::Min(100)].as_ref()) +// .split(f.size()); + +// let block = Block::default() +// .borders(Borders::NONE); +// f.render_widget(block, whole_layout[0]); +// } diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index ead3245..134c112 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -75,31 +75,45 @@ impl BoxLocation { } } - // Should combine with get_vertical_constraints and just return a tuple of (vc, hc)? - pub const fn get_horizontal_constraints( + /// Get both the vertical and hoziztonal constrains + pub const fn get_constraints( self, + blank_horizontal: u16, blank_vertical: u16, + text_lines: u16, + text_width: u16, + ) -> ([Constraint; 3], [Constraint; 3]) { + ( + Self::get_horizontal_constraints(self, blank_horizontal, text_width), + Self::get_vertical_constraints(self, blank_vertical, text_lines), + ) + } + + const fn get_horizontal_constraints( + self, + blank_horizontal: u16, text_width: u16, ) -> [Constraint; 3] { match self { Self::TopLeft | Self::MiddleLeft | Self::BottomLeft => [ Constraint::Max(text_width), - Constraint::Max(blank_vertical), - Constraint::Max(blank_vertical), + Constraint::Max(blank_horizontal), + Constraint::Max(blank_horizontal), ], Self::TopCentre | Self::MiddleCentre | Self::BottomCentre => [ - Constraint::Max(blank_vertical), + Constraint::Max(blank_horizontal), Constraint::Max(text_width), - Constraint::Max(blank_vertical), + Constraint::Max(blank_horizontal), ], Self::TopRight | Self::MiddleRight | Self::BottomRight => [ - Constraint::Max(blank_vertical), - Constraint::Max(blank_vertical), + Constraint::Max(blank_horizontal), + Constraint::Max(blank_horizontal), Constraint::Max(text_width), ], } } - pub const fn get_vertical_constraints( + + const fn get_vertical_constraints( self, blank_vertical: u16, number_lines: u16, @@ -188,13 +202,13 @@ pub enum Status { /// Global gui_state, stored in an Arc #[derive(Debug, Default, Clone)] pub struct GuiState { - panel_map: HashMap, heading_map: HashMap, - loading_icon: Loading, is_loading: HashSet, + loading_icon: Loading, + panel_map: HashMap, status: HashSet, - pub selected_panel: SelectablePanel, pub info_box_text: Option, + pub selected_panel: SelectablePanel, } impl GuiState { /// Clear panels hash map, so on resize can fix the sizes for mouse clicks @@ -273,7 +287,7 @@ impl GuiState { self.is_loading.insert(uuid); } - /// If is_loading has any entries, return the current loading_icon, else an empty string + /// If is_loading has any entries, return the current loading_icon, else an empty string, which needs to take up the same space, hence ' ' pub fn get_loading(&mut self) -> String { if self.is_loading.is_empty() { String::from(" ") diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a7006a1..1e143ae 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,16 +1,18 @@ use anyhow::Result; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event}, + event::{self, DisableMouseCapture, Event}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use parking_lot::Mutex; use std::{ - io::{self, Write}, + io::{self, Stdout, Write}, sync::{atomic::Ordering, Arc}, + time::Duration, }; use std::{sync::atomic::AtomicBool, time::Instant}; use tokio::sync::mpsc::Sender; +use tracing::error; use tui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Direction, Layout}, @@ -28,110 +30,179 @@ use crate::{ input_handler::InputMessages, }; -/// Take control of the terminal in order to draw gui -pub async fn create_ui( +pub struct Ui { app_data: Arc>, docker_sx: Sender, gui_state: Arc>, + input_poll_rate: Duration, is_running: Arc, + now: Instant, sender: Sender, -) -> Result<()> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let res = run_app( - app_data, - docker_sx, - gui_state, - is_running, - sender, - &mut terminal, - ) - .await; - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("error: {err}"); - } - std::io::stdout().flush().unwrap_or(()); - Ok(()) + terminal: Terminal>, } -/// Run a loop to draw the gui -async fn run_app( - app_data: Arc>, - docker_sx: Sender, - gui_state: Arc>, - is_running: Arc, - sender: Sender, - terminal: &mut Terminal, -) -> Result<(), AppError> { - let update_duration = - std::time::Duration::from_millis(u64::from(app_data.lock().args.docker_interval)); - let input_poll_rate = std::time::Duration::from_millis(75); - let status_dockerconnect = gui_state.lock().status_contains(&[Status::DockerConnect]); - let mut now = Instant::now(); - if status_dockerconnect { +impl Ui { + /// Enable mouse capture, but don't enable capture of all the mouse movements, doing so will improve performance, and is part of the fix for the weird mouse event output bug + pub fn enable_mouse_capture() -> Result<()> { + io::stdout().write_all( + concat!( + crossterm::csi!("?1000h"), + crossterm::csi!("?1015h"), + crossterm::csi!("?1006h"), + ) + .as_bytes(), + )?; + Ok(()) + } + + /// Create a new Ui struct, and execute the drawing loop + pub async fn create( + app_data: Arc>, + docker_sx: Sender, + gui_state: Arc>, + is_running: Arc, + sender: Sender, + ) { + if let Ok(terminal) = Self::setup_terminal() { + let mut ui = Self { + app_data, + docker_sx, + gui_state, + input_poll_rate: std::time::Duration::from_millis(100), + is_running, + now: Instant::now(), + sender, + terminal, + }; + if let Err(e) = ui.draw_ui().await { + error!("{e}"); + } + if let Err(e) = ui.reset_terminal() { + error!("{e}"); + }; + } else { + error!("Terminal Error"); + } + } + + /// Setup the terminal for full-screen drawing mode, with mouse capture + fn setup_terminal() -> Result>> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + Self::enable_mouse_capture()?; + let backend = CrosstermBackend::new(stdout); + Ok(Terminal::new(backend)?) + } + + /// This is a fix for mouse-events being printed to screen, read an event and do nothing with it + fn nullify_event_read(&self) { + if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) { + event::read().ok(); + } + } + + /// reset the terminal back to default settings + pub fn reset_terminal(&mut self) -> Result<()> { + self.terminal.clear()?; + + execute!( + self.terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + disable_raw_mode()?; + self.terminal.show_cursor()?; + Ok(()) + } + + /// Draw the the error message ui, for 5 seconds, with a countdown + fn err_loop(&mut self) -> Result<(), AppError> { let mut seconds = 5; loop { - if seconds < 1 { - break; - } - if now.elapsed() >= std::time::Duration::from_secs(1) { + if self.now.elapsed() >= std::time::Duration::from_secs(1) { seconds -= 1; - now = Instant::now(); + self.now = Instant::now(); + if seconds < 1 { + break; + } } - if terminal + + // This is a fix for mouse-events being printed to screen + self.nullify_event_read(); + + if self + .terminal .draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds))) .is_err() { return Err(AppError::Terminal); } } - } else { - while is_running.load(Ordering::SeqCst) { - if crossterm::event::poll(input_poll_rate).unwrap_or(false) { + Ok(()) + } + + /// The loop for drawing the main UI to the terminal + async fn gui_loop(&mut self) -> Result<(), AppError> { + let update_duration = + std::time::Duration::from_millis(u64::from(self.app_data.lock().args.docker_interval)); + + while self.is_running.load(Ordering::SeqCst) { + if self + .terminal + .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state)) + .is_err() + { + return Err(AppError::Terminal); + } + if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { - sender + self.sender .send(InputMessages::ButtonPress(key.code)) .await .unwrap_or(()); } else if let Event::Mouse(m) = event { - sender + self.sender .send(InputMessages::MouseEvent(m)) .await .unwrap_or(()); } else if let Event::Resize(_, _) = event { - gui_state.lock().clear_area_map(); - terminal.autoresize().unwrap_or(()); + self.gui_state.lock().clear_area_map(); + self.terminal.autoresize().unwrap_or(()); } } } - if now.elapsed() >= update_duration { - docker_sx.send(DockerMessage::Update).await.unwrap_or(()); - now = Instant::now(); - } - if terminal.draw(|f| ui(f, &app_data, &gui_state)).is_err() { - return Err(AppError::Terminal); + if self.now.elapsed() >= update_duration { + self.docker_sx + .send(DockerMessage::Update) + .await + .unwrap_or(()); + self.now = Instant::now(); } } + Ok(()) + } + + /// 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 { + self.err_loop()?; + } else { + self.gui_loop().await?; + } + self.nullify_event_read(); + Ok(()) } - terminal.clear().unwrap_or(()); - Ok(()) } -fn ui( +/// Draw the main ui to a frame of the terminal +fn draw_frame( f: &mut Frame<'_, B>, app_data: &Arc>, gui_state: &Arc>, @@ -183,7 +254,7 @@ fn ui( vec![Constraint::Percentage(100)] }; - // Split into 3, containers+controls, logs, then graphs + // Split into 2, logs, and optional charts let lower_main = Layout::default() .direction(Direction::Vertical) .constraints(lower_split.as_ref())