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