use std::io::{self, Stdout, Write};
use crossterm::cursor::{Hide, Show};
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
pub fn restore() {
let mut out = io::stdout();
let _ = crossterm::execute!(out, LeaveAlternateScreen, Show);
let _ = disable_raw_mode();
let _ = out.flush();
}
pub fn install_panic_hook() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
restore();
previous(info);
}));
}
#[track_caller]
pub fn probe_panic_for_test() -> ! {
install_panic_hook();
panic!("shep_term_panic_probe: deliberate panic exercising restore-before-backtrace ordering");
}
pub fn enter() -> io::Result<Stdout> {
enable_raw_mode()?;
let mut out = io::stdout();
if let Err(err) = crossterm::execute!(out, EnterAlternateScreen, Hide) {
restore();
return Err(err);
}
Ok(out)
}
pub struct RestoreGuard {
action: Option<Box<dyn FnOnce()>>,
}
impl core::fmt::Debug for RestoreGuard {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("RestoreGuard")
.field("armed", &self.action.is_some())
.finish()
}
}
impl RestoreGuard {
#[must_use]
pub fn new() -> Self {
Self::with_action(restore)
}
#[must_use]
pub fn with_action(action: impl FnOnce() + 'static) -> Self {
Self {
action: Some(Box::new(action)),
}
}
}
impl Default for RestoreGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for RestoreGuard {
fn drop(&mut self) {
if let Some(action) = self.action.take() {
action();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restore_is_idempotent_outside_raw_mode() {
restore();
restore();
}
#[test]
fn the_guard_restores_when_it_is_dropped() {
let restored = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
{
let _guard = RestoreGuard::with_action({
let restored = std::sync::Arc::clone(&restored);
move || {
restored.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
});
}
assert_eq!(restored.load(std::sync::atomic::Ordering::SeqCst), 1);
}
}