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

Issue:
Implement default crash handler dialog
After #133, implement a default crash dialog, focused on offering debug help.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 204633061..22b8e1ed6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
     - Adds `zng::app::crash_handler`.
     - Can be used to easily implement crash reporting, stacktrace and minidump 
       collection, app restart on crash.
+    - Call `zng::app::crash_handler::init_debug()` to quickly setup panic and minidump collection.
 * Fix view-process kill by user not working after respawn.
 * Fix view-process assuming any signal kill was requested by the user.
 * Fix potential issue retrieving current_exe trough symbolic links.
diff --git a/crates/zng-app/src/crash_handler.rs b/crates/zng-app/src/crash_handler.rs
index 254af61af..19a860c74 100644
--- a/crates/zng-app/src/crash_handler.rs
+++ b/crates/zng-app/src/crash_handler.rs
@@ -9,7 +9,7 @@ use std::{
     fmt,
     io::{BufRead, BufReader},
     path::{Path, PathBuf},
-    time::{Duration, SystemTime},
+    time::SystemTime,
 };
 use zng_layout::unit::TimeUnits as _;
 
@@ -184,18 +184,12 @@ pub struct CrashError {
     /// Minidump file.
     pub minidump: Option<PathBuf>,
 }
+/// Alternate mode `{:#}` prints plain stdout and stderr (no ANSI escape sequences).
 impl fmt::Display for CrashError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        writeln!(
-            f,
-            "timestamp: {}",
-            self.timestamp
-                .duration_since(SystemTime::UNIX_EPOCH)
-                .unwrap_or(Duration::ZERO)
-                .as_secs()
-        )?;
+        writeln!(f, "timestamp: {}", self.unix_time())?;
         if let Some(c) = self.code {
-            writeln!(f, "exit code: {c:#x}")?
+            writeln!(f, "exit code: {c:#X}")?
         }
         if let Some(c) = self.signal {
             writeln!(f, "exit signal: {c}")?
@@ -203,7 +197,11 @@ impl fmt::Display for CrashError {
         if let Some(p) = self.minidump.as_ref() {
             writeln!(f, "minidump: {}", p.display())?
         }
-        write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout, self.stderr)
+        if f.alternate() {
+            write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout_plain(), self.stderr_plain())
+        } else {
+            write!(f, "\nSTDOUT:\n{}\nSTDERR:\n{}\n", self.stdout, self.stderr)
+        }
     }
 }
 impl CrashError {
@@ -233,13 +231,56 @@ impl CrashError {
         }
     }
 
+    /// Seconds since Unix epoch.
+    pub fn unix_time(&self) -> u64 {
+        self.timestamp.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs()
+    }
+
+    /// Gets if `stdout` does not contain any ANSI scape sequences.
+    pub fn is_stdout_plain(&self) -> bool {
+        !self.stdout.contains(CSI)
+    }
+
+    /// Gets if `stderr` does not contain any ANSI scape sequences.
+    pub fn is_stderr_plain(&self) -> bool {
+        !self.stderr.contains(CSI)
+    }
+
+    /// Get `stdout` without any ANSI escape sequences (CSI).
+    pub fn stdout_plain(&self) -> Txt {
+        remove_ansi_csi(&self.stdout)
+    }
+
+    /// Get `stderr` without any ANSI escape sequences (CSI).
+    pub fn stderr_plain(&self) -> Txt {
+        remove_ansi_csi(&self.stderr)
+    }
+
+    /// Gets if `stderr` contains a crash panic.
+    pub fn has_panic(&self) -> bool {
+        if self.code == Some(101) {
+            CrashPanic::contains(&self.stderr_plain())
+        } else {
+            false
+        }
+    }
+
+    /// Gets if `stderr` contains a crash panic that traced widget/window path.
+    pub fn has_panic_widget(&self) -> bool {
+        if self.code == Some(101) {
+            CrashPanic::contains_widget(&self.stderr_plain())
+        } else {
+            false
+        }
+    }
+
     /// Try parse `stderr` for the crash panic.
     ///
     /// Only reliably works if the panic fully printed correctly and was formatted by the panic
     /// hook installed by `crash_handler` or by the display print of [`CrashPanic`].
     pub fn find_panic(&self) -> Option<CrashPanic> {
         if self.code == Some(101) {
-            CrashPanic::find(&self.stderr)
+            CrashPanic::find(&self.stderr_plain())
         } else {
             None
         }
@@ -254,15 +295,17 @@ impl CrashError {
         } else if let Some(msg) = self.minidump_message() {
             msg
         } else {
-            "Error.".into()
+            "".into()
         };
         use std::fmt::Write as _;
 
         if let Some(c) = self.code {
-            write!(&mut msg, " Code: {c:#x}.").unwrap();
+            let sep = if msg.is_empty() { "" } else { "\n" };
+            write!(&mut msg, "{sep}Code: {c:#X}").unwrap();
         }
         if let Some(c) = self.signal {
-            write!(&mut msg, " Signal: {c}.").unwrap();
+            let sep = if msg.is_empty() { "" } else { "\n" };
+            write!(&mut msg, "{sep}Signal: {c}").unwrap();
         }
         msg.end_mut();
         msg
@@ -300,6 +343,29 @@ impl CrashError {
     }
 }
 
