use std::{
env,
panic::{self, UnwindSafe},
path::PathBuf,
process,
};
use color_eyre::{Report, Section, config::HookBuilder, owo_colors::style};
use futures_util::FutureExt;
use tokio::sync::mpsc;
#[derive(Debug)]
pub enum AppError {
UserFacing(UserFacingError),
Unexpected(Report),
}
pub type Result<T, E = AppError> = std::result::Result<T, E>;
pub async fn init<F>(log_path: Option<PathBuf>, fut: F) -> Result<(), Report>
where
F: Future<Output = Result<(), Report>> + UnwindSafe,
{
tracing::trace!("Initializing error handlers");
let panic_section = if let Some(log_path) = log_path {
format!(
"This is a bug. Consider reporting it at {}\nLogs can be found at {}",
env!("CARGO_PKG_REPOSITORY"),
log_path.display()
)
} else {
format!(
"This is a bug. Consider reporting it at {}\nLogs were not generated, consider enabling them on the \
config or running with INTELLI_LOG=debug.",
env!("CARGO_PKG_REPOSITORY")
)
};
let (panic_hook, eyre_hook) = HookBuilder::default()
.panic_section(panic_section.clone())
.display_env_section(false)
.display_location_section(true)
.capture_span_trace_by_default(true)
.into_hooks();
let (panic_tx, mut panic_rx) = mpsc::channel(1);
eyre_hook.install()?;
panic::set_hook(Box::new(move |panic_info| {
let panic_report = panic_hook.panic_report(panic_info).to_string();
tracing::error!("Error: {}", strip_ansi_escapes::strip_str(&panic_report));
if panic_tx.try_send(panic_report).is_err() {
tracing::error!("Error sending panic report",);
process::exit(2);
}
}));
tokio::select! {
biased;
panic_report = panic_rx.recv().fuse() => {
if let Some(report) = panic_report {
eprintln!("{report}");
} else {
eprintln!(
"{}\n\n{panic_section}",
style().bright_red().style("A panic occurred, but the detailed report could not be captured.")
);
tracing::error!("A panic occurred, but the detailed report could not be captured.");
}
process::exit(1);
}
res = Box::pin(fut).catch_unwind() => {
match res {
Ok(r) => r
.with_section(move || panic_section)
.inspect_err(|err| tracing::error!("Error: {}", strip_ansi_escapes::strip_str(format!("{err:?}")))),
Err(err) => {
if let Ok(report) = panic_rx.try_recv() {
eprintln!("{report}");
} else if let Some(err) = err.downcast_ref::<&str>() {
print_panic_msg(err, panic_section);
} else if let Some(err) = err.downcast_ref::<String>() {
print_panic_msg(err, panic_section);
} else {
eprintln!(
"{}\n\n{panic_section}",
style().bright_red().style("An unexpected panic happened")
);
tracing::error!("An unexpected panic happened");
}
process::exit(1);
}
}
}
}
}
fn print_panic_msg(err: impl AsRef<str>, panic_section: String) {
let err = err.as_ref();
eprintln!(
"{}\nMessage: {}\n\n{panic_section}",
style().bright_red().style("The application panicked (crashed)."),
style().blue().style(err)
);
tracing::error!("Panic: {err}");
}
#[derive(Debug, strum::Display)]
pub enum UserFacingError {
#[strum(to_string = "Operation cancelled by user")]
Cancelled,
#[strum(to_string = "Invalid regex pattern")]
InvalidRegex,
#[strum(to_string = "Invalid fuzzy search")]
InvalidFuzzy,
#[strum(to_string = "Command cannot be empty")]
EmptyCommand,
#[strum(to_string = "Command is already bookmarked")]
CommandAlreadyExists,
#[strum(to_string = "Value already exists")]
VariableValueAlreadyExists,
#[strum(to_string = "Variable completion already exists")]
CompletionAlreadyExists,
#[strum(to_string = "Completion command can contain only alphanumeric characters or hyphen")]
CompletionInvalidCommand,
#[strum(to_string = "Completion variable cannot be empty")]
CompletionEmptyVariable,
#[strum(to_string = "Completion variable can't contain pipe, colon or braces")]
CompletionInvalidVariable,
#[strum(to_string = "Completion provider cannot be empty")]
CompletionEmptySuggestionsProvider,
#[strum(to_string = "Invalid completion format: {0}")]
ImportCompletionInvalidFormat(String),
#[strum(to_string = "Import path must be a file; directories and symlinks are not supported")]
ImportLocationNotAFile,
#[strum(to_string = "File not found")]
ImportFileNotFound,
#[strum(to_string = "The path already exists and it's not a file")]
ExportLocationNotAFile,
#[strum(to_string = "Destination directory does not exist")]
ExportFileParentNotFound,
#[strum(to_string = "Cannot export to a gist revision, provide a gist without a revision")]
ExportGistLocationHasSha,
#[strum(to_string = "GitHub token required for Gist export, set GIST_TOKEN env var or update config")]
ExportGistMissingToken,
#[strum(to_string = "Cannot access the file, check {0} permissions")]
FileNotAccessible(&'static str),
#[strum(to_string = "broken pipe")]
FileBrokenPipe,
#[strum(to_string = "Invalid URL, please provide a valid HTTP/S address")]
HttpInvalidUrl,
#[strum(to_string = "HTTP request failed: {0}")]
HttpRequestFailed(String),
#[strum(to_string = "Gist ID is missing, provide it as an argument or in the config file")]
GistMissingId,
#[strum(to_string = "The provided gist is not valid, please provide a valid id or URL")]
GistInvalidLocation,
#[strum(to_string = "File not found within the specified Gist")]
GistFileNotFound,
#[strum(to_string = "Gist request failed: {0}")]
GistRequestFailed(String),
#[strum(to_string = "Could not determine home directory")]
HistoryHomeDirNotFound,
#[strum(to_string = "History file not found at: {0}")]
HistoryFileNotFound(String),
#[strum(to_string = "Nushell not found, make sure it is installed and in your PATH")]
HistoryNushellNotFound,
#[strum(to_string = "Error running nu, maybe it is an old version")]
HistoryNushellFailed,
#[strum(to_string = "Atuin not found, make sure it is installed and in your PATH")]
HistoryAtuinNotFound,
#[strum(to_string = "Error running atuin, maybe it is an old version")]
HistoryAtuinFailed,
#[strum(to_string = "AI feature is disabled, enable it in the config file to use this functionality")]
AiRequired,
#[strum(to_string = "A command must be provided")]
AiEmptyCommand,
#[strum(to_string = "API key in '{0}' env variable is missing, invalid, or lacks permissions")]
AiMissingOrInvalidApiKey(String),
#[strum(to_string = "Request to AI provider timed out")]
AiRequestTimeout,
#[strum(to_string = "AI provider responded with status 503 Service Unavailable")]
AiUnavailable,
#[strum(to_string = "AI request failed: {0}")]
AiRequestFailed(String),
#[strum(to_string = "AI request rate-limited, try again later")]
AiRateLimit,
#[strum(to_string = "Couldn't check for latest version: {0}")]
LatestVersionRequestFailed(String),
#[strum(to_string = "Couldn't fetch GitHub releases: {0}")]
ReleaseRequestFailed(String),
}
impl AppError {
pub fn into_report(self) -> Report {
match self {
AppError::UserFacing(err) => Report::msg(err),
AppError::Unexpected(report) => report,
}
}
}
impl From<UserFacingError> for AppError {
fn from(err: UserFacingError) -> Self {
Self::UserFacing(err)
}
}
impl<T: Into<Report>> From<T> for AppError {
fn from(err: T) -> Self {
Self::Unexpected(err.into())
}
}
#[macro_export]
macro_rules! trace_dbg {
(target: $target:expr, level: $level:expr, $ex:expr) => {
{
match $ex {
value => {
tracing::event!(target: $target, $level, ?value, stringify!($ex));
value
}
}
}
};
(level: $level:expr, $ex:expr) => {
$crate::trace_dbg!(target: module_path!(), level: $level, $ex)
};
(target: $target:expr, $ex:expr) => {
$crate::trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
};
($ex:expr) => {
$crate::trace_dbg!(level: tracing::Level::DEBUG, $ex)
};
}