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}