mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Some basic framework.
This commit is contained in:
parent
82d213dd89
commit
6c7c8d94c9
250
src/rust/Cargo.lock
generated
250
src/rust/Cargo.lock
generated
@ -17,6 +17,18 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy 0.7.32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@ -41,6 +53,12 @@ dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@ -149,7 +167,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -160,7 +178,7 @@ checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -288,7 +306,7 @@ dependencies = [
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
"which",
|
||||
]
|
||||
|
||||
@ -303,9 +321,6 @@ name = "bitflags"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@ -373,6 +388,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.90"
|
||||
@ -505,7 +529,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -530,6 +554,19 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[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 = "cookie"
|
||||
version = "0.18.0"
|
||||
@ -638,22 +675,6 @@ version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
@ -665,7 +686,6 @@ dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
@ -717,6 +737,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
|
||||
dependencies = [
|
||||
"nix 0.28.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.2"
|
||||
@ -741,7 +771,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -813,7 +843,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1058,7 +1088,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1186,6 +1216,10 @@ name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@ -1638,7 +1672,7 @@ dependencies = [
|
||||
"lqos_sys",
|
||||
"lqos_utils",
|
||||
"once_cell",
|
||||
"zerocopy",
|
||||
"zerocopy 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1735,7 +1769,7 @@ dependencies = [
|
||||
"nix 0.28.0",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
"zerocopy",
|
||||
"zerocopy 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1748,7 +1782,7 @@ dependencies = [
|
||||
"notify",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"zerocopy",
|
||||
"zerocopy 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1784,7 +1818,7 @@ dependencies = [
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"zerocopy",
|
||||
"zerocopy 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1801,11 +1835,14 @@ name = "lqtop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossterm 0.27.0",
|
||||
"crossterm",
|
||||
"ctrlc",
|
||||
"lqos_bus",
|
||||
"lqos_utils",
|
||||
"once_cell",
|
||||
"ratatui",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1817,6 +1854,15 @@ dependencies = [
|
||||
"lqos_config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lts_client"
|
||||
version = "0.1.0"
|
||||
@ -2131,7 +2177,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2207,7 +2253,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2233,7 +2279,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2313,7 +2359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2333,7 +2379,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
@ -2385,7 +2431,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2398,7 +2444,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2440,6 +2486,26 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"itertools 0.12.1",
|
||||
"lru",
|
||||
"paste",
|
||||
"stability",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.9.0"
|
||||
@ -2486,7 +2552,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2660,7 +2726,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rocket_http",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
"unicode-xid",
|
||||
"version_check",
|
||||
]
|
||||
@ -2843,7 +2909,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3015,6 +3081,16 @@ dependencies = [
|
||||
"sqlite3-src",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stability"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable-pattern"
|
||||
version = "0.1.0"
|
||||
@ -3033,18 +3109,57 @@ dependencies = [
|
||||
"loom",
|
||||
]
|
||||
|
||||
[[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.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.53"
|
||||
@ -3133,7 +3248,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3229,7 +3344,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3349,7 +3464,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3397,19 +3512,6 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tui"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cassowary",
|
||||
"crossterm 0.25.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
@ -3582,7 +3684,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -3616,7 +3718,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -3961,7 +4063,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
dependencies = [
|
||||
"zerocopy-derive 0.7.32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3972,7 +4083,18 @@ checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3992,5 +4114,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 2.0.53",
|
||||
]
|
||||
|
@ -9,5 +9,8 @@ tokio = { version = "1", features = [ "full" ] }
|
||||
lqos_bus = { path = "../lqos_bus" }
|
||||
lqos_utils = { path = "../lqos_utils" }
|
||||
anyhow = "1"
|
||||
tui = "0.19"
|
||||
crossterm = { version = "0", features = [ "serde" ] }
|
||||
ratatui = "0.26.1"
|
||||
crossterm = "0.27.0"
|
||||
ctrlc = "3.4.4"
|
||||
sysinfo = "0"
|
||||
once_cell = "1.19.0"
|
||||
|
57
src/rust/lqtop/src/bus/cpu_ram.rs
Normal file
57
src/rust/lqtop/src/bus/cpu_ram.rs
Normal file
@ -0,0 +1,57 @@
|
||||
//! Provides a sysinfo link for CPU and RAM tracking
|
||||
|
||||
use crate::ui_base::SHOULD_EXIT;
|
||||
use std::sync::atomic::Ordering;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize};
|
||||
|
||||
const MAX_CPUS_COUNTED: usize = 128;
|
||||
|
||||
/// Stores overall CPU usage
|
||||
pub static CPU_USAGE: Lazy<[AtomicU32; MAX_CPUS_COUNTED]> =
|
||||
Lazy::new(build_empty_cpu_list);
|
||||
|
||||
/// Total number of CPUs detected
|
||||
pub static NUM_CPUS: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Total RAM used (bytes)
|
||||
pub static RAM_USED: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Total RAM installed (bytes)
|
||||
pub static TOTAL_RAM: AtomicU64 = AtomicU64::new(0);
|
||||
pub async fn gather_sysinfo() {
|
||||
use sysinfo::System;
|
||||
let mut sys = System::new_all();
|
||||
|
||||
loop {
|
||||
if SHOULD_EXIT.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Refresh system info
|
||||
sys.refresh_cpu();
|
||||
sys.refresh_memory();
|
||||
|
||||
sys.cpus()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, cpu)| (i, cpu.cpu_usage() as u32)) // Always rounds down
|
||||
.for_each(|(i, cpu)| CPU_USAGE[i].store(cpu, std::sync::atomic::Ordering::Relaxed));
|
||||
|
||||
NUM_CPUS.store(sys.cpus().len(), std::sync::atomic::Ordering::Relaxed);
|
||||
RAM_USED.store(sys.used_memory(), std::sync::atomic::Ordering::Relaxed);
|
||||
TOTAL_RAM.store(sys.total_memory(), std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Sleep
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn build_empty_cpu_list() -> [AtomicU32; MAX_CPUS_COUNTED] {
|
||||
let mut temp = Vec::with_capacity(MAX_CPUS_COUNTED);
|
||||
for _ in 0..MAX_CPUS_COUNTED {
|
||||
temp.push(AtomicU32::new(0));
|
||||
}
|
||||
temp.try_into().expect("This should never happen, sizes are constant.")
|
||||
}
|
||||
|
85
src/rust/lqtop/src/bus/mod.rs
Normal file
85
src/rust/lqtop/src/bus/mod.rs
Normal file
@ -0,0 +1,85 @@
|
||||
//! Handles the communication loop with lqosd.
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
use lqos_bus::{BusClient, BusRequest, BusResponse};
|
||||
use anyhow::{bail, Result};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use crate::ui_base::SHOULD_EXIT;
|
||||
pub mod cpu_ram;
|
||||
pub mod throughput;
|
||||
|
||||
/// Event types to instruct the bus
|
||||
pub enum BusCommand {
|
||||
/// Collect the total throughput
|
||||
CollectTotalThroughput(bool),
|
||||
/// Quit the bus
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// The main loop for the bus.
|
||||
/// Spawns a separate task to handle the bus communication.
|
||||
pub async fn bus_loop() -> Sender<BusCommand> {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<BusCommand>(100);
|
||||
|
||||
tokio::spawn(cpu_ram::gather_sysinfo());
|
||||
tokio::spawn(main_loop_wrapper(rx));
|
||||
|
||||
tx
|
||||
}
|
||||
|
||||
async fn main_loop_wrapper(rx: Receiver<BusCommand>) {
|
||||
let loop_result = main_loop(rx).await;
|
||||
if let Err(e) = loop_result {
|
||||
eprintln!("Error in main loop: {}", e);
|
||||
SHOULD_EXIT.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_loop(mut rx: Receiver<BusCommand>) -> Result<()> {
|
||||
// Collection Settings
|
||||
let mut collect_total_throughput = true;
|
||||
|
||||
let mut bus_client = BusClient::new().await?;
|
||||
if !bus_client.is_connected() {
|
||||
bail!("Failed to connect to the bus");
|
||||
}
|
||||
|
||||
loop {
|
||||
// Do we have any behavior changing commands?
|
||||
if let Ok(cmd) = rx.try_recv() {
|
||||
match cmd {
|
||||
BusCommand::CollectTotalThroughput(val) => {
|
||||
collect_total_throughput = val;
|
||||
}
|
||||
BusCommand::Quit => {
|
||||
SHOULD_EXIT.store(true, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform actual bus collection
|
||||
let mut commands: Vec<BusRequest> = Vec::new();
|
||||
|
||||
if collect_total_throughput {
|
||||
commands.push(BusRequest::GetCurrentThroughput);
|
||||
}
|
||||
|
||||
// Send the requests and process replies
|
||||
for response in bus_client.request(commands).await? {
|
||||
match response {
|
||||
BusResponse::CurrentThroughput{..} => throughput::throughput(&response).await,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should be quitting
|
||||
if SHOULD_EXIT.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Sleep for one tick
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
114
src/rust/lqtop/src/bus/throughput.rs
Normal file
114
src/rust/lqtop/src/bus/throughput.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use std::sync::Mutex;
|
||||
use lqos_bus::BusResponse;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub static THROUGHPUT_RING: Lazy<Mutex<ThroughputRingbuffer>> = Lazy::new(|| Mutex::new(ThroughputRingbuffer::default()));
|
||||
const RINGBUFFER_SIZE: usize = 80;
|
||||
pub static CURRENT_THROUGHPUT: Lazy<Mutex<CurrentThroughput>> = Lazy::new(|| Mutex::new(CurrentThroughput::default()));
|
||||
|
||||
#[derive(Default, Copy, Clone)]
|
||||
pub struct CurrentThroughput {
|
||||
pub bits_per_second: (u64, u64),
|
||||
pub packets_per_second: (u64, u64),
|
||||
pub shaped_bits_per_second: (u64, u64),
|
||||
}
|
||||
|
||||
pub struct ThroughputRingbuffer {
|
||||
current_index: usize,
|
||||
pub ringbuffer: [CurrentThroughput; RINGBUFFER_SIZE],
|
||||
}
|
||||
|
||||
impl ThroughputRingbuffer {
|
||||
fn push(&mut self, current: CurrentThroughput) {
|
||||
self.ringbuffer[self.current_index] = current;
|
||||
self.current_index = (self.current_index + 1) % RINGBUFFER_SIZE;
|
||||
}
|
||||
|
||||
pub fn bits_per_second_vec_up(&self) -> Vec<u64> {
|
||||
let mut result = Vec::with_capacity(RINGBUFFER_SIZE);
|
||||
|
||||
for i in self.current_index..RINGBUFFER_SIZE {
|
||||
result.push(self.ringbuffer[i].bits_per_second.0);
|
||||
}
|
||||
for i in 0..self.current_index {
|
||||
result.push(self.ringbuffer[i].bits_per_second.0);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn bits_per_second_vec_down(&self) -> Vec<u64> {
|
||||
let mut result = Vec::with_capacity(RINGBUFFER_SIZE);
|
||||
|
||||
for i in self.current_index..RINGBUFFER_SIZE {
|
||||
result.push(self.ringbuffer[i].bits_per_second.1);
|
||||
}
|
||||
for i in 0..self.current_index {
|
||||
result.push(self.ringbuffer[i].bits_per_second.1);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn shaped_bits_per_second_vec_up(&self) -> Vec<u64> {
|
||||
let mut result = Vec::with_capacity(RINGBUFFER_SIZE);
|
||||
|
||||
for i in self.current_index..RINGBUFFER_SIZE {
|
||||
result.push(self.ringbuffer[i].shaped_bits_per_second.0);
|
||||
}
|
||||
for i in 0..self.current_index {
|
||||
result.push(self.ringbuffer[i].shaped_bits_per_second.0);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn shaped_bits_per_second_vec_down(&self) -> Vec<u64> {
|
||||
let mut result = Vec::with_capacity(RINGBUFFER_SIZE);
|
||||
|
||||
for i in self.current_index..RINGBUFFER_SIZE {
|
||||
result.push(self.ringbuffer[i].shaped_bits_per_second.1);
|
||||
}
|
||||
for i in 0..self.current_index {
|
||||
result.push(self.ringbuffer[i].shaped_bits_per_second.1);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThroughputRingbuffer {
|
||||
fn default() -> Self {
|
||||
let mut ringbuffer = [CurrentThroughput::default(); RINGBUFFER_SIZE];
|
||||
for i in 0..RINGBUFFER_SIZE {
|
||||
ringbuffer[i].bits_per_second = (0, 0);
|
||||
ringbuffer[i].packets_per_second = (0, 0);
|
||||
ringbuffer[i].shaped_bits_per_second = (0, 0);
|
||||
}
|
||||
ThroughputRingbuffer {
|
||||
current_index: 0,
|
||||
ringbuffer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn throughput(response: &BusResponse) {
|
||||
if let BusResponse::CurrentThroughput {
|
||||
bits_per_second,
|
||||
packets_per_second,
|
||||
shaped_bits_per_second,
|
||||
} = response
|
||||
{
|
||||
let mut rb = THROUGHPUT_RING.lock().unwrap();
|
||||
rb.push(CurrentThroughput {
|
||||
bits_per_second: *bits_per_second,
|
||||
packets_per_second: *packets_per_second,
|
||||
shaped_bits_per_second: *shaped_bits_per_second,
|
||||
});
|
||||
|
||||
let mut current = CURRENT_THROUGHPUT.lock().unwrap();
|
||||
current.bits_per_second = *bits_per_second;
|
||||
current.packets_per_second = *packets_per_second;
|
||||
current.shaped_bits_per_second = *shaped_bits_per_second;
|
||||
}
|
||||
}
|
@ -1,447 +1,20 @@
|
||||
mod ui_base;
|
||||
mod top_level_ui;
|
||||
mod bus;
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{read, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
terminal::enable_raw_mode,
|
||||
};
|
||||
use lqos_bus::{BusClient, BusRequest, BusResponse, IpStats};
|
||||
use lqos_utils::packet_scale::{scale_bits, scale_packets};
|
||||
use std::{io, time::Duration};
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, BorderType, Cell, Paragraph, Row, Table},
|
||||
Terminal,
|
||||
};
|
||||
use ui_base::UiBase;
|
||||
pub mod widgets;
|
||||
|
||||
struct DataResult {
|
||||
totals: (u64, u64, u64, u64),
|
||||
top: Vec<IpStats>,
|
||||
}
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Spawn the bus as an async background task and retrieve
|
||||
// the command sender.
|
||||
let bus_commander = bus::bus_loop().await;
|
||||
|
||||
async fn get_data(client: &mut BusClient, n_rows: u16) -> Result<DataResult> {
|
||||
let mut result = DataResult { totals: (0, 0, 0, 0), top: Vec::new() };
|
||||
let requests = vec![
|
||||
BusRequest::GetCurrentThroughput,
|
||||
BusRequest::GetTopNDownloaders { start: 0, end: n_rows as u32 },
|
||||
];
|
||||
for r in client.request(requests).await? {
|
||||
match r {
|
||||
BusResponse::CurrentThroughput {
|
||||
bits_per_second,
|
||||
packets_per_second,
|
||||
shaped_bits_per_second: _,
|
||||
} => {
|
||||
let tuple = (
|
||||
bits_per_second.0,
|
||||
bits_per_second.1,
|
||||
packets_per_second.0,
|
||||
packets_per_second.1,
|
||||
);
|
||||
result.totals = tuple;
|
||||
}
|
||||
BusResponse::TopDownloaders(top) => {
|
||||
result.top = top.clone();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Initialize the UI
|
||||
let mut ui = UiBase::new(bus_commander.clone())?;
|
||||
ui.event_loop().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn draw_menu<'a>(is_connected: bool) -> Paragraph<'a> {
|
||||
let mut text = Spans::from(vec![
|
||||
Span::styled("Q", Style::default().fg(Color::White)),
|
||||
Span::from("uit"),
|
||||
]);
|
||||
|
||||
if !is_connected {
|
||||
text
|
||||
.0
|
||||
.push(Span::styled(" NOT CONNECTED ", Style::default().fg(Color::Red)))
|
||||
} else {
|
||||
text
|
||||
.0
|
||||
.push(Span::styled(" CONNECTED ", Style::default().fg(Color::Green)))
|
||||
}
|
||||
|
||||
let para = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.border_type(BorderType::Plain)
|
||||
.title("LibreQoS Monitor: "),
|
||||
);
|
||||
|
||||
para
|
||||
}
|
||||
|
||||
fn draw_pps<'a>(
|
||||
packets_per_second: (u64, u64),
|
||||
bits_per_second: (u64, u64),
|
||||
) -> Spans<'a> {
|
||||
Spans::from(vec![
|
||||
Span::from("DOWN: "),
|
||||
Span::from(scale_bits(bits_per_second.0)),
|
||||
Span::from(" "),
|
||||
Span::from(scale_bits(bits_per_second.1)),
|
||||
Span::from(" "),
|
||||
Span::from("UP: "),
|
||||
Span::from(scale_packets(packets_per_second.0)),
|
||||
Span::from(" "),
|
||||
Span::from(scale_packets(packets_per_second.1)),
|
||||
])
|
||||
}
|
||||
|
||||
fn draw_top_pane<'a>(
|
||||
top: &[IpStats],
|
||||
packets_per_second: (u64, u64),
|
||||
bits_per_second: (u64, u64),
|
||||
) -> Table<'a> {
|
||||
let rows: Vec<Row> = top
|
||||
.iter()
|
||||
.map(|stats| {
|
||||
let color = if stats.bits_per_second.0 < 500 {
|
||||
Color::DarkGray
|
||||
} else if stats.tc_handle.as_u32() == 0 {
|
||||
Color::Cyan
|
||||
} else {
|
||||
Color::LightGreen
|
||||
};
|
||||
Row::new(vec![
|
||||
Cell::from(stats.ip_address.clone()),
|
||||
Cell::from(format!("{:<13}", scale_bits(stats.bits_per_second.0))),
|
||||
Cell::from(format!("{:<13}", scale_bits(stats.bits_per_second.1))),
|
||||
Cell::from(format!(
|
||||
"{:<13}",
|
||||
scale_packets(stats.packets_per_second.0)
|
||||
)),
|
||||
Cell::from(format!(
|
||||
"{:<13}",
|
||||
scale_packets(stats.packets_per_second.1)
|
||||
)),
|
||||
Cell::from(format!(
|
||||
"{:<10} ms",
|
||||
format!("{:.2}", stats.median_tcp_rtt)
|
||||
)),
|
||||
Cell::from(format!("{:>7}", stats.tc_handle.to_string())),
|
||||
])
|
||||
.style(Style::default().fg(color))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let header = Row::new(vec![
|
||||
"Local IP",
|
||||
"Download",
|
||||
"Upload",
|
||||
"Pkts Dn",
|
||||
"Pkts Up",
|
||||
"TCP RTT ms",
|
||||
"Shaper",
|
||||
])
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
|
||||
Table::new(rows)
|
||||
.header(header)
|
||||
.block(
|
||||
Block::default().title(draw_pps(packets_per_second, bits_per_second)),
|
||||
)
|
||||
.widths(&[
|
||||
Constraint::Min(42),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(7),
|
||||
])
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn main() -> Result<()> {
|
||||
let mut bus_client = BusClient::new().await?;
|
||||
if !bus_client.is_connected() {
|
||||
println!("ERROR: lqosd bus is not available");
|
||||
std::process::exit(0);
|
||||
}
|
||||
let mut packets = (0, 0);
|
||||
let mut bits = (0, 0);
|
||||
let mut top = Vec::new();
|
||||
// Initialize TUI
|
||||
enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
let mut n_rows = 33;
|
||||
|
||||
loop {
|
||||
if let Ok(result) = get_data(&mut bus_client, n_rows).await {
|
||||
let (bits_down, bits_up, packets_down, packets_up) = result.totals;
|
||||
packets = (packets_down, packets_up);
|
||||
bits = (bits_down, bits_up);
|
||||
top = result.top;
|
||||
}
|
||||
|
||||
//terminal.clear()?;
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[Constraint::Min(1), Constraint::Percentage(100)].as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
f.render_widget(draw_menu(bus_client.is_connected()), chunks[0]);
|
||||
// NOTE: this is where the height of the main panel is calculated.
|
||||
// Resize events are consumed by `tui`, so we never receive them.
|
||||
n_rows = chunks[1].height;
|
||||
f.render_widget(draw_top_pane(&top, packets, bits), chunks[1]);
|
||||
//f.render_widget(bandwidth_chart(datasets.clone(), packets, bits, min, max), chunks[1]);
|
||||
})?;
|
||||
|
||||
if crossterm::event::poll(Duration::from_secs(1)).unwrap() {
|
||||
match read().unwrap() {
|
||||
// FIXME - this needs to absorb multiple resize events. Presently,
|
||||
// When I resize a terminal window, it is not getting one, either.
|
||||
// How to then change n_rows from here is also on my mind
|
||||
Event::Resize(width, height) => {
|
||||
println!("New size = {width}x{height}")
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break,
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break,
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('Z'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // Disconnect from bus, suspend
|
||||
// Event::Key(KeyEvent { escape should do something I don't know what.
|
||||
// code: KeyCode::Char('ESC'),
|
||||
// modifiers: KeyModifiers::CONTROL,}) => break,// go BACK?
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('h'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into help
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into next
|
||||
// e.g. n_rows = screen size
|
||||
// n_start = n_start + screen
|
||||
// size
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into prev
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('?'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into help
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into uploaders
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into downloads
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into cpu
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('l'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME lag meter
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('N'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into next panel
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('P'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into prev panel
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Best
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Worst
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('D'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Drops
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('Q'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Queues
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('W'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME (un)display wider stuff
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('8'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter out fe80
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('6'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Just look at ipv6
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('4'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Just look at ipv4
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('5'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME ipv4 + ipv6
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('U'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME filter on Unshaped
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('M'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME filter on My Network
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('H'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Generate histogram
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('T'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter Tin. This would require an argument BVIL<RET>
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('O'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME "Odd" events - multicast, AI-assistance, people down?
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('F'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter on "something*
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('S'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter on Plan Speed
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('z'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Zoom in
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('Z'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Zoom out
|
||||
// Now I am Dreaming
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('C'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Capture what I am filtering on
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('F'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // FIXME Freeze what I am filtering on
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('S'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // FIXME Step through what I captured on
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('R'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // FIXME Step backwards what I captured on
|
||||
// Left and right cursors also
|
||||
// Dreaming Less now
|
||||
// Use TAB for autocompletion
|
||||
// If I have moved into a panel, the following are ideas
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('/'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Search for ip
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('R'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Traceroute/MTR
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('A'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Alert me on this selection
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('K'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Kill Alert on this
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('V'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME View Selected Alerts
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('B'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // Launch Browser on this customer
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('L'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // Log notebook on this set of filters
|
||||
_ => println!("Not recognized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Undo the crossterm stuff
|
||||
terminal.clear()?;
|
||||
terminal.show_cursor()?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
// Return OK
|
||||
Ok(())
|
||||
}
|
447
src/rust/lqtop/src/main_old.rs
Normal file
447
src/rust/lqtop/src/main_old.rs
Normal file
@ -0,0 +1,447 @@
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{read, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
terminal::enable_raw_mode,
|
||||
};
|
||||
use lqos_bus::{BusClient, BusRequest, BusResponse, IpStats};
|
||||
use lqos_utils::packet_scale::{scale_bits, scale_packets};
|
||||
use std::{io, time::Duration};
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, BorderType, Cell, Paragraph, Row, Table},
|
||||
Terminal,
|
||||
};
|
||||
|
||||
struct DataResult {
|
||||
totals: (u64, u64, u64, u64),
|
||||
top: Vec<IpStats>,
|
||||
}
|
||||
|
||||
async fn get_data(client: &mut BusClient, n_rows: u16) -> Result<DataResult> {
|
||||
let mut result = DataResult { totals: (0, 0, 0, 0), top: Vec::new() };
|
||||
let requests = vec![
|
||||
BusRequest::GetCurrentThroughput,
|
||||
BusRequest::GetTopNDownloaders { start: 0, end: n_rows as u32 },
|
||||
];
|
||||
for r in client.request(requests).await? {
|
||||
match r {
|
||||
BusResponse::CurrentThroughput {
|
||||
bits_per_second,
|
||||
packets_per_second,
|
||||
shaped_bits_per_second: _,
|
||||
} => {
|
||||
let tuple = (
|
||||
bits_per_second.0,
|
||||
bits_per_second.1,
|
||||
packets_per_second.0,
|
||||
packets_per_second.1,
|
||||
);
|
||||
result.totals = tuple;
|
||||
}
|
||||
BusResponse::TopDownloaders(top) => {
|
||||
result.top = top.clone();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn draw_menu<'a>(is_connected: bool) -> Paragraph<'a> {
|
||||
let mut text = Spans::from(vec![
|
||||
Span::styled("Q", Style::default().fg(Color::White)),
|
||||
Span::from("uit"),
|
||||
]);
|
||||
|
||||
if !is_connected {
|
||||
text
|
||||
.0
|
||||
.push(Span::styled(" NOT CONNECTED ", Style::default().fg(Color::Red)))
|
||||
} else {
|
||||
text
|
||||
.0
|
||||
.push(Span::styled(" CONNECTED ", Style::default().fg(Color::Green)))
|
||||
}
|
||||
|
||||
let para = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.border_type(BorderType::Plain)
|
||||
.title("LibreQoS Monitor: "),
|
||||
);
|
||||
|
||||
para
|
||||
}
|
||||
|
||||
fn draw_pps<'a>(
|
||||
packets_per_second: (u64, u64),
|
||||
bits_per_second: (u64, u64),
|
||||
) -> Spans<'a> {
|
||||
Spans::from(vec![
|
||||
Span::from("DOWN: "),
|
||||
Span::from(scale_bits(bits_per_second.0)),
|
||||
Span::from(" "),
|
||||
Span::from(scale_bits(bits_per_second.1)),
|
||||
Span::from(" "),
|
||||
Span::from("UP: "),
|
||||
Span::from(scale_packets(packets_per_second.0)),
|
||||
Span::from(" "),
|
||||
Span::from(scale_packets(packets_per_second.1)),
|
||||
])
|
||||
}
|
||||
|
||||
fn draw_top_pane<'a>(
|
||||
top: &[IpStats],
|
||||
packets_per_second: (u64, u64),
|
||||
bits_per_second: (u64, u64),
|
||||
) -> Table<'a> {
|
||||
let rows: Vec<Row> = top
|
||||
.iter()
|
||||
.map(|stats| {
|
||||
let color = if stats.bits_per_second.0 < 500 {
|
||||
Color::DarkGray
|
||||
} else if stats.tc_handle.as_u32() == 0 {
|
||||
Color::Cyan
|
||||
} else {
|
||||
Color::LightGreen
|
||||
};
|
||||
Row::new(vec![
|
||||
Cell::from(stats.ip_address.clone()),
|
||||
Cell::from(format!("{:<13}", scale_bits(stats.bits_per_second.0))),
|
||||
Cell::from(format!("{:<13}", scale_bits(stats.bits_per_second.1))),
|
||||
Cell::from(format!(
|
||||
"{:<13}",
|
||||
scale_packets(stats.packets_per_second.0)
|
||||
)),
|
||||
Cell::from(format!(
|
||||
"{:<13}",
|
||||
scale_packets(stats.packets_per_second.1)
|
||||
)),
|
||||
Cell::from(format!(
|
||||
"{:<10} ms",
|
||||
format!("{:.2}", stats.median_tcp_rtt)
|
||||
)),
|
||||
Cell::from(format!("{:>7}", stats.tc_handle.to_string())),
|
||||
])
|
||||
.style(Style::default().fg(color))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let header = Row::new(vec![
|
||||
"Local IP",
|
||||
"Download",
|
||||
"Upload",
|
||||
"Pkts Dn",
|
||||
"Pkts Up",
|
||||
"TCP RTT ms",
|
||||
"Shaper",
|
||||
])
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
|
||||
Table::new(rows)
|
||||
.header(header)
|
||||
.block(
|
||||
Block::default().title(draw_pps(packets_per_second, bits_per_second)),
|
||||
)
|
||||
.widths(&[
|
||||
Constraint::Min(42),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(7),
|
||||
])
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
pub async fn main() -> Result<()> {
|
||||
let mut bus_client = BusClient::new().await?;
|
||||
if !bus_client.is_connected() {
|
||||
println!("ERROR: lqosd bus is not available");
|
||||
std::process::exit(0);
|
||||
}
|
||||
let mut packets = (0, 0);
|
||||
let mut bits = (0, 0);
|
||||
let mut top = Vec::new();
|
||||
// Initialize TUI
|
||||
enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
let mut n_rows = 33;
|
||||
|
||||
loop {
|
||||
if let Ok(result) = get_data(&mut bus_client, n_rows).await {
|
||||
let (bits_down, bits_up, packets_down, packets_up) = result.totals;
|
||||
packets = (packets_down, packets_up);
|
||||
bits = (bits_down, bits_up);
|
||||
top = result.top;
|
||||
}
|
||||
|
||||
//terminal.clear()?;
|
||||
terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[Constraint::Min(1), Constraint::Percentage(100)].as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
f.render_widget(draw_menu(bus_client.is_connected()), chunks[0]);
|
||||
// NOTE: this is where the height of the main panel is calculated.
|
||||
// Resize events are consumed by `tui`, so we never receive them.
|
||||
n_rows = chunks[1].height;
|
||||
f.render_widget(draw_top_pane(&top, packets, bits), chunks[1]);
|
||||
//f.render_widget(bandwidth_chart(datasets.clone(), packets, bits, min, max), chunks[1]);
|
||||
})?;
|
||||
|
||||
if crossterm::event::poll(Duration::from_secs(1)).unwrap() {
|
||||
match read().unwrap() {
|
||||
// FIXME - this needs to absorb multiple resize events. Presently,
|
||||
// When I resize a terminal window, it is not getting one, either.
|
||||
// How to then change n_rows from here is also on my mind
|
||||
Event::Resize(width, height) => {
|
||||
println!("New size = {width}x{height}")
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break,
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break,
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('Z'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // Disconnect from bus, suspend
|
||||
// Event::Key(KeyEvent { escape should do something I don't know what.
|
||||
// code: KeyCode::Char('ESC'),
|
||||
// modifiers: KeyModifiers::CONTROL,}) => break,// go BACK?
|
||||
//
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('h'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into help
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into next
|
||||
// e.g. n_rows = screen size
|
||||
// n_start = n_start + screen
|
||||
// size
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into prev
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('?'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into help
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into uploaders
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into downloads
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into cpu
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('l'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME lag meter
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('N'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into next panel
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('P'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME make into prev panel
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('b'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Best
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('w'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Worst
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('D'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Drops
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('Q'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Queues
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('W'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME (un)display wider stuff
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('8'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter out fe80
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('6'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Just look at ipv6
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('4'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Just look at ipv4
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('5'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME ipv4 + ipv6
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('U'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME filter on Unshaped
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('M'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME filter on My Network
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('H'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Generate histogram
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('T'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter Tin. This would require an argument BVIL<RET>
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('O'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME "Odd" events - multicast, AI-assistance, people down?
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('F'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter on "something*
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('S'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Filter on Plan Speed
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('z'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Zoom in
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('Z'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Zoom out
|
||||
// Now I am Dreaming
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('C'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Capture what I am filtering on
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('F'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // FIXME Freeze what I am filtering on
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('S'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // FIXME Step through what I captured on
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('R'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
..
|
||||
}) => break, // FIXME Step backwards what I captured on
|
||||
// Left and right cursors also
|
||||
// Dreaming Less now
|
||||
// Use TAB for autocompletion
|
||||
// If I have moved into a panel, the following are ideas
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('/'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Search for ip
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('R'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Traceroute/MTR
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('A'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Alert me on this selection
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('K'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME Kill Alert on this
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('V'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // FIXME View Selected Alerts
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('B'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // Launch Browser on this customer
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('L'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
..
|
||||
}) => break, // Log notebook on this set of filters
|
||||
_ => println!("Not recognized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Undo the crossterm stuff
|
||||
terminal.clear()?;
|
||||
terminal.show_cursor()?;
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
85
src/rust/lqtop/src/top_level_ui.rs
Normal file
85
src/rust/lqtop/src/top_level_ui.rs
Normal file
@ -0,0 +1,85 @@
|
||||
//! Provides a basic system for the UI framework. Handles
|
||||
//! rendering the basic layout, talking to the UI framework,
|
||||
//! and event-loop events that aren't quitting the program.
|
||||
//!
|
||||
//! It's designed to be the manager from which specific UI
|
||||
//! components are managed.
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use std::io::Stdout;
|
||||
use crate::widgets::*;
|
||||
|
||||
pub struct TopUi {
|
||||
show_cpus: bool,
|
||||
show_throughput_sparkline: bool,
|
||||
}
|
||||
|
||||
impl TopUi {
|
||||
/// Create a new TopUi instance. This will initialize the UI framework.
|
||||
pub fn new() -> Self {
|
||||
TopUi {
|
||||
show_cpus: true,
|
||||
show_throughput_sparkline: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_keypress(&mut self, key: char) {
|
||||
// Handle Mode Switches
|
||||
match key {
|
||||
'c' => self.show_cpus = !self.show_cpus,
|
||||
'n' => self.show_throughput_sparkline = !self.show_throughput_sparkline,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) {
|
||||
terminal
|
||||
.draw(|f| {
|
||||
self.top_level_render(f);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn top_level_render(&self, frame: &mut Frame) {
|
||||
let mut constraints = Vec::new();
|
||||
let mut next_region = 0;
|
||||
|
||||
// Build the layout regions
|
||||
let cpu_region = if self.show_cpus {
|
||||
constraints.push(Constraint::Length(1));
|
||||
next_region += 1;
|
||||
next_region-1
|
||||
} else {
|
||||
next_region
|
||||
};
|
||||
|
||||
let network_spark_region = if self.show_throughput_sparkline {
|
||||
constraints.push(Constraint::Length(10));
|
||||
next_region += 1;
|
||||
next_region-1
|
||||
} else {
|
||||
next_region
|
||||
};
|
||||
|
||||
// With a minimum of 1 row, we can now build the layout
|
||||
if constraints.is_empty() {
|
||||
constraints.push(Constraint::Min(1));
|
||||
}
|
||||
constraints.push(Constraint::Fill(1));
|
||||
|
||||
let main_layout = Layout::new(
|
||||
Direction::Vertical,
|
||||
constraints
|
||||
).split(frame.size());
|
||||
|
||||
// Add Widgets
|
||||
if self.show_cpus {
|
||||
frame.render_widget(cpu_display(), main_layout[cpu_region]);
|
||||
}
|
||||
if self.show_throughput_sparkline {
|
||||
let nspark = NetworkSparkline::new();
|
||||
let render = nspark.render();
|
||||
frame.render_widget(render, main_layout[network_spark_region]);
|
||||
}
|
||||
}
|
||||
}
|
98
src/rust/lqtop/src/ui_base.rs
Normal file
98
src/rust/lqtop/src/ui_base.rs
Normal file
@ -0,0 +1,98 @@
|
||||
//! Provides a basic system for the UI framework.
|
||||
//! Upon starting the program, it performs basic initialization.
|
||||
//! It tracks "drop", so when the program exits, it can perform cleanup.
|
||||
|
||||
use crate::{bus::BusCommand, top_level_ui::TopUi};
|
||||
use anyhow::Result;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use std::{io::stdout, sync::atomic::{AtomicBool, Ordering}};
|
||||
use tokio::{sync::mpsc::Sender, task::yield_now};
|
||||
|
||||
pub static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub struct UiBase {
|
||||
ui: TopUi,
|
||||
bus_commander: Sender<BusCommand>,
|
||||
}
|
||||
|
||||
impl UiBase {
|
||||
/// Create a new UiBase instance. This will initialize the UI framework.
|
||||
pub fn new(bus_commander: Sender<BusCommand>) -> Result<Self> {
|
||||
// Crossterm mode setup
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
|
||||
// Setup Control-C Handler for graceful shutdown
|
||||
ctrlc::set_handler(move || {
|
||||
Self::cleanup();
|
||||
std::process::exit(0);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Return
|
||||
Ok(UiBase {
|
||||
ui: TopUi::new(),
|
||||
bus_commander,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn quit_program(&self) {
|
||||
self.bus_commander.blocking_send(BusCommand::Quit).unwrap();
|
||||
SHOULD_EXIT.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Set the should_exit flag to true, which will cause the event loop to exit.
|
||||
pub async fn event_loop(&mut self) -> Result<()> {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
while !SHOULD_EXIT.load(Ordering::Relaxed) {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
// Retrieve the keypress information
|
||||
if let Event::Key(key) = event::read()? {
|
||||
// Key press (down) event
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
// Quit the program
|
||||
KeyCode::Char('q') => {
|
||||
self.quit_program();
|
||||
}
|
||||
_ => {
|
||||
let char: Option<char> = match key.code {
|
||||
KeyCode::Char(c) => Some(c),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(c) = char {
|
||||
self.ui.handle_keypress(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform rendering
|
||||
self.ui.render(&mut terminal);
|
||||
|
||||
// Ensure that all the event handlers can fire
|
||||
yield_now().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup() {
|
||||
disable_raw_mode().unwrap();
|
||||
stdout()
|
||||
.execute(crossterm::terminal::LeaveAlternateScreen)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for UiBase {
|
||||
fn drop(&mut self) {
|
||||
Self::cleanup();
|
||||
}
|
||||
}
|
41
src/rust/lqtop/src/widgets/cpu.rs
Normal file
41
src/rust/lqtop/src/widgets/cpu.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use ratatui::{style::{Color, Style}, text::Span, widgets::{Block, Borders, Widget}};
|
||||
|
||||
/// Used to display the CPU usage and RAM usage
|
||||
pub fn cpu_display() -> impl Widget {
|
||||
use crate::bus::cpu_ram::*;
|
||||
let num_cpus = NUM_CPUS.load(Ordering::Relaxed);
|
||||
let cpu_usage = CPU_USAGE.iter().take(num_cpus).map(|x| x.load(Ordering::Relaxed)).collect::<Vec<_>>();
|
||||
let total_ram = TOTAL_RAM.load(Ordering::Relaxed);
|
||||
let used_ram = RAM_USED.load(Ordering::Relaxed);
|
||||
|
||||
let ram_percent = 100.0 - ((used_ram as f64 / total_ram as f64) * 100.0);
|
||||
|
||||
let ram_color = if ram_percent < 10.0 {
|
||||
Color::Red
|
||||
} else if ram_percent < 25.0 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::White
|
||||
};
|
||||
|
||||
let mut span_buf = vec![
|
||||
Span::styled(" [ RAM: ", Style::default().fg(Color::Green)),
|
||||
Span::styled(format!("{:.0}% ", ram_percent), Style::default().fg(ram_color)),
|
||||
Span::styled("CPU: ", Style::default().fg(Color::Green)),
|
||||
];
|
||||
for cpu in cpu_usage {
|
||||
let color = if cpu < 10 {
|
||||
Color::White
|
||||
} else if cpu < 25 {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
span_buf.push(Span::styled(format!("{}% ", cpu), Style::default().fg(color)));
|
||||
}
|
||||
span_buf.push(Span::styled(" ] ", Style::default().fg(Color::Green)));
|
||||
|
||||
Block::new().borders(Borders::TOP).title(span_buf)
|
||||
}
|
4
src/rust/lqtop/src/widgets/mod.rs
Normal file
4
src/rust/lqtop/src/widgets/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod cpu;
|
||||
pub use cpu::cpu_display;
|
||||
mod network_sparkline;
|
||||
pub use network_sparkline::*;
|
133
src/rust/lqtop/src/widgets/network_sparkline.rs
Normal file
133
src/rust/lqtop/src/widgets/network_sparkline.rs
Normal file
@ -0,0 +1,133 @@
|
||||
use crate::bus::throughput::{CURRENT_THROUGHPUT, THROUGHPUT_RING};
|
||||
use lqos_utils::packet_scale::scale_bits;
|
||||
use ratatui::{
|
||||
style::{Color, Style},
|
||||
symbols,
|
||||
widgets::{Axis, Block, Borders, Chart, Dataset, Widget},
|
||||
};
|
||||
|
||||
pub struct NetworkSparkline {
|
||||
bps_down: Vec<(f64, f64)>,
|
||||
bps_up: Vec<(f64, f64)>,
|
||||
shaped_down: Vec<(f64, f64)>,
|
||||
shaped_up: Vec<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl NetworkSparkline {
|
||||
pub fn new() -> Self {
|
||||
let raw_data = THROUGHPUT_RING.lock().unwrap().bits_per_second_vec_down();
|
||||
let bps_down = raw_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &val)| (i as f64, val as f64))
|
||||
.collect();
|
||||
|
||||
let raw_data = THROUGHPUT_RING.lock().unwrap().bits_per_second_vec_up();
|
||||
let bps_up = raw_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &val)| (i as f64, 0.0 - val as f64))
|
||||
.collect();
|
||||
|
||||
let raw_data = THROUGHPUT_RING
|
||||
.lock()
|
||||
.unwrap()
|
||||
.shaped_bits_per_second_vec_down();
|
||||
let shaped_down = raw_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &val)| (i as f64, val as f64))
|
||||
.collect();
|
||||
|
||||
let raw_data = THROUGHPUT_RING
|
||||
.lock()
|
||||
.unwrap()
|
||||
.shaped_bits_per_second_vec_up();
|
||||
let shaped_up = raw_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &val)| (i as f64, 0.0 - val as f64))
|
||||
.collect();
|
||||
|
||||
NetworkSparkline {
|
||||
bps_down,
|
||||
bps_up,
|
||||
shaped_down,
|
||||
shaped_up,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self) -> impl Widget + '_ {
|
||||
let (up, down) = CURRENT_THROUGHPUT.lock().unwrap().bits_per_second;
|
||||
let title = format!(
|
||||
" [Throughput (Down: {} Up: {})]",
|
||||
scale_bits(up),
|
||||
scale_bits(down)
|
||||
);
|
||||
|
||||
let block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Green));
|
||||
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("Throughput")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&self.bps_down),
|
||||
Dataset::default()
|
||||
.name("Throughput")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&self.bps_up),
|
||||
Dataset::default()
|
||||
.name("Shaped")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::LightGreen))
|
||||
.data(&self.shaped_down),
|
||||
Dataset::default()
|
||||
.name("Shaped")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::LightGreen))
|
||||
.data(&self.shaped_up),
|
||||
];
|
||||
|
||||
let bps_max = self
|
||||
.bps_down
|
||||
.iter()
|
||||
.map(|(_, val)| *val)
|
||||
.fold(0.0, f64::max);
|
||||
|
||||
let bps_min = self.bps_up.iter().map(|(_, val)| *val).fold(0.0, f64::min);
|
||||
|
||||
let shaped_max = self
|
||||
.shaped_down
|
||||
.iter()
|
||||
.map(|(_, val)| *val)
|
||||
.fold(0.0, f64::max);
|
||||
|
||||
let shaped_min = self
|
||||
.shaped_up
|
||||
.iter()
|
||||
.map(|(_, val)| *val)
|
||||
.fold(0.0, f64::min);
|
||||
|
||||
let max = f64::max(bps_max, shaped_max);
|
||||
let min = f64::min(bps_min, shaped_min);
|
||||
|
||||
Chart::new(datasets)
|
||||
.block(block)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Time")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 80.0]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([min, max]),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user