use std::io;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use futures_util::{Stream, StreamExt};
use ratatui::backend::Backend;
use ratatui::crossterm::event::{Event, KeyEventKind};
use ratatui::Terminal;
use tokio::sync::mpsc;
use tokio::time::MissedTickBehavior;
use super::app::{update, Action, App};
use super::keymap::map_key;
use super::message::{Command, WorkerEvent};
use super::terminal::Signals;
use super::view;
const DRAIN_BUDGET: usize = 256;
pub(crate) const INTERNAL_ERROR: &str = "internal error (see the panic message above)";
pub(crate) const WORKER_STOPPED: &str = "internal error: the network worker stopped unexpectedly";
pub(crate) struct LoopIo<I> {
pub(crate) input: I,
pub(crate) events: mpsc::Receiver<WorkerEvent>,
pub(crate) commands: mpsc::Sender<Command>,
pub(crate) dropped: Arc<AtomicU64>,
pub(crate) fps: u16,
pub(crate) signals: Signals,
pub(crate) alive: fn() -> bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LoopStats {
pub(crate) frames: u64,
}
pub(crate) async fn run<B, I>(
terminal: &mut Terminal<B>,
app: &mut App,
io: &mut LoopIo<I>,
) -> Result<LoopStats, Box<dyn std::error::Error>>
where
B: Backend,
B::Error: Send + Sync + 'static,
I: Stream<Item = io::Result<Event>> + Unpin,
{
let mut stats = LoopStats::default();
let period = Duration::from_secs(1) / u32::from(io.fps.max(1));
let mut frames = tokio::time::interval(period);
frames.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut worker_open = true;
let alive = io.alive;
draw(terminal, app, &mut stats, alive)?;
while !app.quit {
if !alive() {
app.fail(INTERNAL_ERROR, 1);
break;
}
tokio::select! {
input = io.input.next() => match input {
Some(Ok(Event::Key(key))) if key.kind == KeyEventKind::Press => {
if let Some(action) = map_key(app, key) {
apply(app, action, &io.commands);
}
if app.dirty && !app.quit {
draw(terminal, app, &mut stats, alive)?;
}
}
Some(Ok(Event::Resize(..))) => {
apply(app, Action::Resize, &io.commands);
draw(terminal, app, &mut stats, alive)?;
}
Some(Ok(_)) => {}
Some(Err(error)) => return Err(error.into()),
None => app.quit = true,
},
_ = frames.tick() => {
let tick = Action::Tick {
now: tokio::time::Instant::now().into_std(),
dropped: io.dropped.load(Ordering::Relaxed),
log_generation: app.log.generation(),
};
apply(app, tick, &io.commands);
if app.dirty {
draw(terminal, app, &mut stats, alive)?;
}
}
event = io.events.recv(), if worker_open => match event {
Some(event) => {
apply(app, Action::Worker(event), &io.commands);
for _ in 1..DRAIN_BUDGET {
match io.events.try_recv() {
Ok(event) => apply(app, Action::Worker(event), &io.commands),
Err(_) => break,
}
}
}
None => {
worker_open = false;
if !app.quit {
app.fail(WORKER_STOPPED, 1);
}
}
},
signal = io.signals.recv() => {
app.fail(format!("terminated by {}", signal.name), signal.code);
}
}
}
Ok(stats)
}
fn apply(app: &mut App, action: Action, commands: &mpsc::Sender<Command>) {
let prior_link = app.link.clone();
for command in update(app, action) {
if let Err(error) = commands.try_send(command) {
let message =
format!("Command not admitted: {error}; retry after the worker catches up.");
tracing::warn!("{message}");
app.command_rejected(error.into_inner(), prior_link.clone(), message);
}
}
}
fn draw<B>(
terminal: &mut Terminal<B>,
app: &mut App,
stats: &mut LoopStats,
alive: fn() -> bool,
) -> Result<(), Box<dyn std::error::Error>>
where
B: Backend,
B::Error: Send + Sync + 'static,
{
if !alive() {
app.fail(INTERNAL_ERROR, 1);
return Ok(());
}
app.settle();
terminal.draw(|frame| view::draw(frame, app))?;
app.dirty = false;
stats.frames += 1;
Ok(())
}