use std::io::{self, Write};
use std::panic;
use std::sync::Once;
use std::sync::atomic::{AtomicBool, Ordering};
use better_panic::{Settings as BetterPanicSettings, Verbosity as BetterPanicVerbosity};
use human_panic::{Metadata as HumanPanicMetadata, handle_dump as human_panic_dump, print_msg};
use ratatui::crossterm::{
cursor::{MoveToColumn, RestorePosition, SetCursorStyle, Show},
event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, PopKeyboardEnhancementFlags,
},
execute,
terminal::{Clear, ClearType, LeaveAlternateScreen, disable_raw_mode},
};
static TUI_INITIALIZED: AtomicBool = AtomicBool::new(false);
static KEYBOARD_ENHANCEMENTS_PUSHED: AtomicBool = AtomicBool::new(false);
static RESTORE_DONE: AtomicBool = AtomicBool::new(false);
static DEBUG_MODE: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
static COLOR_EYRE_ENABLED: AtomicBool = AtomicBool::new(cfg!(debug_assertions));
static SHOW_DIAGNOSTICS: AtomicBool = AtomicBool::new(false);
static PANIC_HOOK_ONCE: Once = Once::new();
#[cfg(debug_assertions)]
static COLOR_EYRE_SETUP_ONCE: Once = Once::new();
#[cfg(debug_assertions)]
static COLOR_EYRE_PANIC_HOOK: std::sync::OnceLock<color_eyre::config::PanicHook> =
std::sync::OnceLock::new();
static APP_METADATA: std::sync::OnceLock<AppMetadata> = std::sync::OnceLock::new();
#[derive(Clone, Debug)]
struct AppMetadata {
name: &'static str,
version: &'static str,
authors: &'static str,
repository: Option<&'static str>,
}
impl AppMetadata {
fn default_for_tui_crate() -> Self {
Self {
name: env!("CARGO_PKG_NAME"),
version: env!("CARGO_PKG_VERSION"),
authors: env!("CARGO_PKG_AUTHORS"),
repository: Some(env!("CARGO_PKG_REPOSITORY")).filter(|value| !value.is_empty()),
}
}
}
pub fn set_debug_mode(enabled: bool) {
DEBUG_MODE.store(enabled, Ordering::SeqCst);
}
pub fn is_debug_mode() -> bool {
DEBUG_MODE.load(Ordering::SeqCst)
}
pub fn set_color_eyre_enabled(enabled: bool) {
COLOR_EYRE_ENABLED.store(enabled, Ordering::SeqCst);
}
fn is_color_eyre_enabled() -> bool {
COLOR_EYRE_ENABLED.load(Ordering::SeqCst)
}
#[cfg(debug_assertions)]
fn maybe_prepare_color_eyre_hooks() {
if !is_color_eyre_enabled() {
return;
}
COLOR_EYRE_SETUP_ONCE.call_once(|| {
let hooks = color_eyre::config::HookBuilder::default().try_into_hooks();
match hooks {
Ok((panic_hook, eyre_hook)) => {
let _ = COLOR_EYRE_PANIC_HOOK.set(panic_hook);
if let Err(error) = eyre_hook.install() {
eprintln!("warning: failed to install color-eyre hook: {error}");
}
}
Err(error) => {
eprintln!("warning: failed to prepare color-eyre hook: {error}");
}
}
});
}
pub fn print_error_report(error: anyhow::Error) {
if cfg!(debug_assertions) && is_color_eyre_enabled() {
#[cfg(debug_assertions)]
{
maybe_prepare_color_eyre_hooks();
let report = color_eyre::eyre::eyre!("{error:#}");
eprintln!("{report:?}");
return;
}
}
eprintln!("Error: {error:?}");
}
pub fn set_show_diagnostics(enabled: bool) {
SHOW_DIAGNOSTICS.store(enabled, Ordering::SeqCst);
}
pub fn show_diagnostics() -> bool {
SHOW_DIAGNOSTICS.load(Ordering::SeqCst)
}
pub fn set_app_metadata(
name: &'static str,
version: &'static str,
authors: &'static str,
repository: Option<&'static str>,
) {
let _ = APP_METADATA.set(AppMetadata {
name,
version,
authors,
repository: repository.filter(|value| !value.is_empty()),
});
}
fn app_metadata() -> AppMetadata {
APP_METADATA
.get()
.cloned()
.unwrap_or_else(AppMetadata::default_for_tui_crate)
}
pub fn init_panic_hook() {
PANIC_HOOK_ONCE.call_once(|| {
let original_hook = panic::take_hook();
let better_panic_hook = BetterPanicSettings::new()
.verbosity(BetterPanicVerbosity::Full)
.most_recent_first(false)
.lineno_suffix(true)
.create_panic_handler();
panic::set_hook(Box::new(move |panic_info| {
let is_tui = TUI_INITIALIZED.load(Ordering::SeqCst);
let is_debug = DEBUG_MODE.load(Ordering::SeqCst);
if is_tui {
let _ = restore_tui();
}
if cfg!(debug_assertions) && is_debug {
if is_color_eyre_enabled() {
#[cfg(debug_assertions)]
{
maybe_prepare_color_eyre_hooks();
if let Some(panic_hook) = COLOR_EYRE_PANIC_HOOK.get() {
eprintln!("{}", panic_hook.panic_report(panic_info));
return;
}
}
}
better_panic_hook(panic_info);
return;
}
{
let metadata = app_metadata();
let mut report_metadata = HumanPanicMetadata::new(metadata.name, metadata.version)
.authors(format!("authored by {}", metadata.authors));
if let Some(repository) = metadata.repository {
report_metadata = report_metadata
.support(format!("Open a support request at {}", repository));
}
let file_path = human_panic_dump(&report_metadata, panic_info);
if let Err(error) = print_msg(file_path, &report_metadata) {
eprintln!("\nVT Code encountered a critical error and needs to shut down.");
eprintln!("Failed to print crash report details: {}", error);
original_hook(panic_info);
}
}
std::process::exit(1);
}));
});
}
pub fn mark_tui_initialized() {
TUI_INITIALIZED.store(true, Ordering::SeqCst);
RESTORE_DONE.store(false, Ordering::SeqCst);
}
pub fn mark_tui_deinitialized() {
TUI_INITIALIZED.store(false, Ordering::SeqCst);
}
pub fn mark_keyboard_enhancements_pushed(pushed: bool) {
KEYBOARD_ENHANCEMENTS_PUSHED.store(pushed, Ordering::SeqCst);
}
pub fn restore_tui() -> io::Result<()> {
if RESTORE_DONE.swap(true, Ordering::SeqCst) {
return Ok(());
}
mark_tui_deinitialized();
let mut first_error: Option<io::Error> = None;
while let Ok(true) = crossterm::event::poll(std::time::Duration::from_millis(0)) {
let _ = crossterm::event::read();
}
let mut stderr = io::stderr();
if let Err(error) = execute!(stderr, MoveToColumn(0), Clear(ClearType::CurrentLine)) {
first_error.get_or_insert(error);
}
if let Err(error) = execute!(stderr, LeaveAlternateScreen) {
first_error.get_or_insert(error);
}
if let Err(error) = execute!(stderr, DisableBracketedPaste) {
first_error.get_or_insert(error);
}
if let Err(error) = execute!(stderr, DisableFocusChange) {
first_error.get_or_insert(error);
}
if let Err(error) = execute!(stderr, DisableMouseCapture) {
first_error.get_or_insert(error);
}
if KEYBOARD_ENHANCEMENTS_PUSHED.swap(false, Ordering::SeqCst)
&& let Err(error) = execute!(stderr, PopKeyboardEnhancementFlags)
{
first_error.get_or_insert(error);
}
crate::core_tui::runner::terminal_io::reset_mouse_pointer_shape();
if let Err(error) = execute!(
stderr,
SetCursorStyle::DefaultUserShape,
Show,
RestorePosition
) {
first_error.get_or_insert(error);
}
if let Err(error) = disable_raw_mode() {
first_error.get_or_insert(error);
}
if let Err(error) = stderr.flush() {
first_error.get_or_insert(error);
}
match first_error {
Some(error) => Err(error),
None => Ok(()),
}
}
pub struct TuiPanicGuard;
impl TuiPanicGuard {
pub fn new() -> Self {
mark_tui_initialized();
Self
}
}
impl Default for TuiPanicGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for TuiPanicGuard {
fn drop(&mut self) {
mark_tui_deinitialized();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::Ordering;
#[test]
fn test_panic_guard_initialization() {
TUI_INITIALIZED.store(false, Ordering::SeqCst);
{
let _guard = TuiPanicGuard::new();
assert!(
TUI_INITIALIZED.load(Ordering::SeqCst),
"TUI should be marked as initialized"
);
}
assert!(
!TUI_INITIALIZED.load(Ordering::SeqCst),
"TUI should be marked as deinitialized after guard drops"
);
}
#[test]
fn test_restore_terminal_no_panic_when_not_initialized() {
TUI_INITIALIZED.store(false, Ordering::SeqCst);
let result = restore_tui();
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_guard_lifecycle() {
TUI_INITIALIZED.store(false, Ordering::SeqCst);
{
let _guard = TuiPanicGuard::new();
assert!(
TUI_INITIALIZED.load(Ordering::SeqCst),
"Guard should mark TUI as initialized"
);
}
assert!(
!TUI_INITIALIZED.load(Ordering::SeqCst),
"Drop should mark TUI as deinitialized"
);
}
#[test]
fn test_color_eyre_toggle() {
set_color_eyre_enabled(false);
assert!(!is_color_eyre_enabled());
set_color_eyre_enabled(true);
assert!(is_color_eyre_enabled());
}
}