hojicha-runtime 0.2.2

Event handling and async runtime for Hojicha TUI framework
Documentation
//! Global panic handler for graceful TUI recovery
//!
//! This module provides a panic handler that ensures the terminal is restored
//! to a usable state when a panic occurs, and optionally logs panic information.

use log::error;
use std::io::Write;
use std::panic::{self, PanicHookInfo};
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(test)]
use std::sync::Arc;

/// Global flag to track if we're in a TUI context
static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);

/// Cleanup function to be called on panic
static mut CLEANUP_FN: Option<Box<dyn Fn() + Send + Sync>> = None;

/// Install a panic handler that will restore the terminal on panic
///
/// This should be called at the start of your program, before entering the TUI.
///
/// # Example
/// ```no_run
/// use hojicha_runtime::panic_handler;
///
/// panic_handler::install();
/// // ... run your TUI application
/// ```
pub fn install() {
    panic::set_hook(Box::new(|panic_info| {
        handle_panic(panic_info);
    }));
}

/// Install a panic handler with a custom cleanup function
///
/// The cleanup function will be called before the terminal is restored.
/// This is useful for saving application state or performing other cleanup.
///
/// # Safety
/// The cleanup function must be thread-safe as it may be called from any thread.
///
/// # Example
/// ```no_run
/// use hojicha_runtime::panic_handler;
///
/// panic_handler::install_with_cleanup(|| {
///     // Save application state, close files, etc.
///     eprintln!("Saving application state before exit...");
/// });
/// // ... run your TUI application
/// ```
pub fn install_with_cleanup<F>(cleanup: F)
where
    F: Fn() + Send + Sync + 'static,
{
    unsafe {
        CLEANUP_FN = Some(Box::new(cleanup));
    }
    install();
}

/// Mark that the TUI is active
///
/// This should be called when entering TUI mode and ensures that
/// the panic handler knows to restore the terminal.
pub fn set_tui_active(active: bool) {
    TUI_ACTIVE.store(active, Ordering::SeqCst);
}

/// Create a guard that automatically sets TUI active/inactive
pub struct TuiGuard;

impl Default for TuiGuard {
    fn default() -> Self {
        Self::new()
    }
}

impl TuiGuard {
    /// Create a new TUI guard
    pub fn new() -> Self {
        set_tui_active(true);
        TuiGuard
    }
}

impl Drop for TuiGuard {
    fn drop(&mut self) {
        set_tui_active(false);
    }
}

/// The actual panic handler
fn handle_panic(panic_info: &PanicHookInfo) {
    // First, log the panic if logging is available
    error!("PANIC: {}", panic_info);

    // Run custom cleanup if provided
    unsafe {
        if let Some(ref cleanup) = CLEANUP_FN {
            cleanup();
        }
    }

    // If we're in TUI mode, restore the terminal
    if TUI_ACTIVE.load(Ordering::SeqCst) {
        restore_terminal();
    }

    // Print panic information to stderr
    eprintln!("\n\n==================== PANIC ====================");
    eprintln!("{}", panic_info);

    // Print location if available
    if let Some(location) = panic_info.location() {
        eprintln!(
            "\nLocation: {}:{}:{}",
            location.file(),
            location.line(),
            location.column()
        );
    }

    // Print backtrace if available
    if let Ok(var) = std::env::var("RUST_BACKTRACE") {
        if var == "1" || var == "full" {
            eprintln!("\nBacktrace:");
            eprintln!("{:?}", std::backtrace::Backtrace::capture());
        }
    } else {
        eprintln!("\nNote: Set RUST_BACKTRACE=1 to see a backtrace");
    }

    eprintln!("================================================\n");
}

/// Attempt to restore the terminal to a usable state
fn restore_terminal() {
    use crossterm::{
        cursor,
        event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture},
        execute,
        terminal::{self, LeaveAlternateScreen},
    };

    // Try to restore terminal state
    let _ = execute!(
        std::io::stderr(),
        LeaveAlternateScreen,
        DisableMouseCapture,
        DisableBracketedPaste,
        DisableFocusChange,
        cursor::Show,
    );

    // Disable raw mode
    let _ = terminal::disable_raw_mode();

    // Flush stderr to ensure all output is visible
    let _ = std::io::stderr().flush();
}

/// A panic hook that can be used in tests to verify panic behavior
#[cfg(test)]
pub struct TestPanicHook {
    /// Flag indicating whether a panic occurred
    pub panicked: Arc<AtomicBool>,
    /// The panic message, if captured
    pub panic_message: Arc<std::sync::Mutex<Option<String>>>,
}

#[cfg(test)]
impl Default for TestPanicHook {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
impl TestPanicHook {
    /// Create a new test panic hook
    pub fn new() -> Self {
        Self {
            panicked: Arc::new(AtomicBool::new(false)),
            panic_message: Arc::new(std::sync::Mutex::new(None)),
        }
    }

