hojicha_runtime/
panic_handler.rs

1//! Global panic handler for graceful TUI recovery
2//!
3//! This module provides a panic handler that ensures the terminal is restored
4//! to a usable state when a panic occurs, and optionally logs panic information.
5
6use log::error;
7use std::io::Write;
8use std::panic::{self, PanicHookInfo};
9use std::sync::atomic::{AtomicBool, Ordering};
10
11/// Global flag to track if we're in a TUI context
12static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
13
14/// Cleanup function to be called on panic
15static mut CLEANUP_FN: Option<Box<dyn Fn() + Send + Sync>> = None;
16
17/// Install a panic handler that will restore the terminal on panic
18///
19/// This should be called at the start of your program, before entering the TUI.
20///
21/// # Example
22/// ```no_run
23/// use hojicha::panic_handler;
24///
25/// fn main() {
26///     panic_handler::install();
27///     // ... run your TUI application
28/// }
29/// ```
30pub fn install() {
31    panic::set_hook(Box::new(|panic_info| {
32        handle_panic(panic_info);
33    }));
34}
35
36/// Install a panic handler with a custom cleanup function
37///
38/// The cleanup function will be called before the terminal is restored.
39/// This is useful for saving application state or performing other cleanup.
40///
41/// # Safety
42/// The cleanup function must be thread-safe as it may be called from any thread.
43///
44/// # Example
45/// ```no_run
46/// use hojicha::panic_handler;
47///
48/// fn main() {
49///     panic_handler::install_with_cleanup(|| {
50///         // Save application state, close files, etc.
51///         eprintln!("Saving application state before exit...");
52///     });
53///     // ... run your TUI application
54/// }
55/// ```
56pub fn install_with_cleanup<F>(cleanup: F)
57where
58    F: Fn() + Send + Sync + 'static,
59{
60    unsafe {
61        CLEANUP_FN = Some(Box::new(cleanup));
62    }
63    install();
64}
65
66/// Mark that the TUI is active
67///
68/// This should be called when entering TUI mode and ensures that
69/// the panic handler knows to restore the terminal.
70pub fn set_tui_active(active: bool) {
71    TUI_ACTIVE.store(active, Ordering::SeqCst);
72}
73
74/// Create a guard that automatically sets TUI active/inactive
75pub struct TuiGuard;
76
77impl TuiGuard {
78    /// Create a new TUI guard
79    pub fn new() -> Self {
80        set_tui_active(true);
81        TuiGuard
82    }
83}
84
85impl Drop for TuiGuard {
86    fn drop(&mut self) {
87        set_tui_active(false);
88    }
89}
90
91/// The actual panic handler
92fn handle_panic(panic_info: &PanicHookInfo) {
93    // First, log the panic if logging is available
94    error!("PANIC: {}", panic_info);
95
96    // Run custom cleanup if provided
97    unsafe {
98        if let Some(ref cleanup) = CLEANUP_FN {
99            cleanup();
100        }
101    }
102
103    // If we're in TUI mode, restore the terminal
104    if TUI_ACTIVE.load(Ordering::SeqCst) {
105        restore_terminal();
106    }
107
108    // Print panic information to stderr
109    eprintln!("\n\n==================== PANIC ====================");
110    eprintln!("{}", panic_info);
111
112    // Print location if available
113    if let Some(location) = panic_info.location() {
114        eprintln!(
115            "\nLocation: {}:{}:{}",
116            location.file(),
117            location.line(),
118            location.column()
119        );
120    }
121
122    // Print backtrace if available
123    if let Ok(var) = std::env::var("RUST_BACKTRACE") {
124        if var == "1" || var == "full" {
125            eprintln!("\nBacktrace:");
126            eprintln!("{:?}", std::backtrace::Backtrace::capture());
127        }
128    } else {
129        eprintln!("\nNote: Set RUST_BACKTRACE=1 to see a backtrace");
130    }
131
132    eprintln!("================================================\n");
133}
134
135/// Attempt to restore the terminal to a usable state
136fn restore_terminal() {
137    use crossterm::{
138        cursor,
139        event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture},
140        execute,
141        terminal::{self, LeaveAlternateScreen},
142    };
143
144    // Try to restore terminal state
145    let _ = execute!(
146        std::io::stderr(),
147        LeaveAlternateScreen,
148        DisableMouseCapture,
149        DisableBracketedPaste,
150        DisableFocusChange,
151        cursor::Show,
152    );
153
154    // Disable raw mode
155    let _ = terminal::disable_raw_mode();
156
157    // Flush stderr to ensure all output is visible
158    let _ = std::io::stderr().flush();
159}
160
161/// A panic hook that can be used in tests to verify panic behavior
162#[cfg(test)]
163pub struct TestPanicHook {
164    pub panicked: Arc<AtomicBool>,
165    pub panic_message: Arc<std::sync::Mutex<Option<String>>>,
166}
167
168#[cfg(test)]
169impl TestPanicHook {
170    /// Create a new test panic hook
171    pub fn new() -> Self {
172        Self {
173            panicked: Arc::new(AtomicBool::new(false)),
174            panic_message: Arc::new(std::sync::Mutex::new(None)),
175        }
176    }
177
178    /// Install this hook as the panic handler
179    pub fn install(&self) {
180        let panicked = Arc::clone(&self.panicked);
181        let panic_message = Arc::clone(&self.panic_message);
182
183        panic::set_hook(Box::new(move |panic_info| {
184            panicked.store(true, Ordering::SeqCst);
185
186            let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
187                s.to_string()
188            } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
189                s.clone()
190            } else {
191                "Unknown panic".to_string()
192            };
193
194            *panic_message.lock().unwrap() = Some(msg);
195        }));
196    }
197
198    /// Check if a panic occurred
199    pub fn did_panic(&self) -> bool {
200        self.panicked.load(Ordering::SeqCst)
201    }
202
203    /// Get the panic message if one occurred
204    pub fn get_panic_message(&self) -> Option<String> {
205        self.panic_message.lock().unwrap().clone()
206    }
207
208    /// Reset the panic state
209    pub fn reset(&self) {
210        self.panicked.store(false, Ordering::SeqCst);
211        *self.panic_message.lock().unwrap() = None;
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::panic;
219
220    #[test]
221    fn test_panic_hook_captures_panic() {
222        let hook = TestPanicHook::new();
223        hook.install();
224
225        let result = panic::catch_unwind(|| {
226            panic!("Test panic message");
227        });
228
229        assert!(result.is_err());
230        assert!(hook.did_panic());
231        assert_eq!(
232            hook.get_panic_message(),
233            Some("Test panic message".to_string())
234        );
235
236        // Restore default panic hook
237        let _ = panic::take_hook();
238    }
239}