vtcode_tui/core_tui/
panic_hook.rs1use 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 human_panic::{Metadata as HumanPanicMetadata, handle_dump as human_panic_dump, print_msg};
12use ratatui::crossterm::{
13 cursor::{MoveToColumn, RestorePosition, SetCursorStyle, Show},
14 event::{
15 DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, PopKeyboardEnhancementFlags,
16 },
17 execute,
18 terminal::{Clear, ClearType, LeaveAlternateScreen, disable_raw_mode},
19};
20
21static TUI_INITIALIZED: AtomicBool = AtomicBool::new(false);
22static KEYBOARD_ENHANCEMENTS_PUSHED: AtomicBool = AtomicBool::new(false);
23static RESTORE_DONE: AtomicBool = AtomicBool::new(false);
24static DEBUG_MODE: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
25static COLOR_EYRE_ENABLED: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
26static SHOW_DIAGNOSTICS: AtomicBool = AtomicBool::new(false);
27static PANIC_HOOK_ONCE: Once = Once::new();
28#[cfg(debug_assertions)]
29static COLOR_EYRE_SETUP_ONCE: Once = Once::new();
30#[cfg(debug_assertions)]
31static COLOR_EYRE_PANIC_HOOK: std::sync::OnceLock<color_eyre::config::PanicHook> =
32 std::sync::OnceLock::new();
33static APP_METADATA: std::sync::OnceLock<AppMetadata> = std::sync::OnceLock::new();
34
35#[derive(Clone, Debug)]
36struct AppMetadata {
37 name: &'static str,
38 version: &'static str,
39 authors: &'static str,
40 repository: Option<&'static str>,
41}
42
43impl AppMetadata {
44 fn default_for_tui_crate() -> Self {
45 Self {
46 name: env!("CARGO_PKG_NAME"),
47 version: env!("CARGO_PKG_VERSION"),
48 authors: env!("CARGO_PKG_AUTHORS"),
49 repository: Some(env!("CARGO_PKG_REPOSITORY")).filter(|value| !value.is_empty()),
50 }
51 }
52}
53
54pub fn set_debug_mode(enabled: bool) {
58 DEBUG_MODE.store(enabled, Ordering::SeqCst);
59}
60
61pub fn is_debug_mode() -> bool {
63 DEBUG_MODE.load(Ordering::SeqCst)
64}
65
66pub fn set_color_eyre_enabled(enabled: bool) {
68 COLOR_EYRE_ENABLED.store(enabled, Ordering::SeqCst);
69}
70
71fn is_color_eyre_enabled() -> bool {
73 COLOR_EYRE_ENABLED.load(Ordering::SeqCst)
74}
75
76#[cfg(debug_assertions)]
78fn maybe_prepare_color_eyre_hooks() {
79 if !is_color_eyre_enabled() {
80 return;
81 }
82
83 COLOR_EYRE_SETUP_ONCE.call_once(|| {
84 let hooks = color_eyre::config::HookBuilder::default().try_into_hooks();
85 match hooks {
86 Ok((panic_hook, eyre_hook)) => {
87 let _ = COLOR_EYRE_PANIC_HOOK.set(panic_hook);
88 if let Err(error) = eyre_hook.install() {
89 eprintln!("warning: failed to install color-eyre hook: {error}");
90 }
91 }
92 Err(error) => {
93 eprintln!("warning: failed to prepare color-eyre hook: {error}");
94 }
95 }
96 });
97}
98
99pub fn print_error_report(error: anyhow::Error) {
101 if cfg!(debug_assertions) && is_color_eyre_enabled() {
102 #[cfg(debug_assertions)]
103 {
104 maybe_prepare_color_eyre_hooks();
105 let report = color_eyre::eyre::eyre!("{error:#}");
106 eprintln!("{report:?}");
107 return;
108 }
109 }
110
111 eprintln!("Error: {error:?}");
112}
113
114pub fn set_show_diagnostics(enabled: bool) {
117 SHOW_DIAGNOSTICS.store(enabled, Ordering::SeqCst);
118}
119
120pub fn show_diagnostics() -> bool {
122 SHOW_DIAGNOSTICS.load(Ordering::SeqCst)
123}
124
125pub fn set_app_metadata(
129 name: &'static str,
130 version: &'static str,
131 authors: &'static str,
132 repository: Option<&'static str>,
133) {
134 let _ = APP_METADATA.set(AppMetadata {
135 name,
136 version,
137 authors,
138 repository: repository.filter(|value| !value.is_empty()),
139 });
140}
141
142fn app_metadata() -> AppMetadata {
143 APP_METADATA
144 .get()
145 .cloned()
146 .unwrap_or_else(AppMetadata::default_for_tui_crate)
147}
148
149pub fn init_panic_hook() {
156 PANIC_HOOK_ONCE.call_once(|| {
157 let original_hook = panic::take_hook();
159
160 let better_panic_hook = BetterPanicSettings::new()
162 .verbosity(BetterPanicVerbosity::Full)
163 .most_recent_first(false)
164 .lineno_suffix(true)
165 .create_panic_handler();
166
167 panic::set_hook(Box::new(move |panic_info| {
168 let is_tui = TUI_INITIALIZED.load(Ordering::SeqCst);
169 let is_debug = DEBUG_MODE.load(Ordering::SeqCst);
170
171 if is_tui {
173 let _ = restore_tui();
175 }
176
177 if cfg!(debug_assertions) && is_debug {
178 if is_color_eyre_enabled() {
179 #[cfg(debug_assertions)]
180 {
181 maybe_prepare_color_eyre_hooks();
182 if let Some(panic_hook) = COLOR_EYRE_PANIC_HOOK.get() {
183 eprintln!("{}", panic_hook.panic_report(panic_info));
184 return;
185 }
186 }
187 }
188
189 better_panic_hook(panic_info);
190 return;
193 }
194
195 {
196 let metadata = app_metadata();
197 let mut report_metadata = HumanPanicMetadata::new(metadata.name, metadata.version)
198 .authors(format!("authored by {}", metadata.authors));
199
200 if let Some(repository) = metadata.repository {
201 report_metadata = report_metadata
202 .support(format!("Open a support request at {}", repository));
203 }
204
205 let file_path = human_panic_dump(&report_metadata, panic_info);
206 if let Err(error) = print_msg(file_path, &report_metadata) {
207 eprintln!("\nVT Code encountered a critical error and needs to shut down.");
208 eprintln!("Failed to print crash report details: {}", error);
209 original_hook(panic_info);
210 }
211 }
212
213 std::process::exit(1);
215 }));
216 });
217}
218
219pub fn mark_tui_initialized() {
221 TUI_INITIALIZED.store(true, Ordering::SeqCst);
222 RESTORE_DONE.store(false, Ordering::SeqCst);
223}
224
225pub fn mark_tui_deinitialized() {
227 TUI_INITIALIZED.store(false, Ordering::SeqCst);
228}
229
230pub fn mark_keyboard_enhancements_pushed(pushed: bool) {
232 KEYBOARD_ENHANCEMENTS_PUSHED.store(pushed, Ordering::SeqCst);
233}
234
235pub fn restore_tui() -> io::Result<()> {
244 if RESTORE_DONE.swap(true, Ordering::SeqCst) {
247 return Ok(());
248 }
249
250 mark_tui_deinitialized();
251 let mut first_error: Option<io::Error> = None;
252
253 while let Ok(true) = crossterm::event::poll(std::time::Duration::from_millis(0)) {
256 let _ = crossterm::event::read();
257 }
258
259 let mut stderr = io::stderr();
261
262 if let Err(error) = execute!(stderr, MoveToColumn(0), Clear(ClearType::CurrentLine)) {
264 first_error.get_or_insert(error);
265 }
266
267 if let Err(error) = execute!(stderr, LeaveAlternateScreen) {
270 first_error.get_or_insert(error);
271 }
272
273 if let Err(error) = execute!(stderr, DisableBracketedPaste) {
275 first_error.get_or_insert(error);
276 }
277 if let Err(error) = execute!(stderr, DisableFocusChange) {
278 first_error.get_or_insert(error);
279 }
280 if let Err(error) = execute!(stderr, DisableMouseCapture) {
281 first_error.get_or_insert(error);
282 }
283
284 if KEYBOARD_ENHANCEMENTS_PUSHED.swap(false, Ordering::SeqCst)
288 && let Err(error) = execute!(stderr, PopKeyboardEnhancementFlags)
289 {
290 first_error.get_or_insert(error);
291 }
292
293 crate::core_tui::runner::terminal_io::reset_mouse_pointer_shape();
294
295 if let Err(error) = execute!(
297 stderr,
298 SetCursorStyle::DefaultUserShape,
299 Show,
300 RestorePosition
301 ) {
302 first_error.get_or_insert(error);
303 }
304
305 if let Err(error) = disable_raw_mode() {
307 first_error.get_or_insert(error);
308 }
309
310 if let Err(error) = stderr.flush() {
312 first_error.get_or_insert(error);
313 }
314
315 match first_error {
316 Some(error) => Err(error),
317 None => Ok(()),
318 }
319}
320
321pub struct TuiPanicGuard;
326
327impl TuiPanicGuard {
328 pub fn new() -> Self {
332 mark_tui_initialized();
333 Self
334 }
335}
336
337impl Default for TuiPanicGuard {
338 fn default() -> Self {
339 Self::new()
340 }
341}
342
343impl Drop for TuiPanicGuard {
344 fn drop(&mut self) {
345 mark_tui_deinitialized();
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use std::sync::atomic::Ordering;
353
354 #[test]
355 fn test_panic_guard_initialization() {
356 TUI_INITIALIZED.store(false, Ordering::SeqCst);
358
359 {
360 let _guard = TuiPanicGuard::new();
361 assert!(
362 TUI_INITIALIZED.load(Ordering::SeqCst),
363 "TUI should be marked as initialized"
364 );
365
366 }
368
369 assert!(
370 !TUI_INITIALIZED.load(Ordering::SeqCst),
371 "TUI should be marked as deinitialized after guard drops"
372 );
373 }
374
375 #[test]
376 fn test_restore_terminal_no_panic_when_not_initialized() {
377 TUI_INITIALIZED.store(false, Ordering::SeqCst);
379
380 let result = restore_tui();
382 assert!(result.is_ok() || result.is_err());
384 }
385
386 #[test]
387 fn test_guard_lifecycle() {
388 TUI_INITIALIZED.store(false, Ordering::SeqCst);
389
390 {
392 let _guard = TuiPanicGuard::new();
393 assert!(
394 TUI_INITIALIZED.load(Ordering::SeqCst),
395 "Guard should mark TUI as initialized"
396 );
397 }
398
399 assert!(
400 !TUI_INITIALIZED.load(Ordering::SeqCst),
401 "Drop should mark TUI as deinitialized"
402 );
403 }
404
405 #[test]
406 fn test_color_eyre_toggle() {
407 set_color_eyre_enabled(false);
408 assert!(!is_color_eyre_enabled());
409
410 set_color_eyre_enabled(true);
411 assert!(is_color_eyre_enabled());
412 }
413}