1use std::error::Error;
2use std::iter;
3use std::sync::atomic::AtomicBool;
4use std::sync::{LazyLock, Mutex};
5
6#[doc(hidden)]
8pub use anstream;
9#[doc(hidden)]
10pub use owo_colors;
11use owo_colors::{DynColor, OwoColorize};
12use rustc_hash::FxHashSet;
13
14pub static ENABLED: AtomicBool = AtomicBool::new(false);
16
17pub fn enable() {
19 ENABLED.store(true, std::sync::atomic::Ordering::Relaxed);
20}
21
22pub fn disable() {
24 ENABLED.store(false, std::sync::atomic::Ordering::Relaxed);
25}
26
27#[macro_export]
29macro_rules! warn_user {
30 ($($arg:tt)*) => {{
31 use $crate::anstream::eprintln;
32 use $crate::owo_colors::OwoColorize;
33
34 if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
35 let message = format!("{}", format_args!($($arg)*));
36 let formatted = message.bold();
37 eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold());
38 }
39 }};
40}
41
42pub static WARNINGS: LazyLock<Mutex<FxHashSet<String>>> = LazyLock::new(Mutex::default);
43
44#[macro_export]
47macro_rules! warn_user_once {
48 ($($arg:tt)*) => {{
49 use $crate::anstream::eprintln;
50 use $crate::owo_colors::OwoColorize;
51
52 if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
53 if let Ok(mut states) = $crate::WARNINGS.lock() {
54 let message = format!("{}", format_args!($($arg)*));
55 if states.insert(message.clone()) {
56 eprintln!("{}{} {}", "warning".yellow().bold(), ":".bold(), message.bold());
57 }
58 }
59 }
60 }};
61}
62
63pub fn write_error_chain(
87 err: &dyn Error,
88 mut stream: impl std::fmt::Write,
89 level: impl AsRef<str>,
90 color: impl DynColor + Copy,
91) -> std::fmt::Result {
92 writeln!(
93 &mut stream,
94 "{}{} {}",
95 level.as_ref().color(color).bold(),
96 ":".bold(),
97 err.to_string().trim()
98 )?;
99 for source in iter::successors(err.source(), |&err| err.source()) {
100 let msg = source.to_string();
101 let mut lines = msg.lines();
102 if let Some(first) = lines.next() {
103 let padding = " ";
104 let cause = "Caused by";
105 let child_padding = " ".repeat(padding.len() + cause.len() + 2);
106 writeln!(
107 &mut stream,
108 "{}{}: {}",
109 padding,
110 cause.color(color).bold(),
111 first.trim()
112 )?;
113 for line in lines {
114 let line = line.trim_end();
115 if line.is_empty() {
116 writeln!(&mut stream)?;
118 } else {
119 writeln!(&mut stream, "{}{}", child_padding, line.trim_end())?;
120 }
121 }
122 }
123 }
124 Ok(())
125}
126
127#[cfg(test)]
128mod tests {
129 use crate::write_error_chain;
130 use anyhow::anyhow;
131 use indoc::indoc;
132 use insta::assert_snapshot;
133 use owo_colors::AnsiColors;
134
135 #[test]
136 fn format_multiline_message() {
137 let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
138 Server says: This endpoint only support POST requests.
139
140 For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
141 let err = anyhow!("Caused By: HTTP Error 400")
142 .context(err_middle)
143 .context("Failed to download Python 3.12");
144
145 let mut rendered = String::new();
146 write_error_chain(err.as_ref(), &mut rendered, "error", AnsiColors::Red).unwrap();
147 let rendered = anstream::adapter::strip_str(&rendered);
148
149 assert_snapshot!(rendered, @r"
150 error: Failed to download Python 3.12
151 Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
152 Server says: This endpoint only support POST requests.
153
154 For downloads, please refer to https://example.com/download/python3.13.tar.zst
155 Caused by: Caused By: HTTP Error 400
156 ");
157 }
158}