Skip to main content

clawless_tui/
runner.rs

1//! TUI application runner
2//!
3//! This module defines [`ApplicationRunner`], which encapsulates the full lifecycle of a TUI
4//! application: event channel creation, context construction, projection setup, signal handling, and
5//! application execution. The `main!()` macro dispatches to `ApplicationRunner::run` when the
6//! resolved leaf is an application.
7//!
8//! Application authors do not interact with this module directly. The `main!()` macro calls
9//! [`ApplicationRunner::run`] with the resolved matches and the leaf's exec function. The runner
10//! takes any callable, so a caller that builds its command tree at run time can dispatch to a
11//! closure that owns the application it resolved.
12
13use std::future::Future;
14
15use clawless_core::cancellation::Cancellation;
16use clawless_core::context::Context;
17use clawless_core::event::event_channel;
18use clawless_core::output::Output;
19use clawless_core::signal::wait_for_shutdown;
20
21use crate::projection::Projection;
22
23/// TUI application runner
24///
25/// Encapsulates the lifecycle of running a TUI application: creating the event channel,
26/// constructing the [`Context`], building a [`Projection`] inside a Tokio runtime, registering
27/// signal handlers, and calling the application function. Unlike `CommandRunner`, there is no
28/// presenter — the application owns its own render loop and queries the projection for accumulated
29/// state.
30///
31/// # Examples
32///
33/// ```rust,ignore
34/// // This is what main!() expands to for applications:
35/// ResolvedLeaf::Application { matches, exec } => {
36///     clawless::tui::runner::ApplicationRunner::run(matches, exec)
37/// }
38/// ```
39///
40/// [`Context`]: clawless_core::context::Context
41/// [`Projection`]: crate::projection::Projection
42// r[impl dispatch.exec.application-runner]
43#[derive(Debug)]
44pub struct ApplicationRunner;
45
46impl ApplicationRunner {
47    /// Runs a TUI application to completion
48    ///
49    /// Sets up the application lifecycle:
50    ///
51    /// 1. Creates a [`Cancellation`] token for cooperative shutdown
52    /// 2. Creates an event channel and builds the [`Context`]
53    /// 3. Creates a Tokio runtime
54    /// 4. Inside the runtime, creates a [`Projection`] (which spawns a background drain task)
55    /// 5. Spawns the signal handler and calls the application function
56    ///
57    /// The [`Projection`] must be created inside the Tokio runtime because its constructor calls
58    /// `tokio::spawn` to start the background event drain.
59    ///
60    /// # Arguments
61    ///
62    /// * `matches` — The parsed [`ArgMatches`] for this application leaf, as resolved by the
63    ///   subcommand tree walk.
64    /// * `exec` — Executes the application with the given matches, context, and projection. The
65    ///   `#[application]` macro generates a function for this, and any other callable works. A
66    ///   caller that builds its command tree at run time passes a closure that owns the
67    ///   application it resolved.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if context construction fails (e.g., the current working directory cannot be
72    /// determined), if the Tokio runtime cannot be created, or if the application itself fails.
73    ///
74    /// [`ArgMatches`]: clap::ArgMatches
75    /// [`Cancellation`]: clawless_core::cancellation::Cancellation
76    /// [`Context`]: clawless_core::context::Context
77    /// [`Projection`]: crate::projection::Projection
78    // r[impl dispatch.exec.callable]
79    pub fn run<E, F>(matches: clap::ArgMatches, exec: E) -> Result<(), Box<dyn std::error::Error>>
80    where
81        E: FnOnce(clap::ArgMatches, Context, Projection) -> F,
82        F: Future<Output = anyhow::Result<()>> + Send + 'static,
83    {
84        let cancellation = Cancellation::new();
85        let (sender, receiver) = event_channel();
86        let output = Output::new(sender);
87
88        let context = Context::builder()
89            .cancellation(cancellation.clone())
90            .output(output)
91            .build()?;
92
93        let rt = tokio::runtime::Runtime::new()?;
94        rt.block_on(async {
95            let projection = Projection::new(receiver);
96
97            tokio::spawn(wait_for_shutdown(cancellation));
98            exec(matches, context, projection).await
99        })?;
100
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    // An assertion in a test panics by design. A `# Panics` section on every test
108    // would repeat that and give the reader no information.
109    #![allow(clippy::missing_panics_doc)]
110
111    use std::sync::Arc;
112    use std::sync::atomic::{AtomicBool, Ordering};
113
114    use super::*;
115
116    // r[verify dispatch.exec.callable]
117    #[test]
118    fn run_with_a_closure_that_owns_state_executes_the_leaf() {
119        let matches = clap::Command::new("test").get_matches_from(["test"]);
120        let executed = Arc::new(AtomicBool::new(false));
121        let owned = Arc::clone(&executed);
122
123        ApplicationRunner::run(matches, move |_matches, _context, _projection| async move {
124            owned.store(true, Ordering::SeqCst);
125            Ok(())
126        })
127        .expect("the runner runs the leaf to completion");
128
129        assert!(executed.load(Ordering::SeqCst));
130    }
131
132    #[test]
133    fn trait_send() {
134        fn assert_send<T: Send>() {}
135        assert_send::<ApplicationRunner>();
136    }
137
138    #[test]
139    fn trait_sync() {
140        fn assert_sync<T: Sync>() {}
141        assert_sync::<ApplicationRunner>();
142    }
143
144    #[test]
145    fn trait_unpin() {
146        fn assert_unpin<T: Unpin>() {}
147        assert_unpin::<ApplicationRunner>();
148    }
149}