use std::io;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum TerminalResource {
RawMode,
AlternateScreen,
MouseCapture,
HiddenCursor,
}
pub(crate) trait TerminalOps {
fn acquire(&mut self, resource: TerminalResource) -> io::Result<()>;
fn release(&mut self, resource: TerminalResource) -> io::Result<()>;
}
pub(crate) struct TerminalLease<O: TerminalOps> {
operations: O,
pending: [Option<TerminalResource>; 4],
}
impl<O: TerminalOps> TerminalLease<O> {
pub(crate) const fn new(operations: O) -> Self {
Self {
operations,
pending: [None; 4],
}
}
pub(crate) fn acquire(&mut self, resource: TerminalResource) -> io::Result<()> {
if self.pending.contains(&Some(resource)) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"terminal resource is already awaiting restoration",
));
}
let Some(slot) = self.pending.iter_mut().find(|slot| slot.is_none()) else {
return Err(io::Error::other("terminal restoration capacity exhausted"));
};
*slot = Some(resource);
self.operations.acquire(resource)
}
pub(crate) fn has_pending(&self, resource: TerminalResource) -> bool {
self.pending.contains(&Some(resource))
}
pub(crate) fn release_one(&mut self, resource: TerminalResource) -> io::Result<()> {
let Some(index) = self.pending.iter().position(|item| *item == Some(resource)) else {
return Ok(());
};
self.operations.release(resource)?;
self.pending[index] = None;
self.compact();
Ok(())
}
pub(crate) fn restore(&mut self) -> io::Result<()> {
let mut first_error = None;
for slot in self.pending.iter_mut().rev() {
let Some(resource) = *slot else { continue };
match self.operations.release(resource) {
Ok(()) => *slot = None,
Err(error) => {
first_error.get_or_insert(error);
}
}
}
self.compact();
first_error.map_or(Ok(()), Err)
}
fn compact(&mut self) {
let mut retained = [None; 4];
for (index, resource) in self.pending.iter().flatten().enumerate() {
retained[index] = Some(*resource);
}
self.pending = retained;
}
}
impl<O: TerminalOps> Drop for TerminalLease<O> {
fn drop(&mut self) {
let _ = self.restore();
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use super::*;
use TerminalResource::{AlternateScreen, HiddenCursor, MouseCapture, RawMode};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Event {
Acquire(TerminalResource),
Release(TerminalResource),
}
#[derive(Default)]
struct State {
events: Vec<Event>,
active: Vec<TerminalResource>,
acquire_error: Option<TerminalResource>,
release_errors: VecDeque<TerminalResource>,
}
struct Operations(Rc<RefCell<State>>);
impl TerminalOps for Operations {
fn acquire(&mut self, resource: TerminalResource) -> io::Result<()> {
let mut state = self.0.borrow_mut();
state.events.push(Event::Acquire(resource));
state.active.push(resource);
if state.acquire_error == Some(resource) {
Err(io::Error::other(format!("acquire {resource:?}")))
} else {
Ok(())
}
}
fn release(&mut self, resource: TerminalResource) -> io::Result<()> {
let mut state = self.0.borrow_mut();
state.events.push(Event::Release(resource));
if state.release_errors.front() == Some(&resource) {
state.release_errors.pop_front();
Err(io::Error::other(format!("release {resource:?}")))
} else {
state.active.retain(|active| *active != resource);
Ok(())
}
}
}
fn lease(state: &Rc<RefCell<State>>) -> TerminalLease<Operations> {
TerminalLease::new(Operations(Rc::clone(state)))
}
#[test]
fn unused_lease_and_repeated_restoration_do_not_touch_the_terminal() {
let state = Rc::new(RefCell::new(State::default()));
let mut session = lease(&state);
session.restore().unwrap();
session.restore().unwrap();
drop(session);
assert!(state.borrow().events.is_empty());
}
#[test]
fn callers_preserve_distinct_tui_and_pager_acquisition_orders() {
for order in [
[RawMode, AlternateScreen, MouseCapture, HiddenCursor],
[AlternateScreen, RawMode, MouseCapture, HiddenCursor],
] {
let state = Rc::new(RefCell::new(State::default()));
let mut session = lease(&state);
for resource in order {
session.acquire(resource).unwrap();
}
session.restore().unwrap();
session.restore().unwrap();
drop(session);
let expected = order
.into_iter()
.map(Event::Acquire)
.chain(order.into_iter().rev().map(Event::Release))
.collect::<Vec<_>>();
assert_eq!(state.borrow().events, expected);
}
}
#[test]
fn each_setup_failure_restores_only_attempted_resources_in_reverse_order() {
for order in [
[RawMode, AlternateScreen, MouseCapture, HiddenCursor],
[AlternateScreen, RawMode, MouseCapture, HiddenCursor],
] {
for failure in 0..order.len() {
let state = Rc::new(RefCell::new(State {
acquire_error: Some(order[failure]),
..State::default()
}));
let mut session = lease(&state);
let result = order
.into_iter()
.try_for_each(|resource| session.acquire(resource));
assert_eq!(
result.unwrap_err().to_string(),
format!("acquire {:?}", order[failure])
);
assert_eq!(state.borrow().active, order[..=failure]);
drop(session);
let attempted = &order[..=failure];
let expected = attempted
.iter()
.copied()
.map(Event::Acquire)
.chain(attempted.iter().rev().copied().map(Event::Release))
.collect::<Vec<_>>();
assert_eq!(state.borrow().events, expected);
assert!(state.borrow().active.is_empty());
}
}
}
#[test]
fn restoration_attempts_all_resources_preserves_first_error_and_drop_retries_failures() {
let state = Rc::new(RefCell::new(State {
release_errors: VecDeque::from([HiddenCursor, RawMode]),
..State::default()
}));
let mut session = lease(&state);
for resource in [RawMode, AlternateScreen, MouseCapture, HiddenCursor] {
session.acquire(resource).unwrap();
}
assert_eq!(
session.restore().unwrap_err().to_string(),
"release HiddenCursor"
);
assert_eq!(
&state.borrow().events[4..],
&[
Event::Release(HiddenCursor),
Event::Release(MouseCapture),
Event::Release(AlternateScreen),
Event::Release(RawMode),
]
);
drop(session);
assert_eq!(
&state.borrow().events[8..],
&[Event::Release(HiddenCursor), Event::Release(RawMode)]
);
}
#[test]
fn explicit_retry_releases_only_failures_and_keeps_new_acquisitions_last() {
let state = Rc::new(RefCell::new(State {
release_errors: VecDeque::from([RawMode]),
..State::default()
}));
let mut session = lease(&state);
session.acquire(RawMode).unwrap();
session.acquire(AlternateScreen).unwrap();
assert!(session.restore().is_err());
session.acquire(AlternateScreen).unwrap();
session.restore().unwrap();
drop(session);
assert_eq!(
state.borrow().events,
[
Event::Acquire(RawMode),
Event::Acquire(AlternateScreen),
Event::Release(AlternateScreen),
Event::Release(RawMode),
Event::Acquire(AlternateScreen),
Event::Release(AlternateScreen),
Event::Release(RawMode),
]
);
}
#[test]
fn each_release_failure_is_retried_without_releasing_successful_modes_twice() {
let order = [RawMode, AlternateScreen, MouseCapture, HiddenCursor];
for failed in order {
let state = Rc::new(RefCell::new(State {
release_errors: VecDeque::from([failed]),
..State::default()
}));
let mut session = lease(&state);
for resource in order {
session.acquire(resource).unwrap();
}
assert_eq!(
session.restore().unwrap_err().to_string(),
format!("release {failed:?}")
);
assert_eq!(state.borrow().active, [failed]);
session.restore().unwrap();
drop(session);
let expected = order
.into_iter()
.map(Event::Acquire)
.chain(order.into_iter().rev().map(Event::Release))
.chain([Event::Release(failed)])
.collect::<Vec<_>>();
assert_eq!(state.borrow().events, expected);
assert!(state.borrow().active.is_empty());
}
}
#[test]
fn drop_ignores_io_errors_but_still_attempts_every_remaining_release() {
let state = Rc::new(RefCell::new(State {
release_errors: VecDeque::from([MouseCapture, AlternateScreen, RawMode]),
..State::default()
}));
let mut session = lease(&state);
for resource in [RawMode, AlternateScreen, MouseCapture] {
session.acquire(resource).unwrap();
}
drop(session);
assert_eq!(
&state.borrow().events[3..],
&[
Event::Release(MouseCapture),
Event::Release(AlternateScreen),
Event::Release(RawMode),
]
);
}
#[test]
fn duplicate_acquisition_cannot_overwrite_a_restoration_obligation() {
let state = Rc::new(RefCell::new(State {
acquire_error: Some(AlternateScreen),
..State::default()
}));
let mut session = lease(&state);
assert!(session.acquire(AlternateScreen).is_err());
assert_eq!(
session.acquire(AlternateScreen).unwrap_err().kind(),
io::ErrorKind::AlreadyExists
);
drop(session);
assert_eq!(
state.borrow().events,
[
Event::Acquire(AlternateScreen),
Event::Release(AlternateScreen),
]
);
}
}