use std::io::{self, BufRead, IsTerminal, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use malevich::stream::{Live, Rate, Ring};
use malevich::{Line, Plot};
use crate::args::{Args, Output};
use crate::output;
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
pub fn run(args: &Args) -> io::Result<()> {
install_interrupt_handler();
match args.output {
Output::Stdout => drive(io::stdout, args),
_ => drive(io::stderr, args),
}
}
fn drive<W: Write + IsTerminal>(handle: fn() -> W, args: &Args) -> io::Result<()> {
let window = args
.window
.unwrap_or_else(|| output::frame_for(&handle(), args).width.max(1))
.max(1);
let fps = args.fps.unwrap_or(10).max(1);
let interval = Duration::from_millis((1000 / fps as u64).max(1));
let ring = Ring::new(window);
let done = spawn_reader(ring.clone(), args.delimiter, args.rate);
let mut cursor = handle();
let _ = write!(cursor, "\x1b[?25l");
let _ = cursor.flush();
let result = repaint(handle, &ring, args, done, interval);
let _ = write!(cursor, "\x1b[?25h");
let _ = cursor.flush();
match result {
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(()),
other => other,
}
}
fn repaint<W: Write + IsTerminal>(
handle: fn() -> W,
ring: &Ring,
args: &Args,
done: Arc<AtomicBool>,
interval: Duration,
) -> io::Result<()> {
let mut live = Live::new(handle());
loop {
let frame = output::frame_for(&handle(), args);
let plot = plot(ring.snapshot(), args);
live.draw(&plot, &frame)?;
if done.load(Ordering::Relaxed) || INTERRUPTED.load(Ordering::Relaxed) {
return Ok(());
}
thread::sleep(interval);
}
}
fn plot(values: Vec<f64>, args: &Args) -> Plot<'static> {
let mut plot = Plot::new().layer(Line::y(values));
if let Some(title) = &args.title {
plot = plot.title(title);
}
if let Some(xlabel) = &args.xlabel {
plot = plot.x_label(xlabel);
}
if let Some(ylabel) = &args.ylabel {
plot = plot.y_label(ylabel);
}
if let Some((lo, hi)) = args.ylim {
plot = plot.y_domain(lo, hi);
}
if args.log_y {
plot = plot.log_y();
}
plot
}
fn spawn_reader(ring: Ring, delimiter: Option<char>, rate: bool) -> Arc<AtomicBool> {
let done = Arc::new(AtomicBool::new(false));
let eof = done.clone();
thread::spawn(move || {
let stdin = io::stdin();
let mut rate_tracker = Rate::new();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let sample = first_number(&line, delimiter).unwrap_or(f64::NAN);
let value = if rate {
rate_tracker.delta(sample)
} else {
sample
};
ring.push(value);
}
eof.store(true, Ordering::Relaxed);
});
done
}
fn first_number(line: &str, delimiter: Option<char>) -> Option<f64> {
let fields: Box<dyn Iterator<Item = &str>> = match delimiter {
Some(sep) => Box::new(line.split(sep)),
None => Box::new(line.split_whitespace()),
};
fields
.filter_map(|field| field.trim().parse::<f64>().ok())
.find(|value| value.is_finite())
}
fn install_interrupt_handler() {
#[cfg(unix)]
{
extern "C" fn on_interrupt(_signal: libc::c_int) {
INTERRUPTED.store(true, Ordering::Relaxed);
}
unsafe {
libc::signal(
libc::SIGINT,
on_interrupt as *const () as usize as libc::sighandler_t,
);
}
}
}
#[cfg(test)]
#[path = "tests/live_tests.rs"]
mod tests;