use std::io;
use std::panic::PanicHookInfo;
use std::sync::{Arc, Mutex};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use crate::error::{MindError, Result};
type PanicHook = Arc<dyn Fn(&PanicHookInfo) + Sync + Send>;
pub struct TermGuard {
prev_hook: Option<PanicHook>,
}
static TERMINAL: Mutex<Option<Terminal<CrosstermBackend<io::Stdout>>>> = Mutex::new(None);
impl TermGuard {
pub fn enter() -> Result<Self> {
let prev_hook: Arc<dyn Fn(&PanicHookInfo) + Sync + Send> =
Arc::from(std::panic::take_hook());
let hook_for_panic = Arc::clone(&prev_hook);
std::panic::set_hook(Box::new(move |info| {
let _ = restore();
hook_for_panic(info);
}));
enable_raw_mode().map_err(|e| MindError::io("<terminal>", e))?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen).map_err(|e| MindError::io("<terminal>", e))?;
let backend = CrosstermBackend::new(io::stdout());
let terminal = Terminal::new(backend).map_err(|e| MindError::io("<terminal>", e))?;
let mut slot = lock_terminal();
*slot = Some(terminal);
drop(slot);
crate::git::set_noninteractive(true);
Ok(TermGuard {
prev_hook: Some(prev_hook),
})
}
}
impl Drop for TermGuard {
fn drop(&mut self) {
let _ = restore();
crate::git::set_noninteractive(false);
if let Some(prev) = self.prev_hook.take() {
std::panic::set_hook(Box::new(move |info| prev(info)));
}
}
}
fn lock_terminal() -> std::sync::MutexGuard<'static, Option<Terminal<CrosstermBackend<io::Stdout>>>>
{
TERMINAL.lock().unwrap_or_else(|e| e.into_inner())
}
fn restore() -> std::result::Result<(), Box<dyn std::error::Error>> {
let terminal = {
let mut slot = lock_terminal();
slot.take()
};
if let Some(mut t) = terminal {
let _ = disable_raw_mode();
let _ = execute!(t.backend_mut(), LeaveAlternateScreen);
let _ = t.show_cursor();
}
Ok(())
}
pub fn with_suspended<R>(f: impl FnOnce() -> R) -> R {
struct ResumeGuard;
impl Drop for ResumeGuard {
fn drop(&mut self) {
crate::git::set_noninteractive(true);
let _ = enable_raw_mode();
let mut stdout = io::stdout();
let _ = execute!(stdout, EnterAlternateScreen);
if let Some(t) = lock_terminal().as_mut() {
let _ = t.clear();
}
}
}
let _ = disable_raw_mode();
{
let mut stdout = io::stdout();
let _ = execute!(stdout, LeaveAlternateScreen, crossterm::cursor::Show);
}
crate::git::set_noninteractive(false);
let _resume = ResumeGuard;
f()
}
pub struct TerminalGuard(
std::sync::MutexGuard<'static, Option<Terminal<CrosstermBackend<io::Stdout>>>>,
);
impl std::ops::Deref for TerminalGuard {
type Target = Terminal<CrosstermBackend<io::Stdout>>;
fn deref(&self) -> &Self::Target {
self.0.as_ref().expect("terminal not initialized")
}
}
impl std::ops::DerefMut for TerminalGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.as_mut().expect("terminal not initialized")
}
}
pub fn get_terminal() -> TerminalGuard {
TerminalGuard(lock_terminal())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
static SERIAL: Mutex<()> = Mutex::new(());
#[test]
fn restore_recovers_from_poisoned_terminal_mutex() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let handle = std::thread::spawn(|| {
let _guard = TERMINAL.lock().unwrap();
panic!("intentional poison");
});
assert!(
handle.join().is_err(),
"poisoning thread should have panicked"
);
assert!(TERMINAL.is_poisoned(), "mutex should now be poisoned");
let result = std::panic::catch_unwind(restore);
assert!(result.is_ok(), "restore() panicked on a poisoned mutex");
assert!(
matches!(result, Ok(Ok(()))),
"restore() should return Ok even when the mutex is poisoned"
);
let mut slot = lock_terminal();
assert!(slot.is_none(), "restore() should have left the slot empty");
*slot = None;
}
#[test]
fn drop_restores_previous_panic_hook() {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
static SENTINEL_HITS: AtomicUsize = AtomicUsize::new(0);
let harness_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_info| {
SENTINEL_HITS.fetch_add(1, Ordering::SeqCst);
}));
let prev_hook: Arc<dyn Fn(&PanicHookInfo) + Sync + Send> =
Arc::from(std::panic::take_hook());
let hook_for_panic = Arc::clone(&prev_hook);
std::panic::set_hook(Box::new(move |info| {
let _ = restore();
hook_for_panic(info);
}));
let guard = TermGuard {
prev_hook: Some(prev_hook),
};
let before = SENTINEL_HITS.load(Ordering::SeqCst);
let _ = std::panic::catch_unwind(|| panic!("through wrapper"));
assert_eq!(
SENTINEL_HITS.load(Ordering::SeqCst),
before + 1,
"installed hook must still call the previous hook on a panic"
);
drop(guard);
let before = SENTINEL_HITS.load(Ordering::SeqCst);
let _ = std::panic::catch_unwind(|| panic!("after drop"));
assert_eq!(
SENTINEL_HITS.load(Ordering::SeqCst),
before + 1,
"after drop the previous (sentinel) hook should be back in effect"
);
std::panic::set_hook(harness_hook);
}
}