//! Terminal lifecycle wrapper that restores state on success, error, and panic. use std::{ io, panic::{AssertUnwindSafe, catch_unwind, resume_unwind}, }; pub fn run(initialize: I, mut restore: C, operation: F) -> io::Result where I: FnOnce() -> io::Result, C: FnMut() -> io::Result<()>, F: FnOnce(&mut T) -> io::Result, { 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 ); } }