1use std::io::{self, Stdout, stdout};
2use std::sync::Once;
3
4use anyhow::Result;
5use crossterm::{
6 ExecutableCommand,
7 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
8};
9use ratatui::{Terminal, prelude::CrosstermBackend};
10
11use log::debug;
12
13use crate::app::App;
14use crate::ui;
15
16static PANIC_HOOK: Once = Once::new();
17
18pub struct Tui {
19 terminal: Terminal<CrosstermBackend<Stdout>>,
20}
21
22impl Tui {
23 pub fn new() -> Result<Self> {
24 let backend = CrosstermBackend::new(stdout());
25 let terminal = Terminal::new(backend)?;
26 Ok(Self { terminal })
27 }
28
29 pub fn enter(&mut self) -> Result<()> {
31 PANIC_HOOK.call_once(|| {
33 let original_hook = std::panic::take_hook();
34 std::panic::set_hook(Box::new(move |panic_info| {
35 let _ = Self::reset();
36 original_hook(panic_info);
37 }));
38 });
39
40 enable_raw_mode()?;
41 if let Err(e) = io::stdout().execute(EnterAlternateScreen) {
42 disable_raw_mode()?;
43 return Err(e.into());
44 }
45
46 if let Err(e) = self.terminal.hide_cursor() {
47 let _ = Self::reset();
48 return Err(e.into());
49 }
50 if let Err(e) = self.terminal.clear() {
51 let _ = Self::reset();
52 return Err(e.into());
53 }
54 Ok(())
55 }
56
57 pub fn exit(&mut self) -> Result<()> {
59 Self::reset()?;
60 self.terminal.show_cursor()?;
61 Ok(())
62 }
63
64 fn reset() -> Result<()> {
66 disable_raw_mode()?;
67 io::stdout().execute(LeaveAlternateScreen)?;
68 Ok(())
69 }
70
71 pub fn draw(
73 &mut self,
74 app: &mut App,
75 anim: &mut crate::animation::AnimationState,
76 ) -> Result<()> {
77 self.terminal.draw(|frame| ui::render(frame, app, anim))?;
78 Ok(())
79 }
80
81 pub fn force_redraw(&mut self) {
84 if let Err(e) = self.terminal.clear() {
85 debug!("[purple] Failed to clear terminal: {e}");
86 }
87 }
88}
89
90impl Drop for Tui {
91 fn drop(&mut self) {
92 let _ = Self::reset();
93 let _ = self.terminal.show_cursor();
94 }
95}