hojicha-runtime 0.2.2

Event handling and async runtime for Hojicha TUI framework
Documentation
//! Panic recovery utilities for Model trait methods
//!
//! This module provides safe wrappers for Model trait methods that catch
//! panics and allow the application to continue running.

use hojicha_core::commands;
use hojicha_core::core::{Cmd, Model};
use hojicha_core::event::Event;
use log::error;
use std::panic::{self, AssertUnwindSafe};

/// Strategy for handling panics in Model methods
#[derive(Debug, Clone, Copy)]
pub enum PanicRecoveryStrategy {
    /// Continue with current state (default)
    Continue,
    /// Reset model to initial state
    Reset,
    /// Show error screen
    ShowError,
    /// Quit the application
    Quit,
}

impl Default for PanicRecoveryStrategy {
    fn default() -> Self {
        Self::Continue
    }
}

/// Safely call Model::init with panic recovery
pub fn safe_init<M: Model>(model: &mut M, strategy: PanicRecoveryStrategy) -> Cmd<M::Message> {
    let result = panic::catch_unwind(AssertUnwindSafe(|| model.init()));

    match result {
        Ok(cmd) => cmd,
        Err(panic_info) => {
            let msg = format_panic_message(&panic_info, "Model::init");
            error!("Panic in Model::init: {}", msg);

            match strategy {
                PanicRecoveryStrategy::Continue => Cmd::noop(),
                PanicRecoveryStrategy::Reset => Cmd::noop(),
                PanicRecoveryStrategy::ShowError => {
                    // Could return a command to show error screen
                    Cmd::noop()
                }
                PanicRecoveryStrategy::Quit => commands::quit(),
            }
        }
    }
}

/// Safely call Model::update with panic recovery
pub fn safe_update<M: Model>(
    model: &mut M,
    event: Event<M::Message>,
    strategy: PanicRecoveryStrategy,
) -> Cmd<M::Message> {
    // We can't clone the event, so we need to move it into the closure
    // This means we can't log the specific event on panic
    let result = panic::catch_unwind(AssertUnwindSafe(move || model.update(event)));

    match result {
        Ok(cmd) => cmd,
        Err(panic_info) => {
            let msg = format_panic_message(&panic_info, "Model::update");
            error!("Panic in Model::update: {}", msg);

            match strategy {
                PanicRecoveryStrategy::Continue => Cmd::noop(),
                PanicRecoveryStrategy::Reset => {
                    // Could trigger a reset command
                    Cmd::noop()
                }
                PanicRecoveryStrategy::ShowError => {
                    // Could return a command to show error screen
                    Cmd::noop()
                }
                PanicRecoveryStrategy::Quit => commands::quit(),
            }
        }
    }
}

/// Safely call Model::view with panic recovery
///
/// Returns the view string or an error message string, and a bool indicating if the app should quit
pub fn safe_view<M: Model>(model: &M, strategy: PanicRecoveryStrategy) -> (String, bool) {
    let result = panic::catch_unwind(AssertUnwindSafe(|| model.view()));

    match result {
        Ok(view_string) => (view_string, false), // Success, don't quit
        Err(panic_info) => {
            let msg = format_panic_message(&panic_info, "Model::view");
            error!("Panic in Model::view: {}", msg);

            // Create an error view string
            let error_view = match strategy {
                PanicRecoveryStrategy::ShowError => {
                    format!("ERROR: {}\nPress 'q' to quit", msg)
                }
                _ => "An error occurred. Press 'q' to quit.".to_string(),
            };

            // Return error view and whether to quit based on strategy
            let should_quit = matches!(strategy, PanicRecoveryStrategy::Quit);
            (error_view, should_quit)
        }
    }
}

/// Format panic information into a readable message
fn format_panic_message(panic_info: &dyn std::any::Any, context: &str) -> String {
    if let Some(s) = panic_info.downcast_ref::<String>() {
        format!("{} panicked: {}", context, s)
    } else if let Some(s) = panic_info.downcast_ref::<&str>() {
        format!("{} panicked: {}", context, s)
    } else {
        format!("{} panicked with unknown error", context)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct PanickyModel {
        should_panic_init: bool,
        should_panic_update: bool,
        should_panic_view: bool,
    }

    impl Model for PanickyModel {
        type Message = ();

        fn init(&mut self) -> Cmd<Self::Message> {
            if self.should_panic_init {
                panic!("Init panic!");
            }
            Cmd::noop()
        }

        fn update(&mut self, _event: Event<Self::Message>) -> Cmd<Self::Message> {
            if self.should_panic_update {
                panic!("Update panic!");
            }
            Cmd::noop()
        }

        fn view(&self) -> String {
            if self.should_panic_view {
                panic!("View panic!");
            }
            "Test view".to_string()
        }
    }

    #[test]
    fn test_safe_init_catches_panic() {
        let mut model = PanickyModel {
            should_panic_init: true,
            should_panic_update: false,
            should_panic_view: false,
        };

        let cmd = safe_init(&mut model, PanicRecoveryStrategy::Continue);
        assert!(cmd.is_noop());
    }

    #[test]
    fn test_safe_update_catches_panic() {
        let mut model = PanickyModel {
            should_panic_init: false,
            should_panic_update: true,
            should_panic_view: false,
        };

        let cmd = safe_update(&mut model, Event::Tick, PanicRecoveryStrategy::Continue);
        assert!(cmd.is_noop());
    }
}