1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
use std::io::{self, Stdout, stdout};
use std::sync::Once;
use anyhow::Result;
use crossterm::{
ExecutableCommand,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, prelude::CrosstermBackend};
use crate::app::App;
use crate::ui;
static PANIC_HOOK: Once = Once::new();
pub struct Tui {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl Tui {
pub fn new() -> Result<Self> {
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(Self { terminal })
}
/// Enter TUI mode: panic hook (installed once), raw mode, alternate screen.
pub fn enter(&mut self) -> Result<()> {
// Install panic hook BEFORE enabling raw mode to ensure cleanup on panic
PANIC_HOOK.call_once(|| {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = Self::reset();
original_hook(panic_info);
}));
});
enable_raw_mode()?;
if let Err(e) = io::stdout().execute(EnterAlternateScreen) {
disable_raw_mode()?;
return Err(e.into());
}
self.terminal.hide_cursor()?;
self.terminal.clear()?;
Ok(())
}
/// Exit TUI mode: restore terminal.
pub fn exit(&mut self) -> Result<()> {
Self::reset()?;
self.terminal.show_cursor()?;
Ok(())
}
/// Reset terminal to normal mode.
fn reset() -> Result<()> {
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
/// Draw the UI.
pub fn draw(&mut self, app: &mut App) -> Result<()> {
self.terminal.draw(|frame| ui::render(frame, app))?;
Ok(())
}
}
impl Drop for Tui {
fn drop(&mut self) {
let _ = Self::reset();
let _ = self.terminal.show_cursor();
}
}