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}