use std::io;
use std::io::stdout;
use crossterm::cursor::Hide;
use crossterm::cursor::Show;
use crossterm::event::DisableBracketedPaste;
use crossterm::event::DisableFocusChange;
use crossterm::event::EnableBracketedPaste;
use crossterm::event::EnableFocusChange;
use crossterm::execute;
use crossterm::terminal::disable_raw_mode;
use crossterm::terminal::enable_raw_mode;
use crossterm::terminal::EnterAlternateScreen;
use crossterm::terminal::LeaveAlternateScreen;
pub trait TerminalOps {
fn set_raw_mode(&mut self, enabled: bool) -> io::Result<()>;
fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()>;
fn set_focus_events(&mut self, enabled: bool) -> io::Result<()>;
fn set_alternate_screen(&mut self, enabled: bool) -> io::Result<()>;
fn set_cursor_visible(&mut self, visible: bool) -> io::Result<()>;
}
#[derive(Debug, Default)]
pub struct CrosstermTerminalOps;
impl TerminalOps for CrosstermTerminalOps {
fn set_raw_mode(&mut self, enabled: bool) -> io::Result<()> {
if enabled {
enable_raw_mode()
} else {
disable_raw_mode()
}
}
fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
if enabled {
execute!(stdout(), EnableBracketedPaste)
} else {
execute!(stdout(), DisableBracketedPaste)
}
}
fn set_focus_events(&mut self, enabled: bool) -> io::Result<()> {
if enabled {
execute!(stdout(), EnableFocusChange)
} else {
execute!(stdout(), DisableFocusChange)
}
}
fn set_alternate_screen(&mut self, enabled: bool) -> io::Result<()> {
if enabled {
execute!(stdout(), EnterAlternateScreen)
} else {
execute!(stdout(), LeaveAlternateScreen)
}
}
fn set_cursor_visible(&mut self, visible: bool) -> io::Result<()> {
if visible {
execute!(stdout(), Show)
} else {
execute!(stdout(), Hide)
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct ActiveModes {
raw: bool,
bracketed_paste: bool,
focus_events: bool,
alternate_screen: bool,
cursor_hidden: bool,
}
pub struct TerminalGuard<O: TerminalOps> {
ops: O,
active: ActiveModes,
}
impl<O: TerminalOps> TerminalGuard<O> {
pub fn enter(ops: O) -> io::Result<Self> {
let mut guard = Self {
ops,
active: ActiveModes::default(),
};
if let Err(error) = guard.activate() {
let _ = guard.restore();
return Err(error);
}
Ok(guard)
}
fn activate(&mut self) -> io::Result<()> {
self.ops.set_raw_mode(true)?;
self.active.raw = true;
self.ops.set_bracketed_paste(true)?;
self.active.bracketed_paste = true;
self.ops.set_focus_events(true)?;
self.active.focus_events = true;
self.ops.set_alternate_screen(true)?;
self.active.alternate_screen = true;
self.ops.set_cursor_visible(false)?;
self.active.cursor_hidden = true;
Ok(())
}
pub fn restore(&mut self) -> io::Result<()> {
let mut first_error = None;
if self.active.cursor_hidden {
record_result(
&mut first_error,
self.ops.set_cursor_visible(true),
&mut self.active.cursor_hidden,
);
}
if self.active.alternate_screen {
record_result(
&mut first_error,
self.ops.set_alternate_screen(false),
&mut self.active.alternate_screen,
);
}
if self.active.focus_events {
record_result(
&mut first_error,
self.ops.set_focus_events(false),
&mut self.active.focus_events,
);
}
if self.active.bracketed_paste {
record_result(
&mut first_error,
self.ops.set_bracketed_paste(false),
&mut self.active.bracketed_paste,
);
}
if self.active.raw {
record_result(
&mut first_error,
self.ops.set_raw_mode(false),
&mut self.active.raw,
);
}
first_error.map_or(Ok(()), Err)
}
pub fn with_restored<T>(&mut self, action: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
self.restore()?;
let action_result = action();
let activate_result = self.activate();
match (action_result, activate_result) {
(Err(error), _) => Err(error),
(Ok(_), Err(error)) => Err(error),
(Ok(value), Ok(())) => Ok(value),
}
}
pub fn is_active(&self) -> bool {
self.active != ActiveModes::default()
}
}
impl<O: TerminalOps> Drop for TerminalGuard<O> {
fn drop(&mut self) {
let _ = self.restore();
}
}
fn record_result(first_error: &mut Option<io::Error>, result: io::Result<()>, active: &mut bool) {
match result {
Ok(()) => *active = false,
Err(error) => {
first_error.get_or_insert(error);
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::panic::AssertUnwindSafe;
use std::rc::Rc;
use super::*;
#[derive(Clone, Default)]
struct MockOps {
calls: Rc<RefCell<Vec<&'static str>>>,
fail_on: Rc<RefCell<Option<&'static str>>>,
}
impl MockOps {
fn call(&self, name: &'static str) -> io::Result<()> {
self.calls.borrow_mut().push(name);
if self.fail_on.borrow().as_ref() == Some(&name) {
Err(io::Error::other(format!("failed {name}")))
} else {
Ok(())
}
}
}
impl TerminalOps for MockOps {
fn set_raw_mode(&mut self, enabled: bool) -> io::Result<()> {
self.call(if enabled { "raw+" } else { "raw-" })
}
fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
self.call(if enabled { "paste+" } else { "paste-" })
}
fn set_focus_events(&mut self, enabled: bool) -> io::Result<()> {
self.call(if enabled { "focus+" } else { "focus-" })
}
fn set_alternate_screen(&mut self, enabled: bool) -> io::Result<()> {
self.call(if enabled { "screen+" } else { "screen-" })
}
fn set_cursor_visible(&mut self, visible: bool) -> io::Result<()> {
self.call(if visible { "cursor+" } else { "cursor-" })
}
}
const ENTER: &[&str] = &["raw+", "paste+", "focus+", "screen+", "cursor-"];
const RESTORE: &[&str] = &["cursor+", "screen-", "focus-", "paste-", "raw-"];
#[test]
fn normal_drop_restores_every_mode_in_reverse_order_once() {
let ops = MockOps::default();
let calls = Rc::clone(&ops.calls);
{
let guard = TerminalGuard::enter(ops).expect("enter");
assert!(guard.is_active());
}
assert_eq!(&*calls.borrow(), &[ENTER, RESTORE].concat());
}
#[test]
fn partial_setup_failure_rolls_back_the_successful_prefix() {
let ops = MockOps::default();
*ops.fail_on.borrow_mut() = Some("focus+");
let calls = Rc::clone(&ops.calls);
assert!(TerminalGuard::enter(ops).is_err());
assert_eq!(
&*calls.borrow(),
&["raw+", "paste+", "focus+", "paste-", "raw-"]
);
}
#[test]
fn panic_unwind_restores_every_mode() {
let ops = MockOps::default();
let calls = Rc::clone(&ops.calls);
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _guard = TerminalGuard::enter(ops).expect("enter");
panic!("test panic");
}));
assert!(result.is_err());
assert_eq!(&*calls.borrow(), &[ENTER, RESTORE].concat());
}
#[test]
fn external_action_restores_then_reacquires_modes() {
let ops = MockOps::default();
let calls = Rc::clone(&ops.calls);
let mut guard = TerminalGuard::enter(ops).expect("enter");
guard.with_restored(|| Ok(())).expect("resume");
drop(guard);
assert_eq!(&*calls.borrow(), &[ENTER, RESTORE, ENTER, RESTORE].concat());
}
#[test]
fn external_action_error_still_reacquires_and_later_restores() {
let ops = MockOps::default();
let calls = Rc::clone(&ops.calls);
let mut guard = TerminalGuard::enter(ops).expect("enter");
let result = guard.with_restored(|| Err::<(), _>(io::Error::other("action failed")));
assert_eq!(
result.expect_err("action error").to_string(),
"action failed"
);
assert!(guard.is_active());
drop(guard);
assert_eq!(&*calls.borrow(), &[ENTER, RESTORE, ENTER, RESTORE].concat());
}
#[test]
fn restore_attempts_all_modes_after_an_individual_failure() {
let ops = MockOps::default();
let calls = Rc::clone(&ops.calls);
let fail_on = Rc::clone(&ops.fail_on);
let mut guard = TerminalGuard::enter(ops).expect("enter");
*fail_on.borrow_mut() = Some("screen-");
assert_eq!(
guard.restore().expect_err("restore error").to_string(),
"failed screen-"
);
assert!(guard.is_active());
*fail_on.borrow_mut() = None;
drop(guard);
assert_eq!(&*calls.borrow(), &[ENTER, RESTORE, &["screen-"]].concat());
}
}