Some basic framework.

This commit is contained in:
Herbert Wolverson 2024-03-20 15:03:19 -05:00
parent 82d213dd89
commit 6c7c8d94c9
12 changed files with 1271 additions and 509 deletions

250
src/rust/Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View 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.")
}

View 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(())
}

View 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;
}
}

View File

@ -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(())
}

View 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(())
}

View 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]);
}
}
}

View 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();
}
}

View 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)
}

View File

@ -0,0 +1,4 @@
mod cpu;
pub use cpu::cpu_display;
mod network_sparkline;
pub use network_sparkline::*;

View 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]),
)
}
}