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}