Files
IronStorage/apps/tui/src/terminal.rs
2026-08-10 05:46:32 +00:00

121 lines
3.2 KiB
Rust

//! Terminal lifecycle wrapper that restores state on success, error, and panic.
use std::{
io,
panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
};
pub fn run<T, R, I, C, F>(initialize: I, mut restore: C, operation: F) -> io::Result<R>
where
I: FnOnce() -> io::Result<T>,
C: FnMut() -> io::Result<()>,
F: FnOnce(&mut T) -> io::Result<R>,
{
let mut terminal = initialize()?;
let outcome = catch_unwind(AssertUnwindSafe(|| operation(&mut terminal)));
let restore_result = restore();
match outcome {
Ok(Ok(value)) => restore_result.map(|()| value),
Ok(Err(error)) => Err(error),
Err(payload) => resume_unwind(payload),
}
}
#[cfg(test)]
mod tests {
use std::{
cell::Cell,
io::{self, ErrorKind},
panic,
rc::Rc,
};
use super::run;
#[test]
fn restores_after_normal_exit() {
let restored = Rc::new(Cell::new(false));
let observed = Rc::clone(&restored);
let result = run(
|| Ok(()),
move || {
observed.set(true);
Ok(())
},
|_| Ok(42),
);
assert_eq!(result.expect("normal run"), 42);
assert!(restored.get());
}
#[test]
fn restores_after_application_error() {
let restored = Rc::new(Cell::new(false));
let observed = Rc::clone(&restored);
let result = run(
|| Ok(()),
move || {
observed.set(true);
Ok(())
},
|_| Err::<(), _>(io::Error::other("application failed")),
);
assert_eq!(
result.expect_err("error must be preserved").kind(),
ErrorKind::Other
);
assert!(restored.get());
}
#[test]
fn restores_before_resuming_a_panic() {
let restored = Rc::new(Cell::new(false));
let observed = Rc::clone(&restored);
let outcome = panic::catch_unwind(panic::AssertUnwindSafe(|| {
let _ = run(
|| Ok(()),
move || {
observed.set(true);
Ok(())
},
|_| -> io::Result<()> { panic!("application panicked") },
);
}));
assert!(outcome.is_err());
assert!(restored.get());
}
#[test]
fn initialization_failure_does_not_claim_an_active_terminal() {
let restored = Rc::new(Cell::new(false));
let observed = Rc::clone(&restored);
let result = run(
|| Err::<(), _>(io::Error::new(ErrorKind::NotFound, "no terminal")),
move || {
observed.set(true);
Ok(())
},
|_| Ok(()),
);
assert_eq!(
result.expect_err("initialization must fail").kind(),
ErrorKind::NotFound
);
assert!(!restored.get());
}
#[test]
fn restoration_failure_is_reported_after_success() {
let result = run(
|| Ok(()),
|| Err(io::Error::new(ErrorKind::BrokenPipe, "restore failed")),
|_| Ok(()),
);
assert_eq!(
result.expect_err("restore must fail").kind(),
ErrorKind::BrokenPipe
);
}
}