zellij_utils/
errors.rs

1//! Error context system based on a thread-local representation of the call stack, itself based on
2//! the instructions that are sent between threads.
3//!
4//! # Help wanted
5//!
6//! There is an ongoing endeavor to improve the state of error handling in zellij. Currently, many
7//! functions rely on [`unwrap`]ing [`Result`]s rather than returning and hence propagating
8//! potential errors. If you're interested in helping to add error handling to zellij, don't
9//! hesitate to get in touch with us. Additional information can be found in [the docs about error
10//! handling](https://github.com/zellij-org/zellij/tree/main/docs/ERROR_HANDLING.md).
11
12use anyhow::Context;
13use colored::*;
14use log::error;
15use serde::{Deserialize, Serialize};
16use std::fmt::{Display, Error, Formatter};
17use std::path::PathBuf;
18
19/// Re-exports of common error-handling code.
20pub mod prelude {
21    pub use super::FatalError;
22    pub use super::LoggableError;
23    #[cfg(not(target_family = "wasm"))]
24    pub use super::ToAnyhow;
25    pub use super::ZellijError;
26    pub use anyhow::anyhow;
27    pub use anyhow::bail;
28    pub use anyhow::Context;
29    pub use anyhow::Error as anyError;
30    pub use anyhow::Result;
31}
32
33pub trait ErrorInstruction {
34    fn error(err: String) -> Self;
35}
36
37/// Helper trait to easily log error types.
38///
39/// The `print_error` function takes a closure which takes a `&str` and fares with it as necessary
40/// to log the error to some usable location. For convenience, logging to stdout, stderr and
41/// `log::error!` is already implemented.
42///
43/// Note that the trait functions pass the error through unmodified, so they can be chained with
44/// the usual handling of [`std::result::Result`] types.
45pub trait LoggableError<T>: Sized {
46    /// Gives a formatted error message derived from `self` to the closure `fun` for
47    /// printing/logging as appropriate.
48    ///
49    /// # Examples
50    ///
51    /// ```should_panic
52    /// use anyhow;
53    /// use zellij_utils::errors::LoggableError;
54    ///
55    /// let my_err: anyhow::Result<&str> = Err(anyhow::anyhow!("Test error"));
56    /// my_err
57    ///     .print_error(|msg| println!("{msg}"))
58    ///     .unwrap();
59    /// ```
60    #[track_caller]
61    fn print_error<F: Fn(&str)>(self, fun: F) -> Self;
62
63    /// Convenienve function, calls `print_error` and logs the result as error.
64    ///
65    /// This is not a wrapper around `log::error!`, because the `log` crate uses a lot of compile
66    /// time macros from `std` to determine caller locations/module names etc. Since these are
67    /// resolved at compile time in the location they are written, they would always resolve to the
68    /// location in this function where `log::error!` is called, masking the real caller location.
69    /// Hence, we build the log message ourselves. This means that we lose the information about
70    /// the calling module (Because it can only be resolved at compile time), however the callers
71    /// file and line number are preserved.
72    #[track_caller]
73    fn to_log(self) -> Self {
74        let caller = std::panic::Location::caller();
75        self.print_error(|msg| {
76            // Build the log entry manually
77            // NOTE: The log entry has no module path associated with it. This is because `log`
78            // gets the module path from the `std::module_path!()` macro, which is replaced at
79            // compile time in the location it is written!
80            log::logger().log(
81                &log::Record::builder()
82                    .level(log::Level::Error)
83                    .args(format_args!("{}", msg))
84                    .file(Some(caller.file()))
85                    .line(Some(caller.line()))
86                    .module_path(None)
87                    .build(),
88            );
89        })
90    }
91
92    /// Convenienve function, calls `print_error` with the closure `|msg| eprintln!("{}", msg)`.
93    fn to_stderr(self) -> Self {
94        self.print_error(|msg| eprintln!("{}", msg))
95    }
96
97    /// Convenienve function, calls `print_error` with the closure `|msg| println!("{}", msg)`.
98    fn to_stdout(self) -> Self {
99        self.print_error(|msg| println!("{}", msg))
100    }
101}
102
103impl<T> LoggableError<T> for anyhow::Result<T> {
104    fn print_error<F: Fn(&str)>(self, fun: F) -> Self {
105        if let Err(ref err) = self {
106            fun(&format!("{:?}", err));
107        }
108        self
109    }
110}
111
112/// Special trait to mark fatal/non-fatal errors.
113///
114/// This works in tandem with `LoggableError` above and is meant to make reading code easier with
115/// regard to whether an error is fatal or not (i.e. can be ignored, or at least doesn't make the
116/// application crash).
117///
118/// This essentially degrades any `std::result::Result<(), _>` to a simple `()`.
119pub trait FatalError<T> {
120    /// Mark results as being non-fatal.
121    ///
122    /// If the result is an `Err` variant, this will [print the error to the log][`to_log`].
123    /// Discards the result type afterwards.
124    ///
125    /// [`to_log`]: LoggableError::to_log
126    #[track_caller]
127    fn non_fatal(self);
128
129    /// Mark results as being fatal.
130    ///
131    /// If the result is an `Err` variant, this will unwrap the error and panic the application.
132    /// If the result is an `Ok` variant, the inner value is unwrapped and returned instead.
133    ///
134    /// # Panics
135    ///
136    /// If the given result is an `Err` variant.
137    #[track_caller]
138    fn fatal(self) -> T;
139}
140
141/// Helper function to silence `#[warn(unused_must_use)]` cargo warnings. Used exclusively in
142/// `FatalError::non_fatal`!
143fn discard_result<T>(_arg: anyhow::Result<T>) {}
144
145impl<T> FatalError<T> for anyhow::Result<T> {
146    fn non_fatal(self) {
147        if self.is_err() {
148            discard_result(self.context("a non-fatal error occured").to_log());
149        }
150    }
151
152    fn fatal(self) -> T {
153        if let Ok(val) = self {
154            val
155        } else {
156            self.context("a fatal error occured")
157                .expect("Program terminates")
158        }
159    }
160}
161
162/// Different types of calls that form an [`ErrorContext`] call stack.
163///
164/// Complex variants store a variant of a related enum, whose variants can be built from
165/// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`],
166/// [`PtyInstruction`], [`ClientInstruction`], etc).
167#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, Debug)]
168pub enum ContextType {
169    /// A screen-related call.
170    Screen(ScreenContext),
171    /// A PTY-related call.
172    Pty(PtyContext),
173    /// A plugin-related call.
174    Plugin(PluginContext),
175    /// An app-related call.
176    Client(ClientContext),
177    /// A server-related call.
178    IPCServer(ServerContext),
179    StdinHandler,
180    AsyncTask,
181    PtyWrite(PtyWriteContext),
182    BackgroundJob(BackgroundJobContext),
183    /// An empty, placeholder call. This should be thought of as representing no call at all.
184    /// A call stack representation filled with these is the representation of an empty call stack.
185    Empty,
186}
187
188impl Display for ContextType {
189    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
190        if let Some((left, right)) = match *self {
191            ContextType::Screen(c) => Some(("screen_thread:", format!("{:?}", c))),
192            ContextType::Pty(c) => Some(("pty_thread:", format!("{:?}", c))),
193            ContextType::Plugin(c) => Some(("plugin_thread:", format!("{:?}", c))),
194            ContextType::Client(c) => Some(("main_thread:", format!("{:?}", c))),
195            ContextType::IPCServer(c) => Some(("ipc_server:", format!("{:?}", c))),
196            ContextType::StdinHandler => Some(("stdin_handler_thread:", "AcceptInput".to_string())),
197            ContextType::AsyncTask => Some(("stream_terminal_bytes:", "AsyncTask".to_string())),
198            ContextType::PtyWrite(c) => Some(("pty_writer_thread:", format!("{:?}", c))),
199            ContextType::BackgroundJob(c) => Some(("background_jobs_thread:", format!("{:?}", c))),
200            ContextType::Empty => None,
201        } {
202            write!(f, "{} {}", left.purple(), right.green())
203        } else {
204            write!(f, "")
205        }
206    }
207}
208
209// FIXME: Just deriving EnumDiscriminants from strum will remove the need for any of this!!!
210/// Stack call representations corresponding to the different types of [`ScreenInstruction`]s.
211#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
212pub enum ScreenContext {
213    HandlePtyBytes,
214    PluginBytes,
215    Render,
216    NewPane,
217    OpenInPlaceEditor,
218    ToggleFloatingPanes,
219    ShowFloatingPanes,
220    HideFloatingPanes,
221    TogglePaneEmbedOrFloating,
222    HorizontalSplit,
223    VerticalSplit,
224    WriteCharacter,
225    ResizeIncreaseAll,
226    ResizeIncreaseLeft,
227    ResizeIncreaseDown,
228    ResizeIncreaseUp,
229    ResizeIncreaseRight,
230    ResizeDecreaseAll,
231    ResizeDecreaseLeft,
232    ResizeDecreaseDown,
233    ResizeDecreaseUp,
234    ResizeDecreaseRight,
235    ResizeLeft,
236    ResizeRight,
237    ResizeDown,
238    ResizeUp,
239    ResizeIncrease,
240    ResizeDecrease,
241    SwitchFocus,
242    FocusNextPane,
243    FocusPreviousPane,
244    FocusPaneAt,
245    MoveFocusLeft,
246    MoveFocusLeftOrPreviousTab,
247    MoveFocusDown,
248    MoveFocusUp,
249    MoveFocusRight,
250    MoveFocusRightOrNextTab,
251    MovePane,
252    MovePaneBackwards,
253    MovePaneDown,
254    MovePaneUp,
255    MovePaneRight,
256    MovePaneLeft,
257    Exit,
258    ClearScreen,
259    DumpScreen,
260    DumpLayout,
261    EditScrollback,
262    ScrollUp,
263    ScrollUpAt,
264    ScrollDown,
265    ScrollDownAt,
266    ScrollToBottom,
267    ScrollToTop,
268    PageScrollUp,
269    PageScrollDown,
270    HalfPageScrollUp,
271    HalfPageScrollDown,
272    ClearScroll,
273    CloseFocusedPane,
274    ToggleActiveSyncTab,
275    ToggleActiveTerminalFullscreen,
276    TogglePaneFrames,
277    SetSelectable,
278    SetInvisibleBorders,
279    SetFixedHeight,
280    SetFixedWidth,
281    ClosePane,
282    HoldPane,
283    UpdatePaneName,
284    UndoRenamePane,
285    NewTab,
286    ApplyLayout,
287    SwitchTabNext,
288    SwitchTabPrev,
289    CloseTab,
290    GoToTab,
291    GoToTabName,
292    UpdateTabName,
293    UndoRenameTab,
294    MoveTabLeft,
295    MoveTabRight,
296    TerminalResize,
297    TerminalPixelDimensions,
298    TerminalBackgroundColor,
299    TerminalForegroundColor,
300    TerminalColorRegisters,
301    ChangeMode,
302    ChangeModeForAllClients,
303    LeftClick,
304    RightClick,
305    MiddleClick,
306    LeftMouseRelease,
307    RightMouseRelease,
308    MiddleMouseRelease,
309    MouseHoldLeft,
310    MouseHoldRight,
311    MouseHoldMiddle,
312    Copy,
313    ToggleTab,
314    AddClient,
315    RemoveClient,
316    AddOverlay,
317    RemoveOverlay,
318    ConfirmPrompt,
319    DenyPrompt,
320    UpdateSearch,
321    SearchDown,
322    SearchUp,
323    SearchToggleCaseSensitivity,
324    SearchToggleWholeWord,
325    SearchToggleWrap,
326    AddRedPaneFrameColorOverride,
327    ClearPaneFrameColorOverride,
328    PreviousSwapLayout,
329    NextSwapLayout,
330    QueryTabNames,
331    NewTiledPluginPane,
332    StartOrReloadPluginPane,
333    NewFloatingPluginPane,
334    AddPlugin,
335    UpdatePluginLoadingStage,
336    ProgressPluginLoadingOffset,
337    StartPluginLoadingIndication,
338    RequestStateUpdateForPlugins,
339    LaunchOrFocusPlugin,
340    LaunchPlugin,
341    SuppressPane,
342    FocusPaneWithId,
343    RenamePane,
344    RenameTab,
345    RequestPluginPermissions,
346    BreakPane,
347    BreakPaneRight,
348    BreakPaneLeft,
349    UpdateSessionInfos,
350    ReplacePane,
351    NewInPlacePluginPane,
352    DumpLayoutToHd,
353    RenameSession,
354    DumpLayoutToPlugin,
355    ListClientsMetadata,
356    Reconfigure,
357    RerunCommandPane,
358    ResizePaneWithId,
359    EditScrollbackForPaneWithId,
360    WriteToPaneId,
361    MovePaneWithPaneId,
362    MovePaneWithPaneIdInDirection,
363    ClearScreenForPaneId,
364    ScrollUpInPaneId,
365    ScrollDownInPaneId,
366    ScrollToTopInPaneId,
367    ScrollToBottomInPaneId,
368    PageScrollUpInPaneId,
369    PageScrollDownInPaneId,
370    TogglePaneIdFullscreen,
371    TogglePaneEmbedOrEjectForPaneId,
372    CloseTabWithIndex,
373    BreakPanesToNewTab,
374    BreakPanesToTabWithIndex,
375    ListClientsToPlugin,
376}
377
378/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
379#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
380pub enum PtyContext {
381    SpawnTerminal,
382    OpenInPlaceEditor,
383    SpawnTerminalVertically,
384    SpawnTerminalHorizontally,
385    UpdateActivePane,
386    GoToTab,
387    NewTab,
388    ClosePane,
389    CloseTab,
390    ReRunCommandInPane,
391    DropToShellInPane,
392    SpawnInPlaceTerminal,
393    DumpLayout,
394    LogLayoutToHd,
395    FillPluginCwd,
396    DumpLayoutToPlugin,
397    ListClientsMetadata,
398    Reconfigure,
399    ListClientsToPlugin,
400    Exit,
401}
402
403/// Stack call representations corresponding to the different types of [`PluginInstruction`]s.
404#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
405pub enum PluginContext {
406    Load,
407    LoadBackgroundPlugin,
408    Update,
409    Render,
410    Unload,
411    Reload,
412    ReloadPluginWithId,
413    Resize,
414    Exit,
415    AddClient,
416    RemoveClient,
417    NewTab,
418    ApplyCachedEvents,
419    ApplyCachedWorkerMessages,
420    PostMessageToPluginWorker,
421    PostMessageToPlugin,
422    PluginSubscribedToEvents,
423    PermissionRequestResult,
424    DumpLayout,
425    LogLayoutToHd,
426    CliPipe,
427    Message,
428    CachePluginEvents,
429    MessageFromPlugin,
430    UnblockCliPipes,
431    WatchFilesystem,
432    KeybindPipe,
433    DumpLayoutToPlugin,
434    ListClientsMetadata,
435    Reconfigure,
436    FailedToWriteConfigToDisk,
437    ListClientsToPlugin,
438}
439
440/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
441#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
442pub enum ClientContext {
443    Exit,
444    Error,
445    UnblockInputThread,
446    Render,
447    ServerError,
448    SwitchToMode,
449    Connected,
450    ActiveClients,
451    Log,
452    LogError,
453    OwnClientId,
454    StartedParsingStdinQuery,
455    DoneParsingStdinQuery,
456    SwitchSession,
457    SetSynchronisedOutput,
458    UnblockCliPipeInput,
459    CliPipeOutput,
460    QueryTerminalSize,
461    WriteConfigToDisk,
462}
463
464/// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
465#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
466pub enum ServerContext {
467    NewClient,
468    Render,
469    UnblockInputThread,
470    ClientExit,
471    RemoveClient,
472    Error,
473    KillSession,
474    DetachSession,
475    AttachClient,
476    ConnStatus,
477    ActiveClients,
478    Log,
479    LogError,
480    SwitchSession,
481    UnblockCliPipeInput,
482    CliPipeOutput,
483    AssociatePipeWithClient,
484    DisconnectAllClientsExcept,
485    ChangeMode,
486    ChangeModeForAllClients,
487    Reconfigure,
488    ConfigWrittenToDisk,
489    FailedToWriteConfigToDisk,
490    RebindKeys,
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
494pub enum PtyWriteContext {
495    Write,
496    ResizePty,
497    StartCachingResizes,
498    ApplyCachedResizes,
499    Exit,
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
503pub enum BackgroundJobContext {
504    DisplayPaneError,
505    AnimatePluginLoading,
506    StopPluginLoadingAnimation,
507    ReadAllSessionInfosOnMachine,
508    ReportSessionInfo,
509    ReportLayoutInfo,
510    RunCommand,
511    WebRequest,
512    ReportPluginList,
513    Exit,
514}
515
516use thiserror::Error;
517#[derive(Debug, Error)]
518pub enum ZellijError {
519    #[error("could not find command '{command}' for terminal {terminal_id}")]
520    CommandNotFound { terminal_id: u32, command: String },
521
522    #[error("could not determine default editor")]
523    NoEditorFound,
524
525    #[error("failed to allocate another terminal id")]
526    NoMoreTerminalIds,
527
528    #[error("failed to start PTY")]
529    FailedToStartPty,
530
531    #[error(
532        "This version of zellij was built to load the core plugins from
533the globally configured plugin directory. However, a plugin wasn't found:
534
535    Plugin name: '{plugin_path}'
536    Plugin directory: '{plugin_dir}'
537
538If you're a user:
539    Please report this error to the distributor of your current zellij version
540
541If you're a developer:
542    Either make sure to include the plugins with the application (See feature
543    'disable_automatic_asset_installation'), or make them available in the
544    plugin directory.
545
546Possible fix for your problem:
547    Run `zellij setup --dump-plugins`, and optionally point it to your
548    'DATA DIR', visible in e.g. the output of `zellij setup --check`. Without
549    further arguments, it will use the default 'DATA DIR'.
550"
551    )]
552    BuiltinPluginMissing {
553        plugin_path: PathBuf,
554        plugin_dir: PathBuf,
555        #[source]
556        source: anyhow::Error,
557    },
558
559    #[error(
560        "It seems you tried to load the following builtin plugin:
561
562    Plugin name: '{plugin_path}'
563
564This is not a builtin plugin known to this version of zellij. If you were using
565a custom layout, please refer to the layout documentation at:
566
567    https://zellij.dev/documentation/creating-a-layout.html#plugin
568
569If you think this is a bug and the plugin is indeed an internal plugin, please
570open an issue on GitHub:
571
572    https://github.com/zellij-org/zellij/issues
573"
574    )]
575    BuiltinPluginNonexistent {
576        plugin_path: PathBuf,
577        #[source]
578        source: anyhow::Error,
579    },
580
581    // this is a temporary hack until we're able to merge custom errors from within the various
582    // crates themselves without having to move their payload types here
583    #[error("Cannot resize fixed panes")]
584    CantResizeFixedPanes { pane_ids: Vec<(u32, bool)> }, // bool: 0 => terminal_pane, 1 =>
585    // plugin_pane
586    #[error("Pane size remains unchanged")]
587    PaneSizeUnchanged,
588
589    #[error("an error occured")]
590    GenericError { source: anyhow::Error },
591
592    #[error("Client {client_id} is too slow to handle incoming messages")]
593    ClientTooSlow { client_id: u16 },
594
595    #[error("The plugin does not exist")]
596    PluginDoesNotExist,
597}
598
599#[cfg(not(target_family = "wasm"))]
600pub use not_wasm::*;
601
602#[cfg(not(target_family = "wasm"))]
603mod not_wasm {
604    use super::*;
605    use crate::channels::{SenderWithContext, ASYNCOPENCALLS, OPENCALLS};
606    use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme, Report};
607    use std::panic::PanicInfo;
608    use thiserror::Error as ThisError;
609
610    /// The maximum amount of calls an [`ErrorContext`] will keep track
611    /// of in its stack representation. This is a per-thread maximum.
612    const MAX_THREAD_CALL_STACK: usize = 6;
613
614    #[derive(Debug, ThisError, Diagnostic)]
615    #[error("{0}{}", self.show_backtrace())]
616    #[diagnostic(help("{}", self.show_help()))]
617    struct Panic(String);
618
619    impl Panic {
620        // We already capture a backtrace with `anyhow` using the `backtrace` crate in the background.
621        // The advantage is that this is the backtrace of the real errors source (i.e. where we first
622        // encountered the error and turned it into an `anyhow::Error`), whereas the backtrace recorded
623        // here is the backtrace leading to the call to any `panic`ing function. Since now we propagate
624        // errors up before `unwrap`ing them (e.g. in `zellij_server::screen::screen_thread_main`), the
625        // former is what we really want to diagnose.
626        // We still keep the second one around just in case the first backtrace isn't meaningful or
627        // non-existent in the first place (Which really shouldn't happen, but you never know).
628        fn show_backtrace(&self) -> String {
629            if let Ok(var) = std::env::var("RUST_BACKTRACE") {
630                if !var.is_empty() && var != "0" {
631                    return format!("\n\nPanic backtrace:\n{:?}", backtrace::Backtrace::new());
632                }
633            }
634            "".into()
635        }
636
637        fn show_help(&self) -> String {
638            format!(
639                "If you are seeing this message, it means that something went wrong.
640
641-> To get additional information, check the log at: {}
642-> To see a backtrace next time, reproduce the error with: RUST_BACKTRACE=1 zellij [...]
643-> To help us fix this, please open an issue: https://github.com/zellij-org/zellij/issues
644
645",
646                crate::consts::ZELLIJ_TMP_LOG_FILE.display().to_string()
647            )
648        }
649    }
650
651    /// Custom panic handler/hook. Prints the [`ErrorContext`].
652    pub fn handle_panic<T>(info: &PanicInfo<'_>, sender: &SenderWithContext<T>)
653    where
654        T: ErrorInstruction + Clone,
655    {
656        use std::{process, thread};
657        let thread = thread::current();
658        let thread = thread.name().unwrap_or("unnamed");
659
660        let msg = match info.payload().downcast_ref::<&'static str>() {
661            Some(s) => Some(*s),
662            None => info.payload().downcast_ref::<String>().map(|s| &**s),
663        }
664        .unwrap_or("An unexpected error occurred!");
665
666        let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
667
668        let mut report: Report = Panic(format!("\u{1b}[0;31m{}\u{1b}[0;0m", msg)).into();
669
670        let mut location_string = String::new();
671        if let Some(location) = info.location() {
672            location_string = format!(
673                "At {}:{}:{}",
674                location.file(),
675                location.line(),
676                location.column()
677            );
678            report = report.wrap_err(location_string.clone());
679        }
680
681        if !err_ctx.is_empty() {
682            report = report.wrap_err(format!("{}", err_ctx));
683        }
684
685        report = report.wrap_err(format!(
686            "Thread '\u{1b}[0;31m{}\u{1b}[0;0m' panicked.",
687            thread
688        ));
689
690        error!(
691            "{}",
692            format!(
693                "Panic occured:
694             thread: {}
695             location: {}
696             message: {}",
697                thread, location_string, msg
698            )
699        );
700
701        if thread == "main" {
702            // here we only show the first line because the backtrace is not readable otherwise
703            // a better solution would be to escape raw mode before we do this, but it's not trivial
704            // to get os_input here
705            println!("\u{1b}[2J{}", fmt_report(report));
706            process::exit(1);
707        } else {
708            let _ = sender.send(T::error(fmt_report(report)));
709        }
710    }
711
712    pub fn get_current_ctx() -> ErrorContext {
713        ASYNCOPENCALLS
714            .try_with(|ctx| *ctx.borrow())
715            .unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow()))
716    }
717
718    fn fmt_report(diag: Report) -> String {
719        let mut out = String::new();
720        GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
721            .render_report(&mut out, diag.as_ref())
722            .unwrap();
723        out
724    }
725
726    /// A representation of the call stack.
727    #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
728    pub struct ErrorContext {
729        calls: [ContextType; MAX_THREAD_CALL_STACK],
730    }
731
732    impl ErrorContext {
733        /// Returns a new, blank [`ErrorContext`] containing only [`Empty`](ContextType::Empty)
734        /// calls.
735        pub fn new() -> Self {
736            Self {
737                calls: [ContextType::Empty; MAX_THREAD_CALL_STACK],
738            }
739        }
740
741        /// Returns `true` if the calls has all [`Empty`](ContextType::Empty) calls.
742        pub fn is_empty(&self) -> bool {
743            self.calls.iter().all(|c| c == &ContextType::Empty)
744        }
745
746        /// Adds a call to this [`ErrorContext`]'s call stack representation.
747        pub fn add_call(&mut self, call: ContextType) {
748            for ctx in &mut self.calls {
749                if let ContextType::Empty = ctx {
750                    *ctx = call;
751                    break;
752                }
753            }
754            self.update_thread_ctx()
755        }
756
757        /// Updates the thread local [`ErrorContext`].
758        pub fn update_thread_ctx(&self) {
759            ASYNCOPENCALLS
760                .try_with(|ctx| *ctx.borrow_mut() = *self)
761                .unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow_mut() = *self));
762        }
763    }
764
765    impl Default for ErrorContext {
766        fn default() -> Self {
767            Self::new()
768        }
769    }
770
771    impl Display for ErrorContext {
772        fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
773            writeln!(f, "Originating Thread(s)")?;
774            for (index, ctx) in self.calls.iter().enumerate() {
775                if *ctx == ContextType::Empty {
776                    break;
777                }
778                writeln!(f, "\t\u{1b}[0;0m{}. {}", index + 1, ctx)?;
779            }
780            Ok(())
781        }
782    }
783
784    /// Helper trait to convert error types that don't satisfy `anyhow`s trait requirements to
785    /// anyhow errors.
786    pub trait ToAnyhow<U> {
787        fn to_anyhow(self) -> crate::anyhow::Result<U>;
788    }
789
790    /// `SendError` doesn't satisfy `anyhow`s trait requirements due to `T` possibly being a
791    /// `PluginInstruction` type, which wraps an `mpsc::Send` and isn't `Sync`. Due to this, in turn,
792    /// the whole error type isn't `Sync` and doesn't work with `anyhow` (or pretty much any other
793    /// error handling crate).
794    ///
795    /// Takes the `SendError` and creates an `anyhow` error type with the message that was sent
796    /// (formatted as string), attaching the [`ErrorContext`] as anyhow context to it.
797    impl<T: std::fmt::Debug, U> ToAnyhow<U>
798        for Result<U, crate::channels::SendError<(T, ErrorContext)>>
799    {
800        fn to_anyhow(self) -> crate::anyhow::Result<U> {
801            match self {
802                Ok(val) => crate::anyhow::Ok(val),
803                Err(e) => {
804                    let (msg, context) = e.into_inner();
805                    if *crate::consts::DEBUG_MODE.get().unwrap_or(&true) {
806                        Err(crate::anyhow::anyhow!(
807                            "failed to send message to channel: {:#?}",
808                            msg
809                        ))
810                        .with_context(|| context.to_string())
811                    } else {
812                        Err(crate::anyhow::anyhow!("failed to send message to channel"))
813                            .with_context(|| context.to_string())
814                    }
815                },
816            }
817        }
818    }
819
820    impl<U> ToAnyhow<U> for Result<U, std::sync::PoisonError<U>> {
821        fn to_anyhow(self) -> crate::anyhow::Result<U> {
822            match self {
823                Ok(val) => crate::anyhow::Ok(val),
824                Err(e) => {
825                    if *crate::consts::DEBUG_MODE.get().unwrap_or(&true) {
826                        Err(crate::anyhow::anyhow!(
827                            "cannot acquire poisoned lock for {e:#?}"
828                        ))
829                    } else {
830                        Err(crate::anyhow::anyhow!("cannot acquire poisoned lock"))
831                    }
832                },
833            }
834        }
835    }
836}