Skip to main content

zero_tui/app/
terminal.rs

1//! RAII wrapper around the crossterm terminal — enters raw mode +
2//! alternate screen on construction, restores on drop.
3//!
4//! A panic hook is installed so a panic anywhere in the app still
5//! leaves the operator's terminal usable. The hook runs before the
6//! default panic handler, so the backtrace is still visible.
7
8use std::io::{self, Stdout};
9
10use crossterm::execute;
11use crossterm::terminal::{
12    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
13};
14use ratatui::Terminal;
15use ratatui::backend::CrosstermBackend;
16
17pub type Tty = Terminal<CrosstermBackend<Stdout>>;
18
19/// Owns the terminal for the duration of the app. Call
20/// [`TerminalGuard::init`] to enter the app mode; the guard's
21/// `Drop` impl restores on exit.
22pub struct TerminalGuard {
23    pub tty: Tty,
24}
25
26impl std::fmt::Debug for TerminalGuard {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("TerminalGuard").finish_non_exhaustive()
29    }
30}
31
32impl TerminalGuard {
33    /// Enter raw mode + alternate screen and install the panic
34    /// restore hook. Safe to call once per process.
35    ///
36    /// # Errors
37    /// Returns any error from `enable_raw_mode` or screen switch.
38    pub fn init() -> io::Result<Self> {
39        install_panic_hook();
40        enable_raw_mode()?;
41        let mut stdout = io::stdout();
42        execute!(stdout, EnterAlternateScreen)?;
43        let backend = CrosstermBackend::new(stdout);
44        let tty = Terminal::new(backend)?;
45        Ok(Self { tty })
46    }
47}
48
49impl Drop for TerminalGuard {
50    fn drop(&mut self) {
51        let _ = restore();
52    }
53}
54
55/// Restore cooked mode + primary screen. Idempotent.
56fn restore() -> io::Result<()> {
57    let mut stdout = io::stdout();
58    execute!(stdout, LeaveAlternateScreen)?;
59    disable_raw_mode()?;
60    Ok(())
61}
62
63/// Panic hook that flushes the terminal back to a usable state
64/// before the default handler prints the trace. Installed once.
65fn install_panic_hook() {
66    use std::sync::Once;
67    static ONCE: Once = Once::new();
68    ONCE.call_once(|| {
69        let original = std::panic::take_hook();
70        std::panic::set_hook(Box::new(move |info| {
71            let _ = restore();
72            original(info);
73        }));
74    });
75}