rustact 0.1.0

Async terminal UI framework inspired by React, built on top of ratatui and tokio.
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::time::timeout;

use super::super::app::flatten_tree_items;
use super::super::dispatcher::AppMessage;
use crate::runtime::{App, Element, RuntimeDriver, TreeItemNode, TreeRowView, component};

#[test]
fn flatten_tree_items_includes_only_expanded_children() {
    let expanded_parent = TreeItemNode::new("Parent").child(TreeItemNode::new("Child"));
    let collapsed_parent = TreeItemNode::new("Collapsed")
        .children(vec![TreeItemNode::new("Hidden")])
        .expanded(false);

    let rows = flatten_tree_items(vec![expanded_parent, collapsed_parent]);

    assert_eq!(rows.len(), 3);
    assert_row(&rows[0], "Parent", 0, true, true);
    assert_row(&rows[1], "Child", 1, false, false);
    assert_row(&rows[2], "Collapsed", 0, true, false);
}

fn assert_row(row: &TreeRowView, label: &str, depth: usize, has_children: bool, expanded: bool) {
    assert_eq!(row.label, label);
    assert_eq!(row.depth, depth);
    assert_eq!(row.has_children, has_children);
    assert_eq!(row.expanded, expanded);
}

#[tokio::test]
async fn app_run_uses_custom_runtime_driver() {
    let driver = TestRuntimeDriver::default();
    let app = App::new("DriverTest", component("Unit", |_ctx| Element::Empty))
        .with_driver(driver.clone())
        .headless();

    timeout(Duration::from_millis(200), app.run())
        .await
        .expect("runtime exited")
        .expect("app run succeeds");

    let (terminal, tick, shutdown) = driver.call_counts();
    assert_eq!(terminal, 1);
    assert_eq!(tick, 1);
    assert_eq!(shutdown, 1);
}

#[derive(Clone, Default)]
struct TestRuntimeDriver {
    inner: Arc<TestRuntimeDriverInner>,
}

struct TestRuntimeDriverInner {
    terminal_calls: AtomicUsize,
    tick_calls: AtomicUsize,
    shutdown_calls: AtomicUsize,
}

impl Default for TestRuntimeDriverInner {
    fn default() -> Self {
        Self {
            terminal_calls: AtomicUsize::new(0),
            tick_calls: AtomicUsize::new(0),
            shutdown_calls: AtomicUsize::new(0),
        }
    }
}

impl RuntimeDriver for TestRuntimeDriver {
    fn spawn_terminal_events(&self, tx: mpsc::Sender<AppMessage>) -> JoinHandle<()> {
        self.inner.terminal_calls.fetch_add(1, Ordering::SeqCst);
        tokio::spawn(async move {
            let _ = tx.send(AppMessage::Shutdown).await;
        })
    }

    fn spawn_tick_loop(&self, _tx: mpsc::Sender<AppMessage>, _rate: Duration) -> JoinHandle<()> {
        self.inner.tick_calls.fetch_add(1, Ordering::SeqCst);
        tokio::spawn(async {})
    }

    fn spawn_shutdown_watcher(&self, _tx: mpsc::Sender<AppMessage>) -> JoinHandle<()> {
        self.inner.shutdown_calls.fetch_add(1, Ordering::SeqCst);
        tokio::spawn(async {})
    }
}

impl TestRuntimeDriver {
    fn call_counts(&self) -> (usize, usize, usize) {
        (
            self.inner.terminal_calls.load(Ordering::SeqCst),
            self.inner.tick_calls.load(Ordering::SeqCst),
            self.inner.shutdown_calls.load(Ordering::SeqCst),
        )
    }
}