zing/src/main.rs

372 lines
9.8 KiB
Rust

use std::{
sync::{Arc, RwLock},
time::Duration,
};
use arkham::{prelude::*, symbols};
use clap::Parser;
use surge_ping::PingIdentifier;
enum PingResult {
Reply(Duration),
Failure,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
host: String,
#[arg(short, long, default_value = "1")]
interval: u64,
}
fn main() {
let args = Args::parse();
let theme = Theme {
bg_primary: Color::Rgb {
r: 20,
g: 20,
b: 20,
},
bg_secondary: Color::Rgb {
r: 30,
g: 30,
b: 30,
},
bg_tertiary: Color::Rgb {
r: 40,
g: 40,
b: 40,
},
bg_selection: Color::White,
fg_selection: Color::Black,
fg: Color::White,
accent: Color::Rgb {
r: 200,
g: 0,
b: 200,
},
};
let results: Arc<RwLock<Vec<PingResult>>> = Arc::new(RwLock::new(vec![]));
let address = args.host.clone();
let interval = args.interval;
let mut app = App::new(root)
.insert_resource(theme)
.insert_resource(args)
.insert_resource(address.clone())
.insert_resource(results.clone());
let renderer = app.get_renderer();
{
std::thread::spawn(move || {
ping_loop(address, results, renderer, interval).unwrap();
});
}
app.run().unwrap();
}
fn root(ctx: &mut ViewContext, theme: Res<Theme>, args: Res<Args>) {
let width = ctx.width();
let height = ctx.height();
ctx.fill(
Rect::new((0, 0), (width, height)),
Rune::new().content(' ').bg(theme.bg_primary),
);
ctx.fill(
Rect::new((0, 0), (width, 1)),
Rune::new().content(' ').bg(theme.bg_tertiary),
);
ctx.insert(
(2, 0),
"Host"
.to_runes()
.bold()
.bg(theme.bg_tertiary)
.fg(Color::Rgb {
r: 200,
g: 200,
b: 200,
}),
);
ctx.insert(
(12, 0),
args.host
.to_runes()
.bold()
.bg(theme.bg_tertiary)
.fg(Color::Rgb {
r: 255,
g: 255,
b: 255,
}),
);
ctx.component(Rect::new((0, 2), (width, 10)), stats);
ctx.fill(
Rect::new((0, 8), (width, 1)),
Rune::new()
.content(symbols::LINE)
.bg(theme.bg_primary)
.fg(Color::Rgb {
r: 50,
g: 50,
b: 50,
}),
);
if height >= 34 {
ctx.component(Rect::new((0, 10), (width, 20)), graph);
ctx.fill(
Rect::new((0, 31), (width, 1)),
Rune::new()
.content(symbols::LINE)
.bg(theme.bg_primary)
.fg(Color::Rgb {
r: 50,
g: 50,
b: 50,
}),
);
ctx.component(Rect::new((0, 33), (width, height - 34)), last_10);
} else if height >= 24 {
ctx.component(Rect::new((0, 10), (width, 10)), graph);
ctx.fill(
Rect::new((0, 21), (width, 1)),
Rune::new()
.content(symbols::LINE)
.bg(theme.bg_primary)
.fg(Color::Rgb {
r: 50,
g: 50,
b: 50,
}),
);
ctx.component(Rect::new((0, 23), (width, height - 24)), last_10);
} else if height > 12 {
ctx.component(Rect::new((0, 10), (width, height - 11)), graph);
};
}
fn format_us(v: u128) -> String {
if v > 1000000 {
return format!("{}s", v / 1000000);
}
if v > 1000 {
return format!("{}ms", v / 1000);
}
format!("{}μs", v)
}
fn stats(ctx: &mut ViewContext, results: Res<Arc<RwLock<Vec<PingResult>>>>) {
let results = results.read().unwrap();
let width = ctx.width();
let max = results
.iter()
.map(|d| match d {
PingResult::Reply(d) => d.as_micros(),
PingResult::Failure => 0,
})
.max()
.unwrap_or(0);
let min = results
.iter()
.map(|d| match d {
PingResult::Reply(d) => d.as_micros(),
PingResult::Failure => 0,
})
.min()
.unwrap_or(0);
let failures = results
.iter()
.filter(|i| match i {
PingResult::Reply(_) => false,
PingResult::Failure => true,
})
.count();
let sum = results
.iter()
.map(|d| match d {
PingResult::Reply(d) => d.as_micros(),
PingResult::Failure => 0,
})
.sum::<u128>();
let avg = if sum > 0 {
sum / results.len() as u128
} else {
0
};
ctx.component(
Rect::new((5, 0), (width, 1)),
field("Packets", &results.len().to_string()),
);
if results.len() > 0 {
ctx.component(
Rect::new((5, 1), (width, 1)),
field("Maximum", &format_us(max)),
);
ctx.component(
Rect::new((5, 2), (width, 1)),
field("Average", &format_us(avg)),
);
ctx.component(
Rect::new((5, 3), (width, 1)),
field("Minimum", &format_us(min)),
);
ctx.component(
Rect::new((5, 4), (width, 1)),
field(
"Lost",
&format!(
"{} ({}%)",
failures,
(failures as f32 / results.len() as f32 * 100.0) as usize
),
),
);
}
}
fn field<T>(name: T, value: T) -> impl Fn(&mut ViewContext)
where
T: std::fmt::Display,
{
let name = name.to_string();
let value = value.to_string();
move |ctx| {
ctx.insert(
(0, 0),
name.to_runes().fg(Color::Rgb {
r: 200,
g: 200,
b: 200,
}),
);
ctx.insert(
(10, 0),
value.to_runes().fg(Color::Rgb {
r: 255,
g: 255,
b: 255,
}),
);
}
}
fn graph(ctx: &mut ViewContext, results: Res<Arc<RwLock<Vec<PingResult>>>>) {
let width = ctx.width();
let height = ctx.height();
let total_bars = (ctx.width() - 7) / 2;
let results_us = results
.read()
.unwrap()
.iter()
.rev()
.take(total_bars)
.rev()
.map(|d| match d {
PingResult::Reply(d) => Some(d.as_micros()),
PingResult::Failure => None,
})
.collect::<Vec<_>>();
let max = results_us
.iter()
.map(|v| v.unwrap_or(0))
.max()
.unwrap_or_default();
ctx.insert((0, 0), format_us(max));
ctx.insert((0, height - 1), "0");
let results_pct = results_us
.iter()
.map(|i| i.map(|i| i as f32 / max as f32))
.collect::<Vec<_>>();
ctx.component(Rect::new((7, 0), (width - 7, 20)), bars(results_pct));
}
fn bars(values: Vec<Option<f32>>) -> impl Fn(&mut ViewContext) {
move |ctx| {
let total_bars = ctx.width() / 2;
let active_bars = values.len();
let height = ctx.height();
for (idx, value) in values.iter().enumerate() {
ctx.component(Rect::new((idx * 2, 0), (1, height)), bar(*value));
}
for idx in active_bars..total_bars {
ctx.component(Rect::new((idx * 2, 0), (1, height)), bar(Some(0.0)));
}
}
}
fn bar(value: Option<f32>) -> impl Fn(&mut ViewContext, Res<Theme>) {
move |ctx, theme| {
let width = ctx.width();
let height = ctx.height();
if let Some(value) = value {
let filled = (height as f32 * value) as usize;
let unfilled = height - filled;
ctx.fill(
Rect::new((0, unfilled), (1, filled)),
Rune::new().content(' ').bg(theme.accent),
);
ctx.fill(
Rect::new((0, 0), (1, unfilled)),
Rune::new().content(' ').bg(theme.bg_tertiary),
);
} else {
ctx.fill(
Rect::new((0, 0), (width, height)),
Rune::new()
.content(' ')
.bg(Color::Rgb { r: 200, g: 0, b: 0 }),
);
}
}
}
fn last_10(ctx: &mut ViewContext, results: Res<Arc<RwLock<Vec<PingResult>>>>, args: Res<Args>) {
let results = results.read().unwrap();
for (idx, res) in results.iter().rev().take(ctx.height()).enumerate() {
let msg = match res {
PingResult::Reply(v) => {
format!("{} responded in {}", args.host, format_us(v.as_micros()))
}
PingResult::Failure => format!("{} failed to respond", args.host),
};
ctx.insert((1, idx), msg);
}
}
#[tokio::main]
async fn ping_loop(
address: String,
results: Arc<RwLock<Vec<PingResult>>>,
renderer: Renderer,
interval: u64,
) -> anyhow::Result<()> {
let ip = tokio::net::lookup_host(format!("{}:0", address))
.await?
.next()
.map(|val| val.ip())
.unwrap();
let client = surge_ping::Client::new(&surge_ping::Config::default())?;
let mut pinger = client.pinger(ip, PingIdentifier(111)).await;
pinger.timeout(Duration::from_secs(2));
loop {
let payload = [0; 84];
match pinger.ping(surge_ping::PingSequence(0), &payload).await {
Ok((_packet, duration)) => {
results.write().unwrap().push(PingResult::Reply(duration));
}
Err(_) => {
results.write().unwrap().push(PingResult::Failure);
}
}
renderer.render();
tokio::time::sleep(Duration::from_secs(interval)).await;
}
}