+const CSI: &str = "\x1b[";
+
+/// Remove ANSI escape sequences (CSI) from `s`.
+pub fn remove_ansi_csi(mut s: &str) -> Txt {
+    fn is_esc_end(byte: u8) -> bool {
+        (0x40..=0x7e).contains(&byte)
+    }
+
+    let mut r = String::new();
+    while let Some(i) = s.find(CSI) {
+        r.push_str(&s[..i]);
+        s = &s[i + CSI.len()..];
+        let mut esc_end = 0;
+        while esc_end < s.len() && !is_esc_end(s.as_bytes()[esc_end]) {
+            esc_end += 1;
+        }
+        esc_end += 1;
+        s = &s[esc_end..];
+    }
+    r.push_str(s);
+    r.into()
+}
+
 /// Panic parsed from a `stderr` dump.
 #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
 pub struct CrashPanic {
@@ -332,11 +398,32 @@ impl fmt::Display for CrashPanic {
     }
 }
 impl CrashPanic {
+    /// Gets if `stderr` contains a panic that can be parsed by [`find`].
+    ///
+    /// [`find`]: Self::find
+    pub fn contains(stderr: &str) -> bool {
+        Self::find_impl(stderr, false).is_some()
+    }
+
+    /// Gets if `stderr` contains a panic that can be parsed by [`find`] and traced a widget/window path.
+    ///
+    /// [`find`]: Self::find
+    pub fn contains_widget(stderr: &str) -> bool {
+        match Self::find_impl(stderr, false) {
+            Some(p) => !p.widget_path.is_empty(),
+            None => false,
+        }
+    }
+
     /// Try parse `stderr` for the crash panic.
     ///
     /// Only reliably works if the panic fully printed correctly and was formatted by the panic
     /// hook installed by `crash_handler` or by the display print of this type.
     pub fn find(stderr: &str) -> Option<Self> {
+        Self::find_impl(stderr, true)
+    }
+
+    fn find_impl(stderr: &str, parse: bool) -> Option<Self> {
         let mut panic_at = usize::MAX;
         let mut widget_path = usize::MAX;
         let mut stack_backtrace = usize::MAX;
@@ -358,6 +445,22 @@ impl CrashPanic {
             return None;
         }
 
+        if !parse {
+            return Some(Self {
+                thread: Txt::from(""),
+                message: Txt::from(""),
+                file: Txt::from(""),
+                line: 0,
+                column: 0,
+                widget_path: if widget_path < stderr.len() {
+                    Txt::from("true")
+                } else {
+                    Txt::from("")
+                },
+                backtrace: Txt::from(""),
+            });
+        }
+
         let panic_str = stderr[panic_at..].lines().next().unwrap();
         let (thread, location) = panic_str.strip_prefix("thread '").unwrap().split_once("' panicked at ").unwrap();
         let mut location = location.split(':');
@@ -382,7 +485,7 @@ impl CrashPanic {
         }
 
         let widget_path = if widget_path < stderr.len() {
-            stderr[widget_path..].lines().nth(1).unwrap().trim()
+            stderr[widget_path..].lines().next().unwrap().trim()
         } else {
             ""
         };
@@ -482,7 +585,7 @@ fn crash_handler_monitor_process(mut cfg_app: ConfigProcess, mut cfg_dialog: Con
                     }
 
                     tracing::error!(
-                        "app-process crashed with exit code ({:#x}), signal ({:#?}), {} crashes previously",
+                        "app-process crashed with exit code ({:#X}), signal ({:#?}), {} crashes previously",
                         code.unwrap_or(0),
                         signal.unwrap_or(0),
                         dialog_args.app_crashes.len()
diff --git a/crates/zng-view-api/src/app_process.rs b/crates/zng-view-api/src/app_process.rs
index 5e926f320..ea8751ab5 100644
--- a/crates/zng-view-api/src/app_process.rs
+++ b/crates/zng-view-api/src/app_process.rs
@@ -411,7 +411,7 @@ impl Controller {
             if !killed_by_us {
                 let code = code.unwrap_or(0);
                 let signal = signal.unwrap_or(0);
-                tracing::error!(target: "vp_respawn", "view-process exit code: {code:#x}, signal: {signal}");
+                tracing::error!(target: "vp_respawn", "view-process exit code: {code:#X}, signal: {signal}");
             }
 
             let stderr = match String::from_utf8(c.stderr) {
diff --git a/crates/zng/src/app.rs b/crates/zng/src/app.rs
index 8c9e47aaa..9b6531185 100644
--- a/crates/zng/src/app.rs
+++ b/crates/zng/src/app.rs
@@ -537,4 +537,256 @@ pub use zng_ext_single_instance::{is_single_instance, single_instance, single_in
 pub mod crash_handler {
     #[cfg(feature = "crash_handler")]
     pub use zng_app::crash_handler::{init, restart_count, CrashArgs, CrashConfig, CrashError, CrashPanic};
+
+    /// 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)
+    }
+}
+
+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) -> ! {
+        if let Some(c) = &args.dialog_crash {
+            eprintln!("DEBUG CRASH DIALOG CRASHED");
+            eprintln!("   {}", c.message());
+            eprintln!("APP CRASH");
+            eprintln!("   {}", args.latest().message());
+            args.exit(0xBADC0DE)
+        }
+
+        // Just in case no view-process was inited before the crash_handler setup.
+        // Multiple view-process inits are ok because they either do nothing or never return.
+        #[cfg(feature = "view_prebuilt")]
+        crate::view_process::prebuilt::init();
+        #[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;
+        }
+
+        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()).boxed(),
+                ErrorPanel::Stderr => std_panel(error.stderr.clone()).boxed(),
+                ErrorPanel::StdoutPlain => std_plain_panel(error.stdout_plain()).boxed(),
+                ErrorPanel::StderrPlain => std_plain_panel(error.stderr_plain()).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)
+    }
+
+    fn std_panel(std: Txt) -> impl UiNode {
+        Scroll! {
+            child_align = Align::TOP_START;
+            widget::background_color = colors::BLACK;
+            layout::padding = 5;
+            child = AnsiText! {
+                txt = std;
+                font_size = 0.9.em();
+            }
+        }
+    }
+    fn std_plain_panel(std: Txt) -> impl UiNode {
+        plain_panel(std)
+    }
+    fn panic_panel(panic: CrashPanic) -> impl UiNode {
+        plain_panel(panic.to_txt())
+    }
+    fn widget_panel(widget_path: Txt) -> impl UiNode {
+        plain_panel(widget_path)
+    }
+    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())
+    }
+    fn plain_panel(txt: Txt) -> impl UiNode {
+        Scroll! {
+            child_align = Align::TOP_START;
+            widget::background_color = colors::BLACK;
+            layout::padding = 5;
+            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);
+                    });
+                }
+            ];
+        }
+    }
 }
