Skip to main content

clawless_cli/output/
mod.rs

1//! CLI flag configuration for output behavior
2//!
3//! This module defines [`OutputFlags`], which captures the `--quiet`, `--verbose`, and `--json`
4//! flags from the command line. These flags control how the [`TerminalPresenter`] renders events:
5//! [`Verbosity`] controls *whether* an event is shown; [`OutputMode`] controls *where* and *how*.
6//!
7//! Commands produce output through the core [`Output`] type, which sends events into a channel.
8//! `OutputFlags` configures the presenter that consumes those events, not the output itself.
9//!
10//! [`Output`]: clawless_core::output::Output
11//! [`TerminalPresenter`]: crate::presenter::TerminalPresenter
12
13use clap::{Arg, ArgAction, ArgMatches};
14
15pub use self::output_mode::OutputMode;
16pub use self::verbosity::Verbosity;
17
18/// Whether events render as human-readable text or as JSON
19mod output_mode;
20/// How much detail the presenter renders
21mod verbosity;
22
23/// CLI flag configuration for output behavior
24///
25/// `OutputFlags` captures the `--quiet`, `--verbose`, and `--json` flags that the `main!()` macro
26/// adds to every Clawless application. After parsing, the flags are forwarded to the
27/// [`TerminalPresenter`] to control how events are rendered.
28///
29/// `OutputFlags` does not produce output itself. Commands use the core [`Output`] type to emit
30/// events; `OutputFlags` configures the presenter that renders them.
31///
32/// # Examples
33///
34/// ```
35/// use clawless_cli::output::OutputFlags;
36///
37/// let flags = OutputFlags::new(
38///     clawless_cli::output::Verbosity::Default,
39///     clawless_cli::output::OutputMode::Text,
40/// );
41/// assert_eq!(flags.verbosity(), clawless_cli::output::Verbosity::Default);
42/// assert_eq!(flags.mode(), clawless_cli::output::OutputMode::Text);
43/// ```
44///
45/// [`Output`]: clawless_core::output::Output
46/// [`TerminalPresenter`]: crate::presenter::TerminalPresenter
47#[derive(Copy, Clone, Eq, PartialEq, Debug)]
48pub struct OutputFlags {
49    /// How much detail the presenter renders, from the `--quiet` and `--verbose` flags
50    verbosity: Verbosity,
51    /// Whether the presenter renders text or JSON, from the `--json` flag
52    mode: OutputMode,
53}
54
55impl OutputFlags {
56    /// Creates a new [`OutputFlags`] with the given verbosity and mode
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use clawless_cli::output::{OutputFlags, OutputMode, Verbosity};
62    ///
63    /// let flags = OutputFlags::new(Verbosity::Default, OutputMode::Text);
64    /// assert_eq!(flags.verbosity(), Verbosity::Default);
65    /// assert_eq!(flags.mode(), OutputMode::Text);
66    /// ```
67    pub fn new(verbosity: Verbosity, mode: OutputMode) -> Self {
68        Self { verbosity, mode }
69    }
70
71    /// Returns the verbosity level
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use clawless_cli::output::{OutputFlags, OutputMode, Verbosity};
77    ///
78    /// let flags = OutputFlags::new(Verbosity::Verbose, OutputMode::Text);
79    /// assert_eq!(flags.verbosity(), Verbosity::Verbose);
80    /// ```
81    pub fn verbosity(&self) -> Verbosity {
82        self.verbosity
83    }
84
85    /// Returns the output mode
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use clawless_cli::output::{OutputFlags, OutputMode, Verbosity};
91    ///
92    /// let flags = OutputFlags::new(Verbosity::Default, OutputMode::Json);
93    /// assert_eq!(flags.mode(), OutputMode::Json);
94    /// ```
95    pub fn mode(&self) -> OutputMode {
96        self.mode
97    }
98
99    /// Attaches `--quiet`, `--verbose`, and `--json` as global flags to a [`clap::Command`]
100    ///
101    /// The `--quiet` and `--verbose` flags conflict with each other. All three flags are global,
102    /// meaning they can appear before or after the subcommand name.
103    ///
104    /// This method is called by the `main!()` macro expansion. Command authors do not call it
105    /// directly.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// let command = clap::Command::new("test");
111    /// let command = clawless_cli::output::OutputFlags::augment_command(command);
112    /// ```
113    pub fn augment_command(command: clap::Command) -> clap::Command {
114        command
115            .arg(
116                Arg::new("quiet")
117                    .short('q')
118                    .long("quiet")
119                    .help("Suppress informational messages")
120                    .global(true)
121                    .action(ArgAction::SetTrue)
122                    .conflicts_with("verbose"),
123            )
124            .arg(
125                Arg::new("verbose")
126                    .short('v')
127                    .long("verbose")
128                    .help("Show additional detail")
129                    .global(true)
130                    .action(ArgAction::SetTrue)
131                    .conflicts_with("quiet"),
132            )
133            .arg(
134                Arg::new("json")
135                    .long("json")
136                    .help("Output results as JSON")
137                    .global(true)
138                    .action(ArgAction::SetTrue),
139            )
140    }
141
142    /// Constructs an [`OutputFlags`] from parsed CLI flags
143    ///
144    /// Reads the `--quiet`, `--verbose`, and `--json` flags from the given [`ArgMatches`] and
145    /// returns an [`OutputFlags`] configured accordingly. The command must have been augmented with
146    /// [`augment_command`] before parsing.
147    ///
148    /// This method is called by the `main!()` macro expansion. Command authors do not call it
149    /// directly.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// let command = clawless_cli::output::OutputFlags::augment_command(
155    ///     clap::Command::new("test"),
156    /// );
157    /// let matches = command.get_matches_from(vec!["test", "--quiet"]);
158    /// let flags = clawless_cli::output::OutputFlags::from_arg_matches(&matches);
159    /// assert_eq!(flags.verbosity(), clawless_cli::output::Verbosity::Quiet);
160    /// ```
161    ///
162    /// [`augment_command`]: OutputFlags::augment_command
163    pub fn from_arg_matches(matches: &ArgMatches) -> Self {
164        let quiet = matches.get_flag("quiet");
165        let verbose = matches.get_flag("verbose");
166        let json = matches.get_flag("json");
167
168        let verbosity = match (quiet, verbose) {
169            (true, _) => Verbosity::Quiet,
170            (_, true) => Verbosity::Verbose,
171            (false, false) => Verbosity::Default,
172        };
173
174        let mode = match json {
175            true => OutputMode::Json,
176            false => OutputMode::Text,
177        };
178
179        Self::new(verbosity, mode)
180    }
181}
182
183impl Default for OutputFlags {
184    fn default() -> Self {
185        Self::new(Verbosity::default(), OutputMode::default())
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    // An assertion in a test panics by design. A `# Panics` section on every test
192    // would repeat that and give the reader no information.
193    #![allow(clippy::missing_panics_doc)]
194
195    use super::*;
196
197    fn test_command() -> clap::Command {
198        OutputFlags::augment_command(clap::Command::new("test"))
199    }
200
201    #[test]
202    fn augment_command_adds_three_global_args() {
203        let command = test_command();
204        let args: Vec<&str> = command
205            .get_arguments()
206            .filter(|a| a.is_global_set())
207            .map(|a| a.get_id().as_str())
208            .collect();
209
210        assert!(args.contains(&"quiet"));
211        assert!(args.contains(&"verbose"));
212        assert!(args.contains(&"json"));
213        assert_eq!(args.len(), 3);
214    }
215
216    #[test]
217    fn default_is_text_with_default_verbosity() {
218        let flags = OutputFlags::default();
219
220        assert_eq!(flags.verbosity(), Verbosity::Default);
221        assert_eq!(flags.mode(), OutputMode::Text);
222    }
223
224    #[test]
225    fn from_arg_matches_with_defaults() {
226        let matches = test_command().get_matches_from(vec!["test"]);
227
228        let flags = OutputFlags::from_arg_matches(&matches);
229
230        assert_eq!(flags.verbosity(), Verbosity::Default);
231        assert_eq!(flags.mode(), OutputMode::Text);
232    }
233
234    #[test]
235    fn from_arg_matches_with_json_flag() {
236        let matches = test_command().get_matches_from(vec!["test", "--json"]);
237
238        let flags = OutputFlags::from_arg_matches(&matches);
239
240        assert_eq!(flags.mode(), OutputMode::Json);
241        assert_eq!(flags.verbosity(), Verbosity::Default);
242    }
243
244    #[test]
245    fn from_arg_matches_with_quiet_flag() {
246        let matches = test_command().get_matches_from(vec!["test", "--quiet"]);
247
248        let flags = OutputFlags::from_arg_matches(&matches);
249
250        assert_eq!(flags.verbosity(), Verbosity::Quiet);
251        assert_eq!(flags.mode(), OutputMode::Text);
252    }
253
254    #[test]
255    fn from_arg_matches_with_verbose_flag() {
256        let matches = test_command().get_matches_from(vec!["test", "--verbose"]);
257
258        let flags = OutputFlags::from_arg_matches(&matches);
259
260        assert_eq!(flags.verbosity(), Verbosity::Verbose);
261        assert_eq!(flags.mode(), OutputMode::Text);
262    }
263
264    #[test]
265    fn mode_returns_configured_mode() {
266        let flags = OutputFlags::new(Verbosity::Default, OutputMode::Json);
267
268        assert_eq!(flags.mode(), OutputMode::Json);
269    }
270
271    #[test]
272    fn trait_send() {
273        fn assert_send<T: Send>() {}
274        assert_send::<OutputFlags>();
275    }
276
277    #[test]
278    fn trait_sync() {
279        fn assert_sync<T: Sync>() {}
280        assert_sync::<OutputFlags>();
281    }
282
283    #[test]
284    fn trait_unpin() {
285        fn assert_unpin<T: Unpin>() {}
286        assert_unpin::<OutputFlags>();
287    }
288
289    #[test]
290    fn verbosity_returns_configured_verbosity() {
291        let flags = OutputFlags::new(Verbosity::Verbose, OutputMode::Text);
292
293        assert_eq!(flags.verbosity(), Verbosity::Verbose);
294    }
295}