Repository: zng-ui/zng (language: rust)

Issue:
Improve debug crash handler
Add `CrashPanic::backtrace_trim` that removes unnecessary frames , there are some crates that do this, but they do not work from a string (Implemented in #142).

Implement some minidump analysis, at least to show the crash code file and line.

Save scroll position and selection for each panel. (Implemented in #141).


Discussion / hints:

Fix patch:
diff --git a/crates/zng-app/Cargo.toml b/crates/zng-app/Cargo.toml
index 63f25db24..15c371aa6 100644
--- a/crates/zng-app/Cargo.toml
+++ b/crates/zng-app/Cargo.toml
@@ -57,7 +57,7 @@ trace_wgt_item = []
 
 # Allow app-process crash handler.
 #
-# Note that `crash_handler(..)` must be called to enable.
+# Note that `crash_handler::init(_)` must be called to enable.
 crash_handler = ["dep:serde_json", "dep:breakpad-handler", "dep:minidump"]
 
 # Enables IPC tasks and pre-build views and connecting to views running in another process.
diff --git a/crates/zng-app/README.md b/crates/zng-app/README.md
index d21ff2752..f23be800a 100644
--- a/crates/zng-app/README.md
+++ b/crates/zng-app/README.md
@@ -52,7 +52,7 @@ Note that this can cause very large trace files and bad performance.
 #### `"crash_handler"`
 Allow app-process crash handler.
 
-Note that `crash_handler(..)` must be called to enable.
+Note that `crash_handler::init(_)` must be called to enable.
 
 *Enabled by default.*
 
diff --git a/crates/zng-app/src/crash_handler.rs b/crates/zng-app/src/crash_handler.rs
index 85bd7169b..a670a5924 100644
--- a/crates/zng-app/src/crash_handler.rs
+++ b/crates/zng-app/src/crash_handler.rs
@@ -406,13 +406,11 @@ impl fmt::Display for CrashPanic {
             for frame in self.backtrace_frames().skip_while(|f| f.is_after_panic) {
                 write!(f, "{frame}")?;
                 if snippet > 0 {
-                    snippet -= 1;
                     let code = frame.code_snippet();
-                    if code.is_empty() {
-                        snippet = 0;
-                        continue;
+                    if !code.is_empty() {
+                        snippet -= 1;
+                        writeln!(f, "{}", code)?;
                     }
-                    writeln!(f, "{}", code)?;
                 }
             }
             Ok(())
diff --git a/crates/zng-wgt-inspector/Cargo.toml b/crates/zng-wgt-inspector/Cargo.toml
index 31d51c588..bbcdc29f5 100644
--- a/crates/zng-wgt-inspector/Cargo.toml
+++ b/crates/zng-wgt-inspector/Cargo.toml
@@ -17,6 +17,8 @@ default = ["debug_default"]
 debug_default = []
 # Compiles the interactive inspector.
 live = []
+# Compiles the debug crash handler.
+crash_handler = ["zng-app/crash_handler", "dep:open"]
 
 [dependencies]
 zng-wgt = { path = "../zng-wgt", version = "0.2.8" }
@@ -55,6 +57,7 @@ zng-color = { path = "../zng-color", version = "0.2.8" }
 serde = { version = "1.0", features = ["derive"] }
 tracing = "0.1"
 parking_lot = "0.12"
+open = { version = "5.1", optional = true }
 
 [package.metadata.docs.rs]
 all-features = true
diff --git a/crates/zng-wgt-inspector/README.md b/crates/zng-wgt-inspector/README.md
index 3d26584fd..764dc34fa 100644
--- a/crates/zng-wgt-inspector/README.md
+++ b/crates/zng-wgt-inspector/README.md
@@ -5,7 +5,7 @@ This crate is part of the [`zng`](https://github.com/zng-ui/zng?tab=readme-ov-fi
 <!--do doc --readme features-->
 ## Cargo Features
 
-This crate provides 2 feature flags, 1 enabled by default.
+This crate provides 3 feature flags, 1 enabled by default.
 
 #### `"debug_default"`
 Enables the `"live"` feature in debug builds.
@@ -15,6 +15,9 @@ Enables the `"live"` feature in debug builds.
 #### `"live"`
 Compiles the interactive inspector.
 
+#### `"crash_handler"`
+Compiles the debug crash handler.
+
 <!--do doc --readme #SECTION-END-->
 
 
diff --git a/crates/zng-wgt-inspector/src/crash_handler.rs b/crates/zng-wgt-inspector/src/crash_handler.rs
new file mode 100644
index 000000000..0a95682a2
--- /dev/null
+++ b/crates/zng-wgt-inspector/src/crash_handler.rs
@@ -0,0 +1,369 @@
+#![cfg(feature = "crash_handler")]
+
+//! Debug crash handler.
+
+use std::path::PathBuf;
+use zng_app::{crash_handler::*, APP};
+use zng_ext_config::CONFIG;
+use zng_ext_window::{StartPosition, WindowRoot, WINDOWS};
+use zng_wgt::prelude::*;
+use zng_wgt::{align, corner_radius, enabled, margin};
+use zng_wgt_ansi_text::AnsiText;
+use zng_wgt_button::Button;
+use zng_wgt_container::{padding, Container};
+use zng_wgt_fill::background_color;
+use zng_wgt_scroll::Scroll;
+use zng_wgt_stack::Stack;
+use zng_wgt_stack::StackDirection;
+use zng_wgt_style::style_fn;
+use zng_wgt_style::Style;
+use zng_wgt_text::Text;
+use zng_wgt_text_input::selectable::SelectableText;
+use zng_wgt_toggle::{self as toggle, Toggle};
+use zng_wgt_tooltip::{tooltip, Tip};
+use zng_wgt_window::Window;
+use zng_wgt_wrap::Wrap;
+
+/// Debug dialog window.
+///
+/// Used by `zng::app::init_debug`.
+pub fn debug_dialog(args: CrashArgs) -> WindowRoot {
+    let error = args.latest();
+    Window! {
+        title = APP.about().map(|a| formatx!("{}App Crashed", a.title_prefix()));
+        start_position = StartPosition::CenterMonitor;
+        color_scheme = ColorScheme::Dark;
+
+        on_load = hn_once!(|_| {
+            // force to foreground
+            let _ = WINDOWS.focus(WINDOW.id());
+        });
+        on_close = hn_once!(args, |_| {
+            args.exit(0);
+        });
+
+        padding = 5;
+        child_top = header(error), 5;
+        child = panels(error);
+        child_bottom = commands(args), 5;
+    }
+}
+
+fn header(error: &CrashError) -> impl UiNode {
+    SelectableText! {
+        txt = error.message();
+        margin = 10;
+    }
+}
+
+fn panels(error: &CrashError) -> impl UiNode {
+    let mut options = vec![ErrorPanel::Summary];
+    let mut active = ErrorPanel::Summary;
+
+    if !error.stdout.is_empty() {
+        options.push(ErrorPanel::StdoutPlain);
+        active = ErrorPanel::StdoutPlain;
+        if !error.is_stdout_plain() {
+            options.push(ErrorPanel::Stdout);
+            active = ErrorPanel::Stdout;
+        }
+    }
+
+    if !error.stderr.is_empty() {
+        options.push(ErrorPanel::StderrPlain);
+        active = ErrorPanel::StderrPlain;
+        if error.is_stderr_plain() {
+            options.push(ErrorPanel::Stderr);
+            active = ErrorPanel::Stderr;
+        }
+    }
+
+    if error.has_panic() {
+        options.push(ErrorPanel::Panic);
+        active = ErrorPanel::Panic;
+    }
+    if error.has_panic_widget() {
+        options.push(ErrorPanel::Widget);
+    }
+    if error.minidump.is_some() {
+        options.push(ErrorPanel::Minidump);
+        active = ErrorPanel::Minidump;
+    }
+
+    let active = var(active);
+
+    Container! {
+        child_top = Wrap! {
+            toggle::selector = toggle::Selector::single(active.clone());
+            children = options.iter().map(|p| Toggle! {
+                child = Text!(p.title());
+                value = *p;
+            }).collect::<UiNodeVec>();
+            toggle::style_fn = Style! {
+                padding = (2, 4);
+                corner_radius = 2;
+            };
+            spacing = 5;
+        }, 5;
+        child = presenter(active, wgt_fn!(error, |p: ErrorPanel| p.panel(&error)));
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ErrorPanel {
+    Summary,
+    Stdout,
+    Stderr,
+    StdoutPlain,
+    StderrPlain,
+    Panic,
+    Widget,
+    Minidump,
+}
+impl ErrorPanel {
+    fn title(&self) -> Txt {
+        match self {
+            ErrorPanel::Summary => "Summary",
+            ErrorPanel::Stdout => "Stdout",
+            ErrorPanel::Stderr => "Stderr",
+            ErrorPanel::StdoutPlain => "Stdout (plain)",
+            ErrorPanel::StderrPlain => "Stderr (plain)",
+            ErrorPanel::Panic => "Panic",
+            ErrorPanel::Widget => "Widget",
+            ErrorPanel::Minidump => "Minidump",
+        }
+        .into()
+    }
+
+    fn panel(&self, error: &CrashError) -> BoxedUiNode {
+        match self {
+            ErrorPanel::Summary => summary_panel(error).boxed(),
+            ErrorPanel::Stdout => std_panel(error.stdout.clone(), "stdout").boxed(),
+            ErrorPanel::Stderr => std_panel(error.stderr.clone(), "stderr").boxed(),
+            ErrorPanel::StdoutPlain => std_plain_panel(error.stdout_plain(), "stdout").boxed(),
+            ErrorPanel::StderrPlain => std_plain_panel(error.stderr_plain(), "stderr").boxed(),
+            ErrorPanel::Panic => panic_panel(error.find_panic().unwrap()).boxed(),
+            ErrorPanel::Widget => widget_panel(error.find_panic().unwrap().widget_path).boxed(),
+            ErrorPanel::Minidump => minidump_panel(error.minidump.clone().unwrap()).boxed(),
+        }
+    }
+}
+
+fn summary_panel(error: &CrashError) -> impl UiNode {
+    let s = formatx!(
+        "Timestamp: {}\nExit Code: {}\nSignal: {}\nStderr: {} bytes\nStdout: {} bytes\nPanic: {}\nMinidump: {}\n\nArgs: {:?}\n",
+        error.unix_time(),
+        match error.code {
+            Some(c) => format!("{c:#x}"),
+            None => "<none>".to_owned(),
+        },
+        match error.signal {
+            Some(c) => format!("{c}"),
+            None => "<none>".to_owned(),
+        },
+        error.stderr.len(),
+        error.stdout.len(),
+        error.find_panic().is_some(),
+        match &error.minidump {
+            Some(p) => {
+                let path = p.display().to_string();
+                let path = path.trim_start_matches(r"\\?\");
+                path.to_owned()
+            }
+            None => "<none>".to_owned(),
+        },
+        error.args,
+    );
+    plain_panel(s, "summary")
+}
+
+fn std_panel(std: Txt, config_key: &'static str) -> impl UiNode {
+    Scroll! {
+        child_align = Align::TOP_START;
+        background_color = colors::BLACK;
+        padding = 5;
+        horizontal_offset = CONFIG.get(formatx!("{config_key}.scroll.h"), || 0.fct());
+        vertical_offset = CONFIG.get(formatx!("{config_key}.scroll.v"), || 0.fct());
+        child = AnsiText! {
+            txt = std;
+            font_size = 0.9.em();
+        }
+    }
+}
+fn std_plain_panel(std: Txt, config_key: &'static str) -> impl UiNode {
+    plain_panel(std, config_key)
+}
+fn panic_panel(panic: CrashPanic) -> impl UiNode {
+    plain_panel(panic.to_txt(), "panic")
+}
+fn widget_panel(widget_path: Txt) -> impl UiNode {
+    plain_panel(widget_path, "widget")
+}
+fn minidump_panel(path: PathBuf) -> impl UiNode {
+    let path_txt = path.display().to_string().trim_start_matches(r"\\?\").to_txt();
+    Scroll! {
+        child_align = Align::TOP_START;
+        background_color = colors::BLACK;
+        padding = 5;
+        horizontal_offset = CONFIG.get(formatx!("minidump.scroll.h"), || 0.fct());
+        vertical_offset = CONFIG.get(formatx!("minidump.scroll.v"), || 0.fct());
+        child = Stack! {
+            direction = StackDirection::top_to_bottom();
+            spacing = 5;
+            children = ui_vec![
+                SelectableText! {
+                    txt = path_txt;
+                    font_size = 0.9.em();
+                    // same as AnsiText
+                    font_family = ["JetBrains Mono", "Consolas", "monospace"];
+                },
+                Stack! {
+                    direction = StackDirection::top_to_bottom();
+                    zng_wgt_button::style_fn = style_fn!(|_| zng_wgt_button::LinkStyle!());
+                    children = ui_vec![
+                        {
+                            let enabled = var(true);
+                            Button! {
+                                child = Text!("Open Minidump");
+                                on_click = async_hn!(enabled, path, |_| {
+                                    open_path(enabled, path).await;
+                                });
+                                enabled;
+                            }
+                        },
+                        {
+                            let enabled = var(true);
+                            Button! {
+                                child = Text!("Open Minidump Dir");
+                                on_click = async_hn!(enabled, path, |_| {
+                                    open_path(enabled, path.parent().unwrap().to_owned()).await;
+                                });
+                            }
+                        },
+                        {
+                            let enabled = var(true);
+                            Button! {
+                                child = Text!("Save Minidump");
+                                tooltip = Tip!(Text!("Save copy of the minidump"));
+                                on_click = async_hn!(enabled, path, |_| {
+                                    save_copy(enabled, path).await;
+                                });
+                            }
+                        },
+                        {
+                            let enabled = var(true);
+                            Button! {
+                                child = Text!("Delete Minidump");
+                                on_click = async_hn!(enabled, path, |_| {
+                                    remove_path(enabled, path).await;
+                                });
+                            }
+                        },
+                    ]
+                }
+            ]
+        }
+    }
+}
+async fn open_path(enabled: ArcVar<bool>, path: PathBuf) {
+    enabled.set(false);
+
+    if let Err(e) = task::wait(move || open::that(path)).await {
+        error_message(formatx!("Failed to open minidump.\n{e}")).await;
+    }
+
+    enabled.set(true);
+}
+async fn save_copy(enabled: ArcVar<bool>, path: PathBuf) {
+    enabled.set(false);
+
+    let mut dialog = zng_view_api::dialog::FileDialog {
+        title: "Save Copy".into(),
+        starting_dir: path.parent().unwrap().to_owned(),
+        starting_name: "minidump".into(),
+        kind: zng_view_api::dialog::FileDialogKind::SaveFile,
+        ..Default::default()
+    };
+
+    if let Some(ext) = path.extension() {
+        dialog.push_filter("Minidump", &[ext.to_string_lossy()]);
+    }
+
+    let r = WINDOWS.native_file_dialog(WINDOW.id(), dialog).wait_into_rsp().await;
+
+    match r {
+        zng_view_api::dialog::FileDialogResponse::Selected(mut paths) => {
+            let destiny = paths.remove(0);
+            if let Err(e) = task::wait(move || std::fs::copy(path, destiny)).await {
+                error_message(formatx!("Failed so save minidump copy.\n{e}")).await;
+            }
+        }
+        zng_view_api::dialog::FileDialogResponse::Cancel => {}
+        zng_view_api::dialog::FileDialogResponse::Error(e) => error_message(formatx!("Failed to save minidump.\n{e}")).await,
+    }
+
+    enabled.set(true);
+}
+async fn remove_path(enabled: ArcVar<bool>, path: PathBuf) {
+    enabled.set(false);
+
+    if let Err(e) = task::wait(move || std::fs::remove_file(path)).await {
+        if e.kind() != std::io::ErrorKind::NotFound {
+            error_message(formatx!("Failed to open minidump.\n{e}")).await;
+        }
+    }
+
+    enabled.set(true);
+}
+async fn error_message(message: Txt) {
+    WINDOWS
+        .native_message_dialog(
+            WINDOW.id(),
+            zng_view_api::dialog::MsgDialog {
+                title: "Error".into(),
+                message,
+                icon: zng_view_api::dialog::MsgDialogIcon::Error,
+                ..Default::default()
+            },
+        )
+        .wait_into_rsp()
+        .await;
+}
+
+fn plain_panel(txt: Txt, config_key: &'static str) -> impl UiNode {
+    Scroll! {
+        child_align = Align::TOP_START;
+        background_color = colors::BLACK;
+        padding = 5;
+        horizontal_offset = CONFIG.get(formatx!("{config_key}.scroll.h"), || 0.fct());
+        vertical_offset = CONFIG.get(formatx!("{config_key}.scroll.v"), || 0.fct());
+        child = SelectableText! {
+            txt;
+            font_size = 0.9.em();
+            // same as AnsiText
+            font_family = ["JetBrains Mono", "Consolas", "monospace"];
+        }
+    }
+}
+
+fn commands(args: CrashArgs) -> impl UiNode {
+    Stack! {
+        spacing = 5;
+        direction = StackDirection::start_to_end();
+        align = Align::END;
+        children = ui_vec![
+            Button! {
+                child = Text!("Restart App");
+                on_click = hn_once!(args, |_| {
+                    args.restart();
+                });
+            },
+            Button! {
+                child = Text!("Exit App");
+                on_click = hn_once!(|_| {
+                    args.exit(0);
+                });
+            }
+        ];
+    }
+}
diff --git a/crates/zng-wgt-inspector/src/lib.rs b/crates/zng-wgt-inspector/src/lib.rs
index cf5546da6..e29d8f200 100644
--- a/crates/zng-wgt-inspector/src/lib.rs
+++ b/crates/zng-wgt-inspector/src/lib.rs
@@ -1,7 +1,7 @@
 #![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/master/examples/res/image/zng-logo-icon.png")]
 #![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/master/examples/res/image/zng-logo.png")]
 //!
-//! Debug properties and inspector implementation.
+//! Inspector, debug crash handler and debug properties.
 //!
 //! # Crate
 //!
@@ -13,6 +13,7 @@ zng_wgt::enable_widget_macros!();
 
 use zng_wgt::prelude::*;
 
+pub mod crash_handler;
 pub mod debug;
 
 #[cfg(feature = "live")]
diff --git a/crates/zng/Cargo.toml b/crates/zng/Cargo.toml
index 9026bfa04..c450fd557 100644
--- a/crates/zng/Cargo.toml
+++ b/crates/zng/Cargo.toml
@@ -76,8 +76,8 @@ single_instance = ["dep:zng-ext-single-instance"]
 
 # Allow app-process crash handler.
 #
-# Note that `zng::app::crash_handler(..)` must be called to enable.
-crash_handler = ["zng-app/crash_handler"]
+# Note that `zng::app::crash_handler::init(_)` must be called to enable.
+crash_handler = ["zng-app/crash_handler", "zng-wgt-inspector/crash_handler"]
 
 # Instrument every widget outer-most node to trace UI methods.
 trace_widget = ["zng-app/trace_widget"]
diff --git a/crates/zng/README.md b/crates/zng/README.md
index 847d6d68b..7f9a26ff6 100644
--- a/crates/zng/README.md
+++ b/crates/zng/README.md
@@ -100,7 +100,7 @@ Note that `zng::app::single_instance()` must be called to enable single instance
 #### `"crash_handler"`
 Allow app-process crash handler.
 
-Note that `zng::app::crash_handler(..)` must be called to enable.
+Note that `zng::app::crash_handler::init(_)` must be called to enable.
 
 *Enabled by default.*
 
diff --git a/crates/zng/src/app.rs b/crates/zng/src/app.rs
index 833e841b9..878b05173 100644
--- a/crates/zng/src/app.rs
+++ b/crates/zng/src/app.rs
@@ -532,39 +532,24 @@ pub use zng_ext_single_instance::{is_single_instance, single_instance, single_in
 ///
 /// # Full API
 ///
-/// See [`zng_app::crash_handler`] for the full API.
+/// See [`zng_app::crash_handler`] and [`zng_wgt_inspector::crash_handler`] for the full API.
 #[cfg(feature = "crash_handler")]
 pub mod crash_handler {
-    #[cfg(feature = "crash_handler")]
-    pub use zng_app::crash_handler::{init, restart_count, CrashArgs, CrashConfig, CrashError, CrashPanic};
+    use crate::prelude::*;
+    pub use zng_app::crash_handler::{init, BacktraceFrame, CrashArgs, CrashConfig, CrashError, CrashPanic};
+
+    pub use zng_wgt_inspector::crash_handler::debug_dialog;
 
     /// Init a crash-handler with dialog that shows detailed debug info.
     pub fn init_debug() {
-        init(debug_config())
-    }
-
-    /// Config used by [`init_debug`]
-    pub fn debug_config() -> CrashConfig {
-        CrashConfig::new(super::crash_handler_dbg::dialog)
+        init(CrashConfig::new(debug_app))
     }
-}
-
-mod crash_handler_dbg {
-    use std::path::PathBuf;
-
-    use crate::{
-        ansi_text::AnsiText,
-        app::crash_handler::*,
-        color::ColorScheme,
-        prelude::*,
-        widget::node::{presenter, BoxedUiNode},
-    };
 
-    pub fn dialog(args: CrashArgs) -> ! {
+    fn debug_app(args: CrashArgs) -> ! {
         if let Some(c) = &args.dialog_crash {
-            eprintln!("DEBUG CRASH DIALOG CRASHED");
+            eprintln!("DEBUG CRASH DIALOG ALSO CRASHED");
             eprintln!("   {}", c.message());
-            eprintln!("APP CRASH");
+            eprintln!("ORIGINAL APP CRASH");
             eprintln!("   {}", args.latest().message());
             args.exit(0xBADC0DE)
         }
@@ -576,221 +561,9 @@ mod crash_handler_dbg {
         #[cfg(feature = "view")]
         crate::view_process::default::init();
 
-        APP.defaults().run_window(async_clmv!(args, {
-            let error = args.latest();
-            Window! {
-                title = APP.about().map(|a| formatx!("{}App Crashed", a.title_prefix()));
-                start_position = zng::window::StartPosition::CenterMonitor;
-                color_scheme = ColorScheme::Dark;
-
-                on_load = hn_once!(|_| {
-                    // force to foreground
-                    let _ = WINDOWS.focus(WINDOW.id());
-                });
-                on_close = hn_once!(args, |_| {
-                    args.exit(0);
-                });
-
-                padding = 5;
-                child_top = header(error), 5;
-                child = panels(error);
-                child_bottom = commands(args), 5;
-            }
-        }));
-
-        args.exit(0);
-    }
-
-    fn header(error: &CrashError) -> impl UiNode {
-        SelectableText! {
-            txt = error.message();
-            layout::margin = 10;
-        }
-    }
-
-    fn panels(error: &CrashError) -> impl UiNode {
-        let mut options = vec![ErrorPanel::Summary];
-        let mut active = ErrorPanel::Summary;
-
-        if !error.stdout.is_empty() {
-            options.push(ErrorPanel::StdoutPlain);
-            active = ErrorPanel::StdoutPlain;
-            if !error.is_stdout_plain() {
-                options.push(ErrorPanel::Stdout);
-                active = ErrorPanel::Stdout;
-            }
-        }
-
-        if !error.stderr.is_empty() {
-            options.push(ErrorPanel::StderrPlain);
-            active = ErrorPanel::StderrPlain;
-            if error.is_stderr_plain() {
-                options.push(ErrorPanel::Stderr);
-                active = ErrorPanel::Stderr;
-            }
-        }
-
-        if error.has_panic() {
-            options.push(ErrorPanel::Panic);
-            active = ErrorPanel::Panic;
-        }
-        if error.has_panic_widget() {
-            options.push(ErrorPanel::Widget);
-        }
-        if error.minidump.is_some() {
-            options.push(ErrorPanel::Minidump);
-            active = ErrorPanel::Minidump;
-        }
+        APP.defaults()
+            .run_window(async_clmv!(args, { zng_wgt_inspector::crash_handler::debug_dialog(args) }));
 
-        let active = var(active);
-
-        Container! {
-            child_top = Wrap! {
-                toggle::selector = toggle::Selector::single(active.clone());
-                children = options.iter().map(|p| Toggle! {
-                    child = Text!(p.title());
-                    value = *p;
-                }).collect::<UiNodeVec>();
-                toggle::style_fn = Style! {
-                    layout::padding = (2, 4);
-                    widget::corner_radius = 2;
-                };
-                spacing = 5;
-            }, 5;
-            child = presenter(active, wgt_fn!(error, |p: ErrorPanel| p.panel(&error)));
-        }
-    }
-
-    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-    enum ErrorPanel {
-        Summary,
-        Stdout,
-        Stderr,
-        StdoutPlain,
-        StderrPlain,
-        Panic,
-        Widget,
-        Minidump,
-    }
-    impl ErrorPanel {
-        fn title(&self) -> Txt {
-            match self {
-                ErrorPanel::Summary => "Summary",
-                ErrorPanel::Stdout => "Stdout",
-                ErrorPanel::Stderr => "Stderr",
-                ErrorPanel::StdoutPlain => "Stdout (plain)",
-                ErrorPanel::StderrPlain => "Stderr (plain)",
-                ErrorPanel::Panic => "Panic",
-                ErrorPanel::Widget => "Widget",
-                ErrorPanel::Minidump => "Minidump",
-            }
-            .into()
-        }
-
-        fn panel(&self, error: &CrashError) -> BoxedUiNode {
-            match self {
-                ErrorPanel::Summary => summary_panel(error).boxed(),
-                ErrorPanel::Stdout => std_panel(error.stdout.clone(), "stdout").boxed(),
-                ErrorPanel::Stderr => std_panel(error.stderr.clone(), "stderr").boxed(),
-                ErrorPanel::StdoutPlain => std_plain_panel(error.stdout_plain(), "stdout").boxed(),
-                ErrorPanel::StderrPlain => std_plain_panel(error.stderr_plain(), "stderr").boxed(),
-                ErrorPanel::Panic => panic_panel(error.find_panic().unwrap()).boxed(),
-                ErrorPanel::Widget => widget_panel(error.find_panic().unwrap().widget_path).boxed(),
-                ErrorPanel::Minidump => minidump_panel(error.minidump.clone().unwrap()).boxed(),
-            }
-        }
-    }
-
-    fn summary_panel(error: &CrashError) -> impl UiNode {
-        let s = formatx!(
-            "Timestamp: {}\nExit Code: {}\nSignal: {}\nStderr: {} bytes\nStdout: {} bytes\nPanic: {}\nMinidump: {}\n\nArgs: {:?}\n",
-            error.unix_time(),
-            match error.code {
-                Some(c) => format!("{c:#x}"),
-                None => "<none>".to_owned(),
-            },
-            match error.signal {
-                Some(c) => format!("{c}"),
-                None => "<none>".to_owned(),
-            },
-            error.stderr.len(),
-            error.stdout.len(),
-            error.find_panic().is_some(),
-            match &error.minidump {
-                Some(p) => {
-                    let path = p.display().to_string();
-                    let path = path.trim_start_matches(r"\\?\");
-                    path.to_owned()
-                }
-                None => "<none>".to_owned(),
-            },
-            error.args,
-        );
-        plain_panel(s, "summary")
-    }
-
-    fn std_panel(std: Txt, config_key: &'static str) -> impl UiNode {
-        Scroll! {
-            child_align = Align::TOP_START;
-            widget::background_color = colors::BLACK;
-            layout::padding = 5;
-            horizontal_offset = CONFIG.get(formatx!("{config_key}.scroll.h"), || 0.fct());
-            vertical_offset = CONFIG.get(formatx!("{config_key}.scroll.v"), || 0.fct());
-            child = AnsiText! {
-                txt = std;
-                font_size = 0.9.em();
-            }
-        }
-    }
-    fn std_plain_panel(std: Txt, config_key: &'static str) -> impl UiNode {
-        plain_panel(std, config_key)
-    }
-    fn panic_panel(panic: CrashPanic) -> impl UiNode {
-        plain_panel(panic.to_txt(), "panic")
-    }
-    fn widget_panel(widget_path: Txt) -> impl UiNode {
-        plain_panel(widget_path, "widget")
-    }
-    fn minidump_panel(path: PathBuf) -> impl UiNode {
-        let path = path.display().to_string();
-        let path = path.trim_start_matches(r"\\?\");
-        plain_panel(path.to_txt(), "minidump")
-    }
-    fn plain_panel(txt: Txt, config_key: &'static str) -> impl UiNode {
-        Scroll! {
-            child_align = Align::TOP_START;
-            widget::background_color = colors::BLACK;
-            layout::padding = 5;
-            horizontal_offset = CONFIG.get(formatx!("{config_key}.scroll.h"), || 0.fct());
-            vertical_offset = CONFIG.get(formatx!("{config_key}.scroll.v"), || 0.fct());
-            child = SelectableText! {
-                txt;
-                font_size = 0.9.em();
-                // same as AnsiText
-                font_family = ["JetBrains Mono", "Consolas", "monospace"];
-            }
-        }
-    }
-
-    fn commands(args: CrashArgs) -> impl UiNode {
-        Stack! {
-            spacing = 5;
-            direction = StackDirection::start_to_end();
-            layout::align = Align::END;
-            children = ui_vec![
-                Button! {
-                    child = Text!("Restart App");
-                    on_click = hn_once!(args, |_| {
-                        args.restart();
-                    });
-                },
-                Button! {
-                    child = Text!("Exit App");
-                    on_click = hn_once!(|_| {
-                        args.exit(0);
-                    });
-                }
-            ];
-        }
+        args.exit(0)
     }
 }


Test patch: