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