use std::collections::HashMap;
use std::future::poll_fn;
use std::io::{self, Write};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use futures_core::Stream;
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
use crate::app::App;
use crate::input::InputEvent;
use crate::runtime::{RawModeGuard, RunOptions, Runtime};
use crate::subscription::{SubKind, Subscriptions};
use crate::task::{Cancel, Effect, MsgStream};
pub async fn run<A>(app: A) -> io::Result<A::Output>
where
A: App,
A::Msg: Clone + Send + 'static,
{
run_with(app, RunOptions::default()).await
}
pub async fn run_with<A>(app: A, options: RunOptions) -> io::Result<A::Output>
where
A: App,
A::Msg: Clone + Send + 'static,
{
let (width, height) = crossterm::terminal::size()?;
let mut runtime = Runtime::new(app, width, height);
let (tx, mut rx) = unbounded_channel::<A::Msg>();
let mut stdout = io::stdout().lock();
let mut guard = RawModeGuard::enable(options.keyboard, options.screen, options.mouse_capture)?;
if options.screen != crate::runtime::ScreenMode::AltScreen {
crate::runtime::normalize_start_column();
}
let (bytes, init_exit) = runtime.startup();
stdout.write_all(&bytes)?;
stdout.flush()?;
if let Some(output) = init_exit {
shutdown_mouse_sync(&mut guard, &mut stdout);
let persists = runtime.persists();
if !persists.is_idle() {
spawn_effects(runtime.take_effects(), &tx);
wait_for_persists(&persists, options.persist_grace).await;
}
return Ok(output);
}
spawn_effects(runtime.take_effects(), &tx);
let mut subs = ActiveSubscriptions::new(tx.clone());
subs.sync(runtime.app().subscriptions());
let mut events = crossterm::event::EventStream::new();
const FRAME: Duration = Duration::from_millis(8);
let mut pending: Vec<InputEvent> = Vec::new();
let mut last_flush = tokio::time::Instant::now() - FRAME;
let result: io::Result<A::Output> = async {
loop {
let anim = runtime.animation_interval();
let flush_at = (!pending.is_empty()).then(|| last_flush + FRAME);
enum Step<O> {
Out(Vec<u8>, Option<O>),
Flush,
}
let step = tokio::select! {
biased;
maybe_event = poll_fn(|cx| Pin::new(&mut events).poll_next(cx)) => {
use crossterm::event::{Event, KeyEventKind};
match maybe_event {
Some(Ok(first)) => match first {
Event::Key(k)
if matches!(k.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
{
pending.push(InputEvent::Key(k));
Step::Flush
}
Event::Paste(s) => {
pending.push(InputEvent::Paste(s));
Step::Flush
}
Event::Mouse(m) => {
pending.push(InputEvent::Mouse(m));
Step::Flush
}
Event::Resize(w, h) => {
let (mut bytes, mut exit) =
runtime.handle_batch(pending.drain(..));
last_flush = tokio::time::Instant::now();
if exit.is_none() {
let (resize_bytes, resize_exit) =
crate::runtime::resize_with_report(
&mut runtime,
w,
h,
options.screen,
);
bytes.extend_from_slice(&resize_bytes);
exit = resize_exit;
}
Step::Out(bytes, exit)
}
_ => Step::Out(Vec::new(), None),
},
Some(Err(e)) => return Err(e),
None => Step::Out(runtime.finalize(), Some(A::Output::default())),
}
}
Some(msg) = rx.recv() => {
let mut batch = vec![msg];
while batch.len() < 256 {
match rx.try_recv() {
Ok(m) => batch.push(m),
Err(_) => break,
}
}
let (bytes, exit) = runtime.process_batch(batch);
Step::Out(bytes, exit)
}
_ = sleep_opt(flush_at.map(|at| at.saturating_duration_since(tokio::time::Instant::now()))), if flush_at.is_some() => {
Step::Flush
}
_ = sleep_opt(anim), if anim.is_some() => Step::Out(runtime.present(), None),
};
let (bytes, exit) = match step {
Step::Out(bytes, exit) => (bytes, exit),
Step::Flush => {
if !pending.is_empty() && tokio::time::Instant::now() >= last_flush + FRAME {
let out = runtime.handle_batch(pending.drain(..));
last_flush = tokio::time::Instant::now();
out
} else {
(Vec::new(), None)
}
}
};
spawn_effects(runtime.take_effects(), &tx);
subs.sync(runtime.app().subscriptions());
if !bytes.is_empty() {
stdout.write_all(&bytes)?;
stdout.flush()?;
}
if let Some(output) = exit {
shutdown_mouse(&mut guard, &mut stdout, &mut events).await;
return Ok(output);
}
}
}
.await;
wait_for_persists(&runtime.persists(), options.persist_grace).await;
result
}
async fn wait_for_persists(persists: &crate::task::PersistTracker, grace: Option<Duration>) {
match grace {
Some(grace) => {
let _ = tokio::time::timeout(grace, persists.wait()).await;
}
None => persists.wait().await,
}
}
async fn shutdown_mouse(
guard: &mut RawModeGuard,
stdout: &mut impl Write,
events: &mut crossterm::event::EventStream,
) {
if !guard.take_mouse_capture() {
return;
}
let _ = crossterm::execute!(stdout, crossterm::event::DisableMouseCapture);
let deadline = tokio::time::Instant::now() + Duration::from_millis(50);
while tokio::time::Instant::now() < deadline {
match tokio::time::timeout(
Duration::from_millis(5),
poll_fn(|cx| Pin::new(&mut *events).poll_next(cx)),
)
.await
{
Ok(Some(Ok(crossterm::event::Event::Mouse(_)))) => {}
_ => break,
}
}
}
fn shutdown_mouse_sync(guard: &mut RawModeGuard, stdout: &mut impl Write) {
if !guard.take_mouse_capture() {
return;
}
let _ = crossterm::execute!(stdout, crossterm::event::DisableMouseCapture);
let deadline = std::time::Instant::now() + Duration::from_millis(50);
while std::time::Instant::now() < deadline
&& matches!(crossterm::event::poll(Duration::from_millis(5)), Ok(true))
{
if !matches!(
crossterm::event::read(),
Ok(crossterm::event::Event::Mouse(_))
) {
break;
}
}
}
async fn sleep_opt(duration: Option<Duration>) {
match duration {
Some(d) => tokio::time::sleep(d).await,
None => std::future::pending().await,
}
}
pub fn spawn_effects<Msg: Send + 'static>(effects: Vec<Effect<Msg>>, tx: &UnboundedSender<Msg>) {
for effect in effects {
let Effect::Spawn { stream, cancel } = effect;
drive_stream(stream, cancel, tx.clone());
}
}
fn drive_stream<Msg: Send + 'static>(
mut stream: MsgStream<Msg>,
cancel: Arc<Cancel>,
tx: UnboundedSender<Msg>,
) {
tokio::spawn(async move {
loop {
if cancel.is_cancelled() {
break;
}
tokio::select! {
biased;
_ = cancel.cancelled() => break,
item = poll_fn(|cx| stream.as_mut().poll_next(cx)) => match item {
Some(msg) => {
if tx.send(msg).is_err() {
break;
}
}
None => break,
},
}
}
});
}
pub struct ActiveSubscriptions<Msg> {
running: HashMap<String, RunningSub>,
tx: UnboundedSender<Msg>,
}
struct RunningSub {
cancel: Arc<Cancel>,
fingerprint: Fingerprint,
}
#[derive(PartialEq, Eq)]
enum Fingerprint {
Every(Duration),
Stream,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SyncReport {
pub started: Vec<String>,
pub stopped: Vec<String>,
}
impl<Msg: Send + 'static> ActiveSubscriptions<Msg> {
pub fn new(tx: UnboundedSender<Msg>) -> Self {
Self {
running: HashMap::new(),
tx,
}
}
pub fn sync(&mut self, declared: Subscriptions<Msg>) -> SyncReport {
let mut report = SyncReport::default();
let mut seen: Vec<String> = Vec::new();
for (key, kind) in declared.entries {
seen.push(key.clone());
let fingerprint = match &kind {
SubKind::Every { interval, .. } => Fingerprint::Every(*interval),
SubKind::Stream { .. } => Fingerprint::Stream,
};
match self.running.get(&key) {
Some(running) if running.fingerprint == fingerprint => {}
Some(_) => {
self.stop(&key);
report.stopped.push(key.clone());
self.start(&key, kind, fingerprint);
report.started.push(key);
}
None => {
self.start(&key, kind, fingerprint);
report.started.push(key);
}
}
}
let absent: Vec<String> = self
.running
.keys()
.filter(|k| !seen.contains(k))
.cloned()
.collect();
for key in absent {
self.stop(&key);
report.stopped.push(key);
}
report
}
fn start(&mut self, key: &str, kind: SubKind<Msg>, fingerprint: Fingerprint) {
let cancel = Arc::new(Cancel::new());
match kind {
SubKind::Every { interval, make } => {
let tx = self.tx.clone();
let cancel_task = Arc::clone(&cancel);
tokio::spawn(async move {
loop {
tokio::select! {
biased;
_ = cancel_task.cancelled() => break,
_ = tokio::time::sleep(interval) => {
if tx.send(make()).is_err() {
break;
}
}
}
}
});
}
SubKind::Stream { make } => {
drive_stream(make(), Arc::clone(&cancel), self.tx.clone());
}
}
self.running.insert(
key.to_string(),
RunningSub {
cancel,
fingerprint,
},
);
}
fn stop(&mut self, key: &str) {
if let Some(running) = self.running.remove(key) {
running.cancel.cancel();
}
}
}
impl<Msg> Drop for ActiveSubscriptions<Msg> {
fn drop(&mut self) {
for running in self.running.values() {
running.cancel.cancel();
}
}
}