1use std::ffi::OsString;
28use std::fmt;
29use std::io::{self, Write as _};
30use std::marker::PhantomData;
31use std::process::ExitCode;
32
33use anyhow::Result;
34#[cfg(feature = "usage")]
35use clap::Command;
36use clap::{CommandFactory, Parser};
37
38use crate::color::ColorMode;
39use crate::format::OutputFormat;
40use crate::render::RenderOptions;
41use crate::theme::Theme;
42use crate::view::{JsonLayout, Present, View};
43
44type BeforeParse = Box<dyn Fn(&[OsString]) -> Option<ExitCode>>;
45type SelectView<C> = Box<dyn Fn(&C) -> View>;
46#[cfg(feature = "usage")]
47type RenderUsage = Box<dyn Fn(Command, &str) -> String>;
48
49#[derive(Clone, Copy)]
51struct Fallback(Option<u16>);
52
53#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
69pub enum BrokenPipe {
70 #[default]
75 Ignore,
76 Fail,
80}
81
82pub struct App<C> {
90 bin: String,
91 before_parse: Vec<BeforeParse>,
92 select_view: SelectView<C>,
93 automatic_width_buffer: Option<u16>,
94 automatic_width_buffer_envs: Option<&'static [&'static str]>,
95 minimum_automatic_width: Option<u16>,
96 fallback_width: Option<Fallback>,
97 styles: Option<RenderOptions>,
98 json_layout: Option<JsonLayout>,
99 json_layout_envs: Option<&'static [&'static str]>,
100 theme: Option<Theme>,
101 broken_pipe: BrokenPipe,
102 #[cfg(feature = "usage")]
103 mounted_as: Option<String>,
104 #[cfg(feature = "usage")]
105 render_usage: RenderUsage,
106 marker: PhantomData<C>,
107}
108
109impl<C> fmt::Debug for App<C> {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 formatter
113 .debug_struct("App")
114 .field("bin", &self.bin)
115 .finish_non_exhaustive()
116 }
117}
118
119impl<C> App<C> {
120 #[must_use]
125 pub fn new(bin: impl Into<String>) -> Self {
126 Self {
127 bin: bin.into(),
128 before_parse: Vec::new(),
129 select_view: Box::new(|_| View::new(OutputFormat::Pretty, ColorMode::Auto)),
130 automatic_width_buffer: None,
131 automatic_width_buffer_envs: None,
132 minimum_automatic_width: None,
133 fallback_width: None,
134 styles: None,
135 json_layout: None,
136 json_layout_envs: None,
137 theme: None,
138 broken_pipe: BrokenPipe::Ignore,
139 #[cfg(feature = "usage")]
140 mounted_as: None,
141 #[cfg(feature = "usage")]
142 render_usage: Box::new(crate::usage::spec),
143 marker: PhantomData,
144 }
145 }
146
147 #[must_use]
155 pub fn view(mut self, select: impl Fn(&C) -> View + 'static) -> Self {
156 self.select_view = Box::new(select);
157 self
158 }
159
160 #[must_use]
164 pub fn automatic_width_buffer(mut self, cells: u16) -> Self {
165 self.automatic_width_buffer = Some(cells);
166 self
167 }
168
169 #[must_use]
172 pub fn automatic_width_buffer_envs(mut self, names: &'static [&'static str]) -> Self {
173 self.automatic_width_buffer_envs = Some(names);
174 self
175 }
176
177 #[must_use]
179 pub fn minimum_automatic_width(mut self, cells: u16) -> Self {
180 self.minimum_automatic_width = Some(cells);
181 self
182 }
183
184 #[must_use]
187 pub fn fallback_width(mut self, width: Option<u16>) -> Self {
188 self.fallback_width = Some(Fallback(width));
189 self
190 }
191
192 #[cfg(any(feature = "input-yaml", feature = "input-toml"))]
196 #[must_use]
197 pub fn input_limits(self, limits: crate::input::Limits) -> Self {
198 crate::input::set_default_limits(limits);
199 self
200 }
201
202 #[must_use]
206 pub fn styles(mut self, styles: RenderOptions) -> Self {
207 self.styles = Some(styles);
208 self
209 }
210
211 #[must_use]
213 pub fn json_layout(mut self, layout: JsonLayout) -> Self {
214 self.json_layout = Some(layout);
215 self
216 }
217
218 #[must_use]
221 pub fn json_layout_envs(mut self, names: &'static [&'static str]) -> Self {
222 self.json_layout_envs = Some(names);
223 self
224 }
225
226 #[must_use]
231 pub fn theme(mut self, theme: Theme) -> Self {
232 self.theme = Some(theme);
233 self
234 }
235
236 #[must_use]
240 pub fn broken_pipe(mut self, policy: BrokenPipe) -> Self {
241 self.broken_pipe = policy;
242 self
243 }
244
245 fn written(&self, written: io::Result<()>, code: ExitCode) -> ExitCode {
254 match written {
255 Ok(()) => code,
256 Err(error) if error.kind() == io::ErrorKind::BrokenPipe => match self.broken_pipe {
257 BrokenPipe::Fail if code == ExitCode::SUCCESS => ExitCode::FAILURE,
258 BrokenPipe::Ignore | BrokenPipe::Fail => code,
259 },
260 Err(_) => ExitCode::FAILURE,
261 }
262 }
263
264 fn configured_view(&self, mut view: View) -> View {
265 if let Some(buffer) = self.automatic_width_buffer {
266 view = view.automatic_width_buffer(buffer);
267 }
268 if let Some(names) = self.automatic_width_buffer_envs {
269 view = view.automatic_width_buffer_envs(names);
270 }
271 if let Some(minimum) = self.minimum_automatic_width {
272 view = view.minimum_automatic_width(minimum);
273 }
274 if let Some(Fallback(width)) = self.fallback_width {
275 view = view.fallback_width(width);
276 }
277 if let Some(styles) = self.styles {
278 view = view.styles(styles);
279 }
280 if let Some(theme) = self.theme {
281 view = view.theme(theme);
282 }
283 if let Some(layout) = self.json_layout {
284 view = view.json_layout(layout);
285 }
286 if let Some(names) = self.json_layout_envs {
287 view = view.json_layout_envs(names);
288 }
289 view
290 }
291
292 #[must_use]
298 pub fn before_parse(
299 mut self,
300 hook: impl Fn(&[OsString]) -> Option<ExitCode> + 'static,
301 ) -> Self {
302 self.before_parse.push(Box::new(hook));
303 self
304 }
305
306 #[cfg(feature = "completion")]
312 #[must_use]
313 pub fn completion(self) -> Self
314 where
315 C: CommandFactory + 'static,
316 {
317 let bin = self.bin.clone();
318 self.before_parse(move |args| crate::completion::complete::<C>(&bin, args))
319 }
320
321 #[cfg(feature = "usage")]
327 #[must_use]
328 pub fn mounted_as(mut self, task: impl Into<String>) -> Self {
329 self.mounted_as = Some(task.into());
330 self
331 }
332
333 #[cfg(feature = "usage")]
338 #[must_use]
339 pub fn usage_spec(mut self, render: impl Fn(Command, &str) -> String + 'static) -> Self {
340 self.render_usage = Box::new(render);
341 self
342 }
343}
344
345impl<C> App<C>
346where
347 C: Parser + CommandFactory,
348{
349 #[must_use = "return it from main, or the command exits 0"]
359 pub fn run<T>(self, execute: impl FnOnce(C) -> Result<T>) -> ExitCode
360 where
361 T: Present,
362 {
363 self.run_from(std::env::args_os(), execute)
364 }
365
366 #[must_use = "return it from main, or the command exits 0"]
375 pub fn run_from<T>(
376 self,
377 args: impl IntoIterator<Item = impl Into<OsString>>,
378 execute: impl FnOnce(C) -> Result<T>,
379 ) -> ExitCode
380 where
381 T: Present,
382 {
383 let raw = args.into_iter().map(Into::into).collect::<Vec<_>>();
384 let words = raw
385 .iter()
386 .map(|arg| arg.to_string_lossy().into_owned())
387 .collect::<Vec<_>>();
388
389 #[cfg(feature = "usage")]
390 if let Some(task) = &self.mounted_as
391 && let Some(spec_bin) = crate::usage::spec_bin(words.iter().skip(1), task)
392 {
393 let mut command = C::command();
394 command.set_bin_name(&spec_bin);
395 let spec = (self.render_usage)(command, &spec_bin);
396 return self.written(
397 crate::view::write_stdout(spec.as_bytes()),
398 ExitCode::SUCCESS,
399 );
400 }
401
402 for hook in &self.before_parse {
403 if let Some(code) = hook(&raw) {
404 return code;
405 }
406 }
407
408 let raw_view = self.configured_view(raw_view::<C>(&raw));
409 for warning in raw_view.env_warnings(crate::render::process_env) {
410 let _ = writeln!(std::io::stderr().lock(), "{}: warning: {warning}", self.bin);
411 }
412 if words.len() == 1 && crate::parser::requires_input::<C>() {
413 return self.written(
414 crate::help::emit_bare_with_options::<C>(raw_view.render_options()),
415 ExitCode::from(2),
416 );
417 }
418 match crate::help::try_emit_from_with_options::<C>(&words, raw_view.render_options()) {
419 Ok(true) => return ExitCode::SUCCESS,
420 Ok(false) => {}
421 Err(error) => return self.written(Err(error), ExitCode::SUCCESS),
422 }
423
424 let command = crate::parser::apply_defaults(C::command());
425 let command = match crate::flags::env_under_flags(command, &raw) {
426 Ok(command) => command,
427 Err(error) => {
428 return self.clap_error(&error, &parse_error_view(&raw, raw_view), &words);
429 }
430 };
431 let matches = match command.try_get_matches_from(&raw) {
432 Ok(matches) => matches,
433 Err(error) => {
434 return self.clap_error(&error, &parse_error_view(&raw, raw_view), &words);
435 }
436 };
437 let cli = match C::from_arg_matches(&matches) {
438 Ok(cli) => cli,
439 Err(error) => {
440 return self.clap_error(&error, &parse_error_view(&raw, raw_view), &words);
441 }
442 };
443 let view = self.configured_view((self.select_view)(&cli));
444 match execute(cli) {
445 Ok(value) => self.written(
446 view.show(&value).map(|_| ()),
447 ExitCode::from(value.exit_code()),
448 ),
449 Err(error) => self.written(
450 view.emit_err(&self.bin, &format!("{error:#}")).map(|_| ()),
451 ExitCode::FAILURE,
452 ),
453 }
454 }
455
456 fn clap_error(&self, error: &clap::Error, view: &View, args: &[String]) -> ExitCode {
460 use clap::error::ErrorKind;
461
462 let code = exit_code(error.exit_code());
463 let kind = error.kind();
464 if !is_clap_display(kind) {
465 return self.written(
466 view.emit_err(&self.bin, error.to_string().trim())
467 .map(|_| ()),
468 code,
469 );
470 }
471 let options = view.render_options();
472 let written = if kind == ErrorKind::DisplayVersion {
473 crate::view::write_stdout(error.to_string().as_bytes())
474 } else if kind == ErrorKind::DisplayHelp {
475 crate::help::emit_selected::<C>(args, options)
476 } else {
477 crate::help::emit_selected_to_stderr::<C>(args, options)
478 };
479 self.written(written, code)
480 }
481}
482
483fn raw_view<C: CommandFactory>(raw: &[OsString]) -> View {
484 let parsed = crate::parser::parsed_output::<C>(raw);
485 View::new(parsed.format, parsed.color)
486}
487
488fn parse_error_view(raw: &[OsString], mut view: View) -> View {
505 let mut args = raw.iter().skip(1).map(|arg| arg.to_str());
506 let mut last = None;
507 while let Some(arg) = args.next() {
508 let value = match arg {
509 Some("--") => break,
510 Some("--format") => args.next().flatten(),
511 Some(arg) => arg.strip_prefix("--format="),
512 None => None,
513 };
514 if let Some(format) =
515 value.and_then(|value| <OutputFormat as clap::ValueEnum>::from_str(value, false).ok())
516 {
517 last = Some(format);
518 }
519 }
520 if let Some(format) = last {
521 view = view.in_format(format);
522 if format.is_json() {
523 view = view.color(ColorMode::Never);
524 }
525 }
526 view
527}
528
529fn is_clap_display(kind: clap::error::ErrorKind) -> bool {
530 matches!(
531 kind,
532 clap::error::ErrorKind::DisplayVersion
533 | clap::error::ErrorKind::DisplayHelp
534 | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
535 )
536}
537
538fn exit_code(code: i32) -> ExitCode {
539 u8::try_from(code).map_or(ExitCode::FAILURE, ExitCode::from)
540}
541
542#[cfg(test)]
543mod tests {
544 use std::cell::Cell;
545 use std::ffi::OsString;
546 use std::rc::Rc;
547
548 use clap::{Parser, Subcommand};
549 use serde::Serialize;
550
551 use super::{App, BrokenPipe, is_clap_display, parse_error_view, raw_view};
552 use crate::document::{Document, Fields};
553 use crate::render::{RecordStyle, RenderOptions};
554 use crate::view::{JsonLayout, Present, View};
555 use crate::{ColorMode, OutputArgs, OutputFormat, Role, Theme};
556
557 #[derive(Parser)]
558 #[command(version, about = "toy")]
559 struct Cli {
560 #[command(flatten)]
561 output: OutputArgs,
562 #[command(subcommand)]
563 command: Command,
564 }
565
566 #[derive(Subcommand)]
567 enum Command {
568 Status(StatusArgs),
570 }
571
572 #[derive(clap::Args)]
573 struct StatusArgs {
574 #[arg(short = 'm', long, allow_hyphen_values = true)]
576 message: Option<String>,
577 }
578
579 #[derive(Parser)]
580 #[command(version, about = "optional toy")]
581 struct OptionalCli {}
582
583 #[derive(Parser)]
584 #[command(version, about = "nested toy")]
585 struct NestedCli {
586 #[command(subcommand)]
587 command: NestedCommand,
588 }
589
590 #[derive(Subcommand)]
591 enum NestedCommand {
592 Group {
594 #[command(subcommand)]
595 command: GroupCommand,
596 },
597 }
598
599 #[derive(Subcommand)]
600 enum GroupCommand {
601 Status,
603 }
604
605 #[derive(Serialize)]
606 struct Status {
607 pending: usize,
608 }
609
610 impl Present for Status {
611 fn present(&self) -> Document {
612 Document::new().fields(Fields::new().row("pending", self.pending.to_string()))
613 }
614 }
615
616 #[test]
617 fn parses_executes_and_suppresses_quiet_pretty() {
618 let ran = Rc::new(Cell::new(false));
619 let observed = Rc::clone(&ran);
620 let code = App::<Cli>::new("toy")
621 .view(|cli| {
622 View::new(cli.output.format, cli.output.color())
623 .quiet(cli.output.quiet)
624 .width(80)
625 })
626 .run_from(["toy", "status", "--quiet"], move |_| {
627 observed.set(true);
628 Ok(Status { pending: 0 })
629 });
630 assert_eq!(code, std::process::ExitCode::SUCCESS);
631 assert!(ran.get());
632 }
633
634 #[test]
637 fn a_parse_error_honours_a_later_json_format() {
638 let pretty = View::new(OutputFormat::Pretty, ColorMode::Always);
639 let cases = [
640 (&["toy", "status", "--bogus", "--format", "json"][..], true),
641 (&["toy", "status", "--bogus", "--format=json"][..], true),
642 (&["toy", "status", "--bogus"][..], false),
643 (
644 &["toy", "status", "--bogus", "--", "--format", "json"][..],
645 false,
646 ),
647 (
648 &["toy", "status", "--bogus", "--format", "pretty"][..],
649 false,
650 ),
651 (
652 &["toy", "--bogus", "--format", "json", "--format", "pretty"][..],
653 false,
654 ),
655 (
656 &["toy", "--bogus", "--format=pretty", "--format", "json"][..],
657 true,
658 ),
659 ];
660 for (argv, json) in cases {
661 let raw: Vec<OsString> = argv.iter().map(OsString::from).collect();
662 let view = parse_error_view(&raw, pretty);
663 assert_eq!(view.format().is_json(), json, "{argv:?}");
664 if json {
665 assert_eq!(
666 view.render_options(),
667 RenderOptions::new(ColorMode::Never),
668 "{argv:?}"
669 );
670 }
671 }
672 let json_first = View::new(OutputFormat::Json, ColorMode::Never);
673 let raw: Vec<OsString> = ["toy", "--format", "json", "--bogus", "--format", "pretty"]
674 .iter()
675 .map(OsString::from)
676 .collect();
677 assert!(
678 !parse_error_view(&raw, json_first).format().is_json(),
679 "a later --format pretty undoes the pre-parse's JSON"
680 );
681 }
682
683 #[test]
684 fn pre_parse_hook_runs_before_clap() {
685 let code = App::<Cli>::new("toy")
686 .before_parse(|args| {
687 args.iter()
688 .any(|arg| arg == "--complete")
689 .then_some(std::process::ExitCode::SUCCESS)
690 })
691 .run_from(["toy", "--complete"], |_| Ok(Status { pending: 0 }));
692 assert_eq!(code, std::process::ExitCode::SUCCESS);
693 }
694
695 #[cfg(feature = "usage")]
696 #[test]
697 fn mounted_usage_accepts_a_custom_renderer() {
698 use std::cell::RefCell;
699
700 let observed = Rc::new(RefCell::new(None));
701 let callback_observed = Rc::clone(&observed);
702 let code = App::<Cli>::new("toy")
703 .mounted_as("q")
704 .usage_spec(move |command, bin| {
705 *callback_observed.borrow_mut() =
706 Some((bin.to_owned(), command.get_bin_name().map(str::to_owned)));
707 format!("custom {bin}\n")
708 })
709 .run_from(["toy", "--usage-spec=mounted"], |_| {
710 Ok(Status { pending: 0 })
711 });
712 assert_eq!(code, std::process::ExitCode::SUCCESS);
713 assert_eq!(
714 observed.borrow().as_ref(),
715 Some(&("mounted".to_owned(), Some("mounted".to_owned())))
716 );
717 }
718
719 #[test]
720 fn app_width_policy_configures_every_view() {
721 let app = App::<Cli>::new("toy")
722 .automatic_width_buffer(0)
723 .automatic_width_buffer_envs(&["TOY_BUFFER", "LEGACY_BUFFER"])
724 .minimum_automatic_width(8);
725 let view = app.configured_view(View::new(OutputFormat::Pretty, ColorMode::Never));
726 assert_eq!(
727 view.render_options(),
728 RenderOptions::new(ColorMode::Never)
729 .automatic_width_buffer(0)
730 .automatic_width_buffer_envs(&["TOY_BUFFER", "LEGACY_BUFFER"])
731 .minimum_automatic_width(8)
732 );
733 }
734
735 #[test]
736 fn app_json_layout_envs_reach_every_view() {
737 let compact = |name: &str| (name == "TOY_JSON").then(|| "compact".to_owned());
738 let view = App::<Cli>::new("toy")
739 .json_layout_envs(&["TOY_JSON"])
740 .configured_view(View::new(OutputFormat::Json, ColorMode::Never));
741 assert_eq!(view.json_layout_with(compact), JsonLayout::Compact);
742 let disabled = App::<Cli>::new("toy")
743 .json_layout_envs(&[])
744 .configured_view(View::new(OutputFormat::Json, ColorMode::Never));
745 assert_eq!(disabled.json_layout_with(compact), JsonLayout::Pretty);
746 }
747
748 #[test]
749 fn app_styles_and_fallback_reach_every_view() {
750 let app = App::<Cli>::new("toy")
751 .fallback_width(None)
752 .styles(RenderOptions::new(ColorMode::Never).record_style(RecordStyle::Boxed))
753 .json_layout(JsonLayout::Compact);
754 let options = app
755 .configured_view(View::new(OutputFormat::Pretty, ColorMode::Never))
756 .render_options();
757 assert_eq!(
758 options,
759 RenderOptions::new(ColorMode::Never)
760 .fallback_width(None)
761 .record_style(RecordStyle::Boxed)
762 );
763 assert_eq!(options.resolved_record_style(), RecordStyle::Boxed);
764 let json = app
765 .configured_view(View::new(OutputFormat::Json, ColorMode::Never))
766 .capture(&Status { pending: 1 })
767 .unwrap();
768 assert_eq!(json.text(), "{\"pending\":1}\n");
769 }
770
771 #[test]
772 fn app_theme_reaches_every_view_over_the_styles_theme() {
773 let underline = ctl_text::Style::new().effects(ctl_text::anstyle::Effects::UNDERLINE);
774 let theme = Theme::new().style(Role::Heading, underline);
775 let app = App::<Cli>::new("toy")
776 .theme(theme)
777 .styles(RenderOptions::new(ColorMode::Never).record_style(RecordStyle::Boxed));
778 let options = app
779 .configured_view(View::new(OutputFormat::Pretty, ColorMode::Always))
780 .render_options();
781 assert_eq!(
782 options,
783 RenderOptions::new(ColorMode::Always)
784 .theme(theme)
785 .record_style(RecordStyle::Boxed)
786 );
787 assert_eq!(options.resolved_record_style(), RecordStyle::Boxed);
788 }
789
790 #[test]
791 fn a_closed_pipe_exits_as_the_policy_says() {
792 use std::process::ExitCode;
793
794 let closed = || Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe));
795 let other = || Err(std::io::Error::other("disk full"));
796 let ignore = App::<Cli>::new("toy");
797 assert_eq!(
798 ignore.written(closed(), ExitCode::SUCCESS),
799 ExitCode::SUCCESS
800 );
801 assert_eq!(
802 ignore.written(closed(), ExitCode::from(2)),
803 ExitCode::from(2)
804 );
805 assert_eq!(
806 ignore.written(closed(), ExitCode::FAILURE),
807 ExitCode::FAILURE
808 );
809 assert_eq!(
810 ignore.written(other(), ExitCode::SUCCESS),
811 ExitCode::FAILURE
812 );
813 assert_eq!(ignore.written(Ok(()), ExitCode::from(2)), ExitCode::from(2));
814 let fail = App::<Cli>::new("toy").broken_pipe(BrokenPipe::Fail);
815 assert_eq!(fail.written(closed(), ExitCode::SUCCESS), ExitCode::FAILURE);
816 assert_eq!(fail.written(closed(), ExitCode::from(2)), ExitCode::from(2));
817 assert_eq!(fail.written(closed(), ExitCode::FAILURE), ExitCode::FAILURE);
818 }
819
820 #[test]
821 fn defaults_are_pretty_auto() {
822 let app = App::<Cli>::new("toy").view(|_| {
823 View::new(OutputFormat::Pretty, ColorMode::Auto)
824 .quiet(true)
825 .width(80)
826 });
827 let code = app.run_from(["toy", "status"], |_| Ok(Status { pending: 0 }));
828 assert_eq!(code, std::process::ExitCode::SUCCESS);
829 }
830
831 #[test]
832 fn clap_help_kinds_stay_on_claps_display_path() {
833 use clap::error::ErrorKind;
834
835 assert!(is_clap_display(ErrorKind::DisplayVersion));
836 assert!(is_clap_display(ErrorKind::DisplayHelp));
837 assert!(is_clap_display(
838 ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
839 ));
840 assert!(!is_clap_display(ErrorKind::UnknownArgument));
841 }
842
843 #[test]
844 fn missing_nested_subcommand_uses_claps_help_exit() {
845 let code =
846 App::<NestedCli>::new("toy").run_from(["toy", "group"], |_| Ok(Status { pending: 0 }));
847 assert_eq!(code, std::process::ExitCode::from(2));
848 }
849
850 #[test]
851 fn bare_invocation_is_usage_error() {
852 let code = App::<Cli>::new("toy").run_from(["toy"], |_| Ok(Status { pending: 0 }));
853 assert_eq!(code, std::process::ExitCode::from(2));
854 }
855
856 #[test]
857 fn bare_invocation_executes_when_the_root_accepts_it() {
858 let ran = Rc::new(Cell::new(false));
859 let observed = Rc::clone(&ran);
860 let code = App::<OptionalCli>::new("toy").run_from(["toy"], move |_| {
861 observed.set(true);
862 Ok(Status { pending: 0 })
863 });
864 assert_eq!(code, std::process::ExitCode::SUCCESS);
865 assert!(ran.get());
866 }
867
868 #[test]
869 fn explicit_help_used_as_a_domain_value_reaches_execution() {
870 let ran = Rc::new(Cell::new(false));
871 let observed = Rc::clone(&ran);
872 let code = App::<Cli>::new("toy").run_from(["toy", "status", "-m", "--help"], move |_| {
873 observed.set(true);
874 Ok(Status { pending: 0 })
875 });
876 assert_eq!(code, std::process::ExitCode::SUCCESS);
877 assert!(ran.get());
878 }
879
880 #[test]
881 fn raw_view_uses_clap_to_separate_globals_from_domain_values() {
882 let domain_value = ["toy", "status", "-m", "--format=json", "--bogus"].map(OsString::from);
883 let view = raw_view::<Cli>(&domain_value);
884 assert_eq!(view.format(), OutputFormat::Pretty);
885 assert_eq!(view.render_options(), RenderOptions::new(ColorMode::Auto));
886
887 let global_after_subcommand =
888 ["toy", "status", "-fjson", "-cnever", "--bogus"].map(OsString::from);
889 let view = raw_view::<Cli>(&global_after_subcommand);
890 assert_eq!(view.format(), OutputFormat::Json);
891 assert_eq!(view.render_options(), RenderOptions::new(ColorMode::Never));
892 }
893}