372 lines
9.8 KiB
Rust
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;
|
|
}
|
|
}
|