intelli_shell/
errors.rs

1use std::{
2    env,
3    panic::{self, UnwindSafe},
4    path::PathBuf,
5    process,
6};
7
8use color_eyre::{Report, Result, Section, config::HookBuilder, owo_colors::style};
9use futures_util::FutureExt;
10use tokio::sync::mpsc;
11
12/// Initializes error and panics handling
13pub async fn init<F>(log_path: Option<PathBuf>, fut: F) -> Result<()>
14where
15    F: Future<Output = Result<()>> + UnwindSafe,
16{
17    tracing::trace!("Initializing error handlers");
18    // Initialize hooks
19    let panic_section = if let Some(log_path) = log_path {
20        format!(
21            "This is a bug. Consider reporting it at {}\nLogs can be found at {}",
22            env!("CARGO_PKG_REPOSITORY"),
23            log_path.display()
24        )
25    } else {
26        format!(
27            "This is a bug. Consider reporting it at {}\nLogs were not generated, consider enabling them on the \
28             config or running with INTELLI_LOG=debug.",
29            env!("CARGO_PKG_REPOSITORY")
30        )
31    };
32    let (panic_hook, eyre_hook) = HookBuilder::default()
33        .panic_section(panic_section.clone())
34        .display_env_section(false)
35        .display_location_section(true)
36        .capture_span_trace_by_default(true)
37        .into_hooks();
38
39    // Initialize panic notifier
40    let (panic_tx, mut panic_rx) = mpsc::channel(1);
41
42    // Install both hooks
43    eyre_hook.install()?;
44    panic::set_hook(Box::new(move |panic_info| {
45        // At this point the TUI might still be in raw mode, so we can't print to stderr here
46        // Instead, we're sending the report through a channel, to be handled after the main future is dropped
47        let panic_report = panic_hook.panic_report(panic_info).to_string();
48        tracing::error!("Error: {}", strip_ansi_escapes::strip_str(&panic_report));
49        if panic_tx.try_send(panic_report).is_err() {
50            tracing::error!("Error sending panic report",);
51            process::exit(2);
52        }
53    }));
54
55    tokio::select! {
56        biased;
57        // Wait for a panic to be notified
58        panic_report = panic_rx.recv().fuse() => {
59            if let Some(report) = panic_report {
60                eprintln!("{report}");
61            } else {
62                eprintln!(
63                    "{}\n\n{panic_section}",
64                    style().bright_red().style("A panic occurred, but the detailed report could not be captured.")
65                );
66                tracing::error!("A panic occurred, but the detailed report could not be captured.");
67            }
68            // Exit with a non-zero status code
69            process::exit(1);
70        }
71        // Or for the main future to finish, catching unwinding panics
72        res = Box::pin(fut).catch_unwind() => {
73            match res {
74                Ok(r) => r
75                    .with_section(move || panic_section)
76                    .inspect_err(|err| tracing::error!("Error: {}", strip_ansi_escapes::strip_str(format!("{err:?}")))),
77                Err(err) => {
78                    if let Ok(report) = panic_rx.try_recv() {
79                        eprintln!("{report}");
80                    } else if let Some(err) = err.downcast_ref::<&str>() {
81                        print_panic_msg(err, panic_section);
82                    } else if let Some(err) = err.downcast_ref::<String>() {
83                        print_panic_msg(err, panic_section);
84                    } else {
85                        eprintln!(
86                            "{}\n\n{panic_section}",
87                            style().bright_red().style("An unexpected panic happened")
88                        );
89                        tracing::error!("An unexpected panic happened");
90                    }
91                    // Exit with a non-zero status code
92                    process::exit(1);
93                }
94            }
95        }
96    }
97}
98
99fn print_panic_msg(err: impl AsRef<str>, panic_section: String) {
100    let err = err.as_ref();
101    eprintln!(
102        "{}\nMessage: {}\n\n{panic_section}",
103        style().bright_red().style("The application panicked (crashed)."),
104        style().blue().style(err)
105    );
106    tracing::error!("Panic: {err}");
107}
108
109/// Error type for command searching operations
110#[derive(Debug)]
111pub enum SearchError {
112    /// The provided regex is not valid
113    InvalidRegex(regex::Error),
114    /// The provided fuzzy search hasn't any term
115    InvalidFuzzy,
116    /// An unexpected error occurred
117    Unexpected(Report),
118}
119
120/// Error type for add operations
121#[derive(Debug)]
122pub enum InsertError {
123    /// The entity content is not valid
124    Invalid(&'static str),
125    /// The entity already exists
126    AlreadyExists,
127    /// An unexpected error occurred
128    Unexpected(Report),
129}
130
131/// Error type for update operations
132#[derive(Debug)]
133pub enum UpdateError {
134    /// The entity content is not valid
135    Invalid(&'static str),
136    /// The entity already exists
137    AlreadyExists,
138    /// An unexpected error occurred
139    Unexpected(Report),
140}
141
142/// Error type for commands import/export
143#[derive(Debug)]
144pub enum ImportExportError {
145    /// The provided path points to a directory or symlink, not a file
146    NotAFile,
147    /// The file could not be found at the given path
148    FileNotFound,
149    /// The application lacks the necessary permissions to read or write the file
150    FileNotAccessible,
151    /// Content couldn't be written to the file: broken pipe
152    FileBrokenPipe,
153    /// The provided HTTP URL is malformed or invalid
154    HttpInvalidUrl,
155    /// The request to the HTTP URL failed
156    HttpRequestFailed(String),
157    /// A gist id was not provided via arguments or the configuration file
158    GistMissingId,
159    /// The provided gist location is malformed or invalid
160    GistInvalidLocation,
161    /// The provided gist location has a sha version, which is immutable
162    GistLocationHasSha,
163    /// The provided gist file was not found
164    GistFileNotFound,
165    /// A gh token is required to export commands to a gist
166    GistMissingToken,
167    /// The request to the gist API failed
168    GistRequestFailed(String),
169    /// An unexpected error occurred
170    Unexpected(Report),
171}
172
173impl UpdateError {
174    pub fn into_report(self) -> Report {
175        match self {
176            UpdateError::Invalid(msg) => Report::msg(msg),
177            UpdateError::AlreadyExists => Report::msg("Entity already exists"),
178            UpdateError::Unexpected(report) => report,
179        }
180    }
181}
182
183macro_rules! impl_from_report {
184    ($err:ty) => {
185        impl<T> From<T> for $err
186        where
187            T: Into<Report>,
188        {
189            fn from(err: T) -> Self {
190                Self::Unexpected(err.into())
191            }
192        }
193    };
194}
195impl_from_report!(SearchError);
196impl_from_report!(InsertError);
197impl_from_report!(UpdateError);
198impl_from_report!(ImportExportError);
199
200/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
201/// than printing to stdout.
202///
203/// By default, the verbosity level for the generated events is `DEBUG`, but
204/// this can be customized.
205#[macro_export]
206macro_rules! trace_dbg {
207    (target: $target:expr, level: $level:expr, $ex:expr) => {
208        {
209            match $ex {
210                value => {
211                    tracing::event!(target: $target, $level, ?value, stringify!($ex));
212                    value
213                }
214            }
215        }
216    };
217    (level: $level:expr, $ex:expr) => {
218        trace_dbg!(target: module_path!(), level: $level, $ex)
219    };
220    (target: $target:expr, $ex:expr) => {
221        trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
222    };
223    ($ex:expr) => {
224        trace_dbg!(level: tracing::Level::DEBUG, $ex)
225    };
226}