From 1d83d7a8a33f1758c5e6e3a1ae1ef914befce397 Mon Sep 17 00:00:00 2001 From: Krysztal Huang Date: Tue, 25 Mar 2025 23:58:35 +0800 Subject: [PATCH] tload: Basic implementation of `tload` (#362) * tload: add basic tui layout of modern look * tload: set x-axis bound from 0 to the width of terminal * tload: fix mismatched returning type * tload: add `#[allow(clippy::cognitive_complexity)]` * tload: add by-utils test * tload: tweaks for max height of chart * tload: bump version of `ratatui` from `0.28` to `0.29` * tload: fix typo * tload: set exit code to 130 * tload: fix typo * tload: add license header for `tui.rs` --- Cargo.lock | 363 +++++++++++++++++++++++++++++++++++- Cargo.toml | 8 +- README.md | 2 +- src/uu/tload/Cargo.toml | 25 +++ src/uu/tload/src/main.rs | 1 + src/uu/tload/src/tload.rs | 172 +++++++++++++++++ src/uu/tload/src/tui.rs | 119 ++++++++++++ src/uu/tload/tload.md | 7 + tests/by-util/test_tload.rs | 11 ++ tests/tests.rs | 4 + 10 files changed, 704 insertions(+), 8 deletions(-) create mode 100644 src/uu/tload/Cargo.toml create mode 100644 src/uu/tload/src/main.rs create mode 100644 src/uu/tload/src/tload.rs create mode 100644 src/uu/tload/src/tui.rs create mode 100644 src/uu/tload/tload.md create mode 100644 tests/by-util/test_tload.rs diff --git a/Cargo.lock b/Cargo.lock index 613f483..1b7c277 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -104,6 +110,21 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba" +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.92" @@ -208,6 +229,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -239,6 +274,31 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.40", + "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 = "csv" version = "1.3.0" @@ -260,6 +320,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -320,6 +415,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[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.10" @@ -336,6 +437,18 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.2.14" @@ -365,6 +478,23 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +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 = "hermit-abi" version = "0.3.9" @@ -394,6 +524,32 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac48900be4ab1c0dd6f1c2553d86ef371eda69c52b97ffd22af3e4f0a1771eb8" +dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "is-terminal" version = "0.4.12" @@ -405,6 +561,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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.11" @@ -454,18 +619,49 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "nix" version = "0.29.0" @@ -529,7 +725,30 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" dependencies = [ - "unicode-width 0.1.11", + "unicode-width 0.1.14", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", ] [[package]] @@ -541,6 +760,12 @@ dependencies = [ "regex", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "phf" version = "0.11.3" @@ -618,7 +843,7 @@ dependencies = [ "is-terminal", "lazy_static", "term", - "unicode-width 0.1.11", + "unicode-width 0.1.14", ] [[package]] @@ -659,6 +884,7 @@ dependencies = [ "uu_slabtop", "uu_snice", "uu_sysctl", + "uu_tload", "uu_top", "uu_w", "uu_watch", @@ -721,6 +947,27 @@ dependencies = [ "zerocopy 0.8.14", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -741,6 +988,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -843,6 +1099,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.197" @@ -863,12 +1125,48 @@ dependencies = [ "syn", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + [[package]] name = "smawk" version = "0.3.2" @@ -885,12 +1183,40 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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.87" @@ -1048,10 +1374,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] -name = "unicode-width" -version = "0.1.11" +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" @@ -1204,6 +1547,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "uu_tload" +version = "0.0.1" +dependencies = [ + "clap", + "crossterm", + "ratatui", + "uucore", +] + [[package]] name = "uu_top" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index af100e5..c518520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,14 @@ feat_common_core = [ "pgrep", "pidof", "pidwait", + "pkill", "pmap", "ps", "pwdx", "slabtop", "snice", - "pkill", "sysctl", + "tload", "top", "w", "watch", @@ -48,12 +49,14 @@ chrono = { version = "0.4.38", default-features = false, features = ["clock"] } clap = { version = "4.5.4", features = ["wrap_help", "cargo"] } clap_complete = "4.5.2" clap_mangen = "0.2.20" +crossterm = "0.28.1" libc = "0.2.154" nix = { version = "0.29", default-features = false, features = ["process"] } phf = "0.11.2" phf_codegen = "0.11.2" prettytable-rs = "0.10.0" rand = { version = "0.9.0", features = ["small_rng"] } +ratatui = "0.29.0" regex = "1.10.4" sysinfo = "0.33.0" tempfile = "3.10.1" @@ -80,13 +83,14 @@ free = { optional = true, version = "0.0.1", package = "uu_free", path = "src/uu pgrep = { optional = true, version = "0.0.1", package = "uu_pgrep", path = "src/uu/pgrep" } pidof = { optional = true, version = "0.0.1", package = "uu_pidof", path = "src/uu/pidof" } pidwait = { optional = true, version = "0.0.1", package = "uu_pidwait", path = "src/uu/pidwait" } +pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" } pmap = { optional = true, version = "0.0.1", package = "uu_pmap", path = "src/uu/pmap" } ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" } pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu/pwdx" } slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" } snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" } -pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" } sysctl = { optional = true, version = "0.0.1", package = "uu_sysctl", path = "src/uu/sysctl" } +tload = { optional = true, version = "0.0.1", package = "uu_tload", path = "src/uu/tload" } top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" } w = { optional = true, version = "0.0.1", package = "uu_w", path = "src/uu/w" } watch = { optional = true, version = "0.0.1", package = "uu_watch", path = "src/uu/watch" } diff --git a/README.md b/README.md index efb62a3..9a33ad2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Ongoing: * `slabtop`: Displays detailed kernel slab cache information in real time. * `snice`: Changes the scheduling priority of a running process. * `sysctl`: Read or write kernel parameters at run-time. +* `tload`: Prints a graphical representation of system load average to the terminal. * `top`: Displays real-time information about system processes. * `w`: Shows who is logged on and what they are doing. * `watch`: Executes a program periodically, showing output fullscreen. @@ -30,7 +31,6 @@ Ongoing: TODO: * `hugetop`: Report hugepage usage of processes and the system as a whole. * `skill`: Sends a signal to processes based on criteria like user, terminal, etc. -* `tload`: Prints a graphical representation of system load average to the terminal. * `vmstat`: Reports information about processes, memory, paging, block IO, traps, and CPU activity. Elsewhere: diff --git a/src/uu/tload/Cargo.toml b/src/uu/tload/Cargo.toml new file mode 100644 index 0000000..a1b584a --- /dev/null +++ b/src/uu/tload/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "uu_tload" +version = "0.0.1" +edition = "2021" +authors = ["uutils developers"] +license = "MIT" +description = "tload ~ (uutils) graphic representation of system load average" + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps/tree/main/src/uu/tload" +keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + +[dependencies] +clap = { workspace = true } +crossterm = { workspace = true } +ratatui = { workspace = true } +uucore = { workspace = true } + +[lib] +path = "src/tload.rs" + +[[bin]] +name = "tload" +path = "src/main.rs" diff --git a/src/uu/tload/src/main.rs b/src/uu/tload/src/main.rs new file mode 100644 index 0000000..5c5c627 --- /dev/null +++ b/src/uu/tload/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_tload); diff --git a/src/uu/tload/src/tload.rs b/src/uu/tload/src/tload.rs new file mode 100644 index 0000000..bd80530 --- /dev/null +++ b/src/uu/tload/src/tload.rs @@ -0,0 +1,172 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::collections::VecDeque; +use std::sync::{Arc, RwLock}; +use std::thread::{self, sleep}; +use std::time::Duration; + +use clap::{arg, crate_version, value_parser, ArgAction, ArgMatches, Command}; +use crossterm::event::{self, KeyCode, KeyEvent, KeyModifiers}; +use tui::{LegacyTui, ModernTui}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("tload.md"); +const USAGE: &str = help_usage!("tload.md"); + +mod tui; + +#[derive(Debug, Default, Clone)] +struct SystemLoadAvg { + pub(crate) last_1: f32, + pub(crate) last_5: f32, + pub(crate) last_10: f32, +} + +impl SystemLoadAvg { + #[cfg(target_os = "linux")] + fn new() -> UResult { + use std::fs; + use uucore::error::USimpleError; + + let result = fs::read_to_string("/proc/loadavg")?; + let split = result.split(" ").collect::>(); + + // Helper function to keep code clean + fn f(s: &str) -> UResult { + s.parse::() + .map_err(|e| USimpleError::new(1, e.to_string())) + } + + Ok(SystemLoadAvg { + last_1: f(split[0])?, + last_5: f(split[1])?, + last_10: f(split[2])?, + }) + } + + #[cfg(not(target_os = "linux"))] + fn new() -> UResult { + Ok(SystemLoadAvg::default()) + } +} + +#[allow(unused)] +#[derive(Debug)] +struct Settings { + delay: u64, + scale: usize, // Not used + + is_modern: bool, // For modern display +} + +impl Settings { + fn new(matches: &ArgMatches) -> Settings { + Settings { + delay: matches.get_one("delay").cloned().unwrap(), + scale: matches.get_one("scale").cloned().unwrap(), + is_modern: matches.get_flag("modern"), + } + } +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + let settings = Settings::new(&matches); + + let mut terminal = ratatui::init(); + + let data = { + // Why 10240? + // + // Emm, maybe there will be some terminal can display more than 10000 char? + let data = Arc::new(RwLock::new(VecDeque::with_capacity(10240))); + data.write() + .unwrap() + .push_back(SystemLoadAvg::new().unwrap()); + data + }; + let cloned_data = data.clone(); + thread::spawn(move || loop { + sleep(Duration::from_secs(settings.delay)); + + let mut data = cloned_data.write().unwrap(); + if data.iter().len() >= 10240 { + // Keep this VecDeque smaller than 10240 + data.pop_front(); + } + data.push_back(SystemLoadAvg::new().unwrap()); + }); + + loop { + // Now only accept `Ctrl+C` for compatibility with the original implementation + // + // Use `event::poll` for non-blocking event reading + if let Ok(true) = event::poll(Duration::from_millis(10)) { + // If event available, break this loop + if let Ok(event::Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + .. + })) = event::read() + { + // compatibility with the original implementation + uucore::error::set_exit_code(130); + break; + } + } + + terminal.draw(|frame| { + let data = &data.read().unwrap(); + let data = data.iter().cloned().collect::>(); + frame.render_widget( + if settings.is_modern { + ModernTui::new(&data) + } else { + LegacyTui::new(&data) + }, + frame.area(), + ); + })?; + + std::thread::sleep(Duration::from_millis(10)); + } + + ratatui::restore(); + Ok(()) +} + +#[allow(clippy::cognitive_complexity)] +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .args([ + arg!(-d --delay "update delay in seconds") + .value_parser(value_parser!(u64)) + .default_value("5") + .hide_default_value(true), + arg!(-m --modern "modern look").action(ArgAction::SetTrue), + // TODO: Implement this arg + arg!(-s --scale "vertical scale") + .value_parser(value_parser!(usize)) + .default_value("5") + .hide_default_value(true), + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + // It's just a test to make sure if can parsing correctly. + #[test] + fn test_system_load_avg() { + let _ = SystemLoadAvg::new().expect("SystemLoadAvg::new"); + } +} diff --git a/src/uu/tload/src/tui.rs b/src/uu/tload/src/tui.rs new file mode 100644 index 0000000..62ebdc3 --- /dev/null +++ b/src/uu/tload/src/tui.rs @@ -0,0 +1,119 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Rect}, + style::{Style, Stylize}, + symbols::Marker, + text::{Line, Text}, + widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Widget}, +}; + +use crate::SystemLoadAvg; + +pub(crate) struct ModernTui<'a>(&'a [SystemLoadAvg]); + +impl ModernTui<'_> { + fn render_header(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from(vec![ + Line::from(format!( + "Last 1 min load: {:>5}", + self.0.last().unwrap().last_1 + )), + Line::from(format!( + "Last 5 min load: {:>5}", + self.0.last().unwrap().last_5 + )), + Line::from(format!( + "Last 10 min load: {:>5}", + self.0.last().unwrap().last_10 + )), + ]); + + Paragraph::new(text) + .style(Style::default().bold().italic()) + .block( + Block::new() + .borders(Borders::ALL) + .title("System load history"), + ) + .render(area, buf); + } + + fn render_chart(&self, area: Rect, buf: &mut Buffer) { + let result = &self.0[self.0.len().saturating_sub(area.width.into())..] + .iter() + .enumerate() + .map(|(index, load)| (index as f64, load.last_1 as f64)) + .collect::>(); + + let data = Dataset::default() + .graph_type(GraphType::Line) + .marker(Marker::Braille) + .data(result); + + let x_axis = { + let start = Line::from("0"); + let middle = Line::from((area.width / 2).to_string()); + let end = Line::from(area.width.to_string()); + Axis::default() + .title("Time(per delay)") + .bounds([0.0, area.width.into()]) + .labels(vec![start, middle, end]) + }; + + // Why this tweak? + // + // Sometime the chart cannot display all the line because of max height are equals the max + // load of system in the history, so I add 0.2*{max_load} to the height of chart make it + // display beautiful + let y_axis_upper_bound = result.iter().map(|it| it.1).reduce(f64::max).unwrap_or(0.0); + let y_axis_upper_bound = y_axis_upper_bound + y_axis_upper_bound * 0.2; + let label = { + let min = "0.0".to_owned(); + let mid = format!("{:.1}", y_axis_upper_bound / 2.0); + let max = format!("{:.1}", y_axis_upper_bound); + vec![min, mid, max] + }; + let y_axis = Axis::default() + .bounds([0.0, y_axis_upper_bound]) + .labels(label) + .title("System Load"); + + Chart::new(vec![data]) + .x_axis(x_axis) + .y_axis(y_axis) + .render(area, buf); + } +} + +impl ModernTui<'_> { + pub(crate) fn new(input: &[SystemLoadAvg]) -> ModernTui<'_> { + ModernTui(input) + } +} + +impl Widget for ModernTui<'_> { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let layout = Layout::new( + Direction::Vertical, + [Constraint::Length(5), Constraint::Min(0)], + ) + .split(area); + + let header = layout[0]; + let chart = layout[1]; + + self.render_header(header, buf); + self.render_chart(chart, buf); + } +} + +// TODO: Implemented LegacyTui +pub(crate) type LegacyTui<'a> = ModernTui<'a>; diff --git a/src/uu/tload/tload.md b/src/uu/tload/tload.md new file mode 100644 index 0000000..eea64d2 --- /dev/null +++ b/src/uu/tload/tload.md @@ -0,0 +1,7 @@ +# tload + +``` +tload [options] [tty] +``` + +tload prints a graph of the current system load average to the specified tty (or the tty of the tload process if none is specified). diff --git a/tests/by-util/test_tload.rs b/tests/by-util/test_tload.rs new file mode 100644 index 0000000..85ca6f4 --- /dev/null +++ b/tests/by-util/test_tload.rs @@ -0,0 +1,11 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} diff --git a/tests/tests.rs b/tests/tests.rs index 2749cba..62f7e81 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -60,3 +60,7 @@ mod test_pkill; #[cfg(feature = "sysctl")] #[path = "by-util/test_sysctl.rs"] mod test_sysctl; + +#[cfg(feature = "tload")] +#[path = "by-util/test_tload.rs"] +mod test_tload;