Build the Mutt-style TUI shell and action model
This commit is contained in:
120
apps/tui/src/terminal.rs
Normal file
120
apps/tui/src/terminal.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
//! 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user