    /// Install this hook as the panic handler
    pub fn install(&self) {
        let panicked = Arc::clone(&self.panicked);
        let panic_message = Arc::clone(&self.panic_message);

        panic::set_hook(Box::new(move |panic_info| {
            let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
                s.to_string()
            } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
                s.clone()
            } else {
                "Unknown panic".to_string()
            };

            // Only record panics that match our test pattern
            if msg.starts_with("Test panic message for thread") {
                panicked.store(true, Ordering::SeqCst);
                *panic_message.lock().unwrap() = Some(msg);
            }
        }));
    }

    /// Check if a panic occurred
    pub fn did_panic(&self) -> bool {
        self.panicked.load(Ordering::SeqCst)
    }

    /// Get the panic message if one occurred
    pub fn get_panic_message(&self) -> Option<String> {
        self.panic_message.lock().unwrap().clone()
    }

    /// Reset the panic state
    pub fn reset(&self) {
        self.panicked.store(false, Ordering::SeqCst);
        *self.panic_message.lock().unwrap() = None;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;
    use std::panic;
    use std::sync::Mutex;

    // Global mutex to ensure panic hook tests don't interfere with each other
    static PANIC_TEST_MUTEX: Mutex<()> = Mutex::new(());

    #[test]
    fn test_panic_hook_captures_panic() {
        // Use a static mutex to ensure only one panic hook test runs at a time
        let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());

        // Save the current panic hook to restore it later
        let original_hook = panic::take_hook();

        // Create our test hook
        let hook = TestPanicHook::new();
        hook.install();

        // Use a unique message to avoid confusion with other tests
        let test_id = std::thread::current().id();
        let panic_msg = format!("Test panic message for thread {:?}", test_id);
        let expected_msg = panic_msg.clone();

        // Trigger a panic and catch it
        let panic_result = panic::catch_unwind(move || {
            panic!("{}", panic_msg);
        });

        // Verify the panic was caught
        assert!(panic_result.is_err(), "Expected panic to occur");

        // Check if our hook captured it
        let captured = hook.get_panic_message();

        // Restore the original panic hook BEFORE any assertions that might fail
        panic::set_hook(original_hook);

        // Now check our assertions
        assert!(hook.did_panic(), "Hook should have detected panic");
        assert_eq!(
            captured,
            Some(expected_msg),
            "Hook should have captured our specific message"
        );
    }

    /// Behavioral test: TUI state can be set manually
    #[test]
    fn test_manual_tui_state_behavior() {
        // Save initial state
        let initial_state = TUI_ACTIVE.load(Ordering::SeqCst);

        set_tui_active(true);
        assert!(TUI_ACTIVE.load(Ordering::SeqCst));

        set_tui_active(false);
        assert!(!TUI_ACTIVE.load(Ordering::SeqCst));

        // Restore initial state
        set_tui_active(initial_state);
    }

    /// Behavioral test: Panic hook installation doesn't interfere with normal operation
    #[test]
    fn test_panic_hook_installation_behavior() {
        let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());

        // Save original hook
        let original_hook = panic::take_hook();

        // Install our handler
        install();

        // Normal operations should work fine
        let result = std::panic::catch_unwind(|| {
            // This should NOT panic
            let x = 1 + 1;
            assert_eq!(x, 2);
        });

        assert!(result.is_ok());

        // Restore original hook
        panic::set_hook(original_hook);
    }

    /// Behavioral test: Custom cleanup function behavior
    #[test]
    fn test_custom_cleanup_behavior() {
        let _guard = PANIC_TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
        let original_hook = panic::take_hook();

        let cleanup_called = Arc::new(AtomicBool::new(false));
        let cleanup_called_clone = Arc::clone(&cleanup_called);

        // Install with custom cleanup
        install_with_cleanup(move || {
            cleanup_called_clone.store(true, Ordering::SeqCst);
        });

        let test_id = std::thread::current().id();
        let panic_msg = format!("Test cleanup panic for thread {:?}", test_id);

        // Trigger panic to test cleanup
        let _ = panic::catch_unwind(move || {
            panic!("{}", panic_msg);
        });

        // Note: The cleanup might not be called in catch_unwind context
        // This test verifies the installation doesn't break normal operation

        // Restore original hook
        panic::set_hook(original_hook);

        // Test passes if we get here without hanging
    }

    proptest! {
        #[test]
        fn prop_tui_state_atomic(states in prop::collection::vec(prop::bool::ANY, 1..10)) {
            for &state in &states {
                set_tui_active(state);
                prop_assert_eq!(TUI_ACTIVE.load(Ordering::SeqCst), state);
            }
        }
    }
}