use std::io::{self, Stdout};
use std::panic;
use std::sync::atomic::{AtomicBool, Ordering};
use crossterm::cursor::{Hide, Show};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use crate::error::ChainViewError;
pub(crate) trait TerminalOps {
fn enable_raw_mode(&mut self) -> io::Result<()>;
fn disable_raw_mode(&mut self) -> io::Result<()>;
fn enter_alternate_screen(&mut self) -> io::Result<()>;
fn leave_alternate_screen(&mut self) -> io::Result<()>;
fn hide_cursor(&mut self) -> io::Result<()>;
fn show_cursor(&mut self) -> io::Result<()>;
}
pub(crate) struct CrosstermOps;
impl TerminalOps for CrosstermOps {
#[inline]
fn enable_raw_mode(&mut self) -> io::Result<()> {
enable_raw_mode()
}
#[inline]
fn disable_raw_mode(&mut self) -> io::Result<()> {
disable_raw_mode()
}
#[inline]
fn enter_alternate_screen(&mut self) -> io::Result<()> {
let mut out: Stdout = io::stdout();
execute!(out, EnterAlternateScreen)
}
#[inline]
fn leave_alternate_screen(&mut self) -> io::Result<()> {
let mut out: Stdout = io::stdout();
execute!(out, LeaveAlternateScreen)
}
#[inline]
fn hide_cursor(&mut self) -> io::Result<()> {
let mut out: Stdout = io::stdout();
execute!(out, Hide)
}
#[inline]
fn show_cursor(&mut self) -> io::Result<()> {
let mut out: Stdout = io::stdout();
execute!(out, Show)
}
}
#[cold]
#[inline(never)]
fn terminal_error(err: io::Error) -> ChainViewError {
ChainViewError::Terminal(err.to_string())
}
pub(crate) struct Guard<O: TerminalOps> {
ops: O,
raw_enabled: bool,
alt_screen: bool,
cursor_hidden: bool,
restored: bool,
}
impl<O: TerminalOps> Guard<O> {
fn new(ops: O) -> Result<Self, ChainViewError> {
let mut guard = Self {
ops,
raw_enabled: false,
alt_screen: false,
cursor_hidden: false,
restored: false,
};
if let Err(err) = guard.enter() {
guard.restore();
return Err(err);
}
Ok(guard)
}
fn enter(&mut self) -> Result<(), ChainViewError> {
self.ops.enable_raw_mode().map_err(terminal_error)?;
self.raw_enabled = true;
self.ops.enter_alternate_screen().map_err(terminal_error)?;
self.alt_screen = true;
self.ops.hide_cursor().map_err(terminal_error)?;
self.cursor_hidden = true;
Ok(())
}
fn restore(&mut self) {
if self.restored {
return;
}
if self.cursor_hidden && self.ops.show_cursor().is_ok() {
self.cursor_hidden = false;
}
if self.alt_screen && self.ops.leave_alternate_screen().is_ok() {
self.alt_screen = false;
}
if self.raw_enabled && self.ops.disable_raw_mode().is_ok() {
self.raw_enabled = false;
}
self.restored = true;
}
}
impl<O: TerminalOps> Drop for Guard<O> {
fn drop(&mut self) {
self.restore();
}
}
#[must_use = "hold the guard for the terminal's lifetime; dropping it restores the terminal"]
pub struct TerminalGuard {
inner: Guard<CrosstermOps>,
}
impl TerminalGuard {
pub fn new() -> Result<Self, ChainViewError> {
Ok(Self {
inner: Guard::new(CrosstermOps)?,
})
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
self.inner.restore();
}
}
static SUPERVISOR_OWNS_RESTORE: AtomicBool = AtomicBool::new(false);
pub(crate) fn set_supervisor_owns_restore(owned: bool) {
SUPERVISOR_OWNS_RESTORE.store(owned, Ordering::SeqCst);
}
fn supervisor_owns_restore() -> bool {
SUPERVISOR_OWNS_RESTORE.load(Ordering::SeqCst)
}
fn should_hook_restore(supervisor_owns_restore: bool) -> bool {
!supervisor_owns_restore
}
thread_local! {
static CONTAINED_PANIC_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(crate) struct ContainedPanicGuard;
impl ContainedPanicGuard {
pub(crate) fn new() -> Self {
CONTAINED_PANIC_DEPTH.with(|d| {
if let Some(next) = d.get().checked_add(1) {
d.set(next);
}
});
Self
}
}
impl Drop for ContainedPanicGuard {
fn drop(&mut self) {
CONTAINED_PANIC_DEPTH.with(|d| {
let next = match d.get() {
0 => 0,
n => n - 1,
};
d.set(next);
});
}
}
fn contained_panic_active() -> bool {
CONTAINED_PANIC_DEPTH.with(|d| d.get() > 0)
}
pub(crate) fn contained<T>(op: impl FnOnce() -> T) -> Option<T> {
let _contained = ContainedPanicGuard::new();
panic::catch_unwind(panic::AssertUnwindSafe(op)).ok()
}
pub fn install_panic_hook() {
let previous = panic::take_hook();
panic::set_hook(Box::new(move |info| {
if contained_panic_active() {
return;
}
restore_then_chain(restore_on_panic, |i| previous(i), info);
}));
}
#[inline]
fn restore_then_chain<T>(restore: impl FnOnce(), next: impl FnOnce(&T), payload: &T) {
restore();
next(payload);
}
fn restore_on_panic() {
if !should_hook_restore(supervisor_owns_restore()) {
return;
}
let mut out: Stdout = io::stdout();
let _ = execute!(out, Show, LeaveAlternateScreen);
let _ = disable_raw_mode();
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
EnableRaw,
DisableRaw,
EnterAlt,
LeaveAlt,
HideCursor,
ShowCursor,
}
struct FakeOps {
log: Rc<RefCell<Vec<Op>>>,
fail_on: Option<Op>,
}
impl FakeOps {
fn new(log: Rc<RefCell<Vec<Op>>>) -> Self {
Self { log, fail_on: None }
}
fn failing(log: Rc<RefCell<Vec<Op>>>, fail_on: Op) -> Self {
Self {
log,
fail_on: Some(fail_on),
}
}
fn run(&mut self, op: Op) -> io::Result<()> {
if self.fail_on == Some(op) {
return Err(io::Error::other("injected terminal failure"));
}
self.log.borrow_mut().push(op);
Ok(())
}
}
impl TerminalOps for FakeOps {
fn enable_raw_mode(&mut self) -> io::Result<()> {
self.run(Op::EnableRaw)
}
fn disable_raw_mode(&mut self) -> io::Result<()> {
self.run(Op::DisableRaw)
}
fn enter_alternate_screen(&mut self) -> io::Result<()> {
self.run(Op::EnterAlt)
}
fn leave_alternate_screen(&mut self) -> io::Result<()> {
self.run(Op::LeaveAlt)
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.run(Op::HideCursor)
}
fn show_cursor(&mut self) -> io::Result<()> {
self.run(Op::ShowCursor)
}
}
fn new_log() -> Rc<RefCell<Vec<Op>>> {
Rc::new(RefCell::new(Vec::new()))
}
#[test]
fn test_guard_new_records_setup_sequence_in_order() {
let log = new_log();
let guard = match Guard::new(FakeOps::new(Rc::clone(&log))) {
Ok(g) => g,
Err(e) => panic!("expected setup to succeed, got: {e}"),
};
assert_eq!(
*log.borrow(),
vec![Op::EnableRaw, Op::EnterAlt, Op::HideCursor]
);
drop(guard);
}
#[test]
fn test_guard_drop_restores_inverse_sequence() {
let log = new_log();
let guard = match Guard::new(FakeOps::new(Rc::clone(&log))) {
Ok(g) => g,
Err(e) => panic!("expected setup to succeed, got: {e}"),
};
drop(guard);
assert_eq!(
*log.borrow(),
vec![
Op::EnableRaw,
Op::EnterAlt,
Op::HideCursor,
Op::ShowCursor,
Op::LeaveAlt,
Op::DisableRaw,
]
);
}
#[test]
fn test_guard_restore_continues_past_a_failed_step_and_keeps_the_flag_truthful() {
let log = new_log();
let mut guard = match Guard::new(FakeOps::failing(Rc::clone(&log), Op::ShowCursor)) {
Ok(g) => g,
Err(e) => panic!("expected setup to succeed, got: {e}"),
};
guard.restore();
assert_eq!(
*log.borrow(),
vec![
Op::EnableRaw,
Op::EnterAlt,
Op::HideCursor,
Op::LeaveAlt,
Op::DisableRaw,
]
);
assert!(
guard.cursor_hidden,
"a failed show_cursor must not clear the flag"
);
assert!(!guard.alt_screen);
assert!(!guard.raw_enabled);
assert!(guard.restored);
}
#[test]
fn test_guard_double_restore_is_idempotent() {
let log = new_log();
let mut guard = match Guard::new(FakeOps::new(Rc::clone(&log))) {
Ok(g) => g,
Err(e) => panic!("expected setup to succeed, got: {e}"),
};
guard.restore();
guard.restore();
drop(guard);
assert_eq!(
*log.borrow(),
vec![
Op::EnableRaw,
Op::EnterAlt,
Op::HideCursor,
Op::ShowCursor,
Op::LeaveAlt,
Op::DisableRaw,
]
);
}
#[test]
fn test_guard_partial_setup_teardown_undoes_only_applied_steps() {
let log = new_log();
let mut guard = Guard {
ops: FakeOps::new(Rc::clone(&log)),
raw_enabled: true,
alt_screen: false,
cursor_hidden: false,
restored: false,
};
guard.restore();
drop(guard);
assert_eq!(*log.borrow(), vec![Op::DisableRaw]);
}
#[test]
fn test_guard_new_setup_failure_rolls_back_applied_prefix() {
let log = new_log();
let err = match Guard::new(FakeOps::failing(Rc::clone(&log), Op::EnterAlt)) {
Err(e) => e,
Ok(_) => panic!("expected setup to fail at the alternate screen"),
};
assert!(matches!(err, ChainViewError::Terminal(_)));
assert_eq!(*log.borrow(), vec![Op::EnableRaw, Op::DisableRaw]);
}
#[test]
fn test_guard_restore_tolerates_backend_error_and_continues() {
let log = new_log();
let mut guard = Guard {
ops: FakeOps::failing(Rc::clone(&log), Op::ShowCursor),
raw_enabled: true,
alt_screen: true,
cursor_hidden: true,
restored: false,
};
guard.restore();
drop(guard);
assert_eq!(*log.borrow(), vec![Op::LeaveAlt, Op::DisableRaw]);
}
#[test]
fn test_restore_then_chain_runs_restore_before_chained_hook() {
let order: RefCell<Vec<&'static str>> = RefCell::new(Vec::new());
restore_then_chain(
|| order.borrow_mut().push("restore"),
|_payload: &u8| order.borrow_mut().push("chained"),
&0u8,
);
assert_eq!(*order.borrow(), vec!["restore", "chained"]);
}
#[test]
fn test_should_hook_restore_defers_to_an_active_supervisor() {
assert!(
should_hook_restore(false),
"no supervisor active: the panic hook owns the restore"
);
assert!(
!should_hook_restore(true),
"a supervisor owns the ordered restore: the hook defers"
);
}
#[test]
fn test_contained_returns_some_when_the_operation_returns() {
assert_eq!(contained(|| 7_u8), Some(7));
assert_eq!(contained(|| "curve".to_owned()), Some("curve".to_owned()));
}
#[test]
fn test_contained_returns_none_when_the_operation_panics() {
let caught: Option<u8> = contained(|| panic!("upstream blew up"));
assert_eq!(caught, None, "a panicking op yields None, not an unwind");
}
#[test]
fn test_contained_nests_and_restores_the_guard_depth() {
assert!(
!contained_panic_active(),
"no boundary is active before the test"
);
let outer = contained(|| {
assert!(contained_panic_active(), "the outer boundary is active");
let inner: Option<()> = contained(|| panic!("inner"));
assert_eq!(inner, None, "the inner panic is contained");
assert!(
contained_panic_active(),
"the outer boundary survives the inner panic"
);
"done"
});
assert_eq!(outer, Some("done"));
assert!(
!contained_panic_active(),
"the guard depth is restored, so an uncontained panic still runs the hook"
);
}
#[test]
fn test_restore_then_chain_always_invokes_chained_hook() {
let chained = RefCell::new(false);
restore_then_chain(|| {}, |_p: &()| *chained.borrow_mut() = true, &());
assert!(*chained.borrow());
}
}