Skip to main content

zellij_utils/
errors.rs

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