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 DEBUG_MODE: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
23static COLOR_EYRE_ENABLED: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
24static SHOW_DIAGNOSTICS: AtomicBool = AtomicBool::new(false);
25static PANIC_HOOK_ONCE: Once = Once::new();
26static COLOR_EYRE_SETUP_ONCE: Once = Once::new();
27#[cfg(debug_assertions)]
28static COLOR_EYRE_PANIC_HOOK: std::sync::OnceLock<color_eyre::config::PanicHook> =
29 std::sync::OnceLock::new();
30static APP_METADATA: std::sync::OnceLock<AppMetadata> = std::sync::OnceLock::new();
31
32#[derive(Clone, Debug)]
33struct AppMetadata {
34 name: &'static str,
35 version: &'static str,
36 authors: &'static str,
37 repository: Option<&'static str>,
38}
39
40impl AppMetadata {
41 fn default_for_tui_crate() -> Self {
42 Self {
43 name: env!("CARGO_PKG_NAME"),
44 version: env!("CARGO_PKG_VERSION"),
45 authors: env!("CARGO_PKG_AUTHORS"),
46 repository: Some(env!("CARGO_PKG_REPOSITORY")).filter(|value| !value.is_empty()),
47 }
48 }
49}
50
51pub fn set_debug_mode(enabled: bool) {
55 DEBUG_MODE.store(enabled, Ordering::SeqCst);
56}
57
58pub fn is_debug_mode() -> bool {
60 DEBUG_MODE.load(Ordering::SeqCst)
61}
62
63pub fn set_color_eyre_enabled(enabled: bool) {
65 COLOR_EYRE_ENABLED.store(enabled, Ordering::SeqCst);
66}
67
68fn is_color_eyre_enabled() -> bool {
70 COLOR_EYRE_ENABLED.load(Ordering::SeqCst)
71}
72
73fn maybe_prepare_color_eyre_hooks() {
75 if !cfg!(debug_assertions) || !is_color_eyre_enabled() {
76 return;
77 }
78
79 #[cfg(debug_assertions)]
80 COLOR_EYRE_SETUP_ONCE.call_once(|| {
81 let hooks = color_eyre::config::HookBuilder::default().try_into_hooks();
82 match hooks {
83 Ok((panic_hook, eyre_hook)) => {
84 let _ = COLOR_EYRE_PANIC_HOOK.set(panic_hook);
85 if let Err(error) = eyre_hook.install() {
86 eprintln!("warning: failed to install color-eyre hook: {error}");
87 }
88 }
89 Err(error) => {
90 eprintln!("warning: failed to prepare color-eyre hook: {error}");
91 }
92 }
93 });
94}
95
96pub fn print_error_report(error: anyhow::Error) {
98 if cfg!(debug_assertions) && is_color_eyre_enabled() {
99 #[cfg(debug_assertions)]
100 {
101 maybe_prepare_color_eyre_hooks();
102 let report = color_eyre::eyre::eyre!("{error:#}");
103 eprintln!("{report:?}");
104 return;
105 }
106 }
107
108 eprintln!("Error: {error:?}");
109}
110
111pub fn set_show_diagnostics(enabled: bool) {
114 SHOW_DIAGNOSTICS.store(enabled, Ordering::SeqCst);
115}
116
117pub fn show_diagnostics() -> bool {
119 SHOW_DIAGNOSTICS.load(Ordering::SeqCst)
120}
121
122pub fn set_app_metadata(
126 name: &'static str,
127 version: &'static str,
128 authors: &'static str,
129 repository: Option<&'static str>,
130) {
131 let _ = APP_METADATA.set(AppMetadata {
132 name,
133 version,
134 authors,
135 repository: repository.filter(|value| !value.is_empty()),
136 });
137}
138
139fn app_metadata() -> AppMetadata {
140 APP_METADATA
141 .get()
142 .cloned()
143 .unwrap_or_else(AppMetadata::default_for_tui_crate)
144}
145
146pub fn init_panic_hook() {
153 PANIC_HOOK_ONCE.call_once(|| {
154 let original_hook = panic::take_hook();
156
157 let better_panic_hook = BetterPanicSettings::new()
159 .verbosity(BetterPanicVerbosity::Full)
160 .most_recent_first(false)
161 .lineno_suffix(true)
162 .create_panic_handler();
163
164 panic::set_hook(Box::new(move |panic_info| {
165 let is_tui = TUI_INITIALIZED.load(Ordering::SeqCst);
166 let is_debug = DEBUG_MODE.load(Ordering::SeqCst);
167
168 if is_tui {
170 let _ = restore_tui();
172 }
173
174 if cfg!(debug_assertions) && is_debug {
175 if is_color_eyre_enabled() {
176 #[cfg(debug_assertions)]
177 {
178 maybe_prepare_color_eyre_hooks();
179 if let Some(panic_hook) = COLOR_EYRE_PANIC_HOOK.get() {
180 eprintln!("{}", panic_hook.panic_report(panic_info));
181 return;
182 }
183 }
184 }
185
186 better_panic_hook(panic_info);
187 return;
190 }
191
192 {
193 let metadata = app_metadata();
194 let mut report_metadata = HumanPanicMetadata::new(metadata.name, metadata.version)
195 .authors(format!("authored by {}", metadata.authors));
196
197 if let Some(repository) = metadata.repository {
198 report_metadata = report_metadata
199 .support(format!("Open a support request at {}", repository));
200 }
201
202 let file_path = human_panic_dump(&report_metadata, panic_info);
203 if let Err(error) = print_msg(file_path, &report_metadata) {
204 eprintln!("\nVT Code encountered a critical error and needs to shut down.");
205 eprintln!("Failed to print crash report details: {}", error);
206 original_hook(panic_info);
207 }
208 }
209
210 std::process::exit(1);
212 }));
213 });
214}
215
216pub fn mark_tui_initialized() {
218 TUI_INITIALIZED.store(true, Ordering::SeqCst);
219}
220
221pub fn mark_tui_deinitialized() {
223 TUI_INITIALIZED.store(false, Ordering::SeqCst);
224}
225
226pub fn restore_tui() -> io::Result<()> {
235 mark_tui_deinitialized();
236 let mut first_error: Option<io::Error> = None;
237
238 while let Ok(true) = crossterm::event::poll(std::time::Duration::from_millis(0)) {
241 let _ = crossterm::event::read();
242 }
243
244 let mut stderr = io::stderr();
246
247 if let Err(error) = execute!(stderr, MoveToColumn(0), Clear(ClearType::CurrentLine)) {
249 first_error.get_or_insert(error);
250 }
251
252 if let Err(error) = execute!(stderr, LeaveAlternateScreen) {
255 first_error.get_or_insert(error);
256 }
257
258 if let Err(error) = execute!(stderr, DisableBracketedPaste) {
260 first_error.get_or_insert(error);
261 }
262 if let Err(error) = execute!(stderr, DisableFocusChange) {
263 first_error.get_or_insert(error);
264 }
265 if let Err(error) = execute!(stderr, DisableMouseCapture) {
266 first_error.get_or_insert(error);
267 }
268 if let Err(error) = execute!(stderr, PopKeyboardEnhancementFlags) {
269 first_error.get_or_insert(error);
270 }
271
272 if let Err(error) = execute!(
274 stderr,
275 SetCursorStyle::DefaultUserShape,
276 Show,
277 RestorePosition
278 ) {
279 first_error.get_or_insert(error);
280 }
281
282 if let Err(error) = disable_raw_mode() {
284 first_error.get_or_insert(error);
285 }
286
287 if let Err(error) = stderr.flush() {
289 first_error.get_or_insert(error);
290 }
291
292 match first_error {
293 Some(error) => Err(error),
294 None => Ok(()),
295 }
296}
297
298pub struct TuiPanicGuard;
303
304impl TuiPanicGuard {
305 pub fn new() -> Self {
309 mark_tui_initialized();
310 Self
311 }
312}
313
314impl Default for TuiPanicGuard {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320impl Drop for TuiPanicGuard {
321 fn drop(&mut self) {
322 mark_tui_deinitialized();
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use std::sync::atomic::Ordering;
330
331 #[test]
332 fn test_panic_guard_initialization() {
333 TUI_INITIALIZED.store(false, Ordering::SeqCst);
335
336 {
337 let _guard = TuiPanicGuard::new();
338 assert!(
339 TUI_INITIALIZED.load(Ordering::SeqCst),
340 "TUI should be marked as initialized"
341 );
342
343 }
345
346 assert!(
347 !TUI_INITIALIZED.load(Ordering::SeqCst),
348 "TUI should be marked as deinitialized after guard drops"
349 );
350 }
351
352 #[test]
353 fn test_restore_terminal_no_panic_when_not_initialized() {
354 TUI_INITIALIZED.store(false, Ordering::SeqCst);
356
357 let result = restore_tui();
359 assert!(result.is_ok() || result.is_err());
361 }
362
363 #[test]
364 fn test_guard_lifecycle() {
365 TUI_INITIALIZED.store(false, Ordering::SeqCst);
366
367 {
369 let _guard = TuiPanicGuard::new();
370 assert!(
371 TUI_INITIALIZED.load(Ordering::SeqCst),
372 "Guard should mark TUI as initialized"
373 );
374 }
375
376 assert!(
377 !TUI_INITIALIZED.load(Ordering::SeqCst),
378 "Drop should mark TUI as deinitialized"
379 );
380 }
381
382 #[test]
383 fn test_color_eyre_toggle() {
384 set_color_eyre_enabled(false);
385 assert!(!is_color_eyre_enabled());
386
387 set_color_eyre_enabled(true);
388 assert!(is_color_eyre_enabled());
389 }
390}