Skip to main content

vtcode_tui/core_tui/
panic_hook.rs

1//! Panic hook implementation for terminal UI applications
2//! This module provides a panic hook that restores terminal state when a panic occurs,
3//! preventing terminal corruption, and provides enhanced panic formatting for different build types.
4
5use std::io::{self, Write};
6use std::panic;
7use std::sync::Once;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10use better_panic::{Settings as BetterPanicSettings, Verbosity as BetterPanicVerbosity};
11use ratatui::crossterm::{
12    cursor::{MoveToColumn, RestorePosition, SetCursorStyle, Show},
13    event::{
14        DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, PopKeyboardEnhancementFlags,
15    },
16    execute,
17    terminal::{Clear, ClearType, LeaveAlternateScreen, disable_raw_mode},
18};
19
20static TUI_INITIALIZED: AtomicBool = AtomicBool::new(false);
21static DEBUG_MODE: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
22static PANIC_HOOK_ONCE: Once = Once::new();
23
24/// Set whether the application is in debug mode
25///
26/// When debug mode is enabled, panics will show a detailed backtrace.
27pub fn set_debug_mode(enabled: bool) {
28    DEBUG_MODE.store(enabled, Ordering::SeqCst);
29}
30
31/// Get whether the application is in debug mode
32pub fn is_debug_mode() -> bool {
33    DEBUG_MODE.load(Ordering::SeqCst)
34}
35
36/// Initialize the panic hook to restore terminal state on panic and provide better formatting
37///
38/// This function should be called very early in the application lifecycle,
39/// before any TUI operations begin.
40///
41/// Follows Ratatui recipe: https://ratatui.rs/recipes/apps/panic-hooks/
42pub fn init_panic_hook() {
43    PANIC_HOOK_ONCE.call_once(|| {
44        // Keep original hook for concise non-debug panic output.
45        let original_hook = panic::take_hook();
46
47        // Better panic formatting for debug-mode crashes.
48        let better_panic_hook = BetterPanicSettings::new()
49            .verbosity(BetterPanicVerbosity::Full)
50            .most_recent_first(false)
51            .lineno_suffix(true)
52            .create_panic_handler();
53
54        panic::set_hook(Box::new(move |panic_info| {
55            let is_tui = TUI_INITIALIZED.load(Ordering::SeqCst);
56            let is_debug = DEBUG_MODE.load(Ordering::SeqCst);
57
58            // Ratatui recipe: always restore terminal before panic reporting.
59            if is_tui {
60                // Intentionally ignore restoration failures during panic unwind.
61                let _ = restore_tui();
62            }
63
64            if is_debug {
65                better_panic_hook(panic_info);
66            } else {
67                eprintln!("\nVTCode encountered a critical error and needs to shut down.");
68                eprintln!("If this keeps happening, please report it with a backtrace.");
69                eprintln!("Hint: run with --debug and set RUST_BACKTRACE=1.\n");
70                original_hook(panic_info);
71            }
72
73            // Keep current behavior: terminate process after unrecoverable panic.
74            std::process::exit(1);
75        }));
76    });
77}
78
79/// Mark that TUI has been initialized so panic hook knows to restore terminal
80pub fn mark_tui_initialized() {
81    TUI_INITIALIZED.store(true, Ordering::SeqCst);
82}
83
84/// Mark that TUI has been deinitialized to prevent further restoration attempts
85pub fn mark_tui_deinitialized() {
86    TUI_INITIALIZED.store(false, Ordering::SeqCst);
87}
88
89/// Restore terminal to a usable state after a panic
90///
91/// This function attempts to restore the terminal to its original state
92/// by disabling raw mode and leaving alternate screen if they were active.
93/// It handles all errors internally to ensure cleanup happens even if individual
94/// operations fail.
95///
96/// Follows Ratatui recipe: https://ratatui.rs/recipes/apps/panic-hooks/
97pub fn restore_tui() -> io::Result<()> {
98    // 1. Drain any pending crossterm events to prevent them from leaking to the shell
99    // This is a best-effort drain with a zero timeout
100    while let Ok(true) = ratatui::crossterm::event::poll(std::time::Duration::from_millis(0)) {
101        let _ = ratatui::crossterm::event::read();
102    }
103
104    // Get stderr for executing terminal commands
105    let mut stderr = io::stderr();
106
107    // 2. Clear current line to remove any echoed ^C characters from rapid Ctrl+C presses
108    let _ = execute!(stderr, MoveToColumn(0), Clear(ClearType::CurrentLine));
109
110    // 3. Leave alternate screen FIRST (if we were in one)
111    // This is the most critical operation for visual restoration
112    let _ = execute!(stderr, LeaveAlternateScreen);
113
114    // 4. Disable various terminal modes that might have been enabled by the TUI
115    let _ = execute!(stderr, DisableBracketedPaste);
116    let _ = execute!(stderr, DisableFocusChange);
117    let _ = execute!(stderr, DisableMouseCapture);
118    let _ = execute!(stderr, PopKeyboardEnhancementFlags);
119
120    // Ensure cursor state is restored
121    let _ = execute!(
122        stderr,
123        SetCursorStyle::DefaultUserShape,
124        Show,
125        RestorePosition
126    );
127
128    // 5. Disable raw mode LAST to ensure all cleanup commands are sent properly
129    let _ = disable_raw_mode();
130
131    // Additional flush to ensure all escape sequences are processed
132    let _ = stderr.flush();
133
134    Ok(())
135}
136
137/// A guard struct that automatically registers and unregisters TUI state
138/// with the panic hook system.
139///
140/// This ensures that terminal restoration only happens when the TUI was actually active.
141pub struct TuiPanicGuard;
142
143impl TuiPanicGuard {
144    /// Create a new guard and mark TUI as initialized
145    ///
146    /// This should be called when a TUI session begins.
147    pub fn new() -> Self {
148        mark_tui_initialized();
149        Self
150    }
151}
152
153impl Default for TuiPanicGuard {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159impl Drop for TuiPanicGuard {
160    fn drop(&mut self) {
161        mark_tui_deinitialized();
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::sync::atomic::Ordering;
169
170    #[test]
171    fn test_panic_guard_initialization() {
172        // Reset state for test
173        TUI_INITIALIZED.store(false, Ordering::SeqCst);
174
175        {
176            let _guard = TuiPanicGuard::new();
177            assert_eq!(
178                TUI_INITIALIZED.load(Ordering::SeqCst),
179                true,
180                "TUI should be marked as initialized"
181            );
182
183            // Drop happens automatically when leaving scope
184        }
185
186        assert_eq!(
187            TUI_INITIALIZED.load(Ordering::SeqCst),
188            false,
189            "TUI should be marked as deinitialized after guard drops"
190        );
191    }
192
193    #[test]
194    fn test_restore_terminal_no_panic_when_not_initialized() {
195        // Test that restore does not panic when TUI is not initialized
196        TUI_INITIALIZED.store(false, Ordering::SeqCst);
197
198        // This should not cause issues even if terminal is not in expected state
199        let result = restore_tui();
200        // Should return Ok or Err but not panic
201        assert!(result.is_ok() || result.is_err());
202    }
203
204    #[test]
205    fn test_guard_lifecycle() {
206        TUI_INITIALIZED.store(false, Ordering::SeqCst);
207
208        // Create guard in a separate scope to test drop behavior
209        {
210            let _guard = TuiPanicGuard::new();
211            assert_eq!(
212                TUI_INITIALIZED.load(Ordering::SeqCst),
213                true,
214                "Guard should mark TUI as initialized"
215            );
216        }
217
218        assert_eq!(
219            TUI_INITIALIZED.load(Ordering::SeqCst),
220            false,
221            "Drop should mark TUI as deinitialized"
222        );
223    }
224}