use std::io::stdout;
use std::sync::{
Once,
atomic::{AtomicBool, Ordering},
};
use crossterm::{
cursor,
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{
ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
},
};
static TERMINAL_NEEDS_RESTORE: AtomicBool = AtomicBool::new(false);
static PANIC_HOOK_INSTALLED: Once = Once::new();
pub struct TerminalManager {
initialized: bool,
}
impl TerminalManager {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let mut manager = Self { initialized: false };
manager.initialize()?;
Ok(manager)
}
fn initialize(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if enable_raw_mode().is_err() {
return Err("Failed to enable raw mode - terminal not available".into());
}
let mut stdout = stdout();
if execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
crossterm::terminal::Clear(ClearType::All)
)
.is_err()
{
let _ = disable_raw_mode();
return Err("Failed to initialize terminal display".into());
}
TERMINAL_NEEDS_RESTORE.store(true, Ordering::SeqCst);
PANIC_HOOK_INSTALLED.call_once(install_panic_hook);
self.initialized = true;
Ok(())
}
#[allow(dead_code)] pub fn is_initialized(&self) -> bool {
self.initialized
}
}
impl Drop for TerminalManager {
fn drop(&mut self) {
if self.initialized {
restore_terminal();
}
}
}
impl Default for TerminalManager {
fn default() -> Self {
Self::new().unwrap_or_else(|_| Self { initialized: false })
}
}
pub fn restore_terminal() {
if !TERMINAL_NEEDS_RESTORE.swap(false, Ordering::SeqCst) {
return;
}
let mut stdout = std::io::stdout();
let _ = execute!(
stdout,
cursor::Show,
LeaveAlternateScreen,
DisableMouseCapture
);
let _ = disable_raw_mode();
}
fn install_panic_hook() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
restore_terminal();
previous(panic_info);
}));
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
static TEST_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn restore_terminal_is_noop_when_flag_unset() {
let _guard = TEST_MUTEX.lock().unwrap();
TERMINAL_NEEDS_RESTORE.store(false, Ordering::SeqCst);
restore_terminal();
restore_terminal();
assert!(!TERMINAL_NEEDS_RESTORE.load(Ordering::SeqCst));
}
#[test]
fn restore_terminal_clears_flag() {
let _guard = TEST_MUTEX.lock().unwrap();
TERMINAL_NEEDS_RESTORE.store(true, Ordering::SeqCst);
restore_terminal();
assert!(!TERMINAL_NEEDS_RESTORE.load(Ordering::SeqCst));
}
#[test]
fn restore_terminal_is_idempotent() {
let _guard = TEST_MUTEX.lock().unwrap();
TERMINAL_NEEDS_RESTORE.store(true, Ordering::SeqCst);
restore_terminal();
restore_terminal();
restore_terminal();
assert!(!TERMINAL_NEEDS_RESTORE.load(Ordering::SeqCst));
}
}