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    ChangeMode,
314    ChangeModeForAllClients,
315    LeftClick,
316    RightClick,
317    MiddleClick,
318    LeftMouseRelease,
319    RightMouseRelease,
320    MiddleMouseRelease,
321    MouseEvent,
322    Copy,
323    ToggleTab,
324    AddClient,
325    RemoveClient,
326    UpdateSearch,
327    SearchDown,
328    SearchUp,
329    SearchToggleCaseSensitivity,
330    SearchToggleWholeWord,
331    SearchToggleWrap,
332    AddRedPaneFrameColorOverride,
333    ClearPaneFrameColorOverride,
334    SetTabBellFlash,
335    PreviousSwapLayout,
336    NextSwapLayout,
337    OverrideLayout,
338    OverrideLayoutComplete,
339    QueryTabNames,
340    NewTiledPluginPane,
341    StartOrReloadPluginPane,
342    NewFloatingPluginPane,
343    AddPlugin,
344    UpdatePluginLoadingStage,
345    ProgressPluginLoadingOffset,
346    StartPluginLoadingIndication,
347    RequestStateUpdateForPlugins,
348    LaunchOrFocusPlugin,
349    LaunchPlugin,
350    SuppressPane,
351    UnsuppressPane,
352    UnsuppressOrExpandPane,
353    FocusPaneWithId,
354    RenamePane,
355    RenameActivePane,
356    RenameTab,
357    RequestPluginPermissions,
358    BreakPane,
359    BreakPaneRight,
360    BreakPaneLeft,
361    UpdateSessionInfos,
362    UpdateAvailableLayouts,
363    ReplacePane,
364    NewInPlacePluginPane,
365    SerializeLayoutForResurrection,
366    RenameSession,
367    DumpLayoutToPlugin,
368    GetFocusedPaneInfo,
369    GetPaneInfo,
370    GetTabInfo,
371    ListClientsMetadata,
372    ListPanes,
373    ListTabs,
374    GetCurrentTabInfo,
375    Reconfigure,
376    RerunCommandPane,
377    ResizePaneWithId,
378    EditScrollbackForPaneWithId,
379    WriteToPaneId,
380    Paste,
381    SetPaneColor,
382    WriteKeyToPaneId,
383    CopyTextToClipboard,
384    MovePaneWithPaneId,
385    MovePaneWithPaneIdInDirection,
386    ClearScreenForPaneId,
387    ScrollUpInPaneId,
388    ScrollDownInPaneId,
389    ScrollToTopInPaneId,
390    ScrollToBottomInPaneId,
391    PageScrollUpInPaneId,
392    PageScrollDownInPaneId,
393    TogglePaneIdFullscreen,
394    TogglePaneEmbedOrEjectForPaneId,
395    CloseTabWithIndex,
396    BreakPanesToNewTab,
397    BreakPanesToTabWithIndex,
398    ListClientsToPlugin,
399    TogglePanePinned,
400    SetFloatingPanePinned,
401    StackPanes,
402    ChangeFloatingPanesCoordinates,
403    TogglePaneBorderless,
404    SetPaneBorderless,
405    AddHighlightPaneFrameColorOverride,
406    GroupAndUngroupPanes,
407    HighlightAndUnhighlightPanes,
408    FloatMultiplePanes,
409    EmbedMultiplePanes,
410    TogglePaneInGroup,
411    ToggleGroupMarking,
412    SessionSharingStatusChange,
413    SetMouseSelectionSupport,
414    InterceptKeyPresses,
415    ClearKeyPressesIntercepts,
416    ReplacePaneWithExistingPane,
417    AddWatcherClient,
418    RemoveWatcherClient,
419    SetFollowedClient,
420    WatcherTerminalResize,
421    ClearMouseHelpText,
422    SetPluginRegexHighlights,
423    ClearPluginHighlights,
424    DesktopNotificationResponse,
425    SubscribeToPaneRenders,
426    NotifyPaneClosedToSubscribers,
427    // Pane-targeting CLI variants
428    ScrollUpWithPaneId,
429    ScrollDownWithPaneId,
430    ScrollToTopWithPaneId,
431    ScrollToBottomWithPaneId,
432    PageScrollUpWithPaneId,
433    PageScrollDownWithPaneId,
434    HalfPageScrollUpWithPaneId,
435    HalfPageScrollDownWithPaneId,
436    ResizeWithPaneId,
437    MovePaneWithPaneIdCli,
438    MovePaneBackwardsWithPaneId,
439    ClearScreenWithPaneId,
440    EditScrollbackWithPaneId,
441    ToggleFullscreenWithPaneId,
442    TogglePaneEmbedOrFloatingWithPaneId,
443    CloseFocusWithPaneId,
444    RenamePaneWithPaneId,
445    UndoRenamePaneWithPaneId,
446    TogglePanePinnedWithPaneId,
447    // Tab-targeting CLI variants
448    UndoRenameTabWithTabId,
449    ToggleActiveSyncTabWithTabId,
450    ToggleFloatingPanesWithTabId,
451    PreviousSwapLayoutWithTabId,
452    NextSwapLayoutWithTabId,
453    MoveTabWithTabId,
454    PluginSubscribedToAnsiPaneContents,
455    UpdateBackgroundPluginSubscriptions,
456    BroadcastModeUpdate,
457}
458
459/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
460#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
461pub enum PtyContext {
462    SpawnTerminal,
463    OpenInPlaceEditor,
464    SpawnTerminalVertically,
465    SpawnTerminalHorizontally,
466    UpdateActivePane,
467    GoToTab,
468    NewTab,
469    OverrideLayout,
470    ClosePane,
471    CloseTab,
472    ReRunCommandInPane,
473    DropToShellInPane,
474    SpawnInPlaceTerminal,
475    DumpLayout,
476    LogLayoutToHd,
477    SaveSessionToDisk,
478    FillPluginCwd,
479    DumpLayoutToPlugin,
480    ListClientsMetadata,
481    Reconfigure,
482    ListClientsToPlugin,
483    ReportPluginCwd,
484    SendSigintToPaneId,
485    SendSigkillToPaneId,
486    GetPanePid,
487    GetPaneRunningCommand,
488    GetPaneCwd,
489    UpdateAndReportCwds,
490    Exit,
491}
492
493/// Stack call representations corresponding to the different types of [`PluginInstruction`]s.
494#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
495pub enum PluginContext {
496    Load,
497    LoadBackgroundPlugin,
498    Update,
499    Render,
500    Unload,
501    Reload,
502    ReloadPluginWithId,
503    Resize,
504    Exit,
505    AddClient,
506    RemoveClient,
507    NewTab,
508    OverrideLayout,
509    ApplyCachedEvents,
510    ApplyCachedWorkerMessages,
511    PostMessageToPluginWorker,
512    PostMessageToPlugin,
513    PluginSubscribedToEvents,
514    PermissionRequestResult,
515    DumpLayout,
516    LogLayoutToHd,
517    CliPipe,
518    Message,
519    CachePluginEvents,
520    MessageFromPlugin,
521    UnblockCliPipes,
522    WatchFilesystem,
523    KeybindPipe,
524    DumpLayoutToPlugin,
525    ListClientsMetadata,
526    Reconfigure,
527    FailedToWriteConfigToDisk,
528    ListClientsToPlugin,
529    ChangePluginHostDir,
530    WebServerStarted,
531    FailedToStartWebServer,
532    PaneRenderReport,
533    UserInput,
534    LayoutListUpdate,
535    RequestStateUpdateForPlugin,
536    UpdateSessionSaveTime,
537    GetLastSessionSaveTime,
538    DetectPluginConfigChanges,
539    HighlightClicked,
540}
541
542/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
543#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
544pub enum ClientContext {
545    Exit,
546    Error,
547    UnblockInputThread,
548    Render,
549    ServerError,
550    SwitchToMode,
551    Connected,
552    Log,
553    LogError,
554    OwnClientId,
555    StartedParsingStdinQuery,
556    DoneParsingStdinQuery,
557    SwitchSession,
558    SetSynchronisedOutput,
559    UnblockCliPipeInput,
560    CliPipeOutput,
561    QueryTerminalSize,
562    WriteConfigToDisk,
563    StartWebServer,
564    RenamedSession,
565    ConfigFileUpdated,
566}
567
568/// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
569#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
570pub enum ServerContext {
571    NewClient,
572    Render,
573    UnblockInputThread,
574    ClientExit,
575    RemoveClient,
576    Error,
577    KillSession,
578    DetachSession,
579    AttachClient,
580    ConnStatus,
581    Log,
582    LogError,
583    SwitchSession,
584    UnblockCliPipeInput,
585    CliPipeOutput,
586    AssociatePipeWithClient,
587    DisconnectAllClientsExcept,
588    ChangeMode,
589    ChangeModeForAllClients,
590    Reconfigure,
591    ConfigWrittenToDisk,
592    FailedToWriteConfigToDisk,
593    RebindKeys,
594    StartWebServer,
595    ShareCurrentSession,
596    StopSharingCurrentSession,
597    WebServerStarted,
598    FailedToStartWebServer,
599    SendWebClientsForbidden,
600    ClearMouseHelpText,
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
604pub enum PtyWriteContext {
605    Write,
606    ResizePty,
607    StartCachingResizes,
608    ApplyCachedResizes,
609    Exit,
610}
611
612#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
613pub enum BackgroundJobContext {
614    DisplayPaneError,
615    AnimatePluginLoading,
616    StopPluginLoadingAnimation,
617    ReadAllSessionInfosOnMachine,
618    ReportSessionInfo,
619    ReportLayoutInfo,
620    RunCommand,
621    WebRequest,
622    ReportPluginList,
623    ListWebSessions,
624    RenderToClients,
625    HighlightPanesWithMessage,
626    QueryZellijWebServerStatus,
627    ClearHelpText,
628    FlashPaneBell,
629    StopFlashPaneBell,
630    FlashTabBell,
631    StopFlashTabBell,
632    Exit,
633}
634
635use thiserror::Error;
636#[derive(Debug, Error)]
637pub enum ZellijError {
638    #[error("could not find command '{command}' for terminal {terminal_id}")]
639    CommandNotFound { terminal_id: u32, command: String },
640
641    #[error("could not determine default editor")]
642    NoEditorFound,
643
644    #[error("failed to allocate another terminal id")]
645    NoMoreTerminalIds,
646
647    #[error("failed to start PTY")]
648    FailedToStartPty,
649
650    #[error(
651        "This version of zellij was built to load the core plugins from
652the globally configured plugin directory. However, a plugin wasn't found:
653
654    Plugin name: '{plugin_path}'
655    Plugin directory: '{plugin_dir}'
656
657If you're a user:
658    Please report this error to the distributor of your current zellij version
659
660If you're a developer:
661    Either make sure to include the plugins with the application (See feature
662    'disable_automatic_asset_installation'), or make them available in the
663    plugin directory.
664
665Possible fix for your problem:
666    Run `zellij setup --dump-plugins`, and optionally point it to your
667    'DATA DIR', visible in e.g. the output of `zellij setup --check`. Without
668    further arguments, it will use the default 'DATA DIR'.
669"
670    )]
671    BuiltinPluginMissing {
672        plugin_path: PathBuf,
673        plugin_dir: PathBuf,
674        #[source]
675        source: anyhow::Error,
676    },
677
678    #[error(
679        "It seems you tried to load the following builtin plugin:
680
681    Plugin name: '{plugin_path}'
682
683This is not a builtin plugin known to this version of zellij. If you were using
684a custom layout, please refer to the layout documentation at:
685
686    https://zellij.dev/documentation/creating-a-layout.html#plugin
687
688If you think this is a bug and the plugin is indeed an internal plugin, please
689open an issue on GitHub:
690
691    https://github.com/zellij-org/zellij/issues
692"
693    )]
694    BuiltinPluginNonexistent {
695        plugin_path: PathBuf,
696        #[source]
697        source: anyhow::Error,
698    },
699
700    // this is a temporary hack until we're able to merge custom errors from within the various
701    // crates themselves without having to move their payload types here
702    #[error("Cannot resize fixed panes")]
703    CantResizeFixedPanes { pane_ids: Vec<(u32, bool)> }, // bool: 0 => terminal_pane, 1 =>
704    // plugin_pane
705    #[error("Pane size remains unchanged")]
706    PaneSizeUnchanged,
707
708    #[error("an error occured")]
709    GenericError { source: anyhow::Error },
710
711    #[error("Client {client_id} is too slow to handle incoming messages")]
712    ClientTooSlow { client_id: u16 },
713
714    #[error("The plugin does not exist")]
715    PluginDoesNotExist,
716
717    #[error("Ran out of room for spans")]
718    RanOutOfRoomForSpans,
719}
720
721#[cfg(not(target_family = "wasm"))]
722pub use not_wasm::*;
723
724#[cfg(not(target_family = "wasm"))]
725mod not_wasm {
726    use super::*;
727    use crate::channels::{SenderWithContext, ASYNCOPENCALLS, OPENCALLS};
728    use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme, Report};
729    use std::panic::PanicHookInfo;
730    use thiserror::Error as ThisError;
731
732    /// The maximum amount of calls an [`ErrorContext`] will keep track
733    /// of in its stack representation. This is a per-thread maximum.
734    const MAX_THREAD_CALL_STACK: usize = 6;
735
736    #[derive(Debug, ThisError, Diagnostic)]
737    #[error("{0}{}", self.show_backtrace())]
738    #[diagnostic(help("{}", self.show_help()))]
739    struct Panic(String);
740
741    impl Panic {
742        // We already capture a backtrace with `anyhow` using the `backtrace` crate in the background.
743        // The advantage is that this is the backtrace of the real errors source (i.e. where we first
744        // encountered the error and turned it into an `anyhow::Error`), whereas the backtrace recorded
745        // here is the backtrace leading to the call to any `panic`ing function. Since now we propagate
746        // errors up before `unwrap`ing them (e.g. in `zellij_server::screen::screen_thread_main`), the
747        // former is what we really want to diagnose.
748        // We still keep the second one around just in case the first backtrace isn't meaningful or
749        // non-existent in the first place (Which really shouldn't happen, but you never know).
750        fn show_backtrace(&self) -> String {
751            if let Ok(var) = std::env::var("RUST_BACKTRACE") {
752                if !var.is_empty() && var != "0" {
753                    return format!("\n\nPanic backtrace:\n{:?}", backtrace::Backtrace::new());
754                }
755            }
756            "".into()
757        }
758
759        fn show_help(&self) -> String {
760            format!(
761                "If you are seeing this message, it means that something went wrong.
762
763-> To get additional information, check the log at: {}
764-> To see a backtrace next time, reproduce the error with: RUST_BACKTRACE=1 zellij [...]
765-> To help us fix this, please open an issue: https://github.com/zellij-org/zellij/issues
766
767",
768                crate::consts::ZELLIJ_TMP_LOG_FILE.display().to_string()
769            )
770        }
771    }
772
773    /// Custom panic handler/hook. Prints the [`ErrorContext`].
774    pub fn handle_panic<T>(info: &PanicHookInfo<'_>, sender: Option<&SenderWithContext<T>>)
775    where
776        T: ErrorInstruction + Clone,
777    {
778        use std::{process, thread};
779        let thread = thread::current();
780        let thread = thread.name().unwrap_or("unnamed");
781
782        let msg = match info.payload().downcast_ref::<&'static str>() {
783            Some(s) => Some(*s),
784            None => info.payload().downcast_ref::<String>().map(|s| &**s),
785        }
786        .unwrap_or("An unexpected error occurred!");
787
788        let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
789
790        let mut report: Report = Panic(format!("\u{1b}[0;31m{}\u{1b}[0;0m", msg)).into();
791
792        let mut location_string = String::new();
793        if let Some(location) = info.location() {
794            location_string = format!(
795                "At {}:{}:{}",
796                location.file(),
797                location.line(),
798                location.column()
799            );
800            report = report.wrap_err(location_string.clone());
801        }
802
803        if !err_ctx.is_empty() {
804            report = report.wrap_err(format!("{}", err_ctx));
805        }
806
807        report = report.wrap_err(format!(
808            "Thread '\u{1b}[0;31m{}\u{1b}[0;0m' panicked.",
809            thread
810        ));
811
812        error!(
813            "{}",
814            format!(
815                "Panic occured:
816             thread: {}
817             location: {}
818             message: {}",
819                thread, location_string, msg
820            )
821        );
822
823        if thread == "main" || sender.is_none() {
824            // here we only show the first line because the backtrace is not readable otherwise
825            // a better solution would be to escape raw mode before we do this, but it's not trivial
826            // to get os_input here
827            println!("\u{1b}[2J{}", fmt_report(report));
828            process::exit(1);
829        } else {
830            let _ = sender.unwrap().send(T::error(fmt_report(report)));
831        }
832    }
833
834    pub fn get_current_ctx() -> ErrorContext {
835        ASYNCOPENCALLS
836            .try_with(|ctx| *ctx.borrow())
837            .unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow()))
838    }
839
840    fn fmt_report(diag: Report) -> String {
841        let mut out = String::new();
842        GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
843            .render_report(&mut out, diag.as_ref())
844            .unwrap();
845        out
846    }
847
848    /// A representation of the call stack.
849    #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
850    pub struct ErrorContext {
851        calls: [ContextType; MAX_THREAD_CALL_STACK],
852    }
853
854    impl ErrorContext {
855        /// Returns a new, blank [`ErrorContext`] containing only [`Empty`](ContextType::Empty)
856        /// calls.
857        pub fn new() -> Self {
858            Self {
859                calls: [ContextType::Empty; MAX_THREAD_CALL_STACK],
860            }
861        }
862
863        /// Returns `true` if the calls has all [`Empty`](ContextType::Empty) calls.
864        pub fn is_empty(&self) -> bool {
865            self.calls.iter().all(|c| c == &ContextType::Empty)
866        }
867
868        /// Adds a call to this [`ErrorContext`]'s call stack representation.
869        pub fn add_call(&mut self, call: ContextType) {
870            for ctx in &mut self.calls {
871                if let ContextType::Empty = ctx {
872                    *ctx = call;
873                    break;
874                }
875            }
876            self.update_thread_ctx()
877        }
878
879        /// Updates the thread local [`ErrorContext`].
880        pub fn update_thread_ctx(&self) {
881            ASYNCOPENCALLS
882                .try_with(|ctx| *ctx.borrow_mut() = *self)
883                .unwrap_or_else(|_| OPENCALLS.with(|ctx| *ctx.borrow_mut() = *self));
884        }
885    }
886
887    impl Default for ErrorContext {
888        fn default() -> Self {
889            Self::new()
890        }
891    }
892
893    impl Display for ErrorContext {
894        fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
895            writeln!(f, "Originating Thread(s)")?;
896            for (index, ctx) in self.calls.iter().enumerate() {
897                if *ctx == ContextType::Empty {
898                    break;
899                }
900                writeln!(f, "\t\u{1b}[0;0m{}. {}", index + 1, ctx)?;
901            }
902            Ok(())
903        }
904    }
905
906    /// Helper trait to convert error types that don't satisfy `anyhow`s trait requirements to
907    /// anyhow errors.
908    pub trait ToAnyhow<U> {
909        fn to_anyhow(self) -> anyhow::Result<U>;
910    }
911
912    /// `SendError` doesn't satisfy `anyhow`s trait requirements due to `T` possibly being a
913    /// `PluginInstruction` type, which wraps an `mpsc::Send` and isn't `Sync`. Due to this, in turn,
914    /// the whole error type isn't `Sync` and doesn't work with `anyhow` (or pretty much any other
915    /// error handling crate).
916    ///
917    /// Takes the `SendError` and creates an `anyhow` error type with the message that was sent
918    /// (formatted as string), attaching the [`ErrorContext`] as anyhow context to it.
919    impl<T: std::fmt::Debug, U> ToAnyhow<U>
920        for Result<U, crate::channels::SendError<(T, ErrorContext)>>
921    {
922        fn to_anyhow(self) -> anyhow::Result<U> {
923            match self {
924                Ok(val) => anyhow::Ok(val),
925                Err(e) => {
926                    let (msg, context) = e.into_inner();
927                    if *crate::consts::DEBUG_MODE.get().unwrap_or(&true) {
928                        Err(anyhow::anyhow!(
929                            "failed to send message to channel: {:#?}",
930                            msg
931                        ))
932                        .with_context(|| context.to_string())
933                    } else {
934                        Err(anyhow::anyhow!("failed to send message to channel"))
935                            .with_context(|| context.to_string())
936                    }
937                },
938            }
939        }
940    }
941
942    impl<U> ToAnyhow<U> for Result<U, std::sync::PoisonError<U>> {
943        fn to_anyhow(self) -> anyhow::Result<U> {
944            match self {
945                Ok(val) => anyhow::Ok(val),
946                Err(e) => {
947                    if *crate::consts::DEBUG_MODE.get().unwrap_or(&true) {
948                        Err(anyhow::anyhow!("cannot acquire poisoned lock for {e:#?}"))
949                    } else {
950                        Err(anyhow::anyhow!("cannot acquire poisoned lock"))
951                    }
952                },
953            }
954        }
955    }
956}