From 211cf6b0172f6f04392ef885b6c163c395355b15 Mon Sep 17 00:00:00 2001 From: DCreason Date: Thu, 19 Feb 2026 01:19:05 +0000 Subject: [PATCH] v1 implemented it compiles --- .gitignore | 1 + Cargo.lock | 804 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 + PLAN.md | 47 +++ README.md | 60 ++++ src/collectors.rs | 104 ++++++ src/config.rs | 16 + src/main.rs | 91 ++++++ src/state.rs | 95 ++++++ src/ui.rs | 263 +++++++++++++++ 10 files changed, 1498 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 src/collectors.rs create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/state.rs create mode 100644 src/ui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5618f37 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,804 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "sloptop" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "crossterm", + "ratatui", + "sysinfo", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5b82da6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sloptop" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } +crossterm = "0.27" +ratatui = "0.26" +sysinfo = "0.30" + +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..f2cbfc6 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,47 @@ +# TUI Resource Monitor Plan + +## Language Choice: Rust + +Why Rust for this: +- Produces a single static-ish native binary easily (great for self-contained executables). +- Strong ecosystem for TUIs (`ratatui`, `crossterm`) and system stats (`sysinfo`). +- Good performance and low runtime overhead for frequent refresh loops. + +## Plan + +1. Define scope and MVP metrics +- CPU usage (total + per core), memory, swap, disk usage, network throughput, process count, uptime. +- Target Linux first (then add macOS/Windows support if needed). + +2. Bootstrap project structure +- Create a Rust CLI app with modules for `collectors`, `ui`, `state`, and `config`. +- Add dependencies: `ratatui`, `crossterm`, `sysinfo`, `anyhow`, `clap`, `serde` (optional config). + +3. Implement data collection layer +- Build a polling engine (e.g., 500ms–1s interval) that samples resource metrics. +- Normalize all values into a shared state model for UI rendering. +- Add delta-based metrics (network/disk rates) from successive samples. + +4. Build TUI layout and rendering +- Header: host info, uptime, refresh rate. +- Main panes: CPU, memory/swap, disk, network. +- Footer/help bar with keybindings. +- Add responsive layout for small terminal sizes. + +5. Add interaction model +- Keybindings: quit, pause/resume, change refresh interval, toggle detailed view. +- Optional sorting/filtering for top processes view. + +6. Packaging and self-contained build +- Configure release profile (`lto`, `panic=abort`, `strip`) for small fast binary. +- Document build command for optimized executable. +- Optional: static linking strategy for fully portable Linux binary. + +7. Quality gates +- Unit tests for metric calculations and rate/delta logic. +- Smoke test for startup/render loop. +- Run `clippy` and `fmt` checks. + +8. Delivery +- Provide binary + README with usage, keybindings, and supported platforms. +- Include known limitations and next-step roadmap (alerts, historical sparklines, per-process drilldown). diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7e8e50 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# sloptop + +`sloptop` is a Rust TUI resource monitor for Linux. + +## Features + +- CPU usage (total + per-core in detailed view) +- Memory and swap usage +- Disk usage per mounted disk +- Network throughput (RX/TX bytes per second) and totals +- Process count, uptime, host, and 1-minute load average +- Adjustable refresh interval and pause/resume +- Responsive layout for narrow terminals + +## Build + +Prerequisites: +- Rust toolchain (`rustc` + `cargo`) + +Build optimized binary: + +```bash +cargo build --release +``` + +Binary path: + +```bash +./target/release/sloptop +``` + +## Run + +Default refresh interval is 1000 ms: + +```bash +cargo run -- --interval-ms 1000 +``` + +Accepted interval range: `200..=5000` ms. + +## Keybindings + +- `q` / `Esc`: quit +- `p` / `Space`: pause/resume sampling +- `+` / `=`: increase refresh interval by 200 ms +- `-`: decrease refresh interval by 200 ms +- `d`: toggle detailed view (shows per-core CPU list) + +## Notes + +- Target platform is Linux. +- Network throughput is computed as a delta between consecutive samples. +- Disk and network metrics are system-wide aggregates. + +## Next steps + +- Add top-process table with sorting/filtering +- Add historical sparklines and alert thresholds +- Extend platform-specific support and testing for macOS/Windows diff --git a/src/collectors.rs b/src/collectors.rs new file mode 100644 index 0000000..a3c4fa4 --- /dev/null +++ b/src/collectors.rs @@ -0,0 +1,104 @@ +use std::time::{Duration, Instant}; + +use anyhow::Result; +use sysinfo::{CpuRefreshKind, Disks, MemoryRefreshKind, Networks, RefreshKind, System}; + +use crate::state::{DiskSample, Snapshot, Totals}; + +pub struct Collector { + system: System, + networks: Networks, + disks: Disks, + host: String, + last_sample_at: Instant, +} + +impl Collector { + pub fn new() -> Self { + let mut system = System::new_with_specifics( + RefreshKind::new() + .with_cpu(CpuRefreshKind::everything()) + .with_memory(MemoryRefreshKind::everything()), + ); + system.refresh_cpu(); + system.refresh_memory(); + system.refresh_processes(); + + let mut networks = Networks::new_with_refreshed_list(); + networks.refresh(); + + let disks = Disks::new_with_refreshed_list(); + + let host = System::host_name().unwrap_or_else(|| "unknown-host".to_string()); + + Self { + system, + networks, + disks, + host, + last_sample_at: Instant::now(), + } + } + + pub fn sample(&mut self) -> Result<(Snapshot, Duration)> { + self.system.refresh_cpu(); + self.system.refresh_memory(); + self.system.refresh_processes(); + self.networks.refresh(); + self.disks.refresh(); + + let elapsed = self.last_sample_at.elapsed(); + self.last_sample_at = Instant::now(); + + let cpu_total_percent = self.system.global_cpu_info().cpu_usage(); + let cpu_per_core_percent = self + .system + .cpus() + .iter() + .map(|cpu| cpu.cpu_usage()) + .collect::>(); + + let mem_total_bytes = self.system.total_memory(); + let mem_used_bytes = self.system.used_memory(); + let swap_total_bytes = self.system.total_swap(); + let swap_used_bytes = self.system.used_swap(); + + let disk_samples = self + .disks + .iter() + .map(|disk| { + let total = disk.total_space(); + let used = total.saturating_sub(disk.available_space()); + DiskSample { + name: disk.name().to_string_lossy().into_owned(), + total_bytes: total, + used_bytes: used, + } + }) + .collect::>(); + + let (rx_bytes, tx_bytes) = self.networks.iter().fold((0_u64, 0_u64), |acc, (_, data)| { + ( + acc.0.saturating_add(data.total_received()), + acc.1.saturating_add(data.total_transmitted()), + ) + }); + + let snapshot = Snapshot { + host: self.host.clone(), + uptime_seconds: System::uptime(), + load_avg_1m: System::load_average().one, + cpu_total_percent, + cpu_per_core_percent, + mem_used_bytes, + mem_total_bytes, + swap_used_bytes, + swap_total_bytes, + disk_samples, + process_count: self.system.processes().len(), + network_totals: Totals { rx_bytes, tx_bytes }, + }; + + Ok((snapshot, elapsed)) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5c1852c --- /dev/null +++ b/src/config.rs @@ -0,0 +1,16 @@ +use clap::Parser; +use std::time::Duration; + +#[derive(Debug, Clone, Parser)] +#[command(name = "sloptop", about = "A lightweight TUI resource monitor")] +pub struct Config { + /// Polling interval in milliseconds + #[arg(short, long, default_value_t = 1000, value_parser = clap::value_parser!(u64).range(200..=5000))] + pub interval_ms: u64, +} + +impl Config { + pub fn refresh_interval(&self) -> Duration { + Duration::from_millis(self.interval_ms) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..327ce51 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,91 @@ +mod collectors; +mod config; +mod state; +mod ui; + +use std::io; +use std::time::{Duration, Instant}; + +use anyhow::Result; +use clap::Parser; +use collectors::Collector; +use crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use state::{compute_throughput, AppState, Throughput}; + +fn main() -> Result<()> { + let cfg = config::Config::parse(); + run(cfg) +} + +fn run(cfg: config::Config) -> Result<()> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut collector = Collector::new(); + let (first_snapshot, _) = collector.sample()?; + + let mut app = AppState { + snapshot: first_snapshot, + network_rate: Throughput { + rx_bps: 0.0, + tx_bps: 0.0, + }, + paused: false, + detailed_view: false, + refresh_interval: cfg.refresh_interval(), + }; + + let mut last_refresh = Instant::now(); + + let result = loop { + terminal.draw(|f| ui::draw(f, &app))?; + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } + match key.code { + KeyCode::Char('q') | KeyCode::Esc => break Ok(()), + KeyCode::Char('p') | KeyCode::Char(' ') => app.paused = !app.paused, + KeyCode::Char('d') => app.detailed_view = !app.detailed_view, + KeyCode::Char('+') | KeyCode::Char('=') => { + app.refresh_interval = (app.refresh_interval + Duration::from_millis(200)) + .min(Duration::from_millis(5_000)); + } + KeyCode::Char('-') => { + app.refresh_interval = app + .refresh_interval + .saturating_sub(Duration::from_millis(200)) + .max(Duration::from_millis(200)); + } + _ => {} + } + } + } + + if !app.paused && last_refresh.elapsed() >= app.refresh_interval { + let prev_totals = app.snapshot.network_totals; + let (new_snapshot, elapsed) = collector.sample()?; + app.network_rate = + compute_throughput(prev_totals, new_snapshot.network_totals, elapsed); + app.snapshot = new_snapshot; + last_refresh = Instant::now(); + } + }; + + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + result +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b6bf7ce --- /dev/null +++ b/src/state.rs @@ -0,0 +1,95 @@ +use std::time::Duration; + +#[derive(Debug, Clone, Copy)] +pub struct Totals { + pub rx_bytes: u64, + pub tx_bytes: u64, +} + +#[derive(Debug, Clone, Copy)] +pub struct Throughput { + pub rx_bps: f64, + pub tx_bps: f64, +} + +#[derive(Debug, Clone)] +pub struct DiskSample { + pub name: String, + pub total_bytes: u64, + pub used_bytes: u64, +} + +#[derive(Debug, Clone)] +pub struct Snapshot { + pub host: String, + pub uptime_seconds: u64, + pub load_avg_1m: f64, + pub cpu_total_percent: f32, + pub cpu_per_core_percent: Vec, + pub mem_used_bytes: u64, + pub mem_total_bytes: u64, + pub swap_used_bytes: u64, + pub swap_total_bytes: u64, + pub disk_samples: Vec, + pub process_count: usize, + pub network_totals: Totals, +} + +#[derive(Debug, Clone)] +pub struct AppState { + pub snapshot: Snapshot, + pub network_rate: Throughput, + pub paused: bool, + pub detailed_view: bool, + pub refresh_interval: Duration, +} + +pub fn compute_throughput(prev: Totals, curr: Totals, elapsed: Duration) -> Throughput { + let seconds = elapsed.as_secs_f64().max(0.001); + let rx_delta = curr.rx_bytes.saturating_sub(prev.rx_bytes) as f64; + let tx_delta = curr.tx_bytes.saturating_sub(prev.tx_bytes) as f64; + + Throughput { + rx_bps: rx_delta / seconds, + tx_bps: tx_delta / seconds, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn throughput_is_delta_over_time() { + let prev = Totals { + rx_bytes: 1_000, + tx_bytes: 2_000, + }; + let curr = Totals { + rx_bytes: 3_000, + tx_bytes: 5_000, + }; + + let rate = compute_throughput(prev, curr, Duration::from_secs(2)); + + assert_eq!(rate.rx_bps, 1_000.0); + assert_eq!(rate.tx_bps, 1_500.0); + } + + #[test] + fn throughput_saturates_on_counter_reset() { + let prev = Totals { + rx_bytes: 10_000, + tx_bytes: 8_000, + }; + let curr = Totals { + rx_bytes: 1_000, + tx_bytes: 2_000, + }; + + let rate = compute_throughput(prev, curr, Duration::from_secs(1)); + + assert_eq!(rate.rx_bps, 0.0); + assert_eq!(rate.tx_bps, 0.0); + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..3e0e504 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,263 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Gauge, Paragraph, Wrap}, + Frame, +}; + +use crate::state::AppState; + +pub fn draw(frame: &mut Frame, state: &AppState) { + let root = frame.size(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(7), + Constraint::Length(1), + ]) + .split(root); + + draw_header(frame, chunks[0], state); + draw_body(frame, chunks[1], state); + draw_footer(frame, chunks[2], state); +} + +fn draw_header(frame: &mut Frame, area: Rect, state: &AppState) { + let uptime = format_uptime(state.snapshot.uptime_seconds); + let refresh_ms = state.refresh_interval.as_millis(); + let title = format!( + " {} | uptime {} | proc {} | load(1m) {:.2} | {}ms {}", + state.snapshot.host, + uptime, + state.snapshot.process_count, + state.snapshot.load_avg_1m, + refresh_ms, + if state.paused { "[PAUSED]" } else { "" } + ); + + frame.render_widget( + Paragraph::new(title) + .block(Block::default().borders(Borders::ALL).title("sloptop")) + .alignment(Alignment::Left), + area, + ); +} + +fn draw_body(frame: &mut Frame, area: Rect, state: &AppState) { + if area.width < 90 { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(6), + Constraint::Min(4), + ]) + .split(area); + + draw_cpu(frame, chunks[0], state); + draw_memory(frame, chunks[1], state); + draw_network(frame, chunks[2], state); + draw_disks(frame, chunks[3], state); + return; + } + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(area); + + let left_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Min(5), + ]) + .split(chunks[0]); + + draw_cpu(frame, left_chunks[0], state); + draw_memory(frame, left_chunks[1], state); + draw_disks(frame, left_chunks[2], state); + + draw_network(frame, chunks[1], state); +} + +fn draw_cpu(frame: &mut Frame, area: Rect, state: &AppState) { + let gauge = Gauge::default() + .block(Block::default().title("CPU").borders(Borders::ALL)) + .gauge_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ) + .percent(state.snapshot.cpu_total_percent.clamp(0.0, 100.0) as u16) + .label(format!("{:>5.1}%", state.snapshot.cpu_total_percent)); + frame.render_widget(gauge, area); +} + +fn draw_memory(frame: &mut Frame, area: Rect, state: &AppState) { + let mem_pct = percent( + state.snapshot.mem_used_bytes, + state.snapshot.mem_total_bytes, + ); + let swap_pct = percent( + state.snapshot.swap_used_bytes, + state.snapshot.swap_total_bytes, + ); + + let text = vec![Line::from(vec![ + Span::raw(format!("mem {:>5.1}% ", mem_pct)), + Span::styled( + format!( + "{} / {}", + fmt_bytes(state.snapshot.mem_used_bytes), + fmt_bytes(state.snapshot.mem_total_bytes) + ), + Style::default().fg(Color::Green), + ), + Span::raw(format!( + " swap {:>5.1}% {} / {}", + swap_pct, + fmt_bytes(state.snapshot.swap_used_bytes), + fmt_bytes(state.snapshot.swap_total_bytes) + )), + ])]; + + frame.render_widget( + Paragraph::new(text) + .block(Block::default().title("Memory").borders(Borders::ALL)) + .wrap(Wrap { trim: true }), + area, + ); +} + +fn draw_network(frame: &mut Frame, area: Rect, state: &AppState) { + let mut lines = vec![ + Line::from(format!( + "RX: {}/s", + fmt_bytes(state.network_rate.rx_bps as u64) + )), + Line::from(format!( + "TX: {}/s", + fmt_bytes(state.network_rate.tx_bps as u64) + )), + Line::from(format!( + "Total RX: {}", + fmt_bytes(state.snapshot.network_totals.rx_bytes) + )), + Line::from(format!( + "Total TX: {}", + fmt_bytes(state.snapshot.network_totals.tx_bytes) + )), + ]; + + if state.detailed_view { + lines.push(Line::from("")); + lines.push(Line::from("Per-core CPU:")); + for (idx, usage) in state + .snapshot + .cpu_per_core_percent + .iter() + .enumerate() + .take(10) + { + lines.push(Line::from(format!("core {:02}: {:>5.1}%", idx, usage))); + } + if state.snapshot.cpu_per_core_percent.len() > 10 { + lines.push(Line::from(format!( + "... and {} more cores", + state.snapshot.cpu_per_core_percent.len() - 10 + ))); + } + } + + frame.render_widget( + Paragraph::new(lines) + .block( + Block::default() + .title("Network / Details") + .borders(Borders::ALL), + ) + .wrap(Wrap { trim: true }), + area, + ); +} + +fn draw_disks(frame: &mut Frame, area: Rect, state: &AppState) { + let mut lines = Vec::new(); + if state.snapshot.disk_samples.is_empty() { + lines.push(Line::from("No disks reported")); + } else { + for disk in &state.snapshot.disk_samples { + let pct = percent(disk.used_bytes, disk.total_bytes); + lines.push(Line::from(format!( + "{} {:>5.1}% {} / {}", + disk.name, + pct, + fmt_bytes(disk.used_bytes), + fmt_bytes(disk.total_bytes) + ))); + } + } + + frame.render_widget( + Paragraph::new(lines) + .block(Block::default().title("Disk").borders(Borders::ALL)) + .wrap(Wrap { trim: true }), + area, + ); +} + +fn draw_footer(frame: &mut Frame, area: Rect, state: &AppState) { + let mode = if state.detailed_view { + "detail:on" + } else { + "detail:off" + }; + let help = format!("q quit | p pause | +/- interval | d toggle detail | {mode}"); + frame.render_widget(Paragraph::new(help), area); +} + +fn percent(used: u64, total: u64) -> f64 { + if total == 0 { + return 0.0; + } + (used as f64 / total as f64) * 100.0 +} + +fn fmt_bytes(bytes: u64) -> String { + const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; + if bytes == 0 { + return "0 B".to_string(); + } + + let mut value = bytes as f64; + let mut idx = 0; + while value >= 1024.0 && idx < UNITS.len() - 1 { + value /= 1024.0; + idx += 1; + } + + if idx == 0 { + format!("{} {}", bytes, UNITS[idx]) + } else { + format!("{value:.1} {}", UNITS[idx]) + } +} + +fn format_uptime(seconds: u64) -> String { + let days = seconds / 86_400; + let hours = (seconds % 86_400) / 3_600; + let mins = (seconds % 3_600) / 60; + let secs = seconds % 60; + + if days > 0 { + format!("{days}d {hours:02}:{mins:02}:{secs:02}") + } else { + format!("{hours:02}:{mins:02}:{secs:02}") + } +}