diff --git a/.github/release-body.md b/.github/release-body.md
index 166376e..5b199a1 100644
--- a/.github/release-body.md
+++ b/.github/release-body.md
@@ -1,42 +1,18 @@
-### 2024-12-05
+### 2025-02-23
### Chores
-+ dependencies updated, [b78713579c4706d605e5b35fcd832610a0152294], [c6200e8f77f8bb1f0152cb9374029d15cc45df9d]
-+ Rust 1.83 linting, [751d997a3dac823e144ae62e6c1455676e50ddb8]
++ dependencies updated, [e5f355a1928f78abdb64e4c5617d6fac06340016], [4539d8ad0705b46d7c89c51c7be482b696d26e5f], [6aee6181136235a1a4f79af9b9748c1801be8bf8], [64d1bdf2bf88407e02f0eded1e03fcfc5ee2d8e3]
++ .devcontainer dependencies updated, [5c8e76e7bb4d7aab8543c9be09fdbc4ffa446b10]
++ example docker-compose.yml updated, [2354b0b9be1ab3795a421512594b2650b9cbdd74]
++ Rust 1.84 linting, [3065265e26c30d78ba738cfe731d3901ec1948d0]
### Features
-+ `--no-stderr` cli arg, removes Standard error output from logs, closes #52, [c739637b91c8fa742a69f4d888678d7b3964678c]
-+ ContainerPorts use ipaddr, [1b26997d25f748e0d452f41fe41791533046ecdf]
++ Config file introduced, including customizing color scheme of application, closes #47, [f4d54e1ba8ea1516394aef19511a63e6271f27bf]
++ 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
-+ update containerised Dockerfile, [0c6f53228f01196e352c2069383ba1e7a10950a8]
-+ 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]
++ Only draw screen if data or layout has changed, drastically reduces CPU usage, [bfc295c50e982886ccaa5e60b57f10d3690b3f09]
+
see CHANGELOG.md for more details
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05e5d8f..6de7d30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,21 @@
-# v0.9.0
+# v0.10.0
+### 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)
+
+ # v0.9.0
### 2024-12-05
### Chores
@@ -351,7 +368,7 @@
+ dead code removed, [b8f5792d](https://github.com/mrjackwills/oxker/commit/b8f5792d1865d3a398cd7f23aa9473a55dc6ea44)
+ improve the get_width function, [04c26fe8](https://github.com/mrjackwills/oxker/commit/04c26fe8fc7c79506921b9cff42825b1ee132737)
+ place ui methods into a Ui struct, [3437df59](https://github.com/mrjackwills/oxker/commit/3437df59884f084624031fceb34ea3012a8e2251)
-+ get_horizotal/vertical constraints into single method, [e8f5cf9c](https://github.com/mrjackwills/oxker/commit/e8f5cf9c6f8cd5f807a05fb61e31d7cd1426486f)
++ 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)
# v0.2.3
diff --git a/Cargo.lock b/Cargo.lock
index 06e18b7..223b54e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -90,9 +90,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.95"
+version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
[[package]]
name = "autocfg"
@@ -212,9 +212,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.12"
+version = "1.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
+checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
dependencies = [
"shlex",
]
@@ -240,9 +240,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.28"
+version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
+checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -250,9 +250,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.27"
+version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
+checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [
"anstream",
"anstyle",
@@ -416,9 +416,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
@@ -906,6 +906,36 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "js-sys"
version = "0.3.77"
@@ -962,9 +992,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.25"
+version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
+checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
[[package]]
name = "lru"
@@ -983,9 +1013,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
-version = "0.8.3"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
+checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [
"adler2",
]
@@ -1038,9 +1068,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "option-ext"
@@ -1056,7 +1086,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "oxker"
-version = "0.9.0"
+version = "0.10.0"
dependencies = [
"anyhow",
"bollard",
@@ -1065,6 +1095,7 @@ dependencies = [
"crossterm",
"directories",
"futures-util",
+ "jiff",
"parking_lot",
"ratatui",
"serde",
@@ -1125,6 +1156,21 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "powerfmt"
version = "0.2.0"
@@ -1166,7 +1212,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha",
"rand_core",
- "zerocopy 0.8.16",
+ "zerocopy 0.8.20",
]
[[package]]
@@ -1181,12 +1227,12 @@ dependencies = [
[[package]]
name = "rand_core"
-version = "0.9.0"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
+checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
dependencies = [
"getrandom 0.3.1",
- "zerocopy 0.8.16",
+ "zerocopy 0.8.20",
]
[[package]]
@@ -1212,9 +1258,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.5.8"
+version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [
"bitflags",
]
@@ -1269,18 +1315,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
-version = "1.0.217"
+version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.217"
+version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [
"proc-macro2",
"quote",
@@ -1289,9 +1335,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.138"
+version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
+checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
"itoa",
"memchr",
@@ -1415,9 +1461,9 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "socket2"
@@ -1627,9 +1673,9 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.22.23"
+version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap 2.7.1",
"serde",
@@ -1715,9 +1761,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
-version = "1.0.16"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
+checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-segmentation"
@@ -1779,9 +1825,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.13.1"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
+checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
dependencies = [
"getrandom 0.3.1",
"rand",
@@ -1990,9 +2036,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
-version = "0.7.1"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
+checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
dependencies = [
"memchr",
]
@@ -2054,11 +2100,11 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.16"
+version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b8c07a70861ce02bad1607b5753ecb2501f67847b9f9ada7c160fff0ec6300c"
+checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
dependencies = [
- "zerocopy-derive 0.8.16",
+ "zerocopy-derive 0.8.20",
]
[[package]]
@@ -2074,9 +2120,9 @@ dependencies = [
[[package]]
name = "zerocopy-derive"
-version = "0.8.16"
+version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5226bc9a9a9836e7428936cde76bb6b22feea1a8bfdbc0d241136e4d13417e25"
+checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index ac217d8..b62e7c1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "oxker"
-version = "0.9.0"
-edition = "2021"
+version = "0.10.0"
+edition = "2024"
authors = ["Jack Wills "]
description = "A simple tui to view & control docker containers"
repository = "https://github.com/mrjackwills/oxker"
@@ -33,6 +33,7 @@ clap = { version = "4.5", features = ["color", "derive", "unicode"] }
crossterm = "0.28"
directories = "6.0"
futures-util = "0.3"
+jiff = { version = "0.2", features = ["tzdb-bundle-always"] }
parking_lot = { version = "0.12" }
ratatui = "0.29"
serde = { version = "1.0", features = ["derive"] }
@@ -43,7 +44,8 @@ tokio-util = "0.7"
toml = { version = "0.8", default-features = false, features = ["parse"] }
tracing = "0.1"
tracing-subscriber = "0.3"
-uuid = { version = "1.12", features = ["fast-rng", "v4"] }
+uuid = { version = "1.14", features = ["fast-rng", "v4"] }
+
[profile.release]
lto = true
diff --git a/README.md b/README.md
index 1b1fba9..7a347ad 100644
--- a/README.md
+++ b/README.md
@@ -32,13 +32,9 @@ cargo install oxker
### Docker
-Published on Docker Hub and ghcr.io,
+Published on ghcr.io and Docker Hub,
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**
@@ -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
```
+**via Docker Hub**
+```shell
+docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock:ro --pull=always mrjackwills/oxker
+```
+
### Nix
Using nix flakes, oxker can be ran directly with
@@ -100,8 +101,7 @@ curl https://raw.githubusercontent.com/mrjackwills/oxker/main/install.sh | bash
```shell
oxker
```
-
-In application controls
+In application controls, these, amongst many other settings, can be customized with the [config file](#Config-File)
| button| result|
|--|--|
| ```( 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.|
Available command line arguments
+
| argument|result|
|--|--|
|```-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.|
|```-s```| If running via Docker, will display the oxker container.|
|```-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.|
|```--no-stderr```| Do not include stderr output in logs.|
|```--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.|
+### 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.
+
+
+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.
+
+
+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.
+
+
+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
### x86_64
@@ -163,8 +184,10 @@ If no memory information available, try appending either ```/boot/cmdline.txt```
see https://forums.raspberrypi.com/viewtopic.php?t=203128 and https://github.com/docker/for-linux/issues/1112
+
### Untested on other platforms
+
## Tests
~~As of yet untested, needs work~~
diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev
index 8a47e04..f8315da 100644
--- a/containerised/Dockerfile_dev
+++ b/containerised/Dockerfile_dev
@@ -3,7 +3,7 @@
#############
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
# 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
ENTRYPOINT [ "/app/oxker"]
+
# 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
# 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
-## 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
# 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 .
-
-# 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
-# docker build --platform linux/amd64 -t oxker_dev_amd64 -f containerised/Dockerfile .; docker save -o ./oxker_dev_amd64.tar oxker_dev_amd64
-# docker load -i oxker_dev_amd64.tar
-# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro 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_amd64.tar
+# 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 load -i oxker_dev_arm64.tar
-# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev 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_arm64.tar
+# 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 load -i oxker_dev_armv6.tar
-# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev_armv6
\ No newline at end of file
+# docker build --platform linux/arm/v6 -t oxker_armv6 -f containerised/Dockerfile .; docker save -o ./oxker_armv6.tar oxker_armv6
+# docker load -i oxker_armv6.tar
+# docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_armv6
\ No newline at end of file
diff --git a/create_release.sh b/create_release.sh
index 8008aae..7eebeb3 100755
--- a/create_release.sh
+++ b/create_release.sh
@@ -1,7 +1,7 @@
#!/bin/bash
-# rust create_release v0.6.1
-# 2024-10-21
+# rust create_release v0.6.2
+# 2025-02-22
STAR_LINE='****************************************'
CWD=$(pwd)
@@ -202,24 +202,28 @@ check_cross() {
fi
}
+# Build, using cross-rs, for linux x86 musl
cross_build_x86_linux() {
check_cross
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
cross build --target x86_64-unknown-linux-musl --release
}
+# Build, using cross-rs, for linux arm64 musl
cross_build_aarch64_linux() {
check_cross
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
cross build --target aarch64-unknown-linux-musl --release
}
+# Build, using cross-rs, for linux armv6 musl
cross_build_armv6_linux() {
check_cross
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
cross build --target arm-unknown-linux-musleabihf --release
}
+# Build, using cross-rs, for windows x86
cross_build_x86_windows() {
check_cross
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
@@ -266,6 +270,34 @@ check_allow_unused() {
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
release_flow() {
check_allow_unused
@@ -276,6 +308,7 @@ release_flow() {
cargo_test
cross_build_all
+ build_container_all
cargo_publish_dry_run
cd "${CWD}" || error_close "Can't find ${CWD}"
@@ -379,6 +412,45 @@ build_choice() {
;;
esac
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
2 "release" off
3 "build" off
+ 4 "docker builds" off
)
choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
exitStatus=$?
@@ -414,6 +487,11 @@ main() {
main
break
;;
+ 4)
+ build_container_choice
+ main
+ break
+ ;;
esac
done
}
diff --git a/example_config/example.config.json b/example_config/example.config.json
deleted file mode 100644
index 37ef0bf..0000000
--- a/example_config/example.config.json
+++ /dev/null
@@ -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"
- ]
- }
-}
\ No newline at end of file
diff --git a/example_config/example.config.jsonc b/example_config/example.config.jsonc
index db7e7e5..e3eeef1 100644
--- a/example_config/example.config.jsonc
+++ b/example_config/example.config.jsonc
@@ -1,7 +1,7 @@
{
// 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
- // 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
// The `--config-file` cli argument can be used to load configuration files from any readable location
// Docker update interval in ms, minimum effectively 1000
@@ -20,6 +20,11 @@
"gui": true,
// Docker host location
"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
// "save_dir": "$HOME",
// Force use of docker cli when execing into containers, honestly mostly pointless
@@ -246,6 +251,26 @@
// Ports & IP listing text
"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
"popup_help": {
// Background color
diff --git a/example_config/example.config.toml b/example_config/example.config.toml
index feb3da1..8d9180a 100644
--- a/example_config/example.config.toml
+++ b/example_config/example.config.toml
@@ -1,8 +1,5 @@
-# Example toml config file
-
-# This needs to be renamed to "config.toml" in order for oxker to automatically load
+# oxker config 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
# 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
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
# save_dir = "$HOME"
@@ -141,6 +145,13 @@ text_rx="#FFE9C1"
# Text color of the TX column
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
[colors.container_state]
dead="red"
@@ -152,6 +163,20 @@ running_healthy ="green"
running_unhealthy="#FFB224"
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
[colors.commands]
# Background color of panel
diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs
index f888ec1..d1d3786 100644
--- a/src/app_data/container_state.rs
+++ b/src/app_data/container_state.rs
@@ -6,6 +6,7 @@ use std::{
};
use bollard::service::Port;
+use jiff::{Timestamp, tz::TimeZone};
use ratatui::{
style::Color,
widgets::{ListItem, ListState},
@@ -34,6 +35,7 @@ impl ContainerId {
}
/// 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 {
self.0.chars().take(8).collect::()
}
@@ -175,25 +177,24 @@ impl StatefulList {
pub fn next(&mut self) {
if !self.items.is_empty() {
- self.state.select(Some(self.state.selected().map_or(0, |i| {
- if i < self.items.len() - 1 {
- i + 1
- } else {
- i
- }
- })));
+ self.state.select(Some(
+ self.state.selected().map_or(
+ 0,
+ |i| {
+ if i < self.items.len() - 1 { i + 1 } else { i }
+ },
+ ),
+ ));
}
}
pub fn previous(&mut self) {
if !self.items.is_empty() {
- self.state.select(Some(self.state.selected().map_or(0, |i| {
- if i == 0 {
- 0
- } else {
- i - 1
- }
- })));
+ self.state.select(Some(
+ self.state
+ .selected()
+ .map_or(0, |i| if i == 0 { 0 } else { i - 1 }),
+ ));
}
}
@@ -513,21 +514,34 @@ pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State);
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
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::())
- }
-}
-
impl fmt::Display for LogsTz {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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 {
+ self.0.parse::().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,
/// 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
@@ -682,7 +696,7 @@ impl ContainerItem {
self.cpu_stats
.iter()
.enumerate()
- .map(|i| (i.0 as f64, i.1 .0))
+ .map(|i| (i.0 as f64, i.1.0))
.collect::>()
}
@@ -692,7 +706,7 @@ impl ContainerItem {
self.mem_stats
.iter()
.enumerate()
- .map(|i| (i.0 as f64, i.1 .0 as f64))
+ .map(|i| (i.0 as f64, i.1.0 as f64))
.collect::>()
}
@@ -745,15 +759,18 @@ impl Columns {
}
#[cfg(test)]
+#[allow(clippy::unwrap_used)]
mod tests {
+
+ use jiff::tz::TimeZone;
use ratatui::widgets::ListItem;
use crate::{
- app_data::{ContainerImage, Logs, RunningState},
+ app_data::{ContainerImage, Logs, LogsTz, RunningState},
ui::log_sanitizer,
};
- use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
+ use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, State};
#[test]
/// Display CpuStats as a string
@@ -813,11 +830,76 @@ mod tests {
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]
/// Logs can only contain 1 entry per LogzTz
fn test_container_state_logz() {
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 line = log_sanitizer::remove_ansi(input);
@@ -828,7 +910,7 @@ mod tests {
assert_eq!(logs.logs.items.len(), 1);
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);
logs.insert(ListItem::new(line.clone()), tz.clone());
diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs
index 1822cb8..b852d33 100644
--- a/src/app_data/mod.rs
+++ b/src/app_data/mod.rs
@@ -11,10 +11,10 @@ use std::{
mod container_state;
use crate::{
+ ENTRY_POINT,
app_error::AppError,
config::Config,
- ui::{log_sanitizer, GuiState, Status},
- ENTRY_POINT,
+ ui::{GuiState, Redraw, Status, log_sanitizer},
};
pub use container_state::*;
@@ -122,7 +122,9 @@ pub struct AppData {
error: Option,
filter: Filter,
hidden_containers: Vec,
+ redraw: Arc,
sorted_by: Option<(Header, SortedOrder)>,
+ current_sorted_id: Vec,
pub config: Config,
}
@@ -134,18 +136,22 @@ pub struct AppData {
pub error: Option,
pub filter: Filter,
pub hidden_containers: Vec,
+ pub current_sorted_id: Vec,
+ pub redraw: Arc,
pub sorted_by: Option<(Header, SortedOrder)>,
}
impl AppData {
/// Generate a default app_state
- pub fn default(config: Config) -> Self {
+ pub fn new(config: Config, redraw: &Arc) -> Self {
Self {
config,
containers: StatefulList::new(vec![]),
+ current_sorted_id: vec![],
error: None,
filter: Filter::new(),
hidden_containers: vec![],
+ redraw: Arc::clone(redraw),
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
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();
match self.filter.by {
FilterBy::All => {
@@ -186,6 +192,7 @@ impl AppData {
/// sets the state to start if any filtering has occurred
/// Also search in the "hidden" vec for items and insert back into the main containers vec
fn filter_containers(&mut self) {
+ self.redraw.set_true();
let pre_len = self.get_container_len();
if !self.hidden_containers.is_empty() {
@@ -289,6 +296,7 @@ impl AppData {
/// Remove the sorted header & order, and sort by default - created datetime
pub fn reset_sorted(&mut self) {
self.set_sorted(None);
+ self.redraw.set_true();
}
/// Sort containers based on a given header, if headings match, and already ascending, remove sorting
@@ -309,10 +317,19 @@ impl AppData {
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 {
+ self.containers
+ .items
+ .iter()
+ .map(|i| i.id.clone())
+ .collect::>()
+ }
/// 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
pub fn sort_containers(&mut self) {
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 item_ord = match ord {
SortedOrder::Asc => (a, b),
@@ -372,13 +389,19 @@ impl AppData {
.then_with(|| item_ord.0.id.cmp(&item_ord.1.id)),
}
};
+
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| {
a.created
.cmp(&b.created)
.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
pub fn containers_start(&mut self) {
self.containers.start();
+ self.redraw.set_true();
}
/// select the last container
pub fn containers_end(&mut self) {
self.containers.end();
+ self.redraw.set_true();
}
/// Select the next container
pub fn containers_next(&mut self) {
self.containers.next();
+ self.redraw.set_true();
}
/// select the previous container
pub fn containers_previous(&mut self) {
self.containers.previous();
+ self.redraw.set_true();
}
/// Get ListState of containers
@@ -521,6 +548,11 @@ impl AppData {
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
pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> {
self.get_selected_container()
@@ -545,6 +577,7 @@ impl AppData {
pub fn docker_controls_next(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.next();
+ self.redraw.set_true();
}
}
@@ -552,6 +585,7 @@ impl AppData {
pub fn docker_controls_previous(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.previous();
+ self.redraw.set_true();
}
}
@@ -559,6 +593,7 @@ impl AppData {
pub fn docker_controls_start(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.start();
+ self.redraw.set_true();
}
}
@@ -566,6 +601,7 @@ impl AppData {
pub fn docker_controls_end(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.docker_controls.end();
+ self.redraw.set_true();
}
}
@@ -585,7 +621,7 @@ impl AppData {
/// Get the title for log panel for selected container, will be either
/// 1) "logs x/x - container_name - container_image"
/// 2) "logs - container_name - container_image" when no logs found
- /// 3) "" no container currently selected - aka no containers on system
+ /// 3) " " no container currently selected - aka no containers on system
pub fn get_log_title(&self) -> String {
self.get_selected_container()
.map_or_else(String::new, |ci| {
@@ -603,6 +639,7 @@ impl AppData {
pub fn log_next(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.next();
+ self.redraw.set_true();
}
}
@@ -610,6 +647,7 @@ impl AppData {
pub fn log_previous(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.previous();
+ self.redraw.set_true();
}
}
@@ -617,6 +655,7 @@ impl AppData {
pub fn log_end(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.end();
+ self.redraw.set_true();
}
}
@@ -624,6 +663,7 @@ impl AppData {
pub fn log_start(&mut self) {
if let Some(i) = self.get_mut_selected_container() {
i.logs.start();
+ self.redraw.set_true();
}
}
@@ -664,12 +704,14 @@ impl AppData {
/// Remove single app_state error
pub fn remove_error(&mut self) {
self.error = None;
+ self.redraw.set_true();
}
/// Insert single app_state error
pub fn set_error(&mut self, error: AppError, gui_state: &Arc>, status: Status) {
gui_state.lock().status_push(status);
self.error = Some(error);
+ self.redraw.set_true();
}
/// Check if the selected container is a dockerised version of oxker
@@ -758,6 +800,9 @@ impl AppData {
container.tx.update(tx);
container.mem_limit.update(mem_limit);
}
+ if self.is_selected_container(id) {
+ self.redraw.set_true();
+ }
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
if self.containers.items.get(index).is_some() {
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, id: &ContainerId) {
let color = self.config.color_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 !container.is_oxker {
container.last_updated = Self::get_systemtime();
let current_len = container.logs.len();
-
for mut i in logs {
- let tz = LogsTz::from(i.as_str());
- if !timestamp {
- i = i.replace(&tz.to_string(), "");
+ let (log_tz, log_content) = LogsTz::splitter(i.as_str());
+ if show_timestamp {
+ 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 {
log_sanitizer::colorize_logs(&i)
@@ -899,7 +957,7 @@ impl AppData {
} else {
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
@@ -910,6 +968,9 @@ impl AppData {
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");
// On last line of logs
- let logs = (1..=3).map(|i| format!("{i}")).collect::>();
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
app_data.update_log_by_id(logs, &ids[0]);
let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1 - image_1");
@@ -1851,7 +1912,7 @@ mod tests {
assert_eq!(result, " - container_2 - image_2");
// On last line of logs
- let logs = (1..=3).map(|i| format!("{i}")).collect::>();
+ let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>();
app_data.update_log_by_id(logs, &ids[1]);
let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_2 - image_2");
diff --git a/src/config/color_parser.rs b/src/config/color_parser.rs
index bdf7f82..1599637 100644
--- a/src/config/color_parser.rs
+++ b/src/config/color_parser.rs
@@ -73,6 +73,22 @@ impl From