diff --git a/examples/README.md b/examples/README.md
index 028d724d7..2925d1c3f 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -198,7 +198,7 @@ Source: [respawn.rs](./respawn.rs)
 cargo do run respawn
 ```
 
-Demonstrates the view-process respawn error recovery feature.
+Demonstrates app-process crash handler and view-process respawn.
 
 ### `scroll`
 
diff --git a/examples/animation.rs b/examples/animation.rs
index f8108112b..b5f0a6a20 100644
--- a/examples/animation.rs
+++ b/examples/animation.rs
@@ -27,6 +27,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("animation");
 
diff --git a/examples/border.rs b/examples/border.rs
index 9fba4849a..b5f0a0303 100644
--- a/examples/border.rs
+++ b/examples/border.rs
@@ -11,6 +11,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     //let rec = examples_util::record_profile("border");
 
diff --git a/examples/button.rs b/examples/button.rs
index df1ca3fac..d90c8bb94 100644
--- a/examples/button.rs
+++ b/examples/button.rs
@@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("button");
 
diff --git a/examples/calculator.rs b/examples/calculator.rs
index 929d6eb42..d09f9fdf8 100644
--- a/examples/calculator.rs
+++ b/examples/calculator.rs
@@ -12,6 +12,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     //let rec = examples_util::record_profile("calculator");
 
diff --git a/examples/config.rs b/examples/config.rs
index 4c0669950..b4c814fb0 100644
--- a/examples/config.rs
+++ b/examples/config.rs
@@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("button");
 
diff --git a/examples/countdown.rs b/examples/countdown.rs
index 4543c9083..02b3c6c21 100644
--- a/examples/countdown.rs
+++ b/examples/countdown.rs
@@ -8,6 +8,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // view_process::run_same_process(app_main);
     app_main();
diff --git a/examples/cursor.rs b/examples/cursor.rs
index a1cdb4a3d..60131e6ab 100644
--- a/examples/cursor.rs
+++ b/examples/cursor.rs
@@ -15,6 +15,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // view_process::run_same_process(app_main);
     app_main();
diff --git a/examples/extend_view.rs b/examples/extend_view.rs
index 218b28e04..7d9577a9b 100644
--- a/examples/extend_view.rs
+++ b/examples/extend_view.rs
@@ -11,6 +11,7 @@ use zng_view::extensions::ViewExtensions;
 
 fn main() {
     examples_util::print_info();
+    zng::app::crash_handler::init_debug();
 
     // zng_view::init_extended(view_extensions);
     // app_main();
diff --git a/examples/focus.rs b/examples/focus.rs
index 4a981ddc2..c02e6b844 100644
--- a/examples/focus.rs
+++ b/examples/focus.rs
@@ -25,6 +25,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("focus");
 
diff --git a/examples/gradient.rs b/examples/gradient.rs
index 34bf34fec..b5ddc21c6 100644
--- a/examples/gradient.rs
+++ b/examples/gradient.rs
@@ -18,6 +18,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // view_process::run_same_process(app_main);
     app_main();
diff --git a/examples/icon.rs b/examples/icon.rs
index d9cb37600..5db1b795a 100644
--- a/examples/icon.rs
+++ b/examples/icon.rs
@@ -27,6 +27,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("icon");
 
diff --git a/examples/image.rs b/examples/image.rs
index 28429aa04..7436b3fa3 100644
--- a/examples/image.rs
+++ b/examples/image.rs
@@ -27,6 +27,7 @@ use zng_wgt_webrender_debug as wr;
 fn main() {
     examples_util::print_info();
     // view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("image");
 
diff --git a/examples/layer.rs b/examples/layer.rs
index 87cd2359d..de9d62ee8 100644
--- a/examples/layer.rs
+++ b/examples/layer.rs
@@ -17,6 +17,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("layer");
 
diff --git a/examples/localize.rs b/examples/localize.rs
index 9cfa4e2bf..0f646dc3a 100644
--- a/examples/localize.rs
+++ b/examples/localize.rs
@@ -26,6 +26,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("localize");
 
diff --git a/examples/markdown.rs b/examples/markdown.rs
index 797c8f086..7961f43d0 100644
--- a/examples/markdown.rs
+++ b/examples/markdown.rs
@@ -15,6 +15,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("markdown");
 
diff --git a/examples/res/screenshots/respawn.png b/examples/res/screenshots/respawn.png
index e109a0974..be93f3ed2 100644
Binary files a/examples/res/screenshots/respawn.png and b/examples/res/screenshots/respawn.png differ
diff --git a/examples/respawn.rs b/examples/respawn.rs
index ece4d3c8e..fb874876b 100644
--- a/examples/respawn.rs
+++ b/examples/respawn.rs
@@ -15,7 +15,8 @@ fn main() {
     examples_util::print_info();
 
     // init crash-handler before view to use different view for the crash dialog app.
-    zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(app_crash_dialog));
+    zng::app::crash_handler::init_debug();
+    // zng::app::crash_handler::init(zng::app::crash_handler::CrashConfig::new(app_crash_dialog));
 
     // this is the normal app-process:
 
@@ -32,29 +33,49 @@ fn main() {
             child = Stack! {
                 direction = StackDirection::top_to_bottom();
                 layout::margin = 10;
-                spacing = 5;
+                spacing = 10;
                 children_align = Align::TOP;
                 children = ui_vec![
                     Markdown! {
+                        layout::margin = (20, 0, 0, 0);
                         txt = "The renderer and OS windows are created in separate process, the `view-process`. \
                                It automatically respawns in case of a graphics driver crash or other similar fatal error.";
                     },
-                    view_respawn(),
-                    view_crash(),
+                    Wrap! {
+                        spacing = 5;
+                        children = ui_vec![
+                            view_respawn(),
+                            view_crash(),
+                        ],
+                    },
                     Markdown! {
+                        layout::margin = (20, 0, 0, 0);
                         txt = "When the app is instantiated the crash handler takes over the process, becoming the `monitor-process`. \
                                It spawns the `app-process` that is the normal execution. If the `app-process` crashes it spawns the \
                                `dialog-process` that runs a different app that shows an error message.";
                     },
-                    app_crash(true),
-                    app_crash(false),
+                    Wrap! {
+                        spacing = 5;
+                        children = ui_vec![
+                            app_crash("panic"),
+                            app_crash("access violation"),
+                            app_crash("stack overflow"),
+                            app_crash("custom exit"),
+                        ],
+                    },
                     Markdown! {
+                        layout::margin = (20, 0, 0, 0);
                         txt = "The states of these buttons is only preserved for `view-process` crashes. \
                                use `CONFIG` or some other state saving to better recover from `app-process` crashes.";
                     },
-                    click_counter(),
-                    click_counter(),
-                    image(),
+                    Wrap! {
+                        spacing = 5;
+                        children = ui_vec![
+                            click_counter(),
+                            image(),
+                            click_counter(),
+                        ],
+                    },
                 ];
             };
         }
@@ -62,33 +83,28 @@ fn main() {
 }
 
 // Crash dialog app, runs in the dialog-process.
+#[allow(unused)]
 fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! {
     zng::view_process::prebuilt::init();
     APP.defaults().run_window(async move {
         Window! {
             title = "Respawn Example - App Crashed";
             icon = WindowIcon::render(icon);
+
             start_position = zng::window::StartPosition::CenterMonitor;
+            auto_size = window::AutoSize::CONTENT;
+            min_size = (300, 100);
 
             on_load = hn_once!(|_| {
                 // force to foreground
                 let _ = WINDOWS.focus(WINDOW.id());
             });
-            on_close = hn_once!(args, |_| {
-                args.exit(0);
-            });
 
             padding = 5;
-            child_top = Markdown!(
-                "The Respawn Example app has crashed.\n\n{}\n\n**Details:**\n",
+            child = Markdown!(
+                "The Respawn Example app has crashed.\n\n{}\n\n",
                  args.latest().message(),
-            ), 5;
-            child = Scroll! {
-                padding = 5;
-                child_align = Align::TOP_START;
-                child = zng::ansi_text::AnsiText!(args.latest().to_txt());
-                widget::background_color = colors::BLACK;
-            };
+            );
             child_bottom = Stack! {
                 spacing = 5;
                 direction = StackDirection::start_to_end();
@@ -117,7 +133,7 @@ fn app_crash_dialog(args: zng::app::crash_handler::CrashArgs) -> ! {
             }, 5;
         }
     });
-    panic!("dialog app did not respond correctly")
+    std::process::exit(0)
 }
 
 fn view_respawn() -> impl UiNode {
@@ -143,18 +159,32 @@ fn view_crash() -> impl UiNode {
     }
 }
 
-fn app_crash(panic: bool) -> impl UiNode {
+fn app_crash(crash_name: &'static str) -> impl UiNode {
     Button! {
-        child = Text!("Crash App-Process ({})", if panic { "panic" } else { "access violation" });
+        child = Text!("Crash ({crash_name})");
         on_click = hn!(|_| {
-            if panic {
-                panic!("Test app-process crash!");
-            } else {
-                // SAFETY: deliberate access violation
-                #[allow(deref_nullptr)]
-                unsafe {
-                    *std::ptr::null_mut() = true;
+            match crash_name {
+                "panic" => panic!("Test app-process crash!"),
+                "access violation" => {
+                    // SAFETY: deliberate access violation
+                    #[allow(deref_nullptr)]
+                    unsafe {
+                        *std::ptr::null_mut() = true;
+                    }
+                }
+                "stack overflow" => {
+                    fn overflow(c: u64) {
+                        if c < u64::MAX {
+                            overflow(c + 1)
+                        }
+                    }
+                    overflow(0)
+                }
+                "custom exit" => {
+                    eprintln!("custom error");
+                    std::process::exit(0xBAD);
                 }
+                n => panic!("Unknown crash '{n}'"),
             }
         });
     }
@@ -175,13 +205,9 @@ fn click_counter() -> impl UiNode {
 }
 
 fn image() -> impl UiNode {
-    Stack! {
-        direction = StackDirection::top_to_bottom();
-        spacing = 3;
-        children = ui_vec![
-            text::Strong!("Image:"),
-            Image! { source = include_bytes!("res/window/icon-bytes.png"); size = (32, 32); },
-        ];
+    Image! {
+        source = include_bytes!("res/window/icon-bytes.png"); size = (32, 32);
+        tooltip = Tip!(Text!("Image reloads after respawn"));
     }
 }
 
diff --git a/examples/scroll.rs b/examples/scroll.rs
index 3263325ce..dc32ece97 100644
--- a/examples/scroll.rs
+++ b/examples/scroll.rs
@@ -15,6 +15,7 @@ use rand::SeedableRng;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("scroll");
     // view_process::run_same_process(app_main);
diff --git a/examples/shortcut.rs b/examples/shortcut.rs
index 3f2e80ba0..0b9e963c4 100644
--- a/examples/shortcut.rs
+++ b/examples/shortcut.rs
@@ -9,6 +9,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("shortcuts");
 
diff --git a/examples/text.rs b/examples/text.rs
index d8d2e3eaf..a4a861bab 100644
--- a/examples/text.rs
+++ b/examples/text.rs
@@ -32,6 +32,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     //let rec = examples_util::record_profile("text");
 
diff --git a/examples/transform.rs b/examples/transform.rs
index d0b2dde42..bcf5288bb 100644
--- a/examples/transform.rs
+++ b/examples/transform.rs
@@ -21,6 +21,7 @@ use zng::view_process::prebuilt as view_process;
 fn main() {
     examples_util::print_info();
     view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("transform");
 
diff --git a/examples/window.rs b/examples/window.rs
index 93b3fb2f4..5fdfe14cb 100644
--- a/examples/window.rs
+++ b/examples/window.rs
@@ -29,6 +29,7 @@ use zng::view_process::default as view_process;
 fn main() {
     examples_util::print_info();
     // view_process::init();
+    zng::app::crash_handler::init_debug();
 
     // let rec = examples_util::record_profile("window");
 


Test patch: