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