chore: merge release-v0.10.0 into main
This commit is contained in:
+9
-33
@@ -1,42 +1,18 @@
|
|||||||
### 2024-12-05
|
### 2025-02-23
|
||||||
|
|
||||||
### Chores
|
### Chores
|
||||||
+ dependencies updated, [b78713579c4706d605e5b35fcd832610a0152294], [c6200e8f77f8bb1f0152cb9374029d15cc45df9d]
|
+ dependencies updated, [e5f355a1928f78abdb64e4c5617d6fac06340016], [4539d8ad0705b46d7c89c51c7be482b696d26e5f], [6aee6181136235a1a4f79af9b9748c1801be8bf8], [64d1bdf2bf88407e02f0eded1e03fcfc5ee2d8e3]
|
||||||
+ Rust 1.83 linting, [751d997a3dac823e144ae62e6c1455676e50ddb8]
|
+ .devcontainer dependencies updated, [5c8e76e7bb4d7aab8543c9be09fdbc4ffa446b10]
|
||||||
|
+ example docker-compose.yml updated, [2354b0b9be1ab3795a421512594b2650b9cbdd74]
|
||||||
|
+ Rust 1.84 linting, [3065265e26c30d78ba738cfe731d3901ec1948d0]
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
+ `--no-stderr` cli arg, removes Standard error output from logs, closes #52, [c739637b91c8fa742a69f4d888678d7b3964678c]
|
+ Config file introduced, including customizing color scheme of application, closes #47, [f4d54e1ba8ea1516394aef19511a63e6271f27bf]
|
||||||
+ ContainerPorts use ipaddr, [1b26997d25f748e0d452f41fe41791533046ecdf]
|
+ Enable log timestamps to be set to any given timezone, plus custom timestamp format via the config file, closes #56, [7a5e7a25873d2c270e5808730721ebb5427a051]
|
||||||
|
+ update Rust edition to 2024, [7e4a960b888f1dab524d6045504162cea1171d20]
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
+ update containerised Dockerfile, [0c6f53228f01196e352c2069383ba1e7a10950a8]
|
+ Only draw screen if data or layout has changed, drastically reduces CPU usage, [bfc295c50e982886ccaa5e60b57f10d3690b3f09]
|
||||||
+ calculate_usage overflow, [5106a01f3dcb87ce5a8f1fb7bf49dc6b3c25d03e]
|
|
||||||
+ DockerData spawns insertion error, [d4906d33c26b75d92e7d80040c488faa90a257c6]
|
|
||||||
|
|
||||||
### Refactors
|
|
||||||
+ 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 <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
|
||||||
|
|||||||
+18
-1
@@ -1,3 +1,20 @@
|
|||||||
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.10.0'>v0.10.0</a>
|
||||||
|
### 2025-02-23
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
+ dependencies updated, [e5f355a1](https://github.com/mrjackwills/oxker/commit/e5f355a1928f78abdb64e4c5617d6fac06340016), [4539d8ad](https://github.com/mrjackwills/oxker/commit/4539d8ad0705b46d7c89c51c7be482b696d26e5f), [6aee6181](https://github.com/mrjackwills/oxker/commit/6aee6181136235a1a4f79af9b9748c1801be8bf8), [64d1bdf2](https://github.com/mrjackwills/oxker/commit/64d1bdf2bf88407e02f0eded1e03fcfc5ee2d8e3)
|
||||||
|
+ .devcontainer dependencies updated, [5c8e76e7](https://github.com/mrjackwills/oxker/commit/5c8e76e7bb4d7aab8543c9be09fdbc4ffa446b10)
|
||||||
|
+ example docker-compose.yml updated, [2354b0b9](https://github.com/mrjackwills/oxker/commit/2354b0b9be1ab3795a421512594b2650b9cbdd74)
|
||||||
|
+ Rust 1.84 linting, [3065265e](https://github.com/mrjackwills/oxker/commit/3065265e26c30d78ba738cfe731d3901ec1948d0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
+ Config file introduced, including customizing color scheme of application, closes [#47](https://github.com/mrjackwills/oxker/issues/47), [f4d54e1b](https://github.com/mrjackwills/oxker/commit/f4d54e1ba8ea1516394aef19511a63e6271f27bf)
|
||||||
|
+ Enable log timestamps to be set to any given timezone, plus custom timestamp format via the config file, closes [#56](https://github.com/mrjackwills/oxker/issues/56), [7a5e7a25873d2c270e5808730721ebb5427a051]
|
||||||
|
+ update Rust edition to 2024, [7e4a960b](https://github.com/mrjackwills/oxker/commit/7e4a960b888f1dab524d6045504162cea1171d20)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
+ Only draw screen if data or layout has changed, drastically reduces CPU usage, [bfc295c5](https://github.com/mrjackwills/oxker/commit/bfc295c50e982886ccaa5e60b57f10d3690b3f09)
|
||||||
|
|
||||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.9.0'>v0.9.0</a>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.9.0'>v0.9.0</a>
|
||||||
### 2024-12-05
|
### 2024-12-05
|
||||||
|
|
||||||
@@ -351,7 +368,7 @@
|
|||||||
+ dead code removed, [b8f5792d](https://github.com/mrjackwills/oxker/commit/b8f5792d1865d3a398cd7f23aa9473a55dc6ea44)
|
+ dead code removed, [b8f5792d](https://github.com/mrjackwills/oxker/commit/b8f5792d1865d3a398cd7f23aa9473a55dc6ea44)
|
||||||
+ improve the get_width function, [04c26fe8](https://github.com/mrjackwills/oxker/commit/04c26fe8fc7c79506921b9cff42825b1ee132737)
|
+ 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)
|
+ 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)
|
+ get_horizontal/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)
|
+ docker update_everything variables, [074cb957](https://github.com/mrjackwills/oxker/commit/074cb957f274675a468f08fecb1c43ff7453217d)
|
||||||
|
|
||||||
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.3'>v0.2.3</a>
|
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.2.3'>v0.2.3</a>
|
||||||
|
|||||||
Generated
+90
-44
@@ -90,9 +90,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.95"
|
version = "1.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
@@ -212,9 +212,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.12"
|
version = "1.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
|
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
@@ -240,9 +240,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.28"
|
version = "4.5.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
|
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -250,9 +250,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.27"
|
version = "4.5.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
|
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -416,9 +416,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
@@ -906,6 +906,36 @@ version = "1.0.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3590fea8e9e22d449600c9bbd481a8163bef223e4ff938e5f55899f8cf1adb93"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-tzdb",
|
||||||
|
"jiff-tzdb-platform",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-tzdb-platform"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-tzdb",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.77"
|
version = "0.3.77"
|
||||||
@@ -962,9 +992,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.25"
|
version = "0.4.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
@@ -983,9 +1013,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.3"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
]
|
]
|
||||||
@@ -1038,9 +1068,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.20.2"
|
version = "1.20.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
@@ -1056,7 +1086,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bollard",
|
"bollard",
|
||||||
@@ -1065,6 +1095,7 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"directories",
|
"directories",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"jiff",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1125,6 +1156,21 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1166,7 +1212,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"zerocopy 0.8.16",
|
"zerocopy 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1181,12 +1227,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
|
checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"zerocopy 0.8.16",
|
"zerocopy 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1212,9 +1258,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
@@ -1269,18 +1315,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.217"
|
version = "1.0.218"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.217"
|
version = "1.0.218"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1289,9 +1335,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.138"
|
version = "1.0.139"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1415,9 +1461,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -1627,9 +1673,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.23"
|
version = "0.22.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
|
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.7.1",
|
"indexmap 2.7.1",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1715,9 +1761,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.16"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
@@ -1779,9 +1825,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.13.1"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -1990,9 +2036,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.1"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
|
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -2054,11 +2100,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.16"
|
version = "0.8.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b8c07a70861ce02bad1607b5753ecb2501f67847b9f9ada7c160fff0ec6300c"
|
checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive 0.8.16",
|
"zerocopy-derive 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2074,9 +2120,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.16"
|
version = "0.8.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5226bc9a9a9836e7428936cde76bb6b22feea1a8bfdbc0d241136e4d13417e25"
|
checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+5
-3
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "oxker"
|
name = "oxker"
|
||||||
version = "0.9.0"
|
version = "0.10.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
authors = ["Jack Wills <email@mrjackwills.com>"]
|
authors = ["Jack Wills <email@mrjackwills.com>"]
|
||||||
description = "A simple tui to view & control docker containers"
|
description = "A simple tui to view & control docker containers"
|
||||||
repository = "https://github.com/mrjackwills/oxker"
|
repository = "https://github.com/mrjackwills/oxker"
|
||||||
@@ -33,6 +33,7 @@ clap = { version = "4.5", features = ["color", "derive", "unicode"] }
|
|||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
directories = "6.0"
|
directories = "6.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
jiff = { version = "0.2", features = ["tzdb-bundle-always"] }
|
||||||
parking_lot = { version = "0.12" }
|
parking_lot = { version = "0.12" }
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -43,7 +44,8 @@ tokio-util = "0.7"
|
|||||||
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
toml = { version = "0.8", default-features = false, features = ["parse"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
uuid = { version = "1.12", features = ["fast-rng", "v4"] }
|
uuid = { version = "1.14", features = ["fast-rng", "v4"] }
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -32,13 +32,9 @@ cargo install oxker
|
|||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
Published on <a href='https://hub.docker.com/r/mrjackwills/oxker' target='_blank' rel='noopener noreferrer'>Docker Hub</a> and <a href='https://ghcr.io/mrjackwills/oxker' target='_blank' rel='noopener noreferrer'>ghcr.io</a>,
|
Published on <a href='https://ghcr.io/mrjackwills/oxker' target='_blank' rel='noopener noreferrer'>ghcr.io</a> and <a href='https://hub.docker.com/r/mrjackwills/oxker' target='_blank' rel='noopener noreferrer'>Docker Hub</a>,
|
||||||
with images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6`
|
with images built for `linux/amd64`, `linux/arm64`, and `linux/arm/v6`
|
||||||
|
|
||||||
**via Docker Hub**
|
|
||||||
```shell
|
|
||||||
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker
|
|
||||||
```
|
|
||||||
|
|
||||||
**via ghcr.io**
|
**via ghcr.io**
|
||||||
|
|
||||||
@@ -46,6 +42,11 @@ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=alway
|
|||||||
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always ghcr.io/mrjackwills/oxker
|
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always ghcr.io/mrjackwills/oxker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**via Docker Hub**
|
||||||
|
```shell
|
||||||
|
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker
|
||||||
|
```
|
||||||
|
|
||||||
### Nix
|
### Nix
|
||||||
Using nix flakes, oxker can be ran directly with
|
Using nix flakes, oxker can be ran directly with
|
||||||
|
|
||||||
@@ -100,8 +101,7 @@ curl https://raw.githubusercontent.com/mrjackwills/oxker/main/install.sh | bash
|
|||||||
```shell
|
```shell
|
||||||
oxker
|
oxker
|
||||||
```
|
```
|
||||||
|
In application controls, these, amongst many other settings, can be customized with the [config file](#Config-File)
|
||||||
In application controls
|
|
||||||
| button| result|
|
| button| result|
|
||||||
|--|--|
|
|--|--|
|
||||||
| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel.|
|
| ```( tab )``` or ```( shift+tab )``` | Change panel, clicking on a panel also changes the selected panel.|
|
||||||
@@ -118,6 +118,7 @@ In application controls
|
|||||||
| ```( esc )``` | Close dialog.|
|
| ```( esc )``` | Close dialog.|
|
||||||
|
|
||||||
Available command line arguments
|
Available command line arguments
|
||||||
|
|
||||||
| argument|result|
|
| argument|result|
|
||||||
|--|--|
|
|--|--|
|
||||||
|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).|
|
|```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).|
|
||||||
@@ -126,11 +127,31 @@ Available command line arguments
|
|||||||
|```-t```| Remove timestamps from each log entry.|
|
|```-t```| Remove timestamps from each log entry.|
|
||||||
|```-s```| If running via Docker, will display the oxker container.|
|
|```-s```| If running via Docker, will display the oxker container.|
|
||||||
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
||||||
|
|```--config-file [string]```| Location of a `config.toml`/`config.json`/`config.jsonc`. By default will check the users local config directory.|
|
||||||
|```--host [string]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
|
|```--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.|
|
|```--no-stderr```| Do not include stderr output in logs.|
|
||||||
|```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
|
|```--save-dir [string]```| Save exported logs into a custom directory. Defaults to `$HOME`.|
|
||||||
|
|```--timezone [string]```| Display the Docker logs timestamps in a given [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Defaults to `Etc/UTC`.|
|
||||||
|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
|
|```--use-cli```| Use the Docker application when exec-ing into a container, instead of the Docker API.|
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
|
||||||
|
|
||||||
|
A config file enables the user to persist settings, create a custom keymap, set the color scheme used by the application, and more.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Examples of the config file, alsong with explanations of each value, can be found in the [example_config](https://github.com/mrjackwills/oxker/tree/main/example_config) directory. `oxker` supports `.toml`,`.json`, and `.jsonc` file formats.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If not config file is found, `oxker` will create a `config.toml` in the user's local config directory. Command line arguments will take priority over values from the config file.
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
If running an `oxker` container, the default config location will be `/` rather than the automatically detected platform-specific local config directory, and can be mounted as follows;
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro -v /some_location/config.toml:/config.toml:ro ghcr.io/mrjackwills/oxker
|
||||||
|
```
|
||||||
|
|
||||||
## Build step
|
## Build step
|
||||||
|
|
||||||
### x86_64
|
### x86_64
|
||||||
@@ -163,8 +184,10 @@ If no memory information available, try appending either ```/boot/cmdline.txt```
|
|||||||
|
|
||||||
see <a href="https://forums.raspberrypi.com/viewtopic.php?t=203128" target='_blank' rel='noopener noreferrer'>https://forums.raspberrypi.com/viewtopic.php?t=203128</a> and <a href="https://github.com/docker/for-linux/issues/1112" target='_blank' rel='noopener noreferrer'>https://github.com/docker/for-linux/issues/1112</a>
|
see <a href="https://forums.raspberrypi.com/viewtopic.php?t=203128" target='_blank' rel='noopener noreferrer'>https://forums.raspberrypi.com/viewtopic.php?t=203128</a> and <a href="https://github.com/docker/for-linux/issues/1112" target='_blank' rel='noopener noreferrer'>https://github.com/docker/for-linux/issues/1112</a>
|
||||||
|
|
||||||
|
|
||||||
### Untested on other platforms
|
### Untested on other platforms
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
~~As of yet untested, needs work~~
|
~~As of yet untested, needs work~~
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#############
|
#############
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
# Set env that we're running in a container, so that the application can sleep for 250ms at start
|
# Set env that we're running in a container
|
||||||
ENV OXKER_RUNTIME=container
|
ENV OXKER_RUNTIME=container
|
||||||
|
|
||||||
# Copy application binary from builder image
|
# Copy application binary from builder image
|
||||||
@@ -13,38 +13,27 @@ COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/
|
|||||||
# this is used in the application itself, to stop itself show when running from a docker container, so DO NOT EDIT
|
# this is used in the application itself, to stop itself show when running from a docker container, so DO NOT EDIT
|
||||||
ENTRYPOINT [ "/app/oxker"]
|
ENTRYPOINT [ "/app/oxker"]
|
||||||
|
|
||||||
|
|
||||||
# Dev build for testing
|
# Dev build for testing
|
||||||
# docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
# docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
||||||
|
|
||||||
# Dev build one liner, x86 host
|
# Dev build one liner, x86 host
|
||||||
# docker image prune -a; cargo build --release --target x86_64-unknown-linux-musl && docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
# docker image prune -a; cargo build --release --target x86_64-unknown-linux-musl && docker build -t oxker_dev -f containerised/Dockerfile_dev . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
||||||
|
|
||||||
## One liner to build musl program, build docker image, then execute the image
|
|
||||||
# cargo build --release --target x86_64-unknown-linux-musl && docker build -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
|
||||||
|
|
||||||
# Build production version
|
|
||||||
# docker build --platform linux/arm/v6 --platform linux/arm64 --platform linux/amd64 -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
|
||||||
|
|
||||||
# Buildx command to build musl version for all three platforms, should probably be executed in create_release
|
# Buildx command to build musl version for all three platforms, should probably be executed in create_release
|
||||||
# docker buildx create --use
|
# docker buildx create --use
|
||||||
# docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile .
|
# docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile .
|
||||||
|
|
||||||
|
|
||||||
# Build production version for x86 only, then run
|
|
||||||
# docker build --platform linux/amd64 -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
|
|
||||||
|
|
||||||
# docker build --platform linux/arm/v6 -t oxker_dev -f containerised/Dockerfile .
|
|
||||||
|
|
||||||
### Build docker files and save to .tar file
|
### Build docker files and save to .tar file
|
||||||
|
|
||||||
# docker build --platform linux/amd64 -t oxker_dev_amd64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_amd64.tar oxker_dev_amd64
|
# docker build --platform linux/amd64 -t oxker_amd64 -f containerised/Dockerfile .; docker save -o ./oxker_amd64.tar oxker_amd64
|
||||||
# docker load -i oxker_dev_amd64.tar
|
# docker load -i oxker_amd64.tar
|
||||||
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_amd64
|
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_amd64
|
||||||
|
|
||||||
# docker build --platform linux/arm64 -t oxker_dev_arm64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_arm64.tar oxker_dev_arm64
|
# docker build --platform linux/arm64 -t oxker_arm64 -f containerised/Dockerfile .; docker save -o ./oxker_arm64.tar oxker_arm64
|
||||||
# docker load -i oxker_dev_arm64.tar
|
# docker load -i oxker_arm64.tar
|
||||||
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev oxker_dev_arm64
|
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_arm64
|
||||||
|
|
||||||
# docker build --platform linux/arm/v6 -t oxker_dev_armv6 -f containerised/Dockerfile .; docker save -o ./oxker_dev_armv6.tar oxker_dev_armv6
|
# docker build --platform linux/arm/v6 -t oxker_armv6 -f containerised/Dockerfile .; docker save -o ./oxker_armv6.tar oxker_armv6
|
||||||
# docker load -i oxker_dev_armv6.tar
|
# docker load -i oxker_armv6.tar
|
||||||
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_armv6
|
# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_armv6
|
||||||
+80
-2
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# rust create_release v0.6.1
|
# rust create_release v0.6.2
|
||||||
# 2024-10-21
|
# 2025-02-22
|
||||||
|
|
||||||
STAR_LINE='****************************************'
|
STAR_LINE='****************************************'
|
||||||
CWD=$(pwd)
|
CWD=$(pwd)
|
||||||
@@ -202,24 +202,28 @@ check_cross() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build, using cross-rs, for linux x86 musl
|
||||||
cross_build_x86_linux() {
|
cross_build_x86_linux() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
|
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
|
||||||
cross build --target x86_64-unknown-linux-musl --release
|
cross build --target x86_64-unknown-linux-musl --release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build, using cross-rs, for linux arm64 musl
|
||||||
cross_build_aarch64_linux() {
|
cross_build_aarch64_linux() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
|
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
|
||||||
cross build --target aarch64-unknown-linux-musl --release
|
cross build --target aarch64-unknown-linux-musl --release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build, using cross-rs, for linux armv6 musl
|
||||||
cross_build_armv6_linux() {
|
cross_build_armv6_linux() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
|
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
|
||||||
cross build --target arm-unknown-linux-musleabihf --release
|
cross build --target arm-unknown-linux-musleabihf --release
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build, using cross-rs, for windows x86
|
||||||
cross_build_x86_windows() {
|
cross_build_x86_windows() {
|
||||||
check_cross
|
check_cross
|
||||||
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
|
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
|
||||||
@@ -266,6 +270,34 @@ check_allow_unused() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# build container for amd64 platform
|
||||||
|
build_container_amd64() {
|
||||||
|
echo -e "${YELLOW}docker build --platform linux/amd64 --no-cache -t oxker_amd64 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_amd64.tar oxker_amd64${RESET}"
|
||||||
|
docker build --platform linux/amd64 --no-cache -t oxker_amd64 -f containerised/Dockerfile .
|
||||||
|
docker save -o /tmp/oxker_amd64.tar oxker_amd64
|
||||||
|
}
|
||||||
|
# build container for aarm64 platform
|
||||||
|
build_container_arm64() {
|
||||||
|
echo -e "${YELLOW}docker build --platform linux/arm64 --no-cache -t oxker_arm64 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_arm64.tar oxker_arm64${RESET}"
|
||||||
|
docker build --platform linux/arm64 --no-cache -t oxker_arm64 -f containerised/Dockerfile .
|
||||||
|
docker save -o /tmp/oxker_arm64.tar oxker_arm64
|
||||||
|
}
|
||||||
|
# build container for armv6 platform
|
||||||
|
build_container_armv6() {
|
||||||
|
echo -e "${YELLOW}docker build --platform linux/arm/v6 --no-cache -t oxker_armv6 --no-cache -f containerised/Dockerfile .; docker save -o /tmp/oxker_armv6.tar oxker_armv6${RESET}"
|
||||||
|
docker build --platform linux/arm/v6 --no-cache -t oxker_armv6 -f containerised/Dockerfile .
|
||||||
|
docker save -o /tmp/oxker_armv6.tar oxker_armv6
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build all the containers, this get executed in the github action
|
||||||
|
build_container_all() {
|
||||||
|
build_container_amd64
|
||||||
|
ask_continue
|
||||||
|
build_container_arm64
|
||||||
|
ask_continue
|
||||||
|
build_container_armv6
|
||||||
|
}
|
||||||
|
|
||||||
# Full flow to create a new release
|
# Full flow to create a new release
|
||||||
release_flow() {
|
release_flow() {
|
||||||
check_allow_unused
|
check_allow_unused
|
||||||
@@ -276,6 +308,7 @@ release_flow() {
|
|||||||
|
|
||||||
cargo_test
|
cargo_test
|
||||||
cross_build_all
|
cross_build_all
|
||||||
|
build_container_all
|
||||||
cargo_publish_dry_run
|
cargo_publish_dry_run
|
||||||
|
|
||||||
cd "${CWD}" || error_close "Can't find ${CWD}"
|
cd "${CWD}" || error_close "Can't find ${CWD}"
|
||||||
@@ -379,6 +412,45 @@ build_choice() {
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
build_container_choice() {
|
||||||
|
cmd=(dialog --backtitle "Choose option" --radiolist "choose" 14 80 16)
|
||||||
|
options=(
|
||||||
|
1 "x86 " off
|
||||||
|
2 "aarch64" off
|
||||||
|
3 "armv6" off
|
||||||
|
4 "all" off
|
||||||
|
)
|
||||||
|
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
||||||
|
exitStatus=$?
|
||||||
|
clear
|
||||||
|
if [ $exitStatus -ne 0 ]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
for choice in $choices; do
|
||||||
|
case $choice in
|
||||||
|
0)
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
build_container_amd64
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
build_container_arm64
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
build_container_armv6
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
build_container_all
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,6 +460,7 @@ main() {
|
|||||||
1 "test" off
|
1 "test" off
|
||||||
2 "release" off
|
2 "release" off
|
||||||
3 "build" off
|
3 "build" off
|
||||||
|
4 "docker builds" off
|
||||||
)
|
)
|
||||||
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
|
||||||
exitStatus=$?
|
exitStatus=$?
|
||||||
@@ -414,6 +487,11 @@ main() {
|
|||||||
main
|
main
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
|
4)
|
||||||
|
build_container_choice
|
||||||
|
main
|
||||||
|
break
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
{
|
|
||||||
"color_logs": false,
|
|
||||||
"docker_interval": 1000,
|
|
||||||
"gui": true,
|
|
||||||
"host": "/var/run/docker.sock",
|
|
||||||
"raw_logs": false,
|
|
||||||
"show_self": false,
|
|
||||||
"show_std_err": false,
|
|
||||||
"show_timestamp": true,
|
|
||||||
"use_cli": false,
|
|
||||||
"colors": {
|
|
||||||
"borders": {
|
|
||||||
"selected": "lightcyan",
|
|
||||||
"unselected": "grey"
|
|
||||||
},
|
|
||||||
"chart_cpu": {
|
|
||||||
"background": "reset",
|
|
||||||
"border": "white",
|
|
||||||
"max": "#FFB224",
|
|
||||||
"points": "magenta",
|
|
||||||
"title": "green",
|
|
||||||
"y_axis": "white"
|
|
||||||
},
|
|
||||||
"chart_memory": {
|
|
||||||
"background": "reset",
|
|
||||||
"border": "white",
|
|
||||||
"max": "#FFB224",
|
|
||||||
"points": "cyan",
|
|
||||||
"title": "green",
|
|
||||||
"y_axis": "white"
|
|
||||||
},
|
|
||||||
"chart_ports": {
|
|
||||||
"background": "reset",
|
|
||||||
"border": "white",
|
|
||||||
"headings": "yellow",
|
|
||||||
"text": "white",
|
|
||||||
"title": "green"
|
|
||||||
},
|
|
||||||
"commands": {
|
|
||||||
"background": "reset",
|
|
||||||
"delete": "gray",
|
|
||||||
"pause": "yellow",
|
|
||||||
"restart": "magenta",
|
|
||||||
"resume": "blue",
|
|
||||||
"start": "green",
|
|
||||||
"stop": "red"
|
|
||||||
},
|
|
||||||
"container_state": {
|
|
||||||
"dead": "red",
|
|
||||||
"exited": "red",
|
|
||||||
"paused": "yellow",
|
|
||||||
"removing": "lightred",
|
|
||||||
"restarting": "lightgreen",
|
|
||||||
"running_healthy": "green",
|
|
||||||
"running_unhealthy": "#FFB224",
|
|
||||||
"unknown": "red"
|
|
||||||
},
|
|
||||||
"containers": {
|
|
||||||
"background": "reset",
|
|
||||||
"icon": "white",
|
|
||||||
"text": "blue",
|
|
||||||
"text_rx": "#FFE9C1",
|
|
||||||
"text_tx": "#CD8C8C"
|
|
||||||
},
|
|
||||||
"headers_bar": {
|
|
||||||
"background": "magenta",
|
|
||||||
"loading_spinner": "white",
|
|
||||||
"text": "black",
|
|
||||||
"text_selected": "gray"
|
|
||||||
},
|
|
||||||
"popup_delete": {
|
|
||||||
"background": "white",
|
|
||||||
"text": "black",
|
|
||||||
"text_highlight": "red"
|
|
||||||
},
|
|
||||||
"popup_error": {
|
|
||||||
"background": "red",
|
|
||||||
"text": "white"
|
|
||||||
},
|
|
||||||
"popup_help": {
|
|
||||||
"background": "magenta",
|
|
||||||
"text": "black",
|
|
||||||
"text_highlight": "white"
|
|
||||||
},
|
|
||||||
"popup_info": {
|
|
||||||
"background": "blue",
|
|
||||||
"text": "white"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keymap": {
|
|
||||||
"clear": [
|
|
||||||
"c",
|
|
||||||
"esc"
|
|
||||||
],
|
|
||||||
"delete_confirm": [
|
|
||||||
"y"
|
|
||||||
],
|
|
||||||
"delete_deny": [
|
|
||||||
"n"
|
|
||||||
],
|
|
||||||
"exec": [
|
|
||||||
"e"
|
|
||||||
],
|
|
||||||
"filter_mode": [
|
|
||||||
"/",
|
|
||||||
"F1"
|
|
||||||
],
|
|
||||||
"quit": [
|
|
||||||
"q"
|
|
||||||
],
|
|
||||||
"save_logs": [
|
|
||||||
"s"
|
|
||||||
],
|
|
||||||
"scroll_down_many": [
|
|
||||||
"pagedown"
|
|
||||||
],
|
|
||||||
"scroll_down_one": [
|
|
||||||
"down",
|
|
||||||
"j"
|
|
||||||
],
|
|
||||||
"scroll_end": [
|
|
||||||
"end"
|
|
||||||
],
|
|
||||||
"scroll_start": [
|
|
||||||
"home"
|
|
||||||
],
|
|
||||||
"scroll_up_many": [
|
|
||||||
"pageup"
|
|
||||||
],
|
|
||||||
"scroll_up_one": [
|
|
||||||
"up",
|
|
||||||
"k"
|
|
||||||
],
|
|
||||||
"select_next_panel": [
|
|
||||||
"tab"
|
|
||||||
],
|
|
||||||
"select_previous_panel": [
|
|
||||||
"backtab"
|
|
||||||
],
|
|
||||||
"sort_by_cpu": [
|
|
||||||
"4"
|
|
||||||
],
|
|
||||||
"sort_by_id": [
|
|
||||||
"6"
|
|
||||||
],
|
|
||||||
"sort_by_image": [
|
|
||||||
"7"
|
|
||||||
],
|
|
||||||
"sort_by_memory": [
|
|
||||||
"5"
|
|
||||||
],
|
|
||||||
"sort_by_name": [
|
|
||||||
"1"
|
|
||||||
],
|
|
||||||
"sort_by_rx": [
|
|
||||||
"8"
|
|
||||||
],
|
|
||||||
"sort_by_state": [
|
|
||||||
"2"
|
|
||||||
],
|
|
||||||
"sort_by_status": [
|
|
||||||
"3"
|
|
||||||
],
|
|
||||||
"sort_by_tx": [
|
|
||||||
"9"
|
|
||||||
],
|
|
||||||
"sort_reset": [
|
|
||||||
"0"
|
|
||||||
],
|
|
||||||
"toggle_help": [
|
|
||||||
"h"
|
|
||||||
],
|
|
||||||
"toggle_mouse_capture": [
|
|
||||||
"m"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
// Example JSONC config file
|
// Example JSONC config file
|
||||||
// This needs to be renamed to "config.jsonc" ("config.json" will also work, even if the file is actually a jsonc) in order for oxker to automatically load
|
// This needs to be renamed to "config.jsonc" ("config.json" will also work, even if the file is actually a jsonc) in order for oxker to automatically load
|
||||||
// oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
// oxker will also read .toml and .json files which use the same key/value structure & format as this file
|
||||||
// Every key is optional, with defaults that oxker will choose if missing or invalid
|
// Every key is optional, with defaults that oxker will choose if missing or invalid
|
||||||
// The `--config-file` cli argument can be used to load configuration files from any readable location
|
// The `--config-file` cli argument can be used to load configuration files from any readable location
|
||||||
// Docker update interval in ms, minimum effectively 1000
|
// Docker update interval in ms, minimum effectively 1000
|
||||||
@@ -20,6 +20,11 @@
|
|||||||
"gui": true,
|
"gui": true,
|
||||||
// Docker host location
|
// Docker host location
|
||||||
"host": "/var/run/docker.sock",
|
"host": "/var/run/docker.sock",
|
||||||
|
// Display the timestamp in a custom format, if given option is invalid, it will default to %Y-%m-%dT%H:%M:%S.%8f -> 2025-02-18T12:34:56.01234567
|
||||||
|
// *Should* accept any valid strftime string up to 32 chars, see https://strftime.org/
|
||||||
|
"timestamp_format": "%Y-%m-%dT%H:%M:%S.%8f",
|
||||||
|
// Display the container logs timestamp with a given timezone, if timezone is unknown, defaults to UTC
|
||||||
|
"timezone": "Etc/UTC",
|
||||||
// Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
// Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
||||||
// "save_dir": "$HOME",
|
// "save_dir": "$HOME",
|
||||||
// Force use of docker cli when execing into containers, honestly mostly pointless
|
// Force use of docker cli when execing into containers, honestly mostly pointless
|
||||||
@@ -246,6 +251,26 @@
|
|||||||
// Ports & IP listing text
|
// Ports & IP listing text
|
||||||
"text": "white"
|
"text": "white"
|
||||||
},
|
},
|
||||||
|
// The filter panel
|
||||||
|
"filter": {
|
||||||
|
// Background color of panel
|
||||||
|
"background": "reset",
|
||||||
|
// color of text
|
||||||
|
"text": "gray",
|
||||||
|
// background color of the selected filter by item (Name/Image/Status/All)
|
||||||
|
"selected_filter_background": "gray",
|
||||||
|
// text color of the selected filter by item (Name/Image/Status/All)
|
||||||
|
"selected_filter_text": "black",
|
||||||
|
// Highlighted text color
|
||||||
|
"highlight": "magenta"
|
||||||
|
},
|
||||||
|
// The logs panel, will only be applied if color_logs is false
|
||||||
|
"logs": {
|
||||||
|
// Background color of panel
|
||||||
|
"background": "reset",
|
||||||
|
// text color
|
||||||
|
"text": "reset"
|
||||||
|
},
|
||||||
// The help popup
|
// The help popup
|
||||||
"popup_help": {
|
"popup_help": {
|
||||||
// Background color
|
// Background color
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Example toml config file
|
# oxker config file
|
||||||
|
|
||||||
# This needs to be renamed to "config.toml" in order for oxker to automatically load
|
|
||||||
# oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
# oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
||||||
|
|
||||||
# Every key is optional, with defaults that oxker will choose if missing or invalid
|
# Every key is optional, with defaults that oxker will choose if missing or invalid
|
||||||
# The `--config-file` cli argument can be used to load configuration files from any readable location
|
# The `--config-file` cli argument can be used to load configuration files from any readable location
|
||||||
|
|
||||||
@@ -30,6 +27,13 @@ gui = true
|
|||||||
# Docker host location
|
# Docker host location
|
||||||
host = "/var/run/docker.sock"
|
host = "/var/run/docker.sock"
|
||||||
|
|
||||||
|
# Display the container logs timestamp with a given timezone, if timezone is unknown, defaults to UTC
|
||||||
|
timezone = "Etc/UTC"
|
||||||
|
|
||||||
|
# Display the timestamp in a custom format, if given option is invalid, it will default to %Y-%m-%dT%H:%M:%S.%8f -> 2025-02-18T12:34:56.012345678Z
|
||||||
|
# *Should* accept any valid strftime string up to 32 chars, see https://strftime.org/
|
||||||
|
timestamp_format="%Y-%m-%dT%H:%M:%S.%8f"
|
||||||
|
|
||||||
# Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
# Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
||||||
# save_dir = "$HOME"
|
# save_dir = "$HOME"
|
||||||
|
|
||||||
@@ -141,6 +145,13 @@ text_rx="#FFE9C1"
|
|||||||
# Text color of the TX column
|
# Text color of the TX column
|
||||||
text_tx="#CD8C8C"
|
text_tx="#CD8C8C"
|
||||||
|
|
||||||
|
# The logs panel, will only be applied if color_logs is false
|
||||||
|
[colors.logs]
|
||||||
|
# Background color of panel
|
||||||
|
background = "reset"
|
||||||
|
# text color
|
||||||
|
text="reset"
|
||||||
|
|
||||||
# Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
# Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
||||||
[colors.container_state]
|
[colors.container_state]
|
||||||
dead="red"
|
dead="red"
|
||||||
@@ -152,6 +163,20 @@ running_healthy ="green"
|
|||||||
running_unhealthy="#FFB224"
|
running_unhealthy="#FFB224"
|
||||||
unknown="red"
|
unknown="red"
|
||||||
|
|
||||||
|
# The filter panel
|
||||||
|
[colors.filter]
|
||||||
|
# Background color of panel
|
||||||
|
background = "reset"
|
||||||
|
# color of text
|
||||||
|
text="gray"
|
||||||
|
# background color of the selected filter by item (Name/Image/Status/All)
|
||||||
|
selected_filter_background="gray"
|
||||||
|
# text color of the selected filter by item (Name/Image/Status/All)
|
||||||
|
selected_filter_text="black"
|
||||||
|
# Highlighted text color
|
||||||
|
highlight="magenta"
|
||||||
|
|
||||||
|
|
||||||
# The color the of Docker commands available for each container
|
# The color the of Docker commands available for each container
|
||||||
[colors.commands]
|
[colors.commands]
|
||||||
# Background color of panel
|
# Background color of panel
|
||||||
|
|||||||
+109
-27
@@ -6,6 +6,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use bollard::service::Port;
|
use bollard::service::Port;
|
||||||
|
use jiff::{Timestamp, tz::TimeZone};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
style::Color,
|
style::Color,
|
||||||
widgets::{ListItem, ListState},
|
widgets::{ListItem, ListState},
|
||||||
@@ -34,6 +35,7 @@ impl ContainerId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Only return first 8 chars of id, is usually more than enough for uniqueness
|
/// Only return first 8 chars of id, is usually more than enough for uniqueness
|
||||||
|
/// TODO container id is a hex string, so can assume that 0..=8 will always return a 8 char ascii &str - need to update tests to use real ids, or atleast strings of the correct-ish length
|
||||||
pub fn get_short(&self) -> String {
|
pub fn get_short(&self) -> String {
|
||||||
self.0.chars().take(8).collect::<String>()
|
self.0.chars().take(8).collect::<String>()
|
||||||
}
|
}
|
||||||
@@ -175,25 +177,24 @@ impl<T> StatefulList<T> {
|
|||||||
|
|
||||||
pub fn next(&mut self) {
|
pub fn next(&mut self) {
|
||||||
if !self.items.is_empty() {
|
if !self.items.is_empty() {
|
||||||
self.state.select(Some(self.state.selected().map_or(0, |i| {
|
self.state.select(Some(
|
||||||
if i < self.items.len() - 1 {
|
self.state.selected().map_or(
|
||||||
i + 1
|
0,
|
||||||
} else {
|
|i| {
|
||||||
i
|
if i < self.items.len() - 1 { i + 1 } else { i }
|
||||||
}
|
},
|
||||||
})));
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous(&mut self) {
|
pub fn previous(&mut self) {
|
||||||
if !self.items.is_empty() {
|
if !self.items.is_empty() {
|
||||||
self.state.select(Some(self.state.selected().map_or(0, |i| {
|
self.state.select(Some(
|
||||||
if i == 0 {
|
self.state
|
||||||
0
|
.selected()
|
||||||
} else {
|
.map_or(0, |i| if i == 0 { 0 } else { i - 1 }),
|
||||||
i - 1
|
));
|
||||||
}
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,21 +514,34 @@ pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State);
|
|||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct LogsTz(String);
|
pub struct LogsTz(String);
|
||||||
|
|
||||||
/// The docker log, which should always contain a timestamp, is in the format `2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet`
|
|
||||||
/// So just split at the inclusive index of the first space, needs to be inclusive, hence the use of format to at the space, so that we can remove the whole thing when the `-t` flag is set
|
|
||||||
/// Need to make sure that this isn't an empty string?!
|
|
||||||
impl From<&str> for LogsTz {
|
|
||||||
fn from(value: &str) -> Self {
|
|
||||||
Self(value.split_inclusive(' ').take(1).collect::<String>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for LogsTz {
|
impl fmt::Display for LogsTz {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
write!(f, "{}", self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LogsTz {
|
||||||
|
/// With a given &str, split into a logtz and content, so that we only need to `use split_once()` once
|
||||||
|
/// The docker log, which should always contain a timestamp, is in the format `2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet`
|
||||||
|
pub fn splitter(input: &str) -> (Self, String) {
|
||||||
|
let (tz, content) = input.split_once(' ').unwrap_or_default();
|
||||||
|
(Self(tz.to_owned()), content.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display the timestamp in a given format, and if provided, with a timezone offset
|
||||||
|
pub fn display_with_formatter(&self, tz: Option<&TimeZone>, format: &str) -> Option<String> {
|
||||||
|
self.0.parse::<Timestamp>().map_or(None, |t| {
|
||||||
|
if let Some(tz) = tz.as_ref() {
|
||||||
|
let tz = tz.iana_name()?;
|
||||||
|
let z = t.in_tz(tz).ok()?;
|
||||||
|
Some(z.strftime(format).to_string())
|
||||||
|
} else {
|
||||||
|
Some(t.strftime(format).to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp,
|
/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp,
|
||||||
/// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the
|
/// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the
|
||||||
/// stateful list dependent on whethere the timestamp is in the HashSet or not
|
/// stateful list dependent on whethere the timestamp is in the HashSet or not
|
||||||
@@ -745,15 +759,18 @@ impl Columns {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
use jiff::tz::TimeZone;
|
||||||
use ratatui::widgets::ListItem;
|
use ratatui::widgets::ListItem;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{ContainerImage, Logs, RunningState},
|
app_data::{ContainerImage, Logs, LogsTz, RunningState},
|
||||||
ui::log_sanitizer,
|
ui::log_sanitizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
|
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, State};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Display CpuStats as a string
|
/// Display CpuStats as a string
|
||||||
@@ -813,11 +830,76 @@ mod tests {
|
|||||||
assert_eq!(result, "name_01_name_01_name_01_name_01_");
|
assert_eq!(result, "name_01_name_01_name_01_name_01_");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// LogzTz correctly splits a line by timestamp
|
||||||
|
fn test_container_state_logz_splitter() {
|
||||||
|
let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet";
|
||||||
|
let log_tz = LogsTz::splitter(input);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
log_tz.0,
|
||||||
|
super::LogsTz("2023-01-14T12:01:20.012345678Z".to_owned())
|
||||||
|
);
|
||||||
|
assert_eq!(log_tz.1, "Lorem ipsum dolor sit amet");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// LogsTz display correctly formats with a given timestamp string
|
||||||
|
fn test_container_state_logz_display() {
|
||||||
|
let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet";
|
||||||
|
let log_tz = LogsTz::splitter(input);
|
||||||
|
|
||||||
|
let result = log_tz
|
||||||
|
.0
|
||||||
|
.display_with_formatter(None, "%Y-%m-%dT%H:%M:%S.%8f");
|
||||||
|
assert!(result.is_some());
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!(result, "2023-01-14T12:01:20.01234567");
|
||||||
|
|
||||||
|
let result = log_tz.0.display_with_formatter(None, "%Y-%m-%d %H:%M:%S");
|
||||||
|
assert!(result.is_some());
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!(result, "2023-01-14 12:01:20");
|
||||||
|
|
||||||
|
let result = log_tz.0.display_with_formatter(None, "%Y-%j");
|
||||||
|
assert!(result.is_some());
|
||||||
|
let result = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result, "2023-014");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// LogsTz display correctly formats with a given timestamp string & timezone
|
||||||
|
fn test_container_state_logz_display_with_timezone() {
|
||||||
|
let input = "2023-01-14T12:01:20.012345678Z Lorem ipsum dolor sit amet";
|
||||||
|
let log_tz = LogsTz::splitter(input);
|
||||||
|
|
||||||
|
let timezone = Some(TimeZone::get("Asia/Tokyo").unwrap());
|
||||||
|
let result = log_tz
|
||||||
|
.0
|
||||||
|
.display_with_formatter(timezone.as_ref(), "%Y-%m-%dT%H:%M:%S.%8f");
|
||||||
|
assert!(result.is_some());
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!(result, "2023-01-14T21:01:20.01234567");
|
||||||
|
|
||||||
|
let result = log_tz
|
||||||
|
.0
|
||||||
|
.display_with_formatter(timezone.as_ref(), "%Y-%m-%d %H:%M:%S");
|
||||||
|
assert!(result.is_some());
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!(result, "2023-01-14 21:01:20");
|
||||||
|
|
||||||
|
let result = log_tz.0.display_with_formatter(timezone.as_ref(), "%Y-%j");
|
||||||
|
assert!(result.is_some());
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!(result, "2023-014");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Logs can only contain 1 entry per LogzTz
|
/// Logs can only contain 1 entry per LogzTz
|
||||||
fn test_container_state_logz() {
|
fn test_container_state_logz() {
|
||||||
let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
||||||
let tz = LogsTz::from(input);
|
let (tz, _) = LogsTz::splitter(input);
|
||||||
let mut logs = Logs::default();
|
let mut logs = Logs::default();
|
||||||
let line = log_sanitizer::remove_ansi(input);
|
let line = log_sanitizer::remove_ansi(input);
|
||||||
|
|
||||||
@@ -828,7 +910,7 @@ mod tests {
|
|||||||
assert_eq!(logs.logs.items.len(), 1);
|
assert_eq!(logs.logs.items.len(), 1);
|
||||||
|
|
||||||
let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
||||||
let tz = LogsTz::from(input);
|
let (tz, _) = LogsTz::splitter(input);
|
||||||
let line = log_sanitizer::remove_ansi(input);
|
let line = log_sanitizer::remove_ansi(input);
|
||||||
|
|
||||||
logs.insert(ListItem::new(line.clone()), tz.clone());
|
logs.insert(ListItem::new(line.clone()), tz.clone());
|
||||||
|
|||||||
+74
-13
@@ -11,10 +11,10 @@ use std::{
|
|||||||
mod container_state;
|
mod container_state;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
ENTRY_POINT,
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
config::Config,
|
config::Config,
|
||||||
ui::{log_sanitizer, GuiState, Status},
|
ui::{GuiState, Redraw, Status, log_sanitizer},
|
||||||
ENTRY_POINT,
|
|
||||||
};
|
};
|
||||||
pub use container_state::*;
|
pub use container_state::*;
|
||||||
|
|
||||||
@@ -122,7 +122,9 @@ pub struct AppData {
|
|||||||
error: Option<AppError>,
|
error: Option<AppError>,
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
hidden_containers: Vec<ContainerItem>,
|
hidden_containers: Vec<ContainerItem>,
|
||||||
|
redraw: Arc<Redraw>,
|
||||||
sorted_by: Option<(Header, SortedOrder)>,
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
current_sorted_id: Vec<ContainerId>,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,18 +136,22 @@ pub struct AppData {
|
|||||||
pub error: Option<AppError>,
|
pub error: Option<AppError>,
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
pub hidden_containers: Vec<ContainerItem>,
|
pub hidden_containers: Vec<ContainerItem>,
|
||||||
|
pub current_sorted_id: Vec<ContainerId>,
|
||||||
|
pub redraw: Arc<Redraw>,
|
||||||
pub sorted_by: Option<(Header, SortedOrder)>,
|
pub sorted_by: Option<(Header, SortedOrder)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
/// Generate a default app_state
|
/// Generate a default app_state
|
||||||
pub fn default(config: Config) -> Self {
|
pub fn new(config: Config, redraw: &Arc<Redraw>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
containers: StatefulList::new(vec![]),
|
containers: StatefulList::new(vec![]),
|
||||||
|
current_sorted_id: vec![],
|
||||||
error: None,
|
error: None,
|
||||||
filter: Filter::new(),
|
filter: Filter::new(),
|
||||||
hidden_containers: vec![],
|
hidden_containers: vec![],
|
||||||
|
redraw: Arc::clone(redraw),
|
||||||
sorted_by: None,
|
sorted_by: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +173,7 @@ impl AppData {
|
|||||||
|
|
||||||
/// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by
|
/// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by
|
||||||
fn can_insert(&self, container: &ContainerItem) -> bool {
|
fn can_insert(&self, container: &ContainerItem) -> bool {
|
||||||
self.filter.term.as_ref().map_or(true, |term| {
|
self.filter.term.as_ref().is_none_or(|term| {
|
||||||
let term = term.to_lowercase();
|
let term = term.to_lowercase();
|
||||||
match self.filter.by {
|
match self.filter.by {
|
||||||
FilterBy::All => {
|
FilterBy::All => {
|
||||||
@@ -186,6 +192,7 @@ impl AppData {
|
|||||||
/// sets the state to start if any filtering has occurred
|
/// sets the state to start if any filtering has occurred
|
||||||
/// Also search in the "hidden" vec for items and insert back into the main containers vec
|
/// Also search in the "hidden" vec for items and insert back into the main containers vec
|
||||||
fn filter_containers(&mut self) {
|
fn filter_containers(&mut self) {
|
||||||
|
self.redraw.set_true();
|
||||||
let pre_len = self.get_container_len();
|
let pre_len = self.get_container_len();
|
||||||
|
|
||||||
if !self.hidden_containers.is_empty() {
|
if !self.hidden_containers.is_empty() {
|
||||||
@@ -289,6 +296,7 @@ impl AppData {
|
|||||||
/// Remove the sorted header & order, and sort by default - created datetime
|
/// Remove the sorted header & order, and sort by default - created datetime
|
||||||
pub fn reset_sorted(&mut self) {
|
pub fn reset_sorted(&mut self) {
|
||||||
self.set_sorted(None);
|
self.set_sorted(None);
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort containers based on a given header, if headings match, and already ascending, remove sorting
|
/// Sort containers based on a given header, if headings match, and already ascending, remove sorting
|
||||||
@@ -309,10 +317,19 @@ impl AppData {
|
|||||||
self.sorted_by
|
self.sorted_by
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a vec of the containers ID's in the order they are displayed in the containers panel
|
||||||
|
fn get_current_ids(&self) -> Vec<ContainerId> {
|
||||||
|
self.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|i| i.id.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
/// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending,
|
/// Sort the containers vec, based on a heading (and if clash, then by name), either ascending or descending,
|
||||||
/// If not sort set, then sort by created time
|
/// If not sort set, then sort by created time
|
||||||
pub fn sort_containers(&mut self) {
|
pub fn sort_containers(&mut self) {
|
||||||
if let Some((head, ord)) = self.sorted_by {
|
if let Some((head, ord)) = self.sorted_by {
|
||||||
|
let pre_order = self.get_current_ids();
|
||||||
let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
|
let sort_closure = |a: &ContainerItem, b: &ContainerItem| -> std::cmp::Ordering {
|
||||||
let item_ord = match ord {
|
let item_ord = match ord {
|
||||||
SortedOrder::Asc => (a, b),
|
SortedOrder::Asc => (a, b),
|
||||||
@@ -372,13 +389,19 @@ impl AppData {
|
|||||||
.then_with(|| item_ord.0.id.cmp(&item_ord.1.id)),
|
.then_with(|| item_ord.0.id.cmp(&item_ord.1.id)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.containers.items.sort_by(sort_closure);
|
self.containers.items.sort_by(sort_closure);
|
||||||
} else {
|
if pre_order != self.get_current_ids() {
|
||||||
|
self.redraw.set_true();
|
||||||
|
}
|
||||||
|
} else if self.current_sorted_id != self.get_current_ids() {
|
||||||
self.containers.items.sort_by(|a, b| {
|
self.containers.items.sort_by(|a, b| {
|
||||||
a.created
|
a.created
|
||||||
.cmp(&b.created)
|
.cmp(&b.created)
|
||||||
.then_with(|| a.name.get().cmp(b.name.get()))
|
.then_with(|| a.name.get().cmp(b.name.get()))
|
||||||
});
|
});
|
||||||
|
self.redraw.set_true();
|
||||||
|
self.current_sorted_id = self.get_current_ids();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,21 +437,25 @@ impl AppData {
|
|||||||
/// Select the first container
|
/// Select the first container
|
||||||
pub fn containers_start(&mut self) {
|
pub fn containers_start(&mut self) {
|
||||||
self.containers.start();
|
self.containers.start();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// select the last container
|
/// select the last container
|
||||||
pub fn containers_end(&mut self) {
|
pub fn containers_end(&mut self) {
|
||||||
self.containers.end();
|
self.containers.end();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select the next container
|
/// Select the next container
|
||||||
pub fn containers_next(&mut self) {
|
pub fn containers_next(&mut self) {
|
||||||
self.containers.next();
|
self.containers.next();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// select the previous container
|
/// select the previous container
|
||||||
pub fn containers_previous(&mut self) {
|
pub fn containers_previous(&mut self) {
|
||||||
self.containers.previous();
|
self.containers.previous();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get ListState of containers
|
/// Get ListState of containers
|
||||||
@@ -521,6 +548,11 @@ impl AppData {
|
|||||||
self.get_selected_container().map(|i| i.id.clone())
|
self.get_selected_container().map(|i| i.id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a given ID matches the currently selected container
|
||||||
|
pub fn is_selected_container(&self, id: &ContainerId) -> bool {
|
||||||
|
self.get_selected_container().is_some_and(|i| &i.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the Id and State for the currently selected container - used by the exec check method
|
/// Get the Id and State for the currently selected container - used by the exec check method
|
||||||
pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
|
pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
|
||||||
self.get_selected_container()
|
self.get_selected_container()
|
||||||
@@ -545,6 +577,7 @@ impl AppData {
|
|||||||
pub fn docker_controls_next(&mut self) {
|
pub fn docker_controls_next(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.docker_controls.next();
|
i.docker_controls.next();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,6 +585,7 @@ impl AppData {
|
|||||||
pub fn docker_controls_previous(&mut self) {
|
pub fn docker_controls_previous(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.docker_controls.previous();
|
i.docker_controls.previous();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +593,7 @@ impl AppData {
|
|||||||
pub fn docker_controls_start(&mut self) {
|
pub fn docker_controls_start(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.docker_controls.start();
|
i.docker_controls.start();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,6 +601,7 @@ impl AppData {
|
|||||||
pub fn docker_controls_end(&mut self) {
|
pub fn docker_controls_end(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.docker_controls.end();
|
i.docker_controls.end();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,6 +639,7 @@ impl AppData {
|
|||||||
pub fn log_next(&mut self) {
|
pub fn log_next(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.logs.next();
|
i.logs.next();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,6 +647,7 @@ impl AppData {
|
|||||||
pub fn log_previous(&mut self) {
|
pub fn log_previous(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.logs.previous();
|
i.logs.previous();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,6 +655,7 @@ impl AppData {
|
|||||||
pub fn log_end(&mut self) {
|
pub fn log_end(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.logs.end();
|
i.logs.end();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,6 +663,7 @@ impl AppData {
|
|||||||
pub fn log_start(&mut self) {
|
pub fn log_start(&mut self) {
|
||||||
if let Some(i) = self.get_mut_selected_container() {
|
if let Some(i) = self.get_mut_selected_container() {
|
||||||
i.logs.start();
|
i.logs.start();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,12 +704,14 @@ impl AppData {
|
|||||||
/// Remove single app_state error
|
/// Remove single app_state error
|
||||||
pub fn remove_error(&mut self) {
|
pub fn remove_error(&mut self) {
|
||||||
self.error = None;
|
self.error = None;
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert single app_state error
|
/// Insert single app_state error
|
||||||
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
|
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
|
||||||
gui_state.lock().status_push(status);
|
gui_state.lock().status_push(status);
|
||||||
self.error = Some(error);
|
self.error = Some(error);
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the selected container is a dockerised version of oxker
|
/// Check if the selected container is a dockerised version of oxker
|
||||||
@@ -758,6 +800,9 @@ impl AppData {
|
|||||||
container.tx.update(tx);
|
container.tx.update(tx);
|
||||||
container.mem_limit.update(mem_limit);
|
container.mem_limit.update(mem_limit);
|
||||||
}
|
}
|
||||||
|
if self.is_selected_container(id) {
|
||||||
|
self.redraw.set_true();
|
||||||
|
}
|
||||||
self.sort_containers();
|
self.sort_containers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,6 +838,9 @@ impl AppData {
|
|||||||
// Check is some, else can cause out of bounds error, if containers get removed before a docker update
|
// Check is some, else can cause out of bounds error, if containers get removed before a docker update
|
||||||
if self.containers.items.get(index).is_some() {
|
if self.containers.items.get(index).is_some() {
|
||||||
self.containers.items.remove(index);
|
self.containers.items.remove(index);
|
||||||
|
if self.is_selected_container(id) {
|
||||||
|
self.redraw.set_true();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,6 +920,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// self.redraw.set_true("update_containers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,18 +928,27 @@ impl AppData {
|
|||||||
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
|
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
|
||||||
let color = self.config.color_logs;
|
let color = self.config.color_logs;
|
||||||
let raw = self.config.raw_logs;
|
let raw = self.config.raw_logs;
|
||||||
|
let format = self.config.timestamp_format.clone();
|
||||||
|
let config_tz = self.config.timezone.clone();
|
||||||
|
|
||||||
let timestamp = self.config.show_timestamp;
|
let show_timestamp = self.config.show_timestamp;
|
||||||
|
|
||||||
if let Some(container) = self.get_any_container_by_id(id) {
|
if let Some(container) = self.get_any_container_by_id(id) {
|
||||||
if !container.is_oxker {
|
if !container.is_oxker {
|
||||||
container.last_updated = Self::get_systemtime();
|
container.last_updated = Self::get_systemtime();
|
||||||
let current_len = container.logs.len();
|
let current_len = container.logs.len();
|
||||||
|
|
||||||
for mut i in logs {
|
for mut i in logs {
|
||||||
let tz = LogsTz::from(i.as_str());
|
let (log_tz, log_content) = LogsTz::splitter(i.as_str());
|
||||||
if !timestamp {
|
if show_timestamp {
|
||||||
i = i.replace(&tz.to_string(), "");
|
i = format!(
|
||||||
|
"{} {}",
|
||||||
|
log_tz
|
||||||
|
.display_with_formatter(config_tz.as_ref(), &format)
|
||||||
|
.unwrap_or_else(|| log_tz.to_string()),
|
||||||
|
log_content
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
i = log_content;
|
||||||
}
|
}
|
||||||
let lines = if color {
|
let lines = if color {
|
||||||
log_sanitizer::colorize_logs(&i)
|
log_sanitizer::colorize_logs(&i)
|
||||||
@@ -899,7 +957,7 @@ impl AppData {
|
|||||||
} else {
|
} else {
|
||||||
log_sanitizer::remove_ansi(&i)
|
log_sanitizer::remove_ansi(&i)
|
||||||
};
|
};
|
||||||
container.logs.insert(ListItem::new(lines), tz);
|
container.logs.insert(ListItem::new(lines), log_tz);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the logs selected row for each container
|
// Set the logs selected row for each container
|
||||||
@@ -910,6 +968,9 @@ impl AppData {
|
|||||||
container.logs.end();
|
container.logs.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self.is_selected_container(id) {
|
||||||
|
self.redraw.set_true();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1819,7 +1880,7 @@ mod tests {
|
|||||||
assert_eq!(result, " - container_1 - image_1");
|
assert_eq!(result, " - container_1 - image_1");
|
||||||
|
|
||||||
// On last line of logs
|
// On last line of logs
|
||||||
let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>();
|
let logs = (1..=3).map(|i| format!("{i} {i}")).collect::<Vec<_>>();
|
||||||
app_data.update_log_by_id(logs, &ids[0]);
|
app_data.update_log_by_id(logs, &ids[0]);
|
||||||
let result = app_data.get_log_title();
|
let result = app_data.get_log_title();
|
||||||
assert_eq!(result, " 3/3 - container_1 - image_1");
|
assert_eq!(result, " 3/3 - container_1 - image_1");
|
||||||
@@ -1851,7 +1912,7 @@ mod tests {
|
|||||||
assert_eq!(result, " - container_2 - image_2");
|
assert_eq!(result, " - container_2 - image_2");
|
||||||
|
|
||||||
// On last line of logs
|
// On last line of logs
|
||||||
let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>();
|
let logs = (1..=3).map(|i| format!("{i} {i}")).collect::<Vec<_>>();
|
||||||
app_data.update_log_by_id(logs, &ids[1]);
|
app_data.update_log_by_id(logs, &ids[1]);
|
||||||
let result = app_data.get_log_title();
|
let result = app_data.get_log_title();
|
||||||
assert_eq!(result, " 3/3 - container_2 - image_2");
|
assert_eq!(result, " 3/3 - container_2 - image_2");
|
||||||
|
|||||||
@@ -73,6 +73,22 @@ impl From<Option<ConfigColors>> for AppColors {
|
|||||||
Self::map_color(ep.text.as_deref(), &mut app_colors.popup_error.text);
|
Self::map_color(ep.text.as_deref(), &mut app_colors.popup_error.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter panel
|
||||||
|
if let Some(fc) = config_colors.filter {
|
||||||
|
Self::map_color(fc.background.as_deref(), &mut app_colors.filter.background);
|
||||||
|
Self::map_color(fc.highlight.as_deref(), &mut app_colors.filter.highlight);
|
||||||
|
|
||||||
|
Self::map_color(
|
||||||
|
fc.selected_filter_background.as_deref(),
|
||||||
|
&mut app_colors.filter.selected_filter_background,
|
||||||
|
);
|
||||||
|
Self::map_color(
|
||||||
|
fc.selected_filter_text.as_deref(),
|
||||||
|
&mut app_colors.filter.selected_filter_text,
|
||||||
|
);
|
||||||
|
Self::map_color(fc.text.as_deref(), &mut app_colors.filter.text);
|
||||||
|
}
|
||||||
|
|
||||||
// Help Popup
|
// Help Popup
|
||||||
if let Some(hp) = config_colors.popup_help {
|
if let Some(hp) = config_colors.popup_help {
|
||||||
Self::map_color(
|
Self::map_color(
|
||||||
@@ -172,6 +188,12 @@ impl From<Option<ConfigColors>> for AppColors {
|
|||||||
Self::map_color(cc.start.as_deref(), &mut app_colors.commands.start);
|
Self::map_color(cc.start.as_deref(), &mut app_colors.commands.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logs panel
|
||||||
|
if let Some(cl) = config_colors.logs {
|
||||||
|
Self::map_color(cl.background.as_deref(), &mut app_colors.logs.background);
|
||||||
|
Self::map_color(cl.text.as_deref(), &mut app_colors.logs.text);
|
||||||
|
}
|
||||||
|
|
||||||
// Container State
|
// Container State
|
||||||
if let Some(cs) = config_colors.container_state {
|
if let Some(cs) = config_colors.container_state {
|
||||||
Self::map_color(cs.dead.as_deref(), &mut app_colors.container_state.dead);
|
Self::map_color(cs.dead.as_deref(), &mut app_colors.container_state.dead);
|
||||||
@@ -215,7 +237,9 @@ optional_config_struct!(
|
|||||||
ConfigCommands, background, pause, restart, stop, delete, resume, start;
|
ConfigCommands, background, pause, restart, stop, delete, resume, start;
|
||||||
ConfigContainers, background, icon, text, text_rx, text_tx;
|
ConfigContainers, background, icon, text, text_rx, text_tx;
|
||||||
ConfigContainerState, background, dead, exited, paused, removing, restarting, running_healthy, running_unhealthy, unknown;
|
ConfigContainerState, background, dead, exited, paused, removing, restarting, running_healthy, running_unhealthy, unknown;
|
||||||
ConfigHeadersBar, background, loading_spinner, text, text_selected
|
ConfigFilter, background, text, selected_filter_background, selected_filter_text, highlight;
|
||||||
|
ConfigHeadersBar, background, loading_spinner, text, text_selected;
|
||||||
|
ConfigLogs, background, text
|
||||||
);
|
);
|
||||||
|
|
||||||
config_struct!(
|
config_struct!(
|
||||||
@@ -226,7 +250,9 @@ config_struct!(
|
|||||||
Commands, background, pause, restart, stop, delete, resume, start;
|
Commands, background, pause, restart, stop, delete, resume, start;
|
||||||
Containers, background, icon, text, text_rx, text_tx;
|
Containers, background, icon, text, text_rx, text_tx;
|
||||||
ContainerState, dead, exited, paused, removing, restarting, running_healthy, running_unhealthy, unknown;
|
ContainerState, dead, exited, paused, removing, restarting, running_healthy, running_unhealthy, unknown;
|
||||||
|
Filter, background, text, selected_filter_background, selected_filter_text, highlight;
|
||||||
HeadersBar, background, text_selected, loading_spinner, text;
|
HeadersBar, background, text_selected, loading_spinner, text;
|
||||||
|
Logs, background, text;
|
||||||
PopupDelete, background, text, text_highlight;
|
PopupDelete, background, text, text_highlight;
|
||||||
PopupError, background, text;
|
PopupError, background, text;
|
||||||
PopupHelp, background, text, text_highlight;
|
PopupHelp, background, text, text_highlight;
|
||||||
@@ -242,7 +268,9 @@ pub struct ConfigColors {
|
|||||||
commands: Option<ConfigCommands>,
|
commands: Option<ConfigCommands>,
|
||||||
container_state: Option<ConfigContainerState>,
|
container_state: Option<ConfigContainerState>,
|
||||||
containers: Option<ConfigContainers>,
|
containers: Option<ConfigContainers>,
|
||||||
|
filter: Option<ConfigFilter>,
|
||||||
headers_bar: Option<ConfigHeadersBar>,
|
headers_bar: Option<ConfigHeadersBar>,
|
||||||
|
logs: Option<ConfigLogs>,
|
||||||
popup_delete: Option<ConfigBackgroundTextHighlight>,
|
popup_delete: Option<ConfigBackgroundTextHighlight>,
|
||||||
popup_error: Option<ConfigBackgroundText>,
|
popup_error: Option<ConfigBackgroundText>,
|
||||||
popup_help: Option<ConfigBackgroundTextHighlight>,
|
popup_help: Option<ConfigBackgroundTextHighlight>,
|
||||||
@@ -355,6 +383,30 @@ impl ContainerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default colours for the filter panel
|
||||||
|
impl Filter {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
background: Color::Reset,
|
||||||
|
highlight: Color::Magenta,
|
||||||
|
selected_filter_background: Color::Gray,
|
||||||
|
selected_filter_text: Color::Black,
|
||||||
|
text: Color::Gray,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default colours for the logs panel, only applied if color_logs is false
|
||||||
|
impl Logs {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
background: Color::Reset,
|
||||||
|
text: Color::Reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Default colours for the Error popup
|
/// Default colours for the Error popup
|
||||||
impl PopupError {
|
impl PopupError {
|
||||||
const fn new() -> Self {
|
const fn new() -> Self {
|
||||||
@@ -406,7 +458,9 @@ pub struct AppColors {
|
|||||||
pub commands: Commands,
|
pub commands: Commands,
|
||||||
pub container_state: ContainerState,
|
pub container_state: ContainerState,
|
||||||
pub containers: Containers,
|
pub containers: Containers,
|
||||||
|
pub filter: Filter,
|
||||||
pub headers_bar: HeadersBar,
|
pub headers_bar: HeadersBar,
|
||||||
|
pub logs: Logs,
|
||||||
pub popup_delete: PopupDelete,
|
pub popup_delete: PopupDelete,
|
||||||
pub popup_error: PopupError,
|
pub popup_error: PopupError,
|
||||||
pub popup_help: PopupHelp,
|
pub popup_help: PopupHelp,
|
||||||
@@ -423,7 +477,9 @@ impl AppColors {
|
|||||||
commands: Commands::new(),
|
commands: Commands::new(),
|
||||||
container_state: ContainerState::new(),
|
container_state: ContainerState::new(),
|
||||||
containers: Containers::new(),
|
containers: Containers::new(),
|
||||||
|
filter: Filter::new(),
|
||||||
headers_bar: HeadersBar::new(),
|
headers_bar: HeadersBar::new(),
|
||||||
|
logs: Logs::new(),
|
||||||
popup_delete: PopupDelete::new(),
|
popup_delete: PopupDelete::new(),
|
||||||
popup_error: PopupError::new(),
|
popup_error: PopupError::new(),
|
||||||
popup_help: PopupHelp::new(),
|
popup_help: PopupHelp::new(),
|
||||||
|
|||||||
+29
-4
@@ -1,8 +1,5 @@
|
|||||||
# Example toml config file
|
# oxker config file
|
||||||
|
|
||||||
# This needs to be renamed to "config.toml" in order for oxker to automatically load
|
|
||||||
# oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
# oxker will also read .jsonc and .json files which use the same key/value structure & format as this file
|
||||||
|
|
||||||
# Every key is optional, with defaults that oxker will choose if missing or invalid
|
# Every key is optional, with defaults that oxker will choose if missing or invalid
|
||||||
# The `--config-file` cli argument can be used to load configuration files from any readable location
|
# The `--config-file` cli argument can be used to load configuration files from any readable location
|
||||||
|
|
||||||
@@ -30,6 +27,13 @@ gui = true
|
|||||||
# Docker host location
|
# Docker host location
|
||||||
host = "/var/run/docker.sock"
|
host = "/var/run/docker.sock"
|
||||||
|
|
||||||
|
# Display the container logs timestamp with a given timezone, if timezone is unknown, defaults to UTC
|
||||||
|
timezone = "Etc/UTC"
|
||||||
|
|
||||||
|
# Display the timestamp in a custom format, if given option is invalid, it will default to %Y-%m-%dT%H:%M:%S.%8f -> 2025-02-18T12:34:56.012345678Z
|
||||||
|
# *Should* accept any valid strftime string up to 32 chars, see https://strftime.org/
|
||||||
|
timestamp_format="%Y-%m-%dT%H:%M:%S.%8f"
|
||||||
|
|
||||||
# Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
# Directory for saving exported logs, defaults to `$HOME`, this is automatically *correctly* calculated for Linux, Mac, and Windows
|
||||||
# save_dir = "$HOME"
|
# save_dir = "$HOME"
|
||||||
|
|
||||||
@@ -141,6 +145,13 @@ text_rx="#FFE9C1"
|
|||||||
# Text color of the TX column
|
# Text color of the TX column
|
||||||
text_tx="#CD8C8C"
|
text_tx="#CD8C8C"
|
||||||
|
|
||||||
|
# The logs panel, will only be applied if color_logs is false
|
||||||
|
[colors.logs]
|
||||||
|
# Background color of panel
|
||||||
|
background = "reset"
|
||||||
|
# text color
|
||||||
|
text="reset"
|
||||||
|
|
||||||
# Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
# Each state of a container has a color, which is used in multiple places, i.e. chart titles, state/status/cpu/memory columns in the container section
|
||||||
[colors.container_state]
|
[colors.container_state]
|
||||||
dead="red"
|
dead="red"
|
||||||
@@ -152,6 +163,20 @@ running_healthy ="green"
|
|||||||
running_unhealthy="#FFB224"
|
running_unhealthy="#FFB224"
|
||||||
unknown="red"
|
unknown="red"
|
||||||
|
|
||||||
|
# The filter panel
|
||||||
|
[colors.filter]
|
||||||
|
# Background color of panel
|
||||||
|
background = "reset"
|
||||||
|
# color of text
|
||||||
|
text="gray"
|
||||||
|
# background color of the selected filter by item (Name/Image/Status/All)
|
||||||
|
selected_filter_background="gray"
|
||||||
|
# text color of the selected filter by item (Name/Image/Status/All)
|
||||||
|
selected_filter_text="black"
|
||||||
|
# Highlighted text color
|
||||||
|
highlight="magenta"
|
||||||
|
|
||||||
|
|
||||||
# The color the of Docker commands available for each container
|
# The color the of Docker commands available for each container
|
||||||
[colors.commands]
|
[colors.commands]
|
||||||
# Background color of panel
|
# Background color of panel
|
||||||
|
|||||||
+143
-9
@@ -1,6 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use jiff::tz::TimeZone;
|
||||||
use parse_args::Args;
|
use parse_args::Args;
|
||||||
use parse_config_file::ConfigFile;
|
use parse_config_file::ConfigFile;
|
||||||
mod color_parser;
|
mod color_parser;
|
||||||
@@ -17,7 +18,7 @@ mod parse_config_file;
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub app_colors: AppColors,
|
pub app_colors: AppColors,
|
||||||
pub color_logs: bool,
|
pub color_logs: bool,
|
||||||
pub docker_interval: u32,
|
pub docker_interval_ms: u32,
|
||||||
pub gui: bool,
|
pub gui: bool,
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
pub in_container: bool,
|
pub in_container: bool,
|
||||||
@@ -27,17 +28,19 @@ pub struct Config {
|
|||||||
pub show_self: bool,
|
pub show_self: bool,
|
||||||
pub show_std_err: bool,
|
pub show_std_err: bool,
|
||||||
pub show_timestamp: bool,
|
pub show_timestamp: bool,
|
||||||
|
pub timezone: Option<TimeZone>,
|
||||||
|
pub timestamp_format: String,
|
||||||
pub use_cli: bool,
|
pub use_cli: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Args> for Config {
|
impl From<&Args> for Config {
|
||||||
fn from(args: Args) -> Self {
|
fn from(args: &Args) -> Self {
|
||||||
Self {
|
Self {
|
||||||
app_colors: AppColors::new(),
|
app_colors: AppColors::new(),
|
||||||
color_logs: args.color,
|
color_logs: args.color,
|
||||||
docker_interval: args.docker_interval,
|
docker_interval_ms: args.docker_interval,
|
||||||
gui: !args.gui,
|
gui: !args.gui,
|
||||||
host: args.host,
|
host: args.host.clone(),
|
||||||
in_container: Self::check_if_in_container(),
|
in_container: Self::check_if_in_container(),
|
||||||
keymap: Keymap::new(),
|
keymap: Keymap::new(),
|
||||||
raw_logs: args.raw,
|
raw_logs: args.raw,
|
||||||
@@ -45,6 +48,8 @@ impl From<Args> for Config {
|
|||||||
show_self: !args.show_self,
|
show_self: !args.show_self,
|
||||||
show_std_err: !args.no_std_err,
|
show_std_err: !args.no_std_err,
|
||||||
show_timestamp: !args.timestamp,
|
show_timestamp: !args.timestamp,
|
||||||
|
timezone: Self::parse_timezone(args.timezone.clone()),
|
||||||
|
timestamp_format: Self::parse_timestamp_format(None),
|
||||||
use_cli: args.use_cli,
|
use_cli: args.use_cli,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +60,7 @@ impl From<ConfigFile> for Config {
|
|||||||
Self {
|
Self {
|
||||||
app_colors: AppColors::from(config_file.colors),
|
app_colors: AppColors::from(config_file.colors),
|
||||||
color_logs: config_file.color_logs.unwrap_or(false),
|
color_logs: config_file.color_logs.unwrap_or(false),
|
||||||
docker_interval: config_file.docker_interval.unwrap_or(1000),
|
docker_interval_ms: config_file.docker_interval.unwrap_or(1000),
|
||||||
gui: config_file.gui.unwrap_or(true),
|
gui: config_file.gui.unwrap_or(true),
|
||||||
host: config_file.host,
|
host: config_file.host,
|
||||||
in_container: Self::check_if_in_container(),
|
in_container: Self::check_if_in_container(),
|
||||||
@@ -65,12 +70,43 @@ impl From<ConfigFile> for Config {
|
|||||||
show_self: config_file.show_self.unwrap_or(false),
|
show_self: config_file.show_self.unwrap_or(false),
|
||||||
show_std_err: config_file.show_std_err.unwrap_or(true),
|
show_std_err: config_file.show_std_err.unwrap_or(true),
|
||||||
show_timestamp: config_file.show_timestamp.unwrap_or(true),
|
show_timestamp: config_file.show_timestamp.unwrap_or(true),
|
||||||
|
timezone: Self::parse_timezone(config_file.timezone),
|
||||||
|
timestamp_format: Self::parse_timestamp_format(config_file.timestamp_format),
|
||||||
use_cli: config_file.use_cli.unwrap_or(false),
|
use_cli: config_file.use_cli.unwrap_or(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// A basic timestampt format parser, will only take 32 chars, and checks if the parsed timestamp isn't identical to the given formatter
|
||||||
|
fn parse_timestamp_format(input: Option<String>) -> String {
|
||||||
|
let default = || "%Y-%m-%dT%H:%M:%S.%8f".to_owned();
|
||||||
|
input.map_or_else(default, |input| {
|
||||||
|
if input.chars().count() >= 32
|
||||||
|
|| jiff::Timestamp::now().strftime(&input).to_string() == input
|
||||||
|
{
|
||||||
|
default()
|
||||||
|
} else {
|
||||||
|
input
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to parse a timezone into a jiff::tz::TimeZone
|
||||||
|
/// Also return a format to display the timesampt in
|
||||||
|
fn parse_timezone(input: Option<String>) -> Option<TimeZone> {
|
||||||
|
let timezone_str = input?;
|
||||||
|
let Ok(tz) = jiff::tz::TimeZone::get(&timezone_str) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let current_ts = jiff::Timestamp::now();
|
||||||
|
let offset = tz.to_offset(current_ts);
|
||||||
|
if jiff::tz::TimeZone::UTC.to_offset(current_ts) == offset {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(tz)
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Check if oxker is running inside of a container
|
/// Check if oxker is running inside of a container
|
||||||
fn check_if_in_container() -> bool {
|
fn check_if_in_container() -> bool {
|
||||||
std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE)
|
std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE)
|
||||||
@@ -89,28 +125,126 @@ impl Config {
|
|||||||
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned())
|
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Combine config from CLI into config file, the cli take priority
|
||||||
|
/// make sure color_logs and raw_logs can't clash
|
||||||
|
fn merge_args(mut self, config_from_cli: Self) -> Self {
|
||||||
|
self.color_logs = config_from_cli.color_logs;
|
||||||
|
self.docker_interval_ms = config_from_cli.docker_interval_ms;
|
||||||
|
self.gui = config_from_cli.gui;
|
||||||
|
self.raw_logs = config_from_cli.raw_logs;
|
||||||
|
self.show_self = config_from_cli.show_self;
|
||||||
|
self.show_std_err = config_from_cli.show_std_err;
|
||||||
|
self.show_timestamp = config_from_cli.show_timestamp;
|
||||||
|
self.use_cli = config_from_cli.use_cli;
|
||||||
|
|
||||||
|
if config_from_cli.docker_interval_ms < 1000 {
|
||||||
|
self.docker_interval_ms = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(host) = config_from_cli.host {
|
||||||
|
self.host = Some(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(x) = config_from_cli.save_dir {
|
||||||
|
self.save_dir = Some(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tz) = config_from_cli.timezone {
|
||||||
|
self.timezone = Some(tz);
|
||||||
|
}
|
||||||
|
|
||||||
|
if config_from_cli.raw_logs {
|
||||||
|
self.color_logs = false;
|
||||||
|
}
|
||||||
|
if config_from_cli.color_logs {
|
||||||
|
self.raw_logs = false;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate a new config file
|
/// Generate a new config file
|
||||||
/// First check cli args,
|
/// First check cli args,
|
||||||
/// then if a config file location is given check then
|
/// then if a config file location is given check then
|
||||||
/// Else check the default location
|
/// Else check the default location
|
||||||
/// else just return the default config + the cli args
|
/// else just return the default config + the cli args
|
||||||
|
/// cli args will take precedence over config settings
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let in_container = Self::check_if_in_container();
|
let in_container = Self::check_if_in_container();
|
||||||
|
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
let config_from_cli = Self::from(&args);
|
||||||
|
|
||||||
if let Some(config_file) = &args.config_file {
|
if let Some(config_file) = &args.config_file {
|
||||||
if let Some(config_file) =
|
if let Some(config_file) =
|
||||||
parse_config_file::ConfigFile::try_parse_from_file(config_file)
|
parse_config_file::ConfigFile::try_parse_from_file(config_file)
|
||||||
{
|
{
|
||||||
return Self::from(config_file);
|
return Self::from(config_file).merge_args(config_from_cli);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(config_file) = parse_config_file::ConfigFile::try_parse(in_container) {
|
if let Some(config_file) = parse_config_file::ConfigFile::try_parse(in_container) {
|
||||||
return Self::from(config_file);
|
return Self::from(config_file).merge_args(config_from_cli);
|
||||||
|
}
|
||||||
|
config_from_cli
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::from(args)
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
mod tests {
|
||||||
|
use jiff::tz::TimeZone;
|
||||||
|
|
||||||
|
/// Test the basic timestamp_format parsing/checker function
|
||||||
|
#[test]
|
||||||
|
fn test_config_parse_timestamp_format() {
|
||||||
|
let default = "%Y-%m-%dT%H:%M:%S.%8f";
|
||||||
|
|
||||||
|
let result = super::Config::parse_timestamp_format(None);
|
||||||
|
assert_eq!(result, default);
|
||||||
|
|
||||||
|
let result = super::Config::parse_timestamp_format(Some(String::new()));
|
||||||
|
assert_eq!(result, default);
|
||||||
|
|
||||||
|
let result = super::Config::parse_timestamp_format(Some(" ".to_owned()));
|
||||||
|
assert_eq!(result, default);
|
||||||
|
|
||||||
|
let result = super::Config::parse_timestamp_format(Some(" ".to_owned()));
|
||||||
|
assert_eq!(result, default);
|
||||||
|
|
||||||
|
let result =
|
||||||
|
super::Config::parse_timestamp_format(Some("not a valid formatter".to_owned()));
|
||||||
|
assert_eq!(result, default);
|
||||||
|
|
||||||
|
let result = super::Config::parse_timestamp_format(Some(
|
||||||
|
"%A, %B %d, %Y %I:%M %p %A, %B %d, %Y %I:%M %p".to_owned(),
|
||||||
|
));
|
||||||
|
assert_eq!(result, default);
|
||||||
|
|
||||||
|
let input = "%Y-%m-%d %H:%M:%S";
|
||||||
|
let result = super::Config::parse_timestamp_format(Some(input.to_owned()));
|
||||||
|
assert_eq!(result, input);
|
||||||
|
|
||||||
|
let input = "%Y-%j";
|
||||||
|
let result = super::Config::parse_timestamp_format(Some(input.to_owned()));
|
||||||
|
assert_eq!(result, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Test various timezones get parsed correctly
|
||||||
|
fn test_config_parse_timezone() {
|
||||||
|
assert!(super::Config::parse_timezone(None).is_none());
|
||||||
|
|
||||||
|
// Timezone with no offset just return None
|
||||||
|
for i in ["Europe/London", "Africa/Accra"] {
|
||||||
|
assert!(super::Config::parse_timezone(Some(i.to_owned())).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected = Some(TimeZone::get("Asia/Tokyo").unwrap());
|
||||||
|
// string case ignored
|
||||||
|
for i in ["ASIA/TOKYO", "asia/tokyo", "aSiA/tOkYo"] {
|
||||||
|
let result = super::Config::parse_timezone(Some(i.to_owned()));
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ pub struct Args {
|
|||||||
#[clap(long = "no-stderr")]
|
#[clap(long = "no-stderr")]
|
||||||
pub no_std_err: bool,
|
pub no_std_err: bool,
|
||||||
|
|
||||||
|
/// Display the container logs timestamp with a given timezone, default is UTC
|
||||||
|
#[clap(long="timezone", short = None)]
|
||||||
|
pub timezone: Option<String>,
|
||||||
|
|
||||||
/// Directory for saving exported logs, defaults to `$HOME`
|
/// Directory for saving exported logs, defaults to `$HOME`
|
||||||
#[clap(long="save-dir", short = None)]
|
#[clap(long="save-dir", short = None)]
|
||||||
pub save_dir: Option<String>,
|
pub save_dir: Option<String>,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ enum ConfigFileType {
|
|||||||
impl TryFrom<&PathBuf> for ConfigFileType {
|
impl TryFrom<&PathBuf> for ConfigFileType {
|
||||||
type Error = AppError;
|
type Error = AppError;
|
||||||
|
|
||||||
|
/// Only allow toml, json, or jsonc files
|
||||||
fn try_from(value: &PathBuf) -> Result<Self, AppError> {
|
fn try_from(value: &PathBuf) -> Result<Self, AppError> {
|
||||||
let err = || AppError::IO(format!("Can't parse give config file: {}", value.display()));
|
let err = || AppError::IO(format!("Can't parse give config file: {}", value.display()));
|
||||||
let Some(ext) = value.extension() else {
|
let Some(ext) = value.extension() else {
|
||||||
@@ -47,8 +48,8 @@ impl ConfigFileType {
|
|||||||
.map(|base_dirs| base_dirs.config_local_dir().join(env!("CARGO_PKG_NAME")))
|
.map(|base_dirs| base_dirs.config_local_dir().join(env!("CARGO_PKG_NAME")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// should take in a pathbuf as well?
|
/// Return the default filename + path for a given filetype
|
||||||
fn get_default_filename(self, in_container: bool) -> PathBuf {
|
fn get_default_path_name(self, in_container: bool) -> PathBuf {
|
||||||
let suffix = match self {
|
let suffix = match self {
|
||||||
Self::Json | Self::JsoncAsJson => "config.json",
|
Self::Json | Self::JsoncAsJson => "config.json",
|
||||||
Self::Jsonc => "config.jsonc",
|
Self::Jsonc => "config.jsonc",
|
||||||
@@ -58,34 +59,34 @@ impl ConfigFileType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl ConfigFileType
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||||
pub struct ConfigFile {
|
pub struct ConfigFile {
|
||||||
pub color_logs: Option<bool>,
|
pub color_logs: Option<bool>,
|
||||||
|
pub colors: Option<ConfigColors>,
|
||||||
pub docker_interval: Option<u32>,
|
pub docker_interval: Option<u32>,
|
||||||
pub gui: Option<bool>,
|
pub gui: Option<bool>,
|
||||||
pub host: Option<String>,
|
pub host: Option<String>,
|
||||||
|
pub keymap: Option<ConfigKeymap>,
|
||||||
pub raw_logs: Option<bool>,
|
pub raw_logs: Option<bool>,
|
||||||
pub show_timestamp: Option<bool>,
|
|
||||||
pub save_dir: Option<String>,
|
pub save_dir: Option<String>,
|
||||||
pub show_self: Option<bool>,
|
pub show_self: Option<bool>,
|
||||||
pub show_std_err: Option<bool>,
|
pub show_std_err: Option<bool>,
|
||||||
|
pub show_timestamp: Option<bool>,
|
||||||
|
pub timestamp_format: Option<String>,
|
||||||
|
pub timezone: Option<String>,
|
||||||
pub use_cli: Option<bool>,
|
pub use_cli: Option<bool>,
|
||||||
pub colors: Option<ConfigColors>,
|
|
||||||
pub keymap: Option<ConfigKeymap>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigFile {
|
impl ConfigFile {
|
||||||
/// Attempt to create an example.config.toml file, will attempt to recursively create the directories as well
|
/// Attempt to create a config.toml file, will attempt to recursively create the directories as well
|
||||||
fn create_example_file(in_container: bool) -> Result<(), AppError> {
|
fn crate_config_file(in_container: bool) -> Result<(), AppError> {
|
||||||
if in_container {
|
if in_container {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_dir = ConfigFileType::get_config_dir(in_container)
|
let config_dir = ConfigFileType::get_config_dir(in_container)
|
||||||
.ok_or_else(|| AppError::IO("config_dir".to_owned()))?;
|
.ok_or_else(|| AppError::IO("config_dir".to_owned()))?;
|
||||||
let file_name = config_dir.join("example.config.toml");
|
let file_name = config_dir.join("config.toml");
|
||||||
|
|
||||||
if !std::fs::exists(&file_name).map_err(|i| AppError::IO(i.to_string()))? {
|
if !std::fs::exists(&file_name).map_err(|i| AppError::IO(i.to_string()))? {
|
||||||
if !std::fs::exists(&config_dir).map_err(|i| AppError::IO(i.to_string()))? {
|
if !std::fs::exists(&config_dir).map_err(|i| AppError::IO(i.to_string()))? {
|
||||||
@@ -134,6 +135,7 @@ impl ConfigFile {
|
|||||||
|
|
||||||
/// Resolve conflict in the args, this is handled automatically by Clap, basically just by rejecting it
|
/// Resolve conflict in the args, this is handled automatically by Clap, basically just by rejecting it
|
||||||
/// But here we can just change the options - although maybe should be also reject to follow the same behaviour as Clap?
|
/// But here we can just change the options - although maybe should be also reject to follow the same behaviour as Clap?
|
||||||
|
/// TODO I think this is duplicated with the merge_args fn
|
||||||
fn resolve_conflict(&mut self) {
|
fn resolve_conflict(&mut self) {
|
||||||
if let Some(color) = self.color_logs.as_ref() {
|
if let Some(color) = self.color_logs.as_ref() {
|
||||||
if *color {
|
if *color {
|
||||||
@@ -171,7 +173,7 @@ impl ConfigFile {
|
|||||||
ConfigFileType::Json,
|
ConfigFileType::Json,
|
||||||
] {
|
] {
|
||||||
if let Ok(mut config_file) =
|
if let Ok(mut config_file) =
|
||||||
Self::parse_config_file(file_type, &file_type.get_default_filename(in_container))
|
Self::parse_config_file(file_type, &file_type.get_default_path_name(in_container))
|
||||||
{
|
{
|
||||||
Self::resolve_conflict(&mut config_file);
|
Self::resolve_conflict(&mut config_file);
|
||||||
|
|
||||||
@@ -181,7 +183,7 @@ impl ConfigFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.is_none() {
|
if config.is_none() {
|
||||||
Self::create_example_file(in_container).ok();
|
Self::crate_config_file(in_container).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
config
|
config
|
||||||
@@ -222,15 +224,6 @@ mod tests {
|
|||||||
assert_eq!(Keymap::from(result.keymap), Keymap::new());
|
assert_eq!(Keymap::from(result.keymap), Keymap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// make sure example.config.json matches the default keymap
|
|
||||||
fn test_parse_config_keymap_json() {
|
|
||||||
let example_json = include_str!("../../example_config/example.config.json");
|
|
||||||
let result = ConfigFile::parse(super::ConfigFileType::Json, example_json).unwrap();
|
|
||||||
assert!(result.keymap.is_some());
|
|
||||||
assert_eq!(Keymap::from(result.keymap), Keymap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// All configs parsed and are equal
|
/// All configs parsed and are equal
|
||||||
fn test_parse_config_keymap_all() {
|
fn test_parse_config_keymap_all() {
|
||||||
@@ -239,11 +232,6 @@ mod tests {
|
|||||||
assert!(result_jsonc.keymap.is_some());
|
assert!(result_jsonc.keymap.is_some());
|
||||||
let result_jsonc = result_jsonc.keymap.unwrap();
|
let result_jsonc = result_jsonc.keymap.unwrap();
|
||||||
|
|
||||||
let example_json = include_str!("../../example_config/example.config.json");
|
|
||||||
let result_json = ConfigFile::parse(super::ConfigFileType::Json, example_json).unwrap();
|
|
||||||
assert!(result_json.keymap.is_some());
|
|
||||||
let result_json = result_json.keymap.unwrap();
|
|
||||||
|
|
||||||
let example_toml = include_str!("./config.toml");
|
let example_toml = include_str!("./config.toml");
|
||||||
let result_toml = ConfigFile::parse(super::ConfigFileType::Toml, example_toml).unwrap();
|
let result_toml = ConfigFile::parse(super::ConfigFileType::Toml, example_toml).unwrap();
|
||||||
assert!(result_toml.keymap.is_some());
|
assert!(result_toml.keymap.is_some());
|
||||||
@@ -251,7 +239,6 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(Keymap::from(Some(result_toml.clone())), Keymap::new());
|
assert_eq!(Keymap::from(Some(result_toml.clone())), Keymap::new());
|
||||||
assert_eq!(result_toml, result_jsonc);
|
assert_eq!(result_toml, result_jsonc);
|
||||||
assert_eq!(result_jsonc, result_json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -272,15 +259,6 @@ mod tests {
|
|||||||
assert_eq!(AppColors::from(result.colors), AppColors::new());
|
assert_eq!(AppColors::from(result.colors), AppColors::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
/// make sure config.toml matches the default app colors
|
|
||||||
fn test_parse_config_colors_json() {
|
|
||||||
let example_json = include_str!("../../example_config/example.config.json");
|
|
||||||
let result = ConfigFile::parse(super::ConfigFileType::Json, example_json).unwrap();
|
|
||||||
assert!(result.colors.is_some());
|
|
||||||
assert_eq!(AppColors::from(result.colors), AppColors::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// All configs parsed and are equal
|
/// All configs parsed and are equal
|
||||||
fn test_parse_config_colors_all() {
|
fn test_parse_config_colors_all() {
|
||||||
@@ -289,11 +267,6 @@ mod tests {
|
|||||||
assert!(result_jsonc.colors.is_some());
|
assert!(result_jsonc.colors.is_some());
|
||||||
let result_jsonc = result_jsonc.colors.unwrap();
|
let result_jsonc = result_jsonc.colors.unwrap();
|
||||||
|
|
||||||
let example_json = include_str!("../../example_config/example.config.json");
|
|
||||||
let result_json = ConfigFile::parse(super::ConfigFileType::Json, example_json).unwrap();
|
|
||||||
assert!(result_json.colors.is_some());
|
|
||||||
let result_json = result_json.colors.unwrap();
|
|
||||||
|
|
||||||
let example_toml = include_str!("./config.toml");
|
let example_toml = include_str!("./config.toml");
|
||||||
let result_toml = ConfigFile::parse(super::ConfigFileType::Toml, example_toml).unwrap();
|
let result_toml = ConfigFile::parse(super::ConfigFileType::Toml, example_toml).unwrap();
|
||||||
assert!(result_toml.colors.is_some());
|
assert!(result_toml.colors.is_some());
|
||||||
@@ -301,6 +274,5 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(AppColors::from(Some(result_toml.clone())), AppColors::new());
|
assert_eq!(AppColors::from(Some(result_toml.clone())), AppColors::new());
|
||||||
assert_eq!(result_toml, result_jsonc);
|
assert_eq!(result_toml, result_jsonc);
|
||||||
assert_eq!(result_jsonc, result_json);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use bollard::{
|
use bollard::{
|
||||||
|
Docker,
|
||||||
container::{
|
container::{
|
||||||
ListContainersOptions, LogsOptions, MemoryStatsStats, RemoveContainerOptions,
|
ListContainersOptions, LogsOptions, MemoryStatsStats, RemoveContainerOptions,
|
||||||
StartContainerOptions, Stats, StatsOptions,
|
StartContainerOptions, Stats, StatsOptions,
|
||||||
},
|
},
|
||||||
service::ContainerSummary,
|
service::ContainerSummary,
|
||||||
Docker,
|
|
||||||
};
|
};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
sync::{Arc, atomic::AtomicUsize},
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::{Receiver, Sender},
|
sync::mpsc::{Receiver, Sender},
|
||||||
@@ -19,11 +19,11 @@ use tokio::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
ENTRY_POINT,
|
||||||
app_data::{AppData, ContainerId, DockerCommand, State},
|
app_data::{AppData, ContainerId, DockerCommand, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
config::Config,
|
config::Config,
|
||||||
ui::{GuiState, Status},
|
ui::{GuiState, Status},
|
||||||
ENTRY_POINT,
|
|
||||||
};
|
};
|
||||||
mod message;
|
mod message;
|
||||||
pub use message::DockerMessage;
|
pub use message::DockerMessage;
|
||||||
@@ -46,7 +46,7 @@ impl 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
|
/// 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
|
/// 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
|
/// Binate value is toggled when all handles have been spawned off
|
||||||
/// Also effectively means that the docker_update interval minimum will be 1000ms
|
/// Also effectively means that the minimum docker_update interval will be 1000ms
|
||||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
enum Binate {
|
enum Binate {
|
||||||
One,
|
One,
|
||||||
@@ -64,8 +64,8 @@ impl Binate {
|
|||||||
|
|
||||||
pub struct DockerData {
|
pub struct DockerData {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
config: Config,
|
|
||||||
binate: Binate,
|
binate: Binate,
|
||||||
|
config: Config,
|
||||||
docker: Arc<Docker>,
|
docker: Arc<Docker>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
receiver: Receiver<DockerMessage>,
|
receiver: Receiver<DockerMessage>,
|
||||||
@@ -350,6 +350,7 @@ impl DockerData {
|
|||||||
GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if match control {
|
if match control {
|
||||||
DockerCommand::Delete => {
|
DockerCommand::Delete => {
|
||||||
|
gui_state.lock().set_delete_container(None);
|
||||||
docker
|
docker
|
||||||
.remove_container(
|
.remove_container(
|
||||||
id.get(),
|
id.get(),
|
||||||
@@ -400,7 +401,8 @@ impl DockerData {
|
|||||||
|
|
||||||
/// Send an update message every x ms, where x is the args.docker_interval
|
/// Send an update message every x ms, where x is the args.docker_interval
|
||||||
fn heartbeat(config: &Config, docker_tx: Sender<DockerMessage>) {
|
fn heartbeat(config: &Config, docker_tx: Sender<DockerMessage>) {
|
||||||
let update_duration = std::time::Duration::from_millis(u64::from(config.docker_interval));
|
let update_duration =
|
||||||
|
std::time::Duration::from_millis(u64::from(config.docker_interval_ms));
|
||||||
let mut now = std::time::Instant::now();
|
let mut now = std::time::Instant::now();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
+11
-8
@@ -1,16 +1,16 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{Read, Stdout, Write},
|
io::{Read, Stdout, Write},
|
||||||
sync::{atomic::AtomicBool, mpsc::Sender, Arc},
|
sync::{Arc, atomic::AtomicBool, mpsc::Sender},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bollard::{
|
use bollard::{
|
||||||
exec::{CreateExecOptions, ResizeExecOptions, StartExecOptions, StartExecResults},
|
|
||||||
Docker,
|
Docker,
|
||||||
|
exec::{CreateExecOptions, ResizeExecOptions, StartExecOptions, StartExecResults},
|
||||||
};
|
};
|
||||||
use crossterm::terminal::enable_raw_mode;
|
use crossterm::terminal::enable_raw_mode;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
@@ -253,10 +253,7 @@ impl ExecMode {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if let Ok(StartExecResults::Attached {
|
match docker
|
||||||
mut output,
|
|
||||||
mut input,
|
|
||||||
}) = docker
|
|
||||||
.start_exec(
|
.start_exec(
|
||||||
&exec_result.id,
|
&exec_result.id,
|
||||||
Some(StartExecOptions {
|
Some(StartExecOptions {
|
||||||
@@ -266,6 +263,10 @@ impl ExecMode {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
Ok(StartExecResults::Attached {
|
||||||
|
mut output,
|
||||||
|
mut input,
|
||||||
|
}) => {
|
||||||
if let Some(tty) = AsyncTTY::get(&cancel_token) {
|
if let Some(tty) = AsyncTTY::get(&cancel_token) {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
enable_raw_mode().ok();
|
enable_raw_mode().ok();
|
||||||
@@ -298,10 +299,12 @@ impl ExecMode {
|
|||||||
|
|
||||||
self.internal_cleanup()?;
|
self.internal_cleanup()?;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
_ => {
|
||||||
return Err(AppError::Terminal);
|
return Err(AppError::Terminal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{BufWriter, Write},
|
io::{BufWriter, Write},
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{Arc, atomic::AtomicBool},
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ use crate::{
|
|||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
config,
|
config,
|
||||||
docker_data::DockerMessage,
|
docker_data::DockerMessage,
|
||||||
exec::{tty_readable, ExecMode},
|
exec::{ExecMode, tty_readable},
|
||||||
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
||||||
};
|
};
|
||||||
pub use message::InputMessages;
|
pub use message::InputMessages;
|
||||||
@@ -37,17 +37,17 @@ pub struct InputHandler {
|
|||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
mouse_capture: bool,
|
mouse_capture: bool,
|
||||||
rec: Receiver<InputMessages>,
|
rx: Receiver<InputMessages>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputHandler {
|
impl InputHandler {
|
||||||
/// Initialize self, and running the message handling loop
|
/// Initialize self, and running the message handling loop
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
rec: Receiver<InputMessages>,
|
|
||||||
docker_tx: Sender<DockerMessage>,
|
docker_tx: Sender<DockerMessage>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
|
rx: Receiver<InputMessages>,
|
||||||
) {
|
) {
|
||||||
let keymap = app_data.lock().config.keymap.clone();
|
let keymap = app_data.lock().config.keymap.clone();
|
||||||
let mut inner = Self {
|
let mut inner = Self {
|
||||||
@@ -56,7 +56,7 @@ impl InputHandler {
|
|||||||
gui_state,
|
gui_state,
|
||||||
is_running,
|
is_running,
|
||||||
keymap,
|
keymap,
|
||||||
rec,
|
rx,
|
||||||
mouse_capture: true,
|
mouse_capture: true,
|
||||||
};
|
};
|
||||||
inner.message_handler().await;
|
inner.message_handler().await;
|
||||||
@@ -64,7 +64,7 @@ impl InputHandler {
|
|||||||
|
|
||||||
/// check for incoming messages
|
/// check for incoming messages
|
||||||
async fn message_handler(&mut self) {
|
async fn message_handler(&mut self) {
|
||||||
while let Some(message) = self.rec.recv().await {
|
while let Some(message) = self.rx.recv().await {
|
||||||
match message {
|
match message {
|
||||||
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
||||||
InputMessages::MouseEvent(mouse_event) => {
|
InputMessages::MouseEvent(mouse_event) => {
|
||||||
@@ -603,8 +603,6 @@ impl InputHandler {
|
|||||||
|
|
||||||
/// Handle mouse button events
|
/// Handle mouse button events
|
||||||
fn mouse_press(&self, mouse_event: MouseEvent) {
|
fn mouse_press(&self, mouse_event: MouseEvent) {
|
||||||
// If in help panel, ignore?
|
|
||||||
|
|
||||||
let status = self.gui_state.lock().get_status();
|
let status = self.gui_state.lock().get_status();
|
||||||
if status.contains(&Status::Help) {
|
if status.contains(&Status::Help) {
|
||||||
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
|
let mouse_point = Rect::new(mouse_event.column, mouse_event.row, 1, 1);
|
||||||
@@ -627,7 +625,7 @@ impl InputHandler {
|
|||||||
self.gui_state.lock().status_push(Status::Help);
|
self.gui_state.lock().status_push(Status::Help);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gui_state.lock().get_intersect_panel(mouse_point);
|
self.gui_state.lock().check_panel_intersect(mouse_point);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -1,6 +1,6 @@
|
|||||||
use app_data::AppData;
|
use app_data::AppData;
|
||||||
use app_error::AppError;
|
use app_error::AppError;
|
||||||
use bollard::{Docker, API_DEFAULT_VERSION};
|
use bollard::{API_DEFAULT_VERSION, Docker};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use docker_data::DockerData;
|
use docker_data::DockerData;
|
||||||
use input_handler::InputMessages;
|
use input_handler::InputMessages;
|
||||||
@@ -8,12 +8,12 @@ use parking_lot::Mutex;
|
|||||||
use std::{
|
use std::{
|
||||||
process,
|
process,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
use tokio::sync::mpsc::{Receiver, Sender};
|
||||||
use tracing::{error, info, Level};
|
use tracing::{Level, error, info};
|
||||||
|
|
||||||
mod app_data;
|
mod app_data;
|
||||||
mod app_error;
|
mod app_error;
|
||||||
@@ -23,7 +23,7 @@ mod exec;
|
|||||||
mod input_handler;
|
mod input_handler;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use ui::{GuiState, Status, Ui};
|
use ui::{GuiState, Redraw, Status, Ui};
|
||||||
|
|
||||||
use crate::docker_data::DockerMessage;
|
use crate::docker_data::DockerMessage;
|
||||||
|
|
||||||
@@ -87,10 +87,10 @@ fn handler_init(
|
|||||||
) {
|
) {
|
||||||
tokio::spawn(input_handler::InputHandler::start(
|
tokio::spawn(input_handler::InputHandler::start(
|
||||||
Arc::clone(app_data),
|
Arc::clone(app_data),
|
||||||
input_rx,
|
|
||||||
docker_sx.clone(),
|
docker_sx.clone(),
|
||||||
Arc::clone(gui_state),
|
Arc::clone(gui_state),
|
||||||
Arc::clone(is_running),
|
Arc::clone(is_running),
|
||||||
|
input_rx,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +98,10 @@ fn handler_init(
|
|||||||
async fn main() {
|
async fn main() {
|
||||||
setup_tracing();
|
setup_tracing();
|
||||||
let config = config::Config::new();
|
let config = config::Config::new();
|
||||||
|
let redraw = Arc::new(Redraw::new());
|
||||||
|
|
||||||
let app_data = Arc::new(Mutex::new(AppData::default(config.clone())));
|
let app_data = Arc::new(Mutex::new(AppData::new(config.clone(), &redraw)));
|
||||||
let gui_state = Arc::new(Mutex::new(GuiState::default()));
|
let gui_state = Arc::new(Mutex::new(GuiState::new(&redraw)));
|
||||||
let is_running = Arc::new(AtomicBool::new(true));
|
let is_running = Arc::new(AtomicBool::new(true));
|
||||||
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
|
let (docker_tx, docker_rx) = tokio::sync::mpsc::channel(32);
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ async fn main() {
|
|||||||
if config.gui {
|
if config.gui {
|
||||||
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
|
let (input_tx, input_rx) = tokio::sync::mpsc::channel(32);
|
||||||
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
|
||||||
Ui::start(app_data, gui_state, input_tx, is_running).await;
|
Ui::start(app_data, gui_state, input_tx, is_running, redraw).await;
|
||||||
} else {
|
} else {
|
||||||
info!("in debug mode\n");
|
info!("in debug mode\n");
|
||||||
let mut now = std::time::Instant::now();
|
let mut now = std::time::Instant::now();
|
||||||
@@ -120,7 +121,7 @@ async fn main() {
|
|||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
if let Some(Ok(to_sleep)) = u128::from(config.docker_interval)
|
if let Some(Ok(to_sleep)) = u128::from(config.docker_interval_ms)
|
||||||
.checked_sub(now.elapsed().as_millis())
|
.checked_sub(now.elapsed().as_millis())
|
||||||
.map(u64::try_from)
|
.map(u64::try_from)
|
||||||
{
|
{
|
||||||
@@ -148,6 +149,8 @@ async fn main() {
|
|||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bollard::service::{ContainerSummary, Port};
|
use bollard::service::{ContainerSummary, Port};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -156,13 +159,14 @@ mod tests {
|
|||||||
RunningState, State, StatefulList,
|
RunningState, State, StatefulList,
|
||||||
},
|
},
|
||||||
config::{AppColors, Config, Keymap},
|
config::{AppColors, Config, Keymap},
|
||||||
|
ui::Redraw,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Default test config, has timestamps turned off
|
/// Default test config, has timestamps turned off
|
||||||
pub const fn gen_config() -> Config {
|
pub fn gen_config() -> Config {
|
||||||
Config {
|
Config {
|
||||||
color_logs: false,
|
color_logs: false,
|
||||||
docker_interval: 1000,
|
docker_interval_ms: 1000,
|
||||||
gui: true,
|
gui: true,
|
||||||
host: None,
|
host: None,
|
||||||
show_std_err: false,
|
show_std_err: false,
|
||||||
@@ -172,8 +176,10 @@ mod tests {
|
|||||||
show_self: false,
|
show_self: false,
|
||||||
app_colors: AppColors::new(),
|
app_colors: AppColors::new(),
|
||||||
keymap: Keymap::new(),
|
keymap: Keymap::new(),
|
||||||
|
timestamp_format: "HH:MM:SS.NNNNN dd-mm-yyyy".to_owned(),
|
||||||
show_timestamp: false,
|
show_timestamp: false,
|
||||||
use_cli: false,
|
use_cli: false,
|
||||||
|
timezone: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,8 +204,10 @@ mod tests {
|
|||||||
AppData {
|
AppData {
|
||||||
containers: StatefulList::new(containers.to_vec()),
|
containers: StatefulList::new(containers.to_vec()),
|
||||||
hidden_containers: vec![],
|
hidden_containers: vec![],
|
||||||
|
current_sorted_id: vec![],
|
||||||
error: None,
|
error: None,
|
||||||
sorted_by: None,
|
sorted_by: None,
|
||||||
|
redraw: Arc::new(Redraw::new()),
|
||||||
filter: Filter::new(),
|
filter: Filter::new(),
|
||||||
config: gen_config(),
|
config: gen_config(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
pub mod log_sanitizer {
|
pub mod log_sanitizer {
|
||||||
|
|
||||||
use cansi::{v3::categorise_text, Color as CansiColor, Intensity};
|
use cansi::{Color as CansiColor, Intensity, v3::categorise_text};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Attempt to colorize the given string to ratatui standards
|
/// Attempt to colorize the given string to ratatui standards
|
||||||
/// TODO this is somewhat slow/cpu intensive
|
|
||||||
pub fn colorize_logs<'a>(input: &str) -> Vec<Line<'a>> {
|
pub fn colorize_logs<'a>(input: &str) -> Vec<Line<'a>> {
|
||||||
vec![Line::from(
|
vec![Line::from(
|
||||||
categorise_text(input)
|
categorise_text(input)
|
||||||
@@ -92,7 +91,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Return test raw, as in show escape codes
|
/// Return test raw, as in show escape codes
|
||||||
fn color_match_raw() {
|
fn test_color_match_raw() {
|
||||||
let result = log_sanitizer::raw(INPUT);
|
let result = log_sanitizer::raw(INPUT);
|
||||||
let expected = vec![Line {
|
let expected = vec![Line {
|
||||||
spans: [Span {
|
spans: [Span {
|
||||||
@@ -110,7 +109,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Use the escape codes to colorize the text
|
/// Use the escape codes to colorize the text
|
||||||
fn color_match_colorize() {
|
fn test_color_match_colorize() {
|
||||||
let result = log_sanitizer::colorize_logs(INPUT);
|
let result = log_sanitizer::colorize_logs(INPUT);
|
||||||
let expected = vec![Line {
|
let expected = vec![Line {
|
||||||
spans: vec![
|
spans: vec![
|
||||||
@@ -143,7 +142,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Remove all escape ansi codes from given input
|
/// Remove all escape ansi codes from given input
|
||||||
fn color_match_remove_ansi() {
|
fn test_color_match_remove_ansi() {
|
||||||
let result = log_sanitizer::remove_ansi(INPUT);
|
let result = log_sanitizer::remove_ansi(INPUT);
|
||||||
let expected = vec![Line {
|
let expected = vec![Line {
|
||||||
spans: vec![Span {
|
spans: vec![Span {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Direction, Layout, Rect},
|
layout::{Alignment, Direction, Layout, Rect},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
symbols,
|
symbols,
|
||||||
text::Span,
|
text::Span,
|
||||||
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
|
widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{FrameData, CONSTRAINT_50_50};
|
use super::{CONSTRAINT_50_50, FrameData};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{ByteStats, CpuStats, State, Stats},
|
app_data::{ByteStats, CpuStats, State, Stats},
|
||||||
config::AppColors,
|
config::AppColors,
|
||||||
@@ -73,8 +73,6 @@ impl ChartType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mem_stats, mem_dataset, mem.1, "", cpu.2
|
|
||||||
// current, dataset, max, name, state
|
|
||||||
/// Create charts
|
/// Create charts
|
||||||
fn make_chart<'a, T: Stats + Display>(
|
fn make_chart<'a, T: Stats + Display>(
|
||||||
chart_type: ChartType,
|
chart_type: ChartType,
|
||||||
@@ -98,7 +96,6 @@ fn make_chart<'a, T: Stats + Display>(
|
|||||||
.fg(chart_type.get_title_color(colors, state))
|
.fg(chart_type.get_title_color(colors, state))
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
))
|
))
|
||||||
// .bg(chart_type.get_bg_color(colors))
|
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(Style::default().fg(chart_type.get_border_color(colors))),
|
.border_style(Style::default().fg(chart_type.get_border_color(colors))),
|
||||||
@@ -113,16 +110,10 @@ fn make_chart<'a, T: Stats + Display>(
|
|||||||
Style::default().add_modifier(Modifier::BOLD).fg(max_color),
|
Style::default().add_modifier(Modifier::BOLD).fg(max_color),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.style(
|
.style(Style::new().fg(chart_type.get_y_axis_color(colors)))
|
||||||
Style::new()
|
|
||||||
// .bg(chart_type.get_bg_color(colors))
|
|
||||||
.fg(chart_type.get_y_axis_color(colors)),
|
|
||||||
)
|
|
||||||
// Add 0.01, so that max point is always visible?
|
// Add 0.01, so that max point is always visible?
|
||||||
.bounds([0.0, max.get_value() + 0.01]),
|
.bounds([0.0, max.get_value() + 0.01]),
|
||||||
)
|
)
|
||||||
|
|
||||||
// .style(Style::new().bg(chart_type.get_bg_color(colors)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the cpu + mem charts
|
/// Draw the cpu + mem charts
|
||||||
@@ -133,16 +124,20 @@ pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
|||||||
.constraints(CONSTRAINT_50_50)
|
.constraints(CONSTRAINT_50_50)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let cpu_dataset = vec![Dataset::default()
|
let cpu_dataset = vec![
|
||||||
|
Dataset::default()
|
||||||
.marker(symbols::Marker::Dot)
|
.marker(symbols::Marker::Dot)
|
||||||
.style(Style::default().fg(colors.chart_cpu.points))
|
.style(Style::default().fg(colors.chart_cpu.points))
|
||||||
.graph_type(GraphType::Line)
|
.graph_type(GraphType::Line)
|
||||||
.data(&cpu.0)];
|
.data(&cpu.0),
|
||||||
let mem_dataset = vec![Dataset::default()
|
];
|
||||||
|
let mem_dataset = vec![
|
||||||
|
Dataset::default()
|
||||||
.marker(symbols::Marker::Dot)
|
.marker(symbols::Marker::Dot)
|
||||||
.style(Style::default().fg(colors.chart_memory.points))
|
.style(Style::default().fg(colors.chart_memory.points))
|
||||||
.graph_type(GraphType::Line)
|
.graph_type(GraphType::Line)
|
||||||
.data(&mem.0)];
|
.data(&mem.0),
|
||||||
|
];
|
||||||
|
|
||||||
let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
|
let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
@@ -178,10 +173,10 @@ mod tests {
|
|||||||
app_data::State,
|
app_data::State,
|
||||||
config::AppColors,
|
config::AppColors,
|
||||||
ui::{
|
ui::{
|
||||||
draw_blocks::tests::{
|
|
||||||
expected_to_vec, get_result, insert_chart_data, test_setup, COLOR_ORANGE,
|
|
||||||
},
|
|
||||||
FrameData,
|
FrameData,
|
||||||
|
draw_blocks::tests::{
|
||||||
|
COLOR_ORANGE, expected_to_vec, get_result, insert_chart_data, test_setup,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -440,7 +435,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Custom colos correctly applied to each part of the charts
|
/// Custom colos correctly applied to each part of the charts
|
||||||
fn test_custom_colors() {
|
fn test_draw_blocks_charts_custom_colors() {
|
||||||
let mut colors = AppColors::new();
|
let mut colors = AppColors::new();
|
||||||
|
|
||||||
colors.chart_cpu.background = Color::White;
|
colors.chart_cpu.background = Color::White;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Modifier, Style, Stylize},
|
style::{Modifier, Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{List, ListItem, Paragraph},
|
widgets::{List, ListItem, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::generate_block;
|
use super::generate_block;
|
||||||
@@ -61,8 +61,8 @@ mod tests {
|
|||||||
config::AppColors,
|
config::AppColors,
|
||||||
tests::gen_container_summary,
|
tests::gen_container_summary,
|
||||||
ui::{
|
ui::{
|
||||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup, BORDER_CHARS},
|
|
||||||
FrameData,
|
FrameData,
|
||||||
|
draw_blocks::tests::{BORDER_CHARS, expected_to_vec, get_result, test_setup},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use std::sync::Arc;
|
|||||||
use super::MARGIN;
|
use super::MARGIN;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Modifier, Style, Stylize},
|
style::{Modifier, Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{List, ListItem, Paragraph},
|
widgets::{List, ListItem, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -16,7 +16,7 @@ use crate::{
|
|||||||
ui::{FrameData, GuiState, SelectablePanel},
|
ui::{FrameData, GuiState, SelectablePanel},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{generate_block, CIRCLE};
|
use super::{CIRCLE, generate_block};
|
||||||
|
|
||||||
/// Format the container data to display nicely on the screen
|
/// Format the container data to display nicely on the screen
|
||||||
fn format_containers<'a>(colors: AppColors, i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
fn format_containers<'a>(colors: AppColors, i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
||||||
@@ -142,11 +142,11 @@ mod tests {
|
|||||||
app_data::{ContainerImage, ContainerName, ContainerStatus, State, StatefulList},
|
app_data::{ContainerImage, ContainerName, ContainerStatus, State, StatefulList},
|
||||||
config::AppColors,
|
config::AppColors,
|
||||||
ui::{
|
ui::{
|
||||||
draw_blocks::tests::{
|
|
||||||
expected_to_vec, get_result, test_setup, TuiTestSetup, BORDER_CHARS, COLOR_ORANGE,
|
|
||||||
COLOR_RX, COLOR_TX,
|
|
||||||
},
|
|
||||||
FrameData,
|
FrameData,
|
||||||
|
draw_blocks::tests::{
|
||||||
|
BORDER_CHARS, COLOR_ORANGE, COLOR_RX, COLOR_TX, TuiTestSetup, expected_to_vec,
|
||||||
|
get_result, test_setup,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -318,7 +318,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
let colors = setup.app_data.lock().config.app_colors;
|
||||||
@@ -677,7 +677,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
let colors = setup.app_data.lock().config.app_colors;
|
||||||
@@ -778,7 +778,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
let mut colors = AppColors::new();
|
let mut colors = AppColors::new();
|
||||||
@@ -852,7 +852,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
|
||||||
@@ -898,7 +898,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
|
||||||
@@ -945,7 +945,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
@@ -991,7 +991,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
@@ -1037,7 +1037,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
@@ -1083,7 +1083,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
@@ -1129,7 +1129,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
@@ -1175,7 +1175,7 @@ mod tests {
|
|||||||
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Direction, Layout},
|
layout::{Alignment, Direction, Layout},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{CONSTRAINT_BUTTONS, CONSTRAINT_POPUP};
|
use super::{CONSTRAINT_BUTTONS, CONSTRAINT_POPUP};
|
||||||
@@ -14,8 +14,8 @@ use crate::{
|
|||||||
app_data::ContainerName,
|
app_data::ContainerName,
|
||||||
config::{AppColors, Keymap},
|
config::{AppColors, Keymap},
|
||||||
ui::{
|
ui::{
|
||||||
gui_state::{BoxLocation, Region},
|
|
||||||
DeleteButton, GuiState,
|
DeleteButton, GuiState,
|
||||||
|
gui_state::{BoxLocation, Region},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+26
-21
@@ -1,11 +1,11 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::Alignment,
|
layout::Alignment,
|
||||||
style::Style,
|
style::Style,
|
||||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{max_line_width, NAME, VERSION};
|
use super::{NAME, VERSION, max_line_width};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
config::{AppColors, Keymap},
|
config::{AppColors, Keymap},
|
||||||
@@ -16,11 +16,11 @@ use super::popup;
|
|||||||
|
|
||||||
/// Draw an error popup over whole screen
|
/// Draw an error popup over whole screen
|
||||||
pub fn draw(
|
pub fn draw(
|
||||||
f: &mut Frame,
|
colors: AppColors,
|
||||||
error: &AppError,
|
error: &AppError,
|
||||||
|
f: &mut Frame,
|
||||||
keymap: &Keymap,
|
keymap: &Keymap,
|
||||||
seconds: Option<u8>,
|
seconds: Option<u8>,
|
||||||
colors: AppColors,
|
|
||||||
) {
|
) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title(" Error ")
|
.title(" Error ")
|
||||||
@@ -106,16 +106,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Test that the error popup is centered, red background, white border, white text, and displays the correct text
|
/// Test that the error popup is centered, red background, white border, white text, and displays the correct text
|
||||||
fn test_draw_blocks_docker_connect_error() {
|
fn test_draw_blocks_error_docker_connect_error() {
|
||||||
let (w, h) = (46, 9);
|
let (w, h) = (46, 9);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let app_colors = setup.app_data.lock().config.app_colors;
|
|
||||||
let keymap = &setup.app_data.lock().config.keymap;
|
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, &AppError::DockerConnect, keymap, Some(4), app_colors);
|
super::draw(
|
||||||
|
AppColors::new(),
|
||||||
|
&AppError::DockerConnect,
|
||||||
|
f,
|
||||||
|
&Keymap::new(),
|
||||||
|
Some(4),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -153,17 +157,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text
|
/// Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text
|
||||||
fn test_draw_blocks_clearable_error() {
|
fn test_draw_blocks_error_clearable_error() {
|
||||||
let (w, h) = (39, 11);
|
let (w, h) = (39, 11);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
let app_colors = setup.app_data.lock().config.app_colors;
|
|
||||||
let keymap = &setup.app_data.lock().config.keymap;
|
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, &AppError::DockerExec, keymap, Some(4), app_colors);
|
super::draw(
|
||||||
|
AppColors::new(),
|
||||||
|
&AppError::DockerExec,
|
||||||
|
f,
|
||||||
|
&Keymap::new(),
|
||||||
|
Some(4),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -203,12 +210,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Custom colors applied to the error popup correctly
|
/// Custom colors applied to the error popup correctly
|
||||||
fn test_draw_blocks_clearable_error_custom_colors() {
|
fn test_draw_blocks_error_custom_colors() {
|
||||||
let (w, h) = (39, 11);
|
let (w, h) = (39, 11);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
let keymap = &setup.app_data.lock().config.keymap;
|
|
||||||
|
|
||||||
let mut colors = AppColors::new();
|
let mut colors = AppColors::new();
|
||||||
colors.popup_error.background = Color::Yellow;
|
colors.popup_error.background = Color::Yellow;
|
||||||
colors.popup_error.text = Color::Black;
|
colors.popup_error.text = Color::Black;
|
||||||
@@ -216,7 +221,7 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, &AppError::DockerExec, keymap, Some(4), colors);
|
super::draw(colors, &AppError::DockerExec, f, &Keymap::new(), Some(4));
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -256,7 +261,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Custom keymap applied correct with both 1 and 2 definitions
|
/// Custom keymap applied correct with both 1 and 2 definitions
|
||||||
fn test_draw_blocks_clearable_error_custom_keymap() {
|
fn test_draw_blocks_error_custom_keymap() {
|
||||||
let (w, h) = (39, 11);
|
let (w, h) = (39, 11);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
@@ -267,7 +272,7 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
super::draw(AppColors::new(), &AppError::DockerExec, f, &keymap, None);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -299,7 +304,7 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
super::draw(AppColors::new(), &AppError::DockerExec, f, &keymap, None);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -330,7 +335,7 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, &AppError::DockerExec, &keymap, None, AppColors::new());
|
super::draw(AppColors::new(), &AppError::DockerExec, f, &keymap, None);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
+102
-27
@@ -1,16 +1,20 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
|
||||||
style::{Color, Modifier, Style},
|
|
||||||
text::{Line, Span},
|
|
||||||
Frame,
|
Frame,
|
||||||
|
layout::Rect,
|
||||||
|
style::{Modifier, Style, Stylize},
|
||||||
|
text::{Line, Span},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{app_data::FilterBy, ui::FrameData};
|
use crate::{app_data::FilterBy, config::AppColors, ui::FrameData};
|
||||||
|
|
||||||
/// Create the filter_by by spans, coloured dependant on which one is selected
|
/// Create the filter_by by spans, coloured dependant on which one is selected
|
||||||
fn filter_by_spans(fd: &FrameData) -> [Span; 4] {
|
fn filter_by_spans(colors: AppColors, fd: &FrameData) -> [Span; 4] {
|
||||||
let selected = Style::default().bg(Color::Gray).fg(Color::Black);
|
let selected = Style::default()
|
||||||
let not_selected = Style::default().bg(Color::Reset).fg(Color::Reset);
|
.bg(colors.filter.selected_filter_background)
|
||||||
|
.fg(colors.filter.selected_filter_text);
|
||||||
|
let not_selected = Style::default()
|
||||||
|
.bg(colors.filter.background)
|
||||||
|
.fg(colors.filter.text);
|
||||||
|
|
||||||
let name = [" Name ", " Image ", " Status ", " All "];
|
let name = [" Name ", " Image ", " Status ", " All "];
|
||||||
|
|
||||||
@@ -31,9 +35,13 @@ fn filter_by_spans(fd: &FrameData) -> [Span; 4] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the filter bar
|
/// Draw the filter bar
|
||||||
pub fn draw(area: Rect, frame: &mut Frame, fd: &FrameData) {
|
pub fn draw(area: Rect, colors: AppColors, frame: &mut Frame, fd: &FrameData) {
|
||||||
let style_but = Style::default().fg(Color::Black).bg(Color::Magenta);
|
let style_but = Style::default()
|
||||||
let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset);
|
.fg(colors.filter.selected_filter_text)
|
||||||
|
.bg(colors.filter.highlight);
|
||||||
|
let style_desc = Style::default()
|
||||||
|
.fg(colors.filter.text)
|
||||||
|
.bg(colors.filter.background);
|
||||||
|
|
||||||
let mut line = vec![
|
let mut line = vec![
|
||||||
Span::styled(" Esc ", style_but),
|
Span::styled(" Esc ", style_but),
|
||||||
@@ -41,22 +49,22 @@ pub fn draw(area: Rect, frame: &mut Frame, fd: &FrameData) {
|
|||||||
Span::styled(" ← by → ", style_but),
|
Span::styled(" ← by → ", style_but),
|
||||||
Span::from(" "),
|
Span::from(" "),
|
||||||
];
|
];
|
||||||
line.extend_from_slice(&filter_by_spans(fd));
|
line.extend_from_slice(&filter_by_spans(colors, fd));
|
||||||
line.extend_from_slice(&[
|
line.extend_from_slice(&[
|
||||||
Span::styled(
|
Span::styled(
|
||||||
" term: ",
|
" term: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Magenta)
|
.fg(colors.filter.highlight)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
fd.filter_term
|
fd.filter_term
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(String::new(), std::clone::Clone::clone),
|
.map_or(String::new(), std::clone::Clone::clone),
|
||||||
Style::default().fg(Color::Gray),
|
Style::default().fg(colors.filter.text),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(Line::from(line), area);
|
frame.render_widget(Line::from(line).bg(colors.filter.background), area);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -65,9 +73,12 @@ mod tests {
|
|||||||
|
|
||||||
use ratatui::style::{Color, Modifier};
|
use ratatui::style::{Color, Modifier};
|
||||||
|
|
||||||
use crate::ui::{
|
use crate::{
|
||||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
config::AppColors,
|
||||||
|
ui::{
|
||||||
FrameData,
|
FrameData,
|
||||||
|
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -85,24 +96,25 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(setup.area, f, &setup.fd);
|
super::draw(setup.area, AppColors::new(), f, &setup.fd);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let expected = [
|
let expected = [
|
||||||
" Esc clear ← by → Name Image Status All term: "
|
" Esc clear ← by → Name Image Status All term: ",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, result_row) in get_result(&setup, w) {
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
let expected_row = expected_to_vec(&expected, row_index);
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
|
||||||
match result_cell_index {
|
match result_cell_index {
|
||||||
0..=4 | 12..=19 => {
|
0..=4 | 12..=19 => {
|
||||||
assert_eq!(result_cell.bg, Color::Magenta);
|
assert_eq!(result_cell.bg, Color::Magenta);
|
||||||
assert_eq!(result_cell.fg, Color::Black);
|
assert_eq!(result_cell.fg, Color::Black);
|
||||||
}
|
}
|
||||||
5..=11 => {
|
5..=11 | 27..=46 => {
|
||||||
assert_eq!(result_cell.bg, Color::Reset);
|
assert_eq!(result_cell.bg, Color::Reset);
|
||||||
assert_eq!(result_cell.fg, Color::Gray);
|
assert_eq!(result_cell.fg, Color::Gray);
|
||||||
}
|
}
|
||||||
@@ -131,12 +143,12 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(setup.area, f, &fd);
|
super::draw(setup.area, AppColors::new(), f, &fd);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let expected = [
|
let expected = [
|
||||||
" Esc clear ← by → Name Image Status All term: cd "
|
" Esc clear ← by → Name Image Status All term: cd ",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, result_row) in get_result(&setup, w) {
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
@@ -149,7 +161,7 @@ mod tests {
|
|||||||
assert_eq!(result_cell.bg, Color::Magenta);
|
assert_eq!(result_cell.bg, Color::Magenta);
|
||||||
assert_eq!(result_cell.fg, Color::Black);
|
assert_eq!(result_cell.fg, Color::Black);
|
||||||
}
|
}
|
||||||
5..=11 | 54..=55 => {
|
5..=11 | 27..=46 | 54..=55 => {
|
||||||
assert_eq!(result_cell.bg, Color::Reset);
|
assert_eq!(result_cell.bg, Color::Reset);
|
||||||
assert_eq!(result_cell.fg, Color::Gray);
|
assert_eq!(result_cell.fg, Color::Gray);
|
||||||
}
|
}
|
||||||
@@ -170,31 +182,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test when filter_by chances
|
// Test when filter_by changes
|
||||||
setup.app_data.lock().filter_by_next();
|
setup.app_data.lock().filter_by_next();
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(setup.area, f, &fd);
|
super::draw(setup.area, AppColors::new(), f, &fd);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let expected = [
|
let expected = [
|
||||||
" Esc clear ← by → Name Image Status All term: cd "
|
" Esc clear ← by → Name Image Status All term: cd ",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, result_row) in get_result(&setup, w) {
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
let expected_row = expected_to_vec(&expected, row_index);
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
|
||||||
match result_cell_index {
|
match result_cell_index {
|
||||||
0..=4 | 12..=19 => {
|
0..=4 | 12..=19 => {
|
||||||
assert_eq!(result_cell.bg, Color::Magenta);
|
assert_eq!(result_cell.bg, Color::Magenta);
|
||||||
assert_eq!(result_cell.fg, Color::Black);
|
assert_eq!(result_cell.fg, Color::Black);
|
||||||
}
|
}
|
||||||
5..=11 | 54..=55 => {
|
5..=11 | 21..=26 | 34..=46 | 54..=55 => {
|
||||||
assert_eq!(result_cell.bg, Color::Reset);
|
assert_eq!(result_cell.bg, Color::Reset);
|
||||||
assert_eq!(result_cell.fg, Color::Gray);
|
assert_eq!(result_cell.fg, Color::Gray);
|
||||||
}
|
}
|
||||||
@@ -215,4 +226,68 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Make sure custom colors are applied
|
||||||
|
fn test_draw_blocks_filter_row_custom_colors() {
|
||||||
|
let (w, h) = (140, 1);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
|
setup
|
||||||
|
.gui_state
|
||||||
|
.lock()
|
||||||
|
.status_push(crate::ui::Status::Filter);
|
||||||
|
|
||||||
|
setup.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));
|
||||||
|
|
||||||
|
let mut colors = AppColors::new();
|
||||||
|
colors.filter.background = Color::White;
|
||||||
|
colors.filter.highlight = Color::Blue;
|
||||||
|
colors.filter.selected_filter_background = Color::Red;
|
||||||
|
colors.filter.selected_filter_text = Color::Yellow;
|
||||||
|
colors.filter.text = Color::Magenta;
|
||||||
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(setup.area, colors, f, &fd);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
" Esc clear ← by → Name Image Status All term: cd ",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
match result_cell_index {
|
||||||
|
0..=4 | 12..=19 => {
|
||||||
|
assert_eq!(result_cell.bg, Color::Blue);
|
||||||
|
assert_eq!(result_cell.fg, Color::Yellow);
|
||||||
|
}
|
||||||
|
5..=11 | 27..=46 | 54..=55 => {
|
||||||
|
assert_eq!(result_cell.bg, Color::White);
|
||||||
|
assert_eq!(result_cell.fg, Color::Magenta);
|
||||||
|
}
|
||||||
|
21..=26 => {
|
||||||
|
assert_eq!(result_cell.bg, Color::Red);
|
||||||
|
assert_eq!(result_cell.fg, Color::Yellow);
|
||||||
|
}
|
||||||
|
47..=53 => {
|
||||||
|
assert_eq!(result_cell.bg, Color::White);
|
||||||
|
assert_eq!(result_cell.fg, Color::Blue);
|
||||||
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
assert_eq!(result_cell.bg, Color::White);
|
||||||
|
assert_eq!(result_cell.fg, Color::Reset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+298
-137
@@ -1,65 +1,28 @@
|
|||||||
use std::sync::Arc;
|
use std::{rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{CONSTRAINT_100, MARGIN};
|
use super::{CONSTRAINT_100, MARGIN};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{Header, SortedOrder},
|
app_data::{Header, SortedOrder},
|
||||||
config::{AppColors, Keymap},
|
config::{AppColors, Keymap},
|
||||||
ui::{gui_state::Region, FrameData, GuiState, Status},
|
ui::{FrameData, GuiState, Status, gui_state::Region},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw heading bar at top of program, always visible
|
/// Generate a header paragraph with it's width
|
||||||
/// TODO Should separate into loading icon/headers/help functions
|
fn gen_header<'a>(
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn draw(
|
|
||||||
area: Rect,
|
|
||||||
colors: AppColors,
|
colors: AppColors,
|
||||||
frame: &mut Frame,
|
|
||||||
fd: &FrameData,
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
header: Header,
|
||||||
keymap: &Keymap,
|
width: usize,
|
||||||
) {
|
) -> (Paragraph<'a>, u16) {
|
||||||
let gen_style = |bg: Option<Color>, fg: Color| {
|
let block = gen_header_block(colors, fd, header);
|
||||||
bg.map_or_else(
|
|
||||||
|| Style::default().fg(fg),
|
|
||||||
|bg| Style::default().bg(bg).fg(fg),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Block::default().style(gen_style(Some(colors.headers_bar.background), Color::Reset)),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
|
||||||
let header_block = |x: &Header, colors: AppColors| {
|
|
||||||
let mut color = colors.headers_bar.text;
|
|
||||||
let mut suffix = "";
|
|
||||||
if let Some((a, b)) = &fd.sorted_by {
|
|
||||||
if x == a {
|
|
||||||
match b {
|
|
||||||
SortedOrder::Asc => suffix = " ▲",
|
|
||||||
SortedOrder::Desc => suffix = " ▼",
|
|
||||||
}
|
|
||||||
color = colors.headers_bar.text_selected;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
(color, suffix)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate block for the headers, state and status has a specific layout, others all equal
|
|
||||||
// width is dependant on it that column is selected to sort - or not
|
|
||||||
// TODO - yes this is a mess, needs documenting correctly
|
|
||||||
let gen_header = |header: &Header, width: usize, colors: AppColors| {
|
|
||||||
let block = header_block(header, colors);
|
|
||||||
|
|
||||||
let text = format!(
|
let text = format!(
|
||||||
"{x:<width$}{MARGIN}",
|
"{x:<width$}{MARGIN}",
|
||||||
@@ -70,7 +33,99 @@ pub fn draw(
|
|||||||
.style(gen_style(None, block.0))
|
.style(gen_style(None, block.0))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
(status, count)
|
(status, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a block for the header, if the header is currently being used to sort a column, then highlight it white
|
||||||
|
fn gen_header_block<'a>(colors: AppColors, fd: &FrameData, header: Header) -> (Color, &'a str) {
|
||||||
|
let mut color = colors.headers_bar.text;
|
||||||
|
let mut suffix = "";
|
||||||
|
if let Some((a, b)) = &fd.sorted_by {
|
||||||
|
if &header == a {
|
||||||
|
match b {
|
||||||
|
SortedOrder::Asc => suffix = " ▲",
|
||||||
|
SortedOrder::Desc => suffix = " ▼",
|
||||||
|
}
|
||||||
|
color = colors.headers_bar.text_selected;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(color, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_style(bg: Option<Color>, fg: Color) -> Style {
|
||||||
|
bg.map_or_else(
|
||||||
|
|| Style::default().fg(fg),
|
||||||
|
|bg| Style::default().bg(bg).fg(fg),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the text to display on the show help section, as can change with a custom keymap
|
||||||
|
fn gen_help_text(fd: &FrameData, keymap: &Keymap) -> String {
|
||||||
|
let suffix = if fd.status.contains(&Status::Help) {
|
||||||
|
"exit"
|
||||||
|
} else {
|
||||||
|
"show"
|
||||||
|
};
|
||||||
|
|
||||||
|
if keymap.toggle_help == Keymap::new().toggle_help {
|
||||||
|
format!("( h ) {suffix} help{MARGIN}")
|
||||||
|
} else if let Some(secondary) = keymap.toggle_help.1 {
|
||||||
|
format!(
|
||||||
|
" ( {} | {secondary} ) {suffix} help{MARGIN}",
|
||||||
|
keymap.toggle_help.0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(" ( {} ) {suffix} help{MARGIN}", keymap.toggle_help.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the show/hide help section
|
||||||
|
fn draw_help(
|
||||||
|
colors: AppColors,
|
||||||
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
|
help_text: String,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
split_bar: &Rc<[Rect]>,
|
||||||
|
) {
|
||||||
|
let help_text_color = if fd.status.contains(&Status::Help) {
|
||||||
|
colors.headers_bar.text
|
||||||
|
} else {
|
||||||
|
colors.headers_bar.text_selected
|
||||||
|
};
|
||||||
|
|
||||||
|
let help_paragraph = Paragraph::new(help_text)
|
||||||
|
.style(gen_style(None, help_text_color))
|
||||||
|
.alignment(Alignment::Right);
|
||||||
|
|
||||||
|
// If no containers, don't display the headers, could maybe do this first?
|
||||||
|
let help_index = if fd.has_containers { 2 } else { 0 };
|
||||||
|
gui_state
|
||||||
|
.lock()
|
||||||
|
.update_region_map(Region::HelpPanel, split_bar[help_index]);
|
||||||
|
f.render_widget(help_paragraph, split_bar[help_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw loading icon, or not, and a prefix with a single space
|
||||||
|
fn draw_loading_spinner(colors: AppColors, f: &mut Frame, fd: &FrameData, rect: Rect) {
|
||||||
|
let loading_paragraph = Paragraph::new(format!("{:>2}", fd.loading_icon))
|
||||||
|
.style(gen_style(None, colors.headers_bar.loading_spinner))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
f.render_widget(loading_paragraph, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the sortable column headers (name/state/status etc)
|
||||||
|
fn draw_columns(
|
||||||
|
colors: AppColors,
|
||||||
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
split_bar: &Rc<[Rect]>,
|
||||||
|
) {
|
||||||
|
if fd.has_containers {
|
||||||
|
let header_section_width = split_bar[1].width;
|
||||||
|
|
||||||
|
let mut counter = 0;
|
||||||
|
|
||||||
// Meta data to iterate over to create blocks with correct widths
|
// Meta data to iterate over to create blocks with correct widths
|
||||||
let header_meta = [
|
let header_meta = [
|
||||||
@@ -85,59 +140,14 @@ pub fn draw(
|
|||||||
(Header::Tx, fd.columns.net_tx.1),
|
(Header::Tx, fd.columns.net_tx.1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let suffix = if fd.status.contains(&Status::Help) {
|
|
||||||
"exit"
|
|
||||||
} else {
|
|
||||||
"show"
|
|
||||||
};
|
|
||||||
|
|
||||||
let info_text = if keymap.toggle_help == Keymap::new().toggle_help {
|
|
||||||
format!("( h ) {suffix} help{MARGIN}")
|
|
||||||
} else if let Some(secondary) = keymap.toggle_help.1 {
|
|
||||||
format!(
|
|
||||||
" ( {} | {secondary} ) {suffix} help{MARGIN}",
|
|
||||||
keymap.toggle_help.0
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(" ( {} ) {suffix} help{MARGIN}", keymap.toggle_help.0)
|
|
||||||
};
|
|
||||||
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 fd.has_containers {
|
|
||||||
vec![
|
|
||||||
Constraint::Max(4),
|
|
||||||
Constraint::Max(column_width.try_into().unwrap_or_default()),
|
|
||||||
Constraint::Max(info_width.try_into().unwrap_or_default()),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
CONSTRAINT_100.to_vec()
|
|
||||||
};
|
|
||||||
|
|
||||||
let split_bar = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints(splits)
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
// Draw loading icon, or not, and a prefix with a single space
|
|
||||||
let loading_paragraph = Paragraph::new(format!("{:>2}", fd.loading_icon))
|
|
||||||
.style(gen_style(None, colors.headers_bar.loading_spinner))
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
frame.render_widget(loading_paragraph, split_bar[0]);
|
|
||||||
if fd.has_containers {
|
|
||||||
let header_section_width = split_bar[1].width;
|
|
||||||
|
|
||||||
let mut counter = 0;
|
|
||||||
|
|
||||||
// Only show a header if the header cumulative header width is less than the header section width
|
// Only show a header if the header cumulative header width is less than the header section width
|
||||||
let header_data = header_meta
|
let header_data = header_meta
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter_map(|i| {
|
.filter_map(|(header, width)| {
|
||||||
let header_block = gen_header(&i.0, i.1.into(), colors);
|
let header_block = gen_header(colors, fd, header, usize::from(width));
|
||||||
counter += header_block.1;
|
counter += header_block.1;
|
||||||
if counter <= header_section_width {
|
if counter <= header_section_width {
|
||||||
Some((header_block.0, i.0, Constraint::Max(header_block.1)))
|
Some((header_block.0, header, Constraint::Max(header_block.1)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -155,27 +165,55 @@ pub fn draw(
|
|||||||
gui_state
|
gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.update_region_map(Region::Header(header), rect);
|
.update_region_map(Region::Header(header), rect);
|
||||||
frame.render_widget(paragraph, rect);
|
f.render_widget(paragraph, rect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show/hide help
|
// Draw heading bar at top of program, always visible
|
||||||
let help_text_color = if fd.status.contains(&Status::Help) {
|
pub fn draw(
|
||||||
colors.headers_bar.text
|
area: Rect,
|
||||||
} else {
|
colors: AppColors,
|
||||||
colors.headers_bar.text_selected
|
f: &mut Frame,
|
||||||
|
fd: &FrameData,
|
||||||
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
|
keymap: &Keymap,
|
||||||
|
) {
|
||||||
|
let gen_style = |bg: Option<Color>, fg: Color| {
|
||||||
|
bg.map_or_else(
|
||||||
|
|| Style::default().fg(fg),
|
||||||
|
|bg| Style::default().bg(bg).fg(fg),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let help_paragraph = Paragraph::new(info_text)
|
f.render_widget(
|
||||||
.style(gen_style(None, help_text_color))
|
Block::default().style(gen_style(Some(colors.headers_bar.background), Color::Reset)),
|
||||||
.alignment(Alignment::Right);
|
area,
|
||||||
|
);
|
||||||
|
|
||||||
// If no containers, don't display the headers, could maybe do this first?
|
let help_text = gen_help_text(fd, keymap);
|
||||||
let help_index = if fd.has_containers { 2 } else { 0 };
|
let help_width = help_text.chars().count();
|
||||||
gui_state
|
|
||||||
.lock()
|
let column_width = usize::from(area.width).saturating_sub(help_width);
|
||||||
.update_region_map(Region::HelpPanel, split_bar[help_index]);
|
let column_width = if column_width > 0 { column_width } else { 1 };
|
||||||
frame.render_widget(help_paragraph, split_bar[help_index]);
|
let splits = if fd.has_containers {
|
||||||
|
vec![
|
||||||
|
Constraint::Max(4),
|
||||||
|
Constraint::Max(column_width.try_into().unwrap_or_default()),
|
||||||
|
Constraint::Max(help_width.try_into().unwrap_or_default()),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
CONSTRAINT_100.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
let split_bar = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(splits)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
draw_loading_spinner(colors, f, fd, split_bar[0]);
|
||||||
|
draw_columns(colors, f, fd, gui_state, &split_bar);
|
||||||
|
draw_help(colors, f, fd, help_text, gui_state, &split_bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -191,8 +229,8 @@ mod tests {
|
|||||||
app_data::{Header, SortedOrder, StatefulList},
|
app_data::{Header, SortedOrder, StatefulList},
|
||||||
config::{AppColors, Keymap},
|
config::{AppColors, Keymap},
|
||||||
ui::{
|
ui::{
|
||||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
|
||||||
FrameData, Status,
|
FrameData, Status,
|
||||||
|
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,7 +243,9 @@ mod tests {
|
|||||||
|
|
||||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
|
||||||
let expected = [" ( h ) show help "];
|
let expected = [
|
||||||
|
" ( h ) show help ",
|
||||||
|
];
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
@@ -231,7 +271,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fd.status.insert(Status::Help);
|
fd.status.insert(Status::Help);
|
||||||
let expected = [" ( h ) exit help "];
|
let expected = [
|
||||||
|
" ( h ) exit help ",
|
||||||
|
];
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
@@ -263,7 +305,9 @@ mod tests {
|
|||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
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 "];
|
let expected = [
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
];
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
@@ -339,6 +383,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Test all combination of headers & sort by
|
/// Test all combination of headers & sort by
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
fn test_draw_blocks_headers_sort_containers() {
|
fn test_draw_blocks_headers_sort_containers() {
|
||||||
let (w, h) = (140, 1);
|
let (w, h) = (140, 1);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
@@ -385,32 +430,140 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
test(&[" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 1..=17, (Header::Name, SortedOrder::Desc));
|
&[
|
||||||
|
" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
1..=17,
|
||||||
|
(Header::Name, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
1..=17,
|
||||||
|
(Header::Name, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// state
|
// state
|
||||||
test(&[" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "],18..=29, (Header::State, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 18..=29, (Header::State, SortedOrder::Desc));
|
&[
|
||||||
|
" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
18..=29,
|
||||||
|
(Header::State, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
18..=29,
|
||||||
|
(Header::State, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// status
|
// status
|
||||||
test(&[" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "], 30..=41, (Header::Status, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
30..=41,
|
||||||
|
(Header::Status, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
30..=41,
|
||||||
|
(Header::Status, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// cpu
|
// cpu
|
||||||
test(&[" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help "],42..=50, (Header::Cpu, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
42..=50,
|
||||||
|
(Header::Cpu, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
42..=50,
|
||||||
|
(Header::Cpu, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// memory
|
// memory
|
||||||
test(&[" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help "], 51..=70, (Header::Memory, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
51..=70,
|
||||||
|
(Header::Memory, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
51..=70,
|
||||||
|
(Header::Memory, SortedOrder::Desc),
|
||||||
|
);
|
||||||
//id
|
//id
|
||||||
test(&[" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help "], 71..=81, (Header::Id, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
71..=81,
|
||||||
|
(Header::Id, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
71..=81,
|
||||||
|
(Header::Id, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// image
|
// image
|
||||||
test(&[" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help "], 82..=91, (Header::Image, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
82..=91,
|
||||||
|
(Header::Image, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
82..=91,
|
||||||
|
(Header::Image, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// rx
|
// rx
|
||||||
test(&[" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help "], 92..=101, (Header::Rx, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
92..=101,
|
||||||
|
(Header::Rx, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help ",
|
||||||
|
],
|
||||||
|
92..=101,
|
||||||
|
(Header::Rx, SortedOrder::Desc),
|
||||||
|
);
|
||||||
// tx
|
// tx
|
||||||
test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▲ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Asc));
|
test(
|
||||||
test(&[" name state status cpu memory/limit id image ↓ rx ↑ tx ▼ ( h ) show help "], 102..=111, (Header::Tx, SortedOrder::Desc));
|
&[
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ▲ ( h ) show help ",
|
||||||
|
],
|
||||||
|
102..=111,
|
||||||
|
(Header::Tx, SortedOrder::Asc),
|
||||||
|
);
|
||||||
|
test(
|
||||||
|
&[
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ▼ ( h ) show help ",
|
||||||
|
],
|
||||||
|
102..=111,
|
||||||
|
(Header::Tx, SortedOrder::Desc),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -422,7 +575,9 @@ mod tests {
|
|||||||
setup.gui_state.lock().next_loading(uuid);
|
setup.gui_state.lock().next_loading(uuid);
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
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 "];
|
let expected = [
|
||||||
|
" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
];
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
@@ -458,7 +613,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Custom colors are applied correctly
|
/// Custom colors are applied correctly
|
||||||
fn test_draw_blocks_headers_cusomt_colors() {
|
fn test_draw_blocks_headers_custom_colors() {
|
||||||
let (w, h) = (140, 1);
|
let (w, h) = (140, 1);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
@@ -472,7 +627,9 @@ mod tests {
|
|||||||
colors.headers_bar.text = Color::Blue;
|
colors.headers_bar.text = Color::Blue;
|
||||||
colors.headers_bar.text_selected = Color::Yellow;
|
colors.headers_bar.text_selected = Color::Yellow;
|
||||||
|
|
||||||
let expected = [" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "];
|
let expected = [
|
||||||
|
" ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
];
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
@@ -509,7 +666,9 @@ mod tests {
|
|||||||
|
|
||||||
keymap.toggle_help = (KeyCode::Char('T'), None);
|
keymap.toggle_help = (KeyCode::Char('T'), None);
|
||||||
|
|
||||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( T ) show help "];
|
let expected = [
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( T ) show help ",
|
||||||
|
];
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
@@ -532,7 +691,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keymap.toggle_help = (KeyCode::Char('T'), Some(KeyCode::Tab));
|
keymap.toggle_help = (KeyCode::Char('T'), Some(KeyCode::Tab));
|
||||||
let expected = [" name state status cpu memory/limit id image ↓ rx ↑ tx ( T | Tab ) show help "];
|
let expected = [
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( T | Tab ) show help ",
|
||||||
|
];
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
|
|||||||
+193
-51
@@ -1,10 +1,11 @@
|
|||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
|
use jiff::tz::TimeZone;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Constraint, Direction, Layout},
|
layout::{Alignment, Constraint, Direction, Layout},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
widgets::{Block, BorderType, Borders, Clear, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -12,7 +13,7 @@ use crate::{
|
|||||||
ui::gui_state::BoxLocation,
|
ui::gui_state::BoxLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{popup, DESCRIPTION, NAME_TEXT, REPO, VERSION};
|
use super::{DESCRIPTION, NAME_TEXT, REPO, VERSION, popup};
|
||||||
|
|
||||||
/// Help popup box needs these three pieces of information
|
/// Help popup box needs these three pieces of information
|
||||||
struct HelpInfo {
|
struct HelpInfo {
|
||||||
@@ -84,13 +85,13 @@ impl HelpInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the button information span + metadata
|
/// Generate the button information span + metadata
|
||||||
fn gen_keymap_info(colors: AppColors) -> Self {
|
fn gen_keymap_info(colors: AppColors, zone: Option<&TimeZone>, show_timestamp: bool) -> Self {
|
||||||
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
||||||
let button_desc = |x: &str| Self::text_span(x, colors);
|
let button_desc = |x: &str| Self::text_span(x, colors);
|
||||||
let or = || button_desc("or");
|
let or = || button_desc("or");
|
||||||
let space = || button_desc(" ");
|
let space = || button_desc(" ");
|
||||||
|
|
||||||
let lines = [
|
let descriptions = [
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
button_item("tab"),
|
button_item("tab"),
|
||||||
@@ -163,10 +164,23 @@ impl HelpInfo {
|
|||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let mut lines = if show_timestamp {
|
||||||
|
Vec::from([
|
||||||
|
Self::custom_text(colors, &Keymap::new(), zone),
|
||||||
|
Self::empty_span(),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.extend_from_slice(&descriptions);
|
||||||
|
let width = Self::calc_width(&lines);
|
||||||
|
let height = lines.len();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
lines: lines.to_vec(),
|
lines,
|
||||||
width: Self::calc_width(&lines),
|
width,
|
||||||
height: lines.len(),
|
height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +207,31 @@ impl HelpInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Display timezone in timestamps are visible
|
||||||
|
/// Has ability to display if keymap or colors are customized, but currently not in use
|
||||||
|
fn custom_text<'a>(colors: AppColors, _keymap: &Keymap, zone: Option<&TimeZone>) -> Line<'a> {
|
||||||
|
let highlighted = |x: &str| Self::highlighted_text_span(x, colors);
|
||||||
|
let text = |x: &str| Self::text_span(x, colors);
|
||||||
|
|
||||||
|
// if keymap != &Keymap::new() {
|
||||||
|
// op.push(highlighted("customised keymap, "));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if colors != AppColors::new() {
|
||||||
|
// op.push(highlighted("customised app colors, "));
|
||||||
|
// };
|
||||||
|
|
||||||
|
let zone = zone.and_then(|i| i.iana_name()).unwrap_or("Etc/UTC");
|
||||||
|
Line::from(Vec::from([text("logs timezone: "), highlighted(zone)])).centered()
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate the display information when a custom keymap is being used
|
/// Generate the display information when a custom keymap is being used
|
||||||
fn gen_custom_keymap_info(colors: AppColors, km: &Keymap) -> Self {
|
fn gen_custom_keymap_info(
|
||||||
|
colors: AppColors,
|
||||||
|
km: &Keymap,
|
||||||
|
zone: Option<&TimeZone>,
|
||||||
|
show_timestamp: bool,
|
||||||
|
) -> Self {
|
||||||
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
let button_item = |x: &str| Self::highlighted_text_span(&format!(" ( {x} ) "), colors);
|
||||||
let button_desc = |x: &str| Self::text_span(x, colors);
|
let button_desc = |x: &str| Self::text_span(x, colors);
|
||||||
let or = || button_desc("or");
|
let or = || button_desc("or");
|
||||||
@@ -220,11 +257,7 @@ impl HelpInfo {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
let descriptions = [
|
||||||
let lines = [
|
|
||||||
Line::from(vec![Span::from("Custom keymap config in use\n")])
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.style(Style::default().fg(colors.popup_help.text_highlight)),
|
|
||||||
or_secondary(km.select_next_panel, "select next panel"),
|
or_secondary(km.select_next_panel, "select next panel"),
|
||||||
or_secondary(km.select_previous_panel, "select previous panel"),
|
or_secondary(km.select_previous_panel, "select previous panel"),
|
||||||
or_secondary(km.scroll_down_one, "scroll list down by one"),
|
or_secondary(km.scroll_down_one, "scroll list down by one"),
|
||||||
@@ -266,16 +299,32 @@ impl HelpInfo {
|
|||||||
or_secondary(km.quit, "quit at any time"),
|
or_secondary(km.quit, "quit at any time"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let mut lines = if show_timestamp {
|
||||||
|
Vec::from([Self::custom_text(colors, km, zone), Self::empty_span()])
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.extend_from_slice(&descriptions);
|
||||||
|
let width = Self::calc_width(&lines);
|
||||||
|
let height = lines.len();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
lines: lines.to_vec(),
|
lines,
|
||||||
width: Self::calc_width(&lines),
|
width,
|
||||||
height: lines.len(),
|
height,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the help box in the centre of the screen
|
/// Draw the help box in the centre of the screen
|
||||||
pub fn draw(f: &mut Frame, colors: AppColors, keymap: &Keymap) {
|
pub fn draw(
|
||||||
|
colors: AppColors,
|
||||||
|
f: &mut Frame,
|
||||||
|
keymap: &Keymap,
|
||||||
|
show_timestamp: bool,
|
||||||
|
zone: Option<&TimeZone>,
|
||||||
|
) {
|
||||||
let title = format!(" {VERSION} ");
|
let title = format!(" {VERSION} ");
|
||||||
|
|
||||||
let name_info = HelpInfo::gen_name(colors);
|
let name_info = HelpInfo::gen_name(colors);
|
||||||
@@ -283,9 +332,9 @@ pub fn draw(f: &mut Frame, colors: AppColors, keymap: &Keymap) {
|
|||||||
let final_info = HelpInfo::gen_final(colors);
|
let final_info = HelpInfo::gen_final(colors);
|
||||||
|
|
||||||
let button_info = if keymap == &Keymap::new() {
|
let button_info = if keymap == &Keymap::new() {
|
||||||
HelpInfo::gen_keymap_info(colors)
|
HelpInfo::gen_keymap_info(colors, zone, show_timestamp)
|
||||||
} else {
|
} else {
|
||||||
HelpInfo::gen_custom_keymap_info(colors, keymap)
|
HelpInfo::gen_custom_keymap_info(colors, keymap, zone, show_timestamp)
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_line_width = [
|
let max_line_width = [
|
||||||
@@ -364,13 +413,14 @@ pub fn draw(f: &mut Frame, colors: AppColors, keymap: &Keymap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used, clippy::too_many_lines)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AppColors, Keymap},
|
config::{AppColors, Keymap},
|
||||||
ui::draw_blocks::VERSION,
|
ui::draw_blocks::VERSION,
|
||||||
};
|
};
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
|
use jiff::tz::TimeZone;
|
||||||
use ratatui::style::{Color, Modifier};
|
use ratatui::style::{Color, Modifier};
|
||||||
|
|
||||||
use crate::ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup};
|
use crate::ui::draw_blocks::tests::{expected_to_vec, get_result, test_setup};
|
||||||
@@ -380,16 +430,24 @@ mod tests {
|
|||||||
fn test_draw_blocks_help() {
|
fn test_draw_blocks_help() {
|
||||||
let (w, h) = (87, 33);
|
let (w, h) = (87, 33);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
let tz = setup.app_data.lock().config.timezone.clone();
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, colors, &setup.app_data.lock().config.keymap);
|
super::draw(
|
||||||
|
AppColors::new(),
|
||||||
|
f,
|
||||||
|
&setup.app_data.lock().config.keymap,
|
||||||
|
false,
|
||||||
|
tz.as_ref(),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let version_row = format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ ");
|
let version_row = format!(
|
||||||
|
" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "
|
||||||
|
);
|
||||||
let expected = [
|
let expected = [
|
||||||
" ",
|
" ",
|
||||||
version_row.as_str(),
|
version_row.as_str(),
|
||||||
@@ -423,7 +481,7 @@ mod tests {
|
|||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ │ ",
|
" │ │ ",
|
||||||
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||||
" "
|
" ",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, result_row) in get_result(&setup, w) {
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
@@ -476,6 +534,7 @@ mod tests {
|
|||||||
let (w, h) = (87, 33);
|
let (w, h) = (87, 33);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let mut colors = AppColors::new();
|
let mut colors = AppColors::new();
|
||||||
|
let tz = setup.app_data.lock().config.timezone.clone();
|
||||||
|
|
||||||
colors.popup_help.background = Color::Black;
|
colors.popup_help.background = Color::Black;
|
||||||
colors.popup_help.text = Color::Red;
|
colors.popup_help.text = Color::Red;
|
||||||
@@ -484,11 +543,19 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, colors, &setup.app_data.lock().config.keymap);
|
super::draw(
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
&setup.app_data.lock().config.keymap,
|
||||||
|
false,
|
||||||
|
tz.as_ref(),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let version_row = format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ ");
|
let version_row = format!(
|
||||||
|
" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "
|
||||||
|
);
|
||||||
let expected = [
|
let expected = [
|
||||||
" ",
|
" ",
|
||||||
version_row.as_str(),
|
version_row.as_str(),
|
||||||
@@ -522,7 +589,7 @@ mod tests {
|
|||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ │ ",
|
" │ │ ",
|
||||||
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||||
" "
|
" ",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, result_row) in get_result(&setup, w) {
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
@@ -571,10 +638,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Help panel will show custom keymap if in use, with one definition for each entry
|
/// Help panel will show custom keymap if in use, with one definition for each entry
|
||||||
fn test_draw_blocks_custom_keymap_one_definition() {
|
fn test_draw_blocks_help_custom_keymap_one_definition() {
|
||||||
let (w, h) = (98, 48);
|
let (w, h) = (98, 47);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
|
||||||
|
|
||||||
let input = Keymap {
|
let input = Keymap {
|
||||||
clear: (KeyCode::Char('a'), None),
|
clear: (KeyCode::Char('a'), None),
|
||||||
@@ -609,11 +675,13 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, colors, &input);
|
super::draw(AppColors::new(), f, &input, false, None);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let version_row = format!(" ╭ {VERSION} ─────────────────────────────────────────────────────────────────────────────────────╮ ");
|
let version_row = format!(
|
||||||
|
" ╭ {VERSION} ─────────────────────────────────────────────────────────────────────────────────────╮ "
|
||||||
|
);
|
||||||
let expected = [
|
let expected = [
|
||||||
" ",
|
" ",
|
||||||
version_row.as_str(),
|
version_row.as_str(),
|
||||||
@@ -629,7 +697,6 @@ mod tests {
|
|||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ A simple tui to view & control docker containers │ ",
|
" │ A simple tui to view & control docker containers │ ",
|
||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ Custom keymap config in use │ ",
|
|
||||||
" │ ( 0 ) select next panel │ ",
|
" │ ( 0 ) select next panel │ ",
|
||||||
" │ ( 2 ) select previous panel │ ",
|
" │ ( 2 ) select previous panel │ ",
|
||||||
" │ ( q ) scroll list down by one │ ",
|
" │ ( q ) scroll list down by one │ ",
|
||||||
@@ -662,28 +729,24 @@ mod tests {
|
|||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ │ ",
|
" │ │ ",
|
||||||
" ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
" ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||||
" "
|
" ",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, result_row) in get_result(&setup, w) {
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
let expected_row = expected_to_vec(&expected, row_index);
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
if row_index == 14 && (36..=62).contains(&result_cell_index) {
|
|
||||||
assert_eq!(result_cell.fg, Color::White);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Help panel will show custom keymap if in use, with two definition for each entry
|
/// Help panel will show custom keymap if in use, with two definition for each entry
|
||||||
fn test_draw_blocks_custom_keymap_two_definitions() {
|
fn test_draw_blocks_help_custom_keymap_two_definitions() {
|
||||||
let (w, h) = (110, 48);
|
let (w, h) = (110, 47);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
|
||||||
|
|
||||||
let input = Keymap {
|
let keymap = Keymap {
|
||||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||||
delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))),
|
delete_deny: (KeyCode::Char('c'), Some(KeyCode::Char('d'))),
|
||||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||||
@@ -716,11 +779,13 @@ mod tests {
|
|||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, colors, &input);
|
super::draw(AppColors::new(), f, &keymap, false, None);
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let version_row = format!(" ╭ {VERSION} ───────────────────────────────────────────────────────────────────────────────────────────────────╮ ");
|
let version_row = format!(
|
||||||
|
" ╭ {VERSION} ───────────────────────────────────────────────────────────────────────────────────────────────────╮ "
|
||||||
|
);
|
||||||
let expected = [
|
let expected = [
|
||||||
" ",
|
" ",
|
||||||
version_row.as_str(),
|
version_row.as_str(),
|
||||||
@@ -736,7 +801,6 @@ mod tests {
|
|||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ A simple tui to view & control docker containers │ ",
|
" │ A simple tui to view & control docker containers │ ",
|
||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ Custom keymap config in use │ ",
|
|
||||||
" │ ( 0 ) or ( 1 ) select next panel │ ",
|
" │ ( 0 ) or ( 1 ) select next panel │ ",
|
||||||
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
||||||
" │ ( q ) or ( r ) scroll list down by one │ ",
|
" │ ( q ) or ( r ) scroll list down by one │ ",
|
||||||
@@ -782,12 +846,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Help panel will show custom keymap if in use, with either one or two definition for each entry
|
/// Help panel will show custom keymap if in use, with either one or two definition for each entry
|
||||||
fn test_draw_blocks_custom_keymap_one_and_two_definitions() {
|
fn test_draw_blocks_help_one_and_two_definitions() {
|
||||||
let (w, h) = (110, 48);
|
let (w, h) = (110, 47);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
|
||||||
|
|
||||||
let input = Keymap {
|
let keymap = Keymap {
|
||||||
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
clear: (KeyCode::Char('a'), Some(KeyCode::Char('b'))),
|
||||||
delete_deny: (KeyCode::Char('c'), None),
|
delete_deny: (KeyCode::Char('c'), None),
|
||||||
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
delete_confirm: (KeyCode::Char('e'), Some(KeyCode::Char('f'))),
|
||||||
@@ -817,14 +880,18 @@ mod tests {
|
|||||||
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
toggle_mouse_capture: (KeyCode::PageDown, Some(KeyCode::PageUp)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tz = setup.app_data.lock().config.timezone.clone();
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
super::draw(f, colors, &input);
|
super::draw(AppColors::new(), f, &keymap, false, tz.as_ref());
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let version_row = format!(" ╭ {VERSION} ───────────────────────────────────────────────────────────────────────────────────────────────────╮ ");
|
let version_row = format!(
|
||||||
|
" ╭ {VERSION} ───────────────────────────────────────────────────────────────────────────────────────────────────╮ "
|
||||||
|
);
|
||||||
let expected = [
|
let expected = [
|
||||||
" ",
|
" ",
|
||||||
version_row.as_str(),
|
version_row.as_str(),
|
||||||
@@ -840,7 +907,6 @@ mod tests {
|
|||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ A simple tui to view & control docker containers │ ",
|
" │ A simple tui to view & control docker containers │ ",
|
||||||
" │ │ ",
|
" │ │ ",
|
||||||
" │ Custom keymap config in use │ ",
|
|
||||||
" │ ( 0 ) select next panel │ ",
|
" │ ( 0 ) select next panel │ ",
|
||||||
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
" │ ( 2 ) or ( 3 ) select previous panel │ ",
|
||||||
" │ ( q ) or ( r ) scroll list down by one │ ",
|
" │ ( q ) or ( r ) scroll list down by one │ ",
|
||||||
@@ -883,4 +949,80 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draw_blocks_help_show_timezone() {
|
||||||
|
let (w, h) = (87, 35);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
AppColors::new(),
|
||||||
|
f,
|
||||||
|
&Keymap::new(),
|
||||||
|
true,
|
||||||
|
Some(&TimeZone::get("asia/tokyo").unwrap()),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let version_row = format!(
|
||||||
|
" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "
|
||||||
|
);
|
||||||
|
let expected = [
|
||||||
|
" ",
|
||||||
|
version_row.as_str(),
|
||||||
|
" │ │ ",
|
||||||
|
" │ 88 │ ",
|
||||||
|
" │ 88 │ ",
|
||||||
|
" │ 88 │ ",
|
||||||
|
" │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ",
|
||||||
|
r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#,
|
||||||
|
r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#,
|
||||||
|
r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#,
|
||||||
|
r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#,
|
||||||
|
" │ │ ",
|
||||||
|
" │ A simple tui to view & control docker containers │ ",
|
||||||
|
" │ │ ",
|
||||||
|
" │ logs timezone: Asia/Tokyo │ ",
|
||||||
|
" │ │ ",
|
||||||
|
" │ ( tab ) or ( shift+tab ) change panels │ ",
|
||||||
|
" │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ",
|
||||||
|
" │ ( enter ) send docker container command │ ",
|
||||||
|
" │ ( e ) exec into a container │ ",
|
||||||
|
" │ ( h ) toggle this help information - or click heading │ ",
|
||||||
|
" │ ( s ) save logs to file │ ",
|
||||||
|
" │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ",
|
||||||
|
" │ ( F1 ) or ( / ) enter filter mode │ ",
|
||||||
|
" │ ( 0 ) stop sort │ ",
|
||||||
|
" │ ( 1 - 9 ) sort by header - or click header │ ",
|
||||||
|
" │ ( esc ) close dialog │ ",
|
||||||
|
" │ ( q ) quit at any time │ ",
|
||||||
|
" │ │ ",
|
||||||
|
" │ currently an early work in progress, all and any input appreciated │ ",
|
||||||
|
" │ https://github.com/mrjackwills/oxker │ ",
|
||||||
|
" │ │ ",
|
||||||
|
" │ │ ",
|
||||||
|
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ",
|
||||||
|
" ",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
match (row_index, result_cell_index) {
|
||||||
|
(14, 31..=45) => {
|
||||||
|
assert_eq!(result_cell.fg, AppColors::new().popup_help.text);
|
||||||
|
}
|
||||||
|
(14, 46..=55) => {
|
||||||
|
assert_eq!(result_cell.fg, AppColors::new().popup_help.text_highlight);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ use std::{sync::Arc, time::Instant};
|
|||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::Alignment,
|
layout::Alignment,
|
||||||
style::Style,
|
style::Style,
|
||||||
widgets::{Block, Borders, Clear, Paragraph},
|
widgets::{Block, Borders, Clear, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::AppColors,
|
config::AppColors,
|
||||||
ui::{gui_state::BoxLocation, GuiState},
|
ui::{GuiState, gui_state::BoxLocation},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{max_line_width, popup};
|
use super::{max_line_width, popup};
|
||||||
|
|||||||
+268
-18
@@ -2,10 +2,10 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Rect},
|
|
||||||
style::{Modifier, Style},
|
|
||||||
widgets::{List, Paragraph},
|
|
||||||
Frame,
|
Frame,
|
||||||
|
layout::{Alignment, Rect},
|
||||||
|
style::{Modifier, Style, Stylize},
|
||||||
|
widgets::{List, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -14,7 +14,7 @@ use crate::{
|
|||||||
ui::{FrameData, GuiState, SelectablePanel, Status},
|
ui::{FrameData, GuiState, SelectablePanel, Status},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{generate_block, RIGHT_ARROW};
|
use super::{RIGHT_ARROW, generate_block};
|
||||||
|
|
||||||
/// Draw the logs panel
|
/// Draw the logs panel
|
||||||
pub fn draw(
|
pub fn draw(
|
||||||
@@ -25,22 +25,41 @@ pub fn draw(
|
|||||||
fd: &FrameData,
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
) {
|
) {
|
||||||
let block = generate_block(area, colors, fd, gui_state, SelectablePanel::Logs);
|
let mut block = generate_block(area, colors, fd, gui_state, SelectablePanel::Logs);
|
||||||
|
if !fd.color_logs {
|
||||||
|
block = block.bg(colors.logs.background);
|
||||||
|
}
|
||||||
|
|
||||||
if fd.status.contains(&Status::Init) {
|
if fd.status.contains(&Status::Init) {
|
||||||
let paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
let mut paragraph = Paragraph::new(format!("parsing logs {}", fd.loading_icon))
|
||||||
.style(Style::default())
|
|
||||||
.block(block)
|
.block(block)
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
|
if !fd.color_logs {
|
||||||
|
paragraph = paragraph.fg(colors.logs.text);
|
||||||
|
}
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
} else {
|
} else {
|
||||||
let logs = app_data.lock().get_logs();
|
let logs = app_data.lock().get_logs();
|
||||||
if logs.is_empty() {
|
if logs.is_empty() {
|
||||||
let paragraph = Paragraph::new("no logs found")
|
let mut paragraph = Paragraph::new("no logs found")
|
||||||
.block(block)
|
.block(block)
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
|
if !fd.color_logs {
|
||||||
|
paragraph = paragraph.fg(colors.logs.text);
|
||||||
|
}
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
|
} else if fd.color_logs {
|
||||||
|
let items = List::new(logs)
|
||||||
|
.block(block)
|
||||||
|
.highlight_symbol(RIGHT_ARROW)
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||||
|
// This should always return Some, as logs is not empty
|
||||||
|
if let Some(log_state) = app_data.lock().get_log_state() {
|
||||||
|
f.render_stateful_widget(items, area, log_state);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let items = List::new(logs)
|
let items = List::new(logs)
|
||||||
|
.fg(colors.logs.text)
|
||||||
.block(block)
|
.block(block)
|
||||||
.highlight_symbol(RIGHT_ARROW)
|
.highlight_symbol(RIGHT_ARROW)
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
||||||
@@ -60,11 +79,12 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{ContainerImage, ContainerName},
|
app_data::{ContainerImage, ContainerName},
|
||||||
|
config::AppColors,
|
||||||
ui::{
|
ui::{
|
||||||
draw_blocks::tests::{
|
|
||||||
expected_to_vec, get_result, insert_logs, test_setup, BORDER_CHARS,
|
|
||||||
},
|
|
||||||
FrameData, Status,
|
FrameData, Status,
|
||||||
|
draw_blocks::tests::{
|
||||||
|
BORDER_CHARS, expected_to_vec, get_result, insert_logs, test_setup,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +184,6 @@ mod tests {
|
|||||||
|
|
||||||
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
fd.status.insert(Status::Init);
|
fd.status.insert(Status::Init);
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
@@ -172,7 +191,7 @@ mod tests {
|
|||||||
super::draw(
|
super::draw(
|
||||||
&setup.app_data,
|
&setup.app_data,
|
||||||
setup.area,
|
setup.area,
|
||||||
colors,
|
AppColors::new(),
|
||||||
f,
|
f,
|
||||||
&fd,
|
&fd,
|
||||||
&setup.gui_state,
|
&setup.gui_state,
|
||||||
@@ -218,7 +237,7 @@ mod tests {
|
|||||||
super::draw(
|
super::draw(
|
||||||
&setup.app_data,
|
&setup.app_data,
|
||||||
setup.area,
|
setup.area,
|
||||||
colors,
|
AppColors::new(),
|
||||||
f,
|
f,
|
||||||
&fd,
|
&fd,
|
||||||
&setup.gui_state,
|
&setup.gui_state,
|
||||||
@@ -251,7 +270,6 @@ mod tests {
|
|||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
insert_logs(&setup);
|
insert_logs(&setup);
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
setup
|
setup
|
||||||
@@ -260,7 +278,7 @@ mod tests {
|
|||||||
super::draw(
|
super::draw(
|
||||||
&setup.app_data,
|
&setup.app_data,
|
||||||
setup.area,
|
setup.area,
|
||||||
colors,
|
AppColors::new(),
|
||||||
f,
|
f,
|
||||||
&fd,
|
&fd,
|
||||||
&setup.gui_state,
|
&setup.gui_state,
|
||||||
@@ -303,7 +321,7 @@ mod tests {
|
|||||||
super::draw(
|
super::draw(
|
||||||
&setup.app_data,
|
&setup.app_data,
|
||||||
setup.area,
|
setup.area,
|
||||||
colors,
|
AppColors::new(),
|
||||||
f,
|
f,
|
||||||
&fd,
|
&fd,
|
||||||
&setup.gui_state,
|
&setup.gui_state,
|
||||||
@@ -360,7 +378,230 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
let colors = setup.app_data.lock().config.app_colors;
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
&setup.app_data,
|
||||||
|
setup.area,
|
||||||
|
AppColors::new(),
|
||||||
|
f,
|
||||||
|
&fd,
|
||||||
|
&setup.gui_state,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draw_blocks_logs_custom_colors_parsing() {
|
||||||
|
let (w, h) = (32, 6);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
setup.gui_state.lock().next_loading(uuid);
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
"╭ Logs - container_1 - image_1 ╮",
|
||||||
|
"│ parsing logs ⠙ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"╰──────────────────────────────╯",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
fd.status.insert(Status::Init);
|
||||||
|
|
||||||
|
let mut colors = AppColors::new();
|
||||||
|
colors.logs.background = Color::Green;
|
||||||
|
colors.logs.text = Color::Black;
|
||||||
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
&setup.app_data,
|
||||||
|
setup.area,
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
&fd,
|
||||||
|
&setup.gui_state,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Green);
|
||||||
|
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, Color::Black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fd.color_logs = true;
|
||||||
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
&setup.app_data,
|
||||||
|
setup.area,
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
&fd,
|
||||||
|
&setup.gui_state,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Reset);
|
||||||
|
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, Color::Reset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
|
||||||
|
fn test_draw_blocks_logs_custom_colors_no_logs() {
|
||||||
|
let (w, h) = (35, 6);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
"╭ Logs - container_1 - image_1 ───╮",
|
||||||
|
"│ no logs found │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"╰─────────────────────────────────╯",
|
||||||
|
];
|
||||||
|
let mut colors = AppColors::new();
|
||||||
|
colors.logs.background = Color::Green;
|
||||||
|
colors.logs.text = Color::Black;
|
||||||
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
&setup.app_data,
|
||||||
|
setup.area,
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
&setup.fd,
|
||||||
|
&setup.gui_state,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Green);
|
||||||
|
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, Color::Black);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.fd.color_logs = true;
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
&setup.app_data,
|
||||||
|
setup.area,
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
&setup.fd,
|
||||||
|
&setup.gui_state,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Reset);
|
||||||
|
if let (1..=4, 1..=29) = (row_index, result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, Color::Reset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Logs correct displayed with custom colors
|
||||||
|
fn test_draw_blocks_logs_custom_colors_logs() {
|
||||||
|
let (w, h) = (36, 6);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
insert_logs(&setup);
|
||||||
|
|
||||||
|
let mut colors = setup.app_data.lock().config.app_colors;
|
||||||
|
colors.logs.background = Color::Green;
|
||||||
|
colors.logs.text = Color::Black;
|
||||||
|
let mut fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
fd.color_logs = true;
|
||||||
|
|
||||||
|
// Standard colors when color_logs is true
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(
|
||||||
|
&setup.app_data,
|
||||||
|
setup.area,
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
&fd,
|
||||||
|
&setup.gui_state,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let expected = [
|
||||||
|
"╭ Logs 3/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);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Reset);
|
||||||
|
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, Color::Reset);
|
||||||
|
if row_index == 3 && (1..=34).contains(&result_cell_index) {
|
||||||
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||||
|
} else {
|
||||||
|
assert!(result_cell.modifier.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fd.color_logs = false;
|
||||||
|
|
||||||
setup
|
setup
|
||||||
.terminal
|
.terminal
|
||||||
@@ -380,6 +621,15 @@ mod tests {
|
|||||||
let expected_row = expected_to_vec(&expected, row_index);
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Green);
|
||||||
|
if let (1..=4, 1..=34) = (row_index, result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, Color::Black);
|
||||||
|
if row_index == 3 && (1..=34).contains(&result_cell_index) {
|
||||||
|
assert_eq!(result_cell.modifier, Modifier::BOLD);
|
||||||
|
} else {
|
||||||
|
assert!(result_cell.modifier.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use ratatui::{
|
|||||||
|
|
||||||
use crate::config::AppColors;
|
use crate::config::AppColors;
|
||||||
|
|
||||||
use super::{gui_state::Region, FrameData, GuiState, SelectablePanel, Status};
|
use super::{FrameData, GuiState, SelectablePanel, Status, gui_state::Region};
|
||||||
|
|
||||||
pub mod charts;
|
pub mod charts;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
@@ -118,12 +118,12 @@ pub mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{backend::TestBackend, layout::Rect, style::Color, Terminal};
|
use ratatui::{Terminal, backend::TestBackend, layout::Rect, style::Color};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
|
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
|
||||||
tests::{gen_appdata, gen_containers},
|
tests::{gen_appdata, gen_containers},
|
||||||
ui::{draw_frame, GuiState},
|
ui::{GuiState, Redraw, draw_frame},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::FrameData;
|
use super::FrameData;
|
||||||
@@ -142,6 +142,7 @@ pub mod tests {
|
|||||||
pub const COLOR_TX: Color = Color::Rgb(205, 140, 140);
|
pub const COLOR_TX: Color = Color::Rgb(205, 140, 140);
|
||||||
pub const COLOR_ORANGE: Color = Color::Rgb(255, 178, 36);
|
pub const COLOR_ORANGE: Color = Color::Rgb(255, 178, 36);
|
||||||
|
|
||||||
|
/// Create a FrameData struct from two Arc<mutex>'s, instead of from UI
|
||||||
impl From<(&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)> for FrameData {
|
impl From<(&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)> for FrameData {
|
||||||
fn from(data: (&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)) -> Self {
|
fn from(data: (&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)) -> Self {
|
||||||
let (app_data, gui_data) = (data.0.lock(), data.1.lock());
|
let (app_data, gui_data) = (data.0.lock(), data.1.lock());
|
||||||
@@ -158,6 +159,7 @@ pub mod tests {
|
|||||||
Self {
|
Self {
|
||||||
chart_data: app_data.get_chart_data(),
|
chart_data: app_data.get_chart_data(),
|
||||||
columns: app_data.get_width(),
|
columns: app_data.get_width(),
|
||||||
|
color_logs: app_data.config.color_logs,
|
||||||
container_title: app_data.get_container_title(),
|
container_title: app_data.get_container_title(),
|
||||||
delete_confirm: gui_data.get_delete_container(),
|
delete_confirm: gui_data.get_delete_container(),
|
||||||
filter_by,
|
filter_by,
|
||||||
@@ -192,7 +194,8 @@ pub mod tests {
|
|||||||
app_data.containers_start();
|
app_data.containers_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
let gui_state = GuiState::default();
|
let redraw = Arc::new(Redraw::new());
|
||||||
|
let gui_state = GuiState::new(&redraw);
|
||||||
|
|
||||||
let app_data = Arc::new(Mutex::new(app_data));
|
let app_data = Arc::new(Mutex::new(app_data));
|
||||||
let gui_state = Arc::new(Mutex::new(gui_state));
|
let gui_state = Arc::new(Mutex::new(gui_state));
|
||||||
@@ -216,6 +219,7 @@ pub mod tests {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just a shorthand for when enumerating over result cells
|
||||||
pub fn get_result(
|
pub fn get_result(
|
||||||
setup: &TuiTestSetup,
|
setup: &TuiTestSetup,
|
||||||
w: u16,
|
w: u16,
|
||||||
|
|||||||
+134
-6
@@ -1,9 +1,9 @@
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
layout::{Alignment, Rect},
|
layout::{Alignment, Rect},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, BorderType, Borders, Paragraph},
|
widgets::{Block, BorderType, Borders, Paragraph},
|
||||||
Frame,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{app_data::State, config::AppColors, ui::FrameData};
|
use crate::{app_data::State, config::AppColors, ui::FrameData};
|
||||||
@@ -24,12 +24,12 @@ pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.style(Style::new().fg(colors.chart_ports.border))
|
.style(Style::new().fg(colors.chart_ports.border))
|
||||||
// .bg(colors.chart_ports.border))
|
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.title(Span::styled(
|
.title(Span::styled(
|
||||||
" ports ",
|
" ports ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(get_port_title_color(colors, ports.1))
|
.fg(get_port_title_color(colors, ports.1))
|
||||||
|
.bg(colors.chart_ports.background)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -42,7 +42,8 @@ pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
|||||||
};
|
};
|
||||||
let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
|
let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(block);
|
.block(block)
|
||||||
|
.bg(colors.chart_ports.background);
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
} else {
|
} else {
|
||||||
let mut output = vec![Line::from(
|
let mut output = vec![Line::from(
|
||||||
@@ -62,7 +63,9 @@ pub fn draw(area: Rect, colors: AppColors, f: &mut Frame, fd: &FrameData) {
|
|||||||
];
|
];
|
||||||
output.push(Line::from(line));
|
output.push(Line::from(line));
|
||||||
}
|
}
|
||||||
let paragraph = Paragraph::new(output).block(block);
|
let paragraph = Paragraph::new(output)
|
||||||
|
.block(block)
|
||||||
|
.bg(colors.chart_ports.background);
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,10 +79,13 @@ mod tests {
|
|||||||
use ratatui::style::{Color, Modifier};
|
use ratatui::style::{Color, Modifier};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{ContainerPorts, State},
|
app_data::{ContainerPorts, RunningState, State},
|
||||||
|
config::AppColors,
|
||||||
ui::{
|
ui::{
|
||||||
draw_blocks::tests::{expected_to_vec, get_result, test_setup},
|
|
||||||
FrameData,
|
FrameData,
|
||||||
|
draw_blocks::tests::{
|
||||||
|
COLOR_ORANGE, COLOR_RX, COLOR_TX, expected_to_vec, get_result, test_setup,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -317,4 +323,126 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Custom colors applied to ports panel
|
||||||
|
fn test_draw_blocks_ports_custom_colors() {
|
||||||
|
let (w, h) = (32, 8);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
|
let mut colors = AppColors::new();
|
||||||
|
colors.chart_ports.background = Color::Black;
|
||||||
|
colors.chart_ports.border = Color::Yellow;
|
||||||
|
colors.chart_ports.headings = Color::Red;
|
||||||
|
colors.chart_ports.text = Color::Green;
|
||||||
|
colors.chart_ports.title = Color::Magenta;
|
||||||
|
|
||||||
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(setup.area, colors, f, &fd);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
"╭─────────── ports ────────────╮",
|
||||||
|
"│ ip private public │",
|
||||||
|
"│ 8001 │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"╰──────────────────────────────╯",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
assert_eq!(result_cell.bg, Color::Black);
|
||||||
|
|
||||||
|
match (row_index, result_cell_index) {
|
||||||
|
// title => {
|
||||||
|
(0, 12..=18) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Magenta);
|
||||||
|
}
|
||||||
|
// title
|
||||||
|
(1, 1..=24) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Red);
|
||||||
|
}
|
||||||
|
// text
|
||||||
|
(2, 1..=24) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Green);
|
||||||
|
}
|
||||||
|
// border & everything else
|
||||||
|
_ => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Yellow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Custom state color applied to ports panel title
|
||||||
|
fn test_draw_blocks_ports_custom_colors_state() {
|
||||||
|
let (w, h) = (32, 8);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
|
let mut colors = AppColors::new();
|
||||||
|
colors.container_state.dead = Color::Green;
|
||||||
|
colors.container_state.exited = Color::Magenta;
|
||||||
|
colors.container_state.paused = Color::Gray;
|
||||||
|
colors.container_state.removing = COLOR_ORANGE;
|
||||||
|
colors.container_state.restarting = COLOR_RX;
|
||||||
|
colors.container_state.running_healthy = COLOR_TX;
|
||||||
|
colors.container_state.running_unhealthy = Color::Cyan;
|
||||||
|
colors.container_state.unknown = Color::LightMagenta;
|
||||||
|
|
||||||
|
colors.chart_ports.title = Color::DarkGray;
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
"╭─────────── ports ────────────╮",
|
||||||
|
"│ ip private public │",
|
||||||
|
"│ 8001 │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"│ │",
|
||||||
|
"╰──────────────────────────────╯",
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in [
|
||||||
|
(State::Dead, Color::Green),
|
||||||
|
(State::Exited, Color::Magenta),
|
||||||
|
(State::Paused, Color::Gray),
|
||||||
|
(State::Removing, COLOR_ORANGE),
|
||||||
|
(State::Restarting, COLOR_RX),
|
||||||
|
(State::Unknown, Color::LightMagenta),
|
||||||
|
(State::Running(RunningState::Healthy), Color::DarkGray),
|
||||||
|
(State::Running(RunningState::Unhealthy), Color::DarkGray),
|
||||||
|
] {
|
||||||
|
setup.app_data.lock().containers.items[0].state = i.0;
|
||||||
|
|
||||||
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::draw(setup.area, colors, f, &fd);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
|
||||||
|
if row_index == 0 && (12..=18).contains(&result_cell_index) {
|
||||||
|
assert_eq!(result_cell.fg, i.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-10
@@ -13,6 +13,8 @@ use crate::{
|
|||||||
exec::ExecMode,
|
exec::ExecMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::Redraw;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
pub enum SelectablePanel {
|
pub enum SelectablePanel {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -171,22 +173,40 @@ pub enum Status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct GuiState {
|
pub struct GuiState {
|
||||||
delete_container: Option<ContainerId>,
|
delete_container: Option<ContainerId>,
|
||||||
exec_mode: Option<ExecMode>,
|
exec_mode: Option<ExecMode>,
|
||||||
loading_handle: Option<JoinHandle<()>>,
|
|
||||||
loading_index: u8,
|
|
||||||
loading_set: HashSet<Uuid>,
|
|
||||||
intersect_delete: HashMap<DeleteButton, Rect>,
|
intersect_delete: HashMap<DeleteButton, Rect>,
|
||||||
intersect_heading: HashMap<Header, Rect>,
|
intersect_heading: HashMap<Header, Rect>,
|
||||||
intersect_help: Option<Rect>,
|
intersect_help: Option<Rect>,
|
||||||
intersect_panel: HashMap<SelectablePanel, Rect>,
|
intersect_panel: HashMap<SelectablePanel, Rect>,
|
||||||
|
loading_handle: Option<JoinHandle<()>>,
|
||||||
|
loading_index: u8,
|
||||||
|
loading_set: HashSet<Uuid>,
|
||||||
|
redraw: Arc<Redraw>,
|
||||||
selected_panel: SelectablePanel,
|
selected_panel: SelectablePanel,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
pub info_box_text: Option<(String, Instant)>,
|
pub info_box_text: Option<(String, Instant)>,
|
||||||
}
|
}
|
||||||
impl GuiState {
|
impl GuiState {
|
||||||
|
pub fn new(redraw: &Arc<Redraw>) -> Self {
|
||||||
|
Self {
|
||||||
|
delete_container: None,
|
||||||
|
exec_mode: None,
|
||||||
|
info_box_text: None,
|
||||||
|
intersect_delete: HashMap::new(),
|
||||||
|
intersect_heading: HashMap::new(),
|
||||||
|
intersect_help: None,
|
||||||
|
intersect_panel: HashMap::new(),
|
||||||
|
loading_handle: None,
|
||||||
|
loading_index: 0,
|
||||||
|
loading_set: HashSet::new(),
|
||||||
|
redraw: Arc::clone(redraw),
|
||||||
|
selected_panel: SelectablePanel::default(),
|
||||||
|
status: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
/// Clear panels hash map, so on resize can fix the sizes for mouse clicks
|
||||||
pub fn clear_area_map(&mut self) {
|
pub fn clear_area_map(&mut self) {
|
||||||
self.intersect_panel.clear();
|
self.intersect_panel.clear();
|
||||||
@@ -198,7 +218,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||||
pub fn get_intersect_panel(&mut self, rect: Rect) {
|
pub fn check_panel_intersect(&mut self, rect: Rect) {
|
||||||
if let Some(data) = self
|
if let Some(data) = self
|
||||||
.intersect_panel
|
.intersect_panel
|
||||||
.iter()
|
.iter()
|
||||||
@@ -207,6 +227,7 @@ impl GuiState {
|
|||||||
.first()
|
.first()
|
||||||
{
|
{
|
||||||
self.selected_panel = *data.0;
|
self.selected_panel = *data.0;
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +297,10 @@ impl GuiState {
|
|||||||
self.status.insert(Status::DeleteConfirm);
|
self.status.insert(Status::DeleteConfirm);
|
||||||
} else {
|
} else {
|
||||||
self.intersect_delete.clear();
|
self.intersect_delete.clear();
|
||||||
self.status.remove(&Status::DeleteConfirm);
|
self.status_del(Status::DeleteConfirm);
|
||||||
}
|
}
|
||||||
self.delete_container = id;
|
self.delete_container = id;
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a copy of the Status HashSet
|
/// Return a copy of the Status HashSet
|
||||||
@@ -299,6 +321,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inset the ExecMode into self, and set the Status as exec
|
/// Inset the ExecMode into self, and set the Status as exec
|
||||||
@@ -307,6 +330,7 @@ impl GuiState {
|
|||||||
pub fn set_exec_mode(&mut self, mode: ExecMode) {
|
pub fn set_exec_mode(&mut self, mode: ExecMode) {
|
||||||
self.exec_mode = Some(mode);
|
self.exec_mode = Some(mode);
|
||||||
self.status.insert(Status::Exec);
|
self.status.insert(Status::Exec);
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_exec_mode(&self) -> Option<ExecMode> {
|
pub fn get_exec_mode(&self) -> Option<ExecMode> {
|
||||||
@@ -316,22 +340,22 @@ impl GuiState {
|
|||||||
/// Insert a gui_status into the current gui_status HashSet
|
/// Insert a gui_status into the current gui_status HashSet
|
||||||
/// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
|
/// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
|
||||||
pub fn status_push(&mut self, status: Status) {
|
pub fn status_push(&mut self, status: Status) {
|
||||||
match status {
|
if status != Status::Exec {
|
||||||
Status::Exec => (),
|
|
||||||
_ => {
|
|
||||||
self.status.insert(status);
|
self.status.insert(status);
|
||||||
}
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change to next selectable panel
|
/// Change to next selectable panel
|
||||||
pub fn next_panel(&mut self) {
|
pub fn next_panel(&mut self) {
|
||||||
self.selected_panel = self.selected_panel.next();
|
self.selected_panel = self.selected_panel.next();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change to previous selectable panel
|
/// Change to previous selectable panel
|
||||||
pub fn previous_panel(&mut self) {
|
pub fn previous_panel(&mut self) {
|
||||||
self.selected_panel = self.selected_panel.prev();
|
self.selected_panel = self.selected_panel.prev();
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array
|
/// Insert a new loading_uuid into HashSet, and advance the loading_index by one frame, or reset to 0 if at end of array
|
||||||
@@ -342,6 +366,7 @@ impl GuiState {
|
|||||||
self.loading_index += 1;
|
self.loading_index += 1;
|
||||||
}
|
}
|
||||||
self.loading_set.insert(uuid);
|
self.loading_set.insert(uuid);
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_loading(&self) -> bool {
|
pub fn is_loading(&self) -> bool {
|
||||||
@@ -374,6 +399,7 @@ impl GuiState {
|
|||||||
/// Stop the loading_spin function, and reset gui loading status
|
/// Stop the loading_spin function, and reset gui loading status
|
||||||
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
|
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
|
||||||
self.loading_set.remove(&loading_uuid);
|
self.loading_set.remove(&loading_uuid);
|
||||||
|
self.redraw.set_true();
|
||||||
if self.loading_set.is_empty() {
|
if self.loading_set.is_empty() {
|
||||||
self.loading_index = 0;
|
self.loading_index = 0;
|
||||||
if let Some(h) = &self.loading_handle {
|
if let Some(h) = &self.loading_handle {
|
||||||
@@ -386,10 +412,12 @@ impl GuiState {
|
|||||||
/// Set info box content
|
/// Set info box content
|
||||||
pub fn set_info_box(&mut self, text: &str) {
|
pub fn set_info_box(&mut self, text: &str) {
|
||||||
self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
|
self.info_box_text = Some((text.to_owned(), std::time::Instant::now()));
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove info box content
|
/// Remove info box content
|
||||||
pub fn reset_info_box(&mut self) {
|
pub fn reset_info_box(&mut self) {
|
||||||
self.info_box_text = None;
|
self.info_box_text = None;
|
||||||
|
self.redraw.set_true();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-16
@@ -2,18 +2,18 @@ use anyhow::Result;
|
|||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, DisableMouseCapture, Event},
|
event::{self, DisableMouseCapture, Event},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
Frame, Terminal,
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction, Layout, Position},
|
layout::{Constraint, Direction, Layout, Position},
|
||||||
Frame, Terminal,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
io::{self, Stdout, Write},
|
io::{self, Stdout, Write},
|
||||||
sync::{atomic::Ordering, Arc},
|
sync::{Arc, atomic::Ordering},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use std::{sync::atomic::AtomicBool, time::Instant};
|
use std::{sync::atomic::AtomicBool, time::Instant};
|
||||||
@@ -23,6 +23,8 @@ use tracing::error;
|
|||||||
mod color_match;
|
mod color_match;
|
||||||
mod draw_blocks;
|
mod draw_blocks;
|
||||||
mod gui_state;
|
mod gui_state;
|
||||||
|
mod redraw;
|
||||||
|
pub use redraw::Redraw;
|
||||||
|
|
||||||
pub use self::color_match::*;
|
pub use self::color_match::*;
|
||||||
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||||
@@ -37,16 +39,19 @@ use crate::{
|
|||||||
input_handler::InputMessages,
|
input_handler::InputMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
const POLL_RATE: Duration = std::time::Duration::from_millis(100);
|
const POLL_RATE: Duration = std::time::Duration::from_millis(50);
|
||||||
|
|
||||||
|
// could have a render struct, which takes in poll rate, and docker
|
||||||
|
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
|
cursor_position: Position,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
input_tx: Sender<InputMessages>,
|
input_tx: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
|
redraw: Arc<Redraw>,
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
cursor_position: Position,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
@@ -68,8 +73,10 @@ impl Ui {
|
|||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
input_tx: Sender<InputMessages>,
|
input_tx: Sender<InputMessages>,
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
|
redraw: Arc<Redraw>,
|
||||||
) {
|
) {
|
||||||
if let Ok(mut terminal) = Self::setup_terminal() {
|
match Self::setup_terminal() {
|
||||||
|
Ok(mut terminal) => {
|
||||||
let cursor_position = terminal.get_cursor_position().unwrap_or_default();
|
let cursor_position = terminal.get_cursor_position().unwrap_or_default();
|
||||||
let mut ui = Self {
|
let mut ui = Self {
|
||||||
app_data,
|
app_data,
|
||||||
@@ -78,6 +85,7 @@ impl Ui {
|
|||||||
input_tx,
|
input_tx,
|
||||||
is_running,
|
is_running,
|
||||||
now: Instant::now(),
|
now: Instant::now(),
|
||||||
|
redraw,
|
||||||
terminal,
|
terminal,
|
||||||
};
|
};
|
||||||
if let Err(e) = ui.draw_ui().await {
|
if let Err(e) = ui.draw_ui().await {
|
||||||
@@ -86,10 +94,12 @@ impl Ui {
|
|||||||
if let Err(e) = ui.reset_terminal() {
|
if let Err(e) = ui.reset_terminal() {
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
};
|
};
|
||||||
} else {
|
}
|
||||||
|
_ => {
|
||||||
error!("Terminal Error");
|
error!("Terminal Error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Setup the terminal for full-screen drawing mode, with mouse capture
|
/// Setup the terminal for full-screen drawing mode, with mouse capture
|
||||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||||
@@ -126,30 +136,35 @@ impl Ui {
|
|||||||
let mut seconds = 5;
|
let mut seconds = 5;
|
||||||
let colors = self.app_data.lock().config.app_colors;
|
let colors = self.app_data.lock().config.app_colors;
|
||||||
let keymap = self.app_data.lock().config.keymap.clone();
|
let keymap = self.app_data.lock().config.keymap.clone();
|
||||||
|
let mut redraw = true;
|
||||||
loop {
|
loop {
|
||||||
if self.now.elapsed() >= std::time::Duration::from_secs(1) {
|
if self.now.elapsed() >= std::time::Duration::from_secs(1) {
|
||||||
seconds -= 1;
|
seconds -= 1;
|
||||||
self.now = Instant::now();
|
self.now = Instant::now();
|
||||||
|
redraw = true;
|
||||||
if seconds < 1 {
|
if seconds < 1 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if redraw
|
||||||
|
&& self
|
||||||
.terminal
|
.terminal
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
draw_blocks::error::draw(
|
draw_blocks::error::draw(
|
||||||
f,
|
colors,
|
||||||
&AppError::DockerConnect,
|
&AppError::DockerConnect,
|
||||||
|
f,
|
||||||
&keymap,
|
&keymap,
|
||||||
Some(seconds),
|
Some(seconds),
|
||||||
colors,
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return Err(AppError::Terminal);
|
return Err(AppError::Terminal);
|
||||||
}
|
}
|
||||||
|
redraw = false;
|
||||||
|
std::thread::sleep(POLL_RATE);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -173,12 +188,27 @@ impl Ui {
|
|||||||
self.gui_state.lock().status_del(Status::Exec);
|
self.gui_state.lock().status_del(Status::Exec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use the previously redrawn time, the current time, the docker_interval, and the redraw struct, to calculate
|
||||||
|
/// if the screen should be redrawn or not
|
||||||
|
fn should_redraw(&self, previous: &mut Instant, docker_interval_ms: u128) -> bool {
|
||||||
|
let result = self.redraw.swap() || previous.elapsed().as_millis() >= docker_interval_ms;
|
||||||
|
if result {
|
||||||
|
*previous = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
/// The loop for drawing the main UI to the terminal
|
/// The loop for drawing the main UI to the terminal
|
||||||
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
async fn gui_loop(&mut self) -> Result<(), AppError> {
|
||||||
let colors = self.app_data.lock().config.app_colors;
|
let colors = self.app_data.lock().config.app_colors;
|
||||||
let keymap = self.app_data.lock().config.keymap.clone();
|
let keymap = self.app_data.lock().config.keymap.clone();
|
||||||
|
let docker_interval_ms = u128::from(self.app_data.lock().config.docker_interval_ms);
|
||||||
|
let mut drawn_at = std::time::Instant::now();
|
||||||
|
|
||||||
while self.is_running.load(Ordering::SeqCst) {
|
while self.is_running.load(Ordering::SeqCst) {
|
||||||
|
if self.should_redraw(&mut drawn_at, docker_interval_ms) {
|
||||||
let fd = FrameData::from(&*self);
|
let fd = FrameData::from(&*self);
|
||||||
|
|
||||||
let exec = fd.status.contains(&Status::Exec);
|
let exec = fd.status.contains(&Status::Exec);
|
||||||
if exec {
|
if exec {
|
||||||
self.exec().await;
|
self.exec().await;
|
||||||
@@ -193,6 +223,7 @@ impl Ui {
|
|||||||
{
|
{
|
||||||
return Err(AppError::Terminal);
|
return Err(AppError::Terminal);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if crossterm::event::poll(POLL_RATE).unwrap_or(false) {
|
if crossterm::event::poll(POLL_RATE).unwrap_or(false) {
|
||||||
if let Ok(event) = event::read() {
|
if let Ok(event) = event::read() {
|
||||||
@@ -237,8 +268,8 @@ impl Ui {
|
|||||||
/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
|
/// Frequent data required by multiple frame drawing functions, can reduce mutex reads by placing it all in here
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FrameData {
|
pub struct FrameData {
|
||||||
// app_colors: AppColors,
|
|
||||||
chart_data: Option<(CpuTuple, MemTuple)>,
|
chart_data: Option<(CpuTuple, MemTuple)>,
|
||||||
|
color_logs: bool,
|
||||||
columns: Columns,
|
columns: Columns,
|
||||||
container_title: String,
|
container_title: String,
|
||||||
delete_confirm: Option<ContainerId>,
|
delete_confirm: Option<ContainerId>,
|
||||||
@@ -272,8 +303,8 @@ impl From<&Ui> for FrameData {
|
|||||||
|
|
||||||
let (filter_by, filter_term) = app_data.get_filter();
|
let (filter_by, filter_term) = app_data.get_filter();
|
||||||
Self {
|
Self {
|
||||||
// app_colors: app_data.config.app_colors,
|
|
||||||
chart_data: app_data.get_chart_data(),
|
chart_data: app_data.get_chart_data(),
|
||||||
|
color_logs: app_data.config.color_logs,
|
||||||
columns: app_data.get_width(),
|
columns: app_data.get_width(),
|
||||||
container_title: app_data.get_container_title(),
|
container_title: app_data.get_container_title(),
|
||||||
delete_confirm: gui_data.get_delete_container(),
|
delete_confirm: gui_data.get_delete_container(),
|
||||||
@@ -303,7 +334,6 @@ fn draw_frame(
|
|||||||
f: &mut Frame,
|
f: &mut Frame,
|
||||||
fd: &FrameData,
|
fd: &FrameData,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
gui_state: &Arc<Mutex<GuiState>>,
|
||||||
// should pass in the colors here, then I only need to get it once from app+data
|
|
||||||
) {
|
) {
|
||||||
let whole_constraints = if fd.status.contains(&Status::Filter) {
|
let whole_constraints = if fd.status.contains(&Status::Filter) {
|
||||||
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
||||||
@@ -353,7 +383,7 @@ fn draw_frame(
|
|||||||
|
|
||||||
// Draw filter bar
|
// Draw filter bar
|
||||||
if let Some(rect) = whole_layout.get(2) {
|
if let Some(rect) = whole_layout.get(2) {
|
||||||
draw_blocks::filter::draw(*rect, f, fd);
|
draw_blocks::filter::draw(*rect, colors, f, fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = fd.delete_confirm.as_ref() {
|
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||||
@@ -393,10 +423,17 @@ fn draw_frame(
|
|||||||
|
|
||||||
// Check if error, and show popup if so
|
// Check if error, and show popup if so
|
||||||
if fd.status.contains(&Status::Help) {
|
if fd.status.contains(&Status::Help) {
|
||||||
draw_blocks::help::draw(f, colors, keymap);
|
let tz = app_data.lock().config.timezone.clone();
|
||||||
|
draw_blocks::help::draw(
|
||||||
|
colors,
|
||||||
|
f,
|
||||||
|
keymap,
|
||||||
|
app_data.lock().config.show_timestamp,
|
||||||
|
tz.as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(error) = fd.has_error.as_ref() {
|
if let Some(error) = fd.has_error.as_ref() {
|
||||||
draw_blocks::error::draw(f, error, keymap, None, colors);
|
draw_blocks::error::draw(colors, error, f, keymap, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Redraw(AtomicBool);
|
||||||
|
|
||||||
|
impl Redraw {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self(AtomicBool::new(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_true(&self) {
|
||||||
|
self.0.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the value of the self, and set to false
|
||||||
|
pub fn swap(&self) -> bool {
|
||||||
|
match self
|
||||||
|
.0
|
||||||
|
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
Ok(previous_value) => previous_value,
|
||||||
|
Err(current_value) => current_value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user