clawless-tui 0.6.1

Presentation layer for TUI-based applications using the Clawless framework
Documentation
//! TUI application runner
//!
//! This module defines [`ApplicationRunner`], which encapsulates the full lifecycle of a TUI
//! application: event channel creation, context construction, projection setup, signal handling, and
//! application execution. The `main!()` macro dispatches to `ApplicationRunner::run` when the
//! resolved leaf is an application.
//!
//! Application authors do not interact with this module directly. The `main!()` macro calls
//! [`ApplicationRunner::run`] with the resolved matches and the leaf's exec function. The runner
//! takes any callable, so a caller that builds its command tree at run time can dispatch to a
//! closure that owns the application it resolved.

use std::future::Future;

use clawless_core::cancellation::Cancellation;
use clawless_core::context::Context;
use clawless_core::event::event_channel;
use clawless_core::output::Output;
use clawless_core::signal::wait_for_shutdown;

use crate::projection::Projection;

/// TUI application runner
///
/// Encapsulates the lifecycle of running a TUI application: creating the event channel,
/// constructing the [`Context`], building a [`Projection`] inside a Tokio runtime, registering
/// signal handlers, and calling the application function. Unlike `CommandRunner`, there is no
/// presenter — the application owns its own render loop and queries the projection for accumulated
/// state.
///
/// # Examples
///
/// ```rust,ignore
/// // This is what main!() expands to for applications:
/// ResolvedLeaf::Application { matches, exec } => {
///     clawless::tui::runner::ApplicationRunner::run(matches, exec)
/// }
/// ```
///
/// [`Context`]: clawless_core::context::Context
/// [`Projection`]: crate::projection::Projection
// r[impl dispatch.exec.application-runner]
#[derive(Debug)]
pub struct ApplicationRunner;

impl ApplicationRunner {
    /// Runs a TUI application to completion
    ///
    /// Sets up the application lifecycle:
    ///
    /// 1. Creates a [`Cancellation`] token for cooperative shutdown
    /// 2. Creates an event channel and builds the [`Context`]
    /// 3. Creates a Tokio runtime
    /// 4. Inside the runtime, creates a [`Projection`] (which spawns a background drain task)
    /// 5. Spawns the signal handler and calls the application function
    ///
    /// The [`Projection`] must be created inside the Tokio runtime because its constructor calls
    /// `tokio::spawn` to start the background event drain.
    ///
    /// # Arguments
    ///
    /// * `matches` — The parsed [`ArgMatches`] for this application leaf, as resolved by the
    ///   subcommand tree walk.
    /// * `exec` — Executes the application with the given matches, context, and projection. The
    ///   `#[application]` macro generates a function for this, and any other callable works. A
    ///   caller that builds its command tree at run time passes a closure that owns the
    ///   application it resolved.
    ///
    /// # Errors
    ///
    /// Returns an error if context construction fails (e.g., the current working directory cannot be
    /// determined), if the Tokio runtime cannot be created, or if the application itself fails.
    ///
    /// [`ArgMatches`]: clap::ArgMatches
    /// [`Cancellation`]: clawless_core::cancellation::Cancellation
    /// [`Context`]: clawless_core::context::Context
    /// [`Projection`]: crate::projection::Projection
    // r[impl dispatch.exec.callable]
    pub fn run<E, F>(matches: clap::ArgMatches, exec: E) -> Result<(), Box<dyn std::error::Error>>
    where
        E: FnOnce(clap::ArgMatches, Context, Projection) -> F,
        F: Future<Output = anyhow::Result<()>> + Send + 'static,
    {
        let cancellation = Cancellation::new();
        let (sender, receiver) = event_channel();
        let output = Output::new(sender);

        let context = Context::builder()
            .cancellation(cancellation.clone())
            .output(output)
            .build()?;

        let rt = tokio::runtime::Runtime::new()?;
        rt.block_on(async {
            let projection = Projection::new(receiver);

            tokio::spawn(wait_for_shutdown(cancellation));
            exec(matches, context, projection).await
        })?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    // An assertion in a test panics by design. A `# Panics` section on every test
    // would repeat that and give the reader no information.
    #![allow(clippy::missing_panics_doc)]

    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};

    use super::*;

    // r[verify dispatch.exec.callable]
    #[test]
    fn run_with_a_closure_that_owns_state_executes_the_leaf() {
        let matches = clap::Command::new("test").get_matches_from(["test"]);
        let executed = Arc::new(AtomicBool::new(false));
        let owned = Arc::clone(&executed);

        ApplicationRunner::run(matches, move |_matches, _context, _projection| async move {
            owned.store(true, Ordering::SeqCst);
            Ok(())
        })
        .expect("the runner runs the leaf to completion");

        assert!(executed.load(Ordering::SeqCst));
    }

    #[test]
    fn trait_send() {
        fn assert_send<T: Send>() {}
        assert_send::<ApplicationRunner>();
    }

    #[test]
    fn trait_sync() {
        fn assert_sync<T: Sync>() {}
        assert_sync::<ApplicationRunner>();
    }

    #[test]
    fn trait_unpin() {
        fn assert_unpin<T: Unpin>() {}
        assert_unpin::<ApplicationRunner>();
    }
}