1use crate::error::OcelotError;
2use crate::result::OcelotResult;
3use std::fmt::Write as _;
4use std::process::ExitCode;
5
6pub fn try_main(run: impl FnOnce() -> OcelotResult<()>) -> ExitCode {
11 match run() {
12 Ok(()) => ExitCode::SUCCESS,
13 Err(error) => {
14 eprint!("{}", format_cli_error("operation failed", &error));
15 ExitCode::FAILURE
16 }
17 }
18}
19
20pub fn try_main_with_headline(headline: &str, run: impl FnOnce() -> OcelotResult<()>) -> ExitCode {
25 match run() {
26 Ok(()) => ExitCode::SUCCESS,
27 Err(error) => {
28 eprint!("{}", format_cli_error(headline, &error));
29 ExitCode::FAILURE
30 }
31 }
32}
33
34pub fn format_cli_error(headline: &str, error: &OcelotError) -> String {
39 let mut rendered = String::new();
40 let _ = writeln!(&mut rendered, "\u{1b}[1;31m━━ {}\u{1b}[0m", headline);
41 if error.write_to(&mut rendered).is_err() {
42 let _ = writeln!(
43 &mut rendered,
44 "\u{1b}[1;31m× error\u{1b}[0m failed to render detailed error output"
45 );
46 }
47
48 let mut causes = Vec::new();
49 let mut current = error.source();
50 while let Some(cause) = current {
51 causes.push((cause.kind().to_string(), cause.location()));
52 current = cause.source();
53 }
54
55 if !causes.is_empty() {
56 let simple_causes: Vec<_> = causes
57 .iter()
58 .filter(|(cause, _)| !cause.contains('\n'))
59 .collect();
60 if simple_causes.is_empty() {
61 return rendered;
62 }
63
64 rendered.push('\n');
65 rendered.push_str("\u{1b}[1;33m━━ cause chain\u{1b}[0m\n");
66 for (cause, location) in simple_causes {
67 let _ = writeln!(&mut rendered, " • {}", cause);
68 let _ = writeln!(
69 &mut rendered,
70 " at {}:{}:{}",
71 location.file(),
72 location.line(),
73 location.column()
74 );
75 }
76 }
77
78 rendered
79}
80
81#[cfg(test)]
82mod tests {
83 use crate::error::OcelotError;
84 use expect_test::expect;
85
86 use super::format_cli_error;
87
88 #[test]
89 fn format_cli_error_renders_headline_and_cause_chain() {
90 let error = OcelotError::message("failed to verify")
91 .with_source(OcelotError::message("missing reference output"));
92
93 expect!([r#"
94 ━━ verification failed
95 × error failed to verify
96 at crates/base/src/cli.rs:90:21
97 caused by: missing reference output
98 at crates/base/src/cli.rs:91:26
99
100 ━━ cause chain
101 • missing reference output
102 at crates/base/src/cli.rs:91:26
103 "#])
104 .assert_eq(&crate::unansi(&format_cli_error(
105 "verification failed",
106 &error,
107 )));
108 }
109
110 #[test]
111 fn format_cli_error_skips_cause_chain_for_multiline_cause() {
112 let error = OcelotError::message("failed to load recipe")
113 .with_source(OcelotError::message("line one\nline two"));
114
115 expect!([r#"
116 ━━ recipe failed
117 × error failed to load recipe
118 at crates/base/src/cli.rs:112:21
119 caused by:
120 line one
121 line two
122 at crates/base/src/cli.rs:113:26
123 "#])
124 .assert_eq(&crate::unansi(&format_cli_error("recipe failed", &error)));
125 }
126}