rustact 0.1.0

Async terminal UI framework inspired by React, built on top of ratatui and tokio.
Documentation
use std::time::Duration;

use crossterm::event::EventStream;
use futures::StreamExt;
use tokio::signal;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;

use tracing::{debug, warn};

use crate::events::{FrameworkEvent, is_ctrl_c, map_terminal_event};

use super::dispatcher::AppMessage;

pub trait RuntimeDriver: Send + Sync {
    fn spawn_terminal_events(&self, tx: mpsc::Sender<AppMessage>) -> JoinHandle<()>;
    fn spawn_tick_loop(&self, tx: mpsc::Sender<AppMessage>, rate: Duration) -> JoinHandle<()>;
    fn spawn_shutdown_watcher(&self, tx: mpsc::Sender<AppMessage>) -> JoinHandle<()>;
}

#[derive(Default)]
pub struct DefaultRuntimeDriver;

impl RuntimeDriver for DefaultRuntimeDriver {
    fn spawn_terminal_events(&self, tx: mpsc::Sender<AppMessage>) -> JoinHandle<()> {
        spawn_terminal_events(tx)
    }

    fn spawn_tick_loop(&self, tx: mpsc::Sender<AppMessage>, rate: Duration) -> JoinHandle<()> {
        spawn_tick_loop(tx, rate)
    }

    fn spawn_shutdown_watcher(&self, tx: mpsc::Sender<AppMessage>) -> JoinHandle<()> {
        spawn_shutdown_watcher(tx)
    }
}

fn spawn_terminal_events(tx: mpsc::Sender<AppMessage>) -> JoinHandle<()> {
    debug!("spawning terminal event listener");
    tokio::spawn(async move {
        let mut events = EventStream::new();
        while let Some(event) = events.next().await {
            match event {
                Ok(evt) => {
                    if let Some(mapped) = map_terminal_event(evt) {
                        let shutdown = is_ctrl_c(&mapped);
                        if tx.send(AppMessage::ExternalEvent(mapped)).await.is_err() {
                            break;
                        }
                        if shutdown {
                            debug!("ctrl+c detected; requesting shutdown");
                            let _ = tx.send(AppMessage::Shutdown).await;
                            break;
                        }
                    }
                }
                Err(err) => {
                    warn!(error = ?err, "failed to read terminal event");
                    break;
                }
            }
        }
        debug!("terminal event listener exited");
    })
}

fn spawn_tick_loop(tx: mpsc::Sender<AppMessage>, rate: Duration) -> JoinHandle<()> {
    debug!(?rate, "spawning tick loop");
    tokio::spawn(async move {
        let mut ticker = tokio::time::interval(rate);
        loop {
            ticker.tick().await;
            if tx
                .send(AppMessage::ExternalEvent(FrameworkEvent::Tick))
                .await
                .is_err()
            {
                break;
            }
        }
        debug!("tick loop exited");
    })
}

fn spawn_shutdown_watcher(tx: mpsc::Sender<AppMessage>) -> JoinHandle<()> {
    debug!("spawning shutdown watcher");
    tokio::spawn(async move {
        if signal::ctrl_c().await.is_ok() {
            let _ = tx.send(AppMessage::Shutdown).await;
        }
        debug!("shutdown watcher exited");
    })
}