Skip to main content

clawless_cli/
runner.rs

1//! CLI command runner
2//!
3//! This module defines [`CommandRunner`], which encapsulates the full lifecycle of a CLI command:
4//! event channel creation, context construction, signal handling, and terminal presentation. The
5//! `main!()` macro dispatches to `CommandRunner::run` when the resolved leaf is a command.
6//!
7//! Application authors do not interact with this module directly. The `main!()` macro calls
8//! [`CommandRunner::run`] with the resolved matches and exec function pointer.
9
10use std::future::Future;
11
12use clawless_core::cancellation::Cancellation;
13use clawless_core::context::Context;
14use clawless_core::event::event_channel;
15use clawless_core::output::Output;
16use clawless_core::signal::wait_for_shutdown;
17
18use crate::error::CommandResult;
19use crate::output::OutputFlags;
20use crate::presenter::{Presenter, TerminalPresenter};
21
22/// CLI command runner
23///
24/// Encapsulates the lifecycle of running a CLI command: creating the event channel, constructing
25/// the [`Context`], registering signal handlers, and presenting output through a
26/// [`TerminalPresenter`]. The `main!()` macro dispatches to [`CommandRunner::run`] when the
27/// resolved leaf is a `ResolvedLeaf::Command`.
28///
29/// # Examples
30///
31/// ```rust,ignore
32/// // This is what main!() expands to for commands:
33/// ResolvedLeaf::Command { matches, exec } => {
34///     clawless::runner::CommandRunner::run(matches, exec)
35/// }
36/// ```
37// r[impl dispatch.exec.command-runner]
38#[derive(Debug)]
39pub struct CommandRunner;
40
41impl CommandRunner {
42    /// Runs a CLI command to completion
43    ///
44    /// Sets up the command lifecycle:
45    ///
46    /// 1. Creates a [`Cancellation`] token for cooperative shutdown
47    /// 2. Extracts output flags from the pre-parsed argument matches
48    /// 3. Creates an event channel and builds the [`Context`]
49    /// 4. Configures a [`TerminalPresenter`] with the parsed output flags
50    /// 5. Creates a Tokio runtime, spawns the signal handler, and runs the command through the
51    ///    presenter
52    ///
53    /// Output flags (`--quiet`, `--verbose`, `--json`) are augmented at the root level by
54    /// `main!()` with `.global(true)`, so they are available in every leaf's [`ArgMatches`].
55    ///
56    /// # Arguments
57    ///
58    /// * `matches` — The parsed [`ArgMatches`] for this command leaf, as resolved by the
59    ///   subcommand tree walk.
60    /// * `exec` — Executes the command with the given matches and context. Generated by the
61    ///   `#[command]` macro.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if context construction fails (e.g., the current working directory cannot be
66    /// determined), if the Tokio runtime cannot be created, or if the command itself fails.
67    ///
68    /// [`ArgMatches`]: clap::ArgMatches
69    /// [`Cancellation`]: clawless_core::cancellation::Cancellation
70    /// [`Context`]: clawless_core::context::Context
71    /// [`TerminalPresenter`]: crate::presenter::TerminalPresenter
72    pub fn run<F>(
73        matches: clap::ArgMatches,
74        exec: fn(clap::ArgMatches, Context) -> F,
75    ) -> Result<(), Box<dyn std::error::Error>>
76    where
77        F: Future<Output = CommandResult> + Send + 'static,
78    {
79        let cancellation = Cancellation::new();
80        let output_flags = OutputFlags::from_arg_matches(&matches);
81
82        let (sender, receiver) = event_channel();
83        let output = Output::new(sender);
84
85        let context = Context::builder()
86            .cancellation(cancellation.clone())
87            .output(output)
88            .build()?;
89
90        let presenter = TerminalPresenter::builder()
91            .receiver(receiver)
92            .verbosity(output_flags.verbosity())
93            .mode(output_flags.mode())
94            .build();
95
96        let rt = tokio::runtime::Runtime::new()?;
97        rt.block_on(async {
98            tokio::spawn(wait_for_shutdown(cancellation));
99
100            presenter.present(Box::pin(exec(matches, context))).await
101        })?;
102
103        Ok(())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    // An assertion in a test panics by design. A `# Panics` section on every test
110    // would repeat that and give the reader no information.
111    #![allow(clippy::missing_panics_doc)]
112
113    use super::*;
114
115    #[test]
116    fn trait_send() {
117        fn assert_send<T: Send>() {}
118        assert_send::<CommandRunner>();
119    }
120
121    #[test]
122    fn trait_sync() {
123        fn assert_sync<T: Sync>() {}
124        assert_sync::<CommandRunner>();
125    }
126
127    #[test]
128    fn trait_unpin() {
129        fn assert_unpin<T: Unpin>() {}
130        assert_unpin::<CommandRunner>();
131    }
132}