use anyhow::Result;
use crossterm::{
cursor,
terminal::{self, ClearType},
ExecutableCommand,
};
use std::io::Write;
use std::time::Duration;
use tokio::signal::ctrl_c;
use tokio::sync::oneshot;
use tokio::time::sleep;
pub async fn run_watch<F, Fut>(
interval_ms: u64,
mut render_fn: F,
cancel: Option<oneshot::Receiver<()>>,
) -> Result<()>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<String>>,
{
let interval = Duration::from_millis(interval_ms.max(1));
let mut stdout = std::io::stdout();
enum CancelFut {
Active(oneshot::Receiver<()>),
Absent,
Fired,
}
let mut cancel_fut = match cancel {
Some(rx) => CancelFut::Active(rx),
None => CancelFut::Absent,
};
loop {
if matches!(cancel_fut, CancelFut::Fired) {
writeln!(stdout)?;
break;
}
let frame = render_fn().await?;
stdout.execute(terminal::Clear(ClearType::All))?;
stdout.execute(cursor::MoveTo(0, 0))?;
write!(stdout, "{frame}")?;
stdout.flush()?;
match cancel_fut {
CancelFut::Active(rx) => {
let mut rx = rx;
tokio::select! {
_ = sleep(interval) => {
cancel_fut = CancelFut::Active(rx);
}
_ = ctrl_c() => {
writeln!(stdout)?;
break;
}
result = &mut rx => {
let _ = result;
cancel_fut = CancelFut::Fired;
continue;
}
}
}
CancelFut::Absent => {
tokio::select! {
_ = sleep(interval) => {
cancel_fut = CancelFut::Absent;
}
_ = ctrl_c() => {
writeln!(stdout)?;
break;
}
}
}
CancelFut::Fired => {
writeln!(stdout)?;
break;
}
}
}
Ok(())
}
pub async fn run_watch_secs<F, Fut>(interval_secs: u64, render_fn: F) -> Result<()>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<String>>,
{
let ms = interval_secs.max(1).saturating_mul(1_000);
run_watch(ms, render_fn, None).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_watch_exits_on_signal() {
let (tx, rx) = oneshot::channel::<()>();
tx.send(()).expect("send should not fail");
let result = run_watch(
0, || async { Ok("frame".to_string()) },
Some(rx),
)
.await;
assert!(
result.is_ok(),
"watch loop should exit without error on cancel"
);
}
}