Skip to main content

vfa_tui/ui/
mod.rs

1pub mod layout;
2pub mod nav;
3pub mod theme;
4pub mod widgets;
5
6use std::io::{self, Stdout};
7
8use crossterm::{
9    execute,
10    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
11};
12use ratatui::{backend::CrosstermBackend, layout::Rect, Frame, Terminal};
13
14/// Manages the terminal lifecycle: raw mode, alternate screen, and rendering.
15///
16/// Guarantees terminal state restoration on drop (normal exit, error, or panic).
17/// The panic hook installed via [`install_panic_hook`] provides an additional
18/// safety net for restoration before the panic message is printed.
19pub struct TerminalManager {
20    terminal: Terminal<CrosstermBackend<Stdout>>,
21}
22
23impl TerminalManager {
24    /// Initialize terminal: enable raw mode, switch to alternate screen, hide cursor.
25    ///
26    /// This must be called before any rendering. The terminal is restored
27    /// automatically when the `TerminalManager` is dropped.
28    pub fn new() -> anyhow::Result<Self> {
29        enable_raw_mode()?;
30        let mut stdout = io::stdout();
31        execute!(stdout, EnterAlternateScreen, crossterm::cursor::Hide)?;
32        let backend = CrosstermBackend::new(stdout);
33        let terminal = Terminal::new(backend)?;
34        Ok(Self { terminal })
35    }
36
37    /// Restore the terminal to its original state: disable raw mode,
38    /// leave alternate screen, show cursor.
39    pub fn restore(&mut self) -> anyhow::Result<()> {
40        disable_raw_mode()?;
41        execute!(
42            self.terminal.backend_mut(),
43            LeaveAlternateScreen,
44            crossterm::cursor::Show
45        )?;
46        Ok(())
47    }
48
49    /// Draw a single frame. On terminal resize, ratatui automatically picks up
50    /// the new dimensions from the backend, so calling `draw` after a resize
51    /// event produces a correctly laid-out frame.
52    pub fn draw<F>(&mut self, f: F) -> anyhow::Result<()>
53    where
54        F: FnOnce(&mut Frame),
55    {
56        self.terminal.draw(f)?;
57        Ok(())
58    }
59
60    /// Return the current terminal size as a [`Rect`].
61    pub fn size(&self) -> anyhow::Result<Rect> {
62        Ok(self.terminal.size()?.into())
63    }
64}
65
66impl Drop for TerminalManager {
67    fn drop(&mut self) {
68        // Best-effort restoration — errors are intentionally ignored here
69        // because we may be dropping during a panic or error unwind.
70        let _ = self.restore();
71    }
72}
73
74/// Install a panic hook that restores the terminal before printing the panic message.
75///
76/// This must be called **before** any terminal setup so that panics occurring
77/// during or after terminal initialization still produce readable output.
78/// The hook restores: raw mode disabled, alternate screen left, cursor shown.
79pub fn install_panic_hook() {
80    let original_hook = std::panic::take_hook();
81    std::panic::set_hook(Box::new(move |panic_info| {
82        // Best-effort terminal restoration before the panic message is printed.
83        // Errors are ignored because we're already in a panic path.
84        let _ = disable_raw_mode();
85        let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::cursor::Show);
86        original_hook(panic_info);
87    }));
88}