1use std::io::{self, Stdout, stdout};
2use std::sync::Once;
3
4use anyhow::Result;
5use crossterm::{
6 ExecutableCommand, QueueableCommand,
7 cursor::MoveTo,
8 style::ResetColor,
9 terminal::{
10 Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
11 enable_raw_mode,
12 },
13};
14use ratatui::{Terminal, prelude::CrosstermBackend};
15
16use log::debug;
17
18use crate::app::App;
19use crate::ui;
20
21static PANIC_HOOK: Once = Once::new();
22
23pub struct Tui {
24 terminal: Terminal<CrosstermBackend<Stdout>>,
25}
26
27impl Tui {
28 pub fn new() -> Result<Self> {
29 let backend = CrosstermBackend::new(stdout());
30 let terminal = Terminal::new(backend)?;
31 Ok(Self { terminal })
32 }
33
34 pub fn enter(&mut self) -> Result<()> {
36 PANIC_HOOK.call_once(|| {
38 let original_hook = std::panic::take_hook();
39 std::panic::set_hook(Box::new(move |panic_info| {
40 let _ = Self::reset();
41 original_hook(panic_info);
42 }));
43 });
44
45 enable_raw_mode()?;
46 if let Err(e) = io::stdout().execute(EnterAlternateScreen) {
47 disable_raw_mode()?;
48 return Err(e.into());
49 }
50
51 if let Err(e) = self.terminal.hide_cursor() {
52 let _ = Self::reset();
53 return Err(e.into());
54 }
55 if let Err(e) = self.terminal.clear() {
56 let _ = Self::reset();
57 return Err(e.into());
58 }
59 Ok(())
60 }
61
62 pub fn exit(&mut self) -> Result<()> {
64 Self::reset()?;
65 self.terminal.show_cursor()?;
66 Ok(())
67 }
68
69 pub fn suspend(&mut self) -> Result<()> {
74 self.exit()?;
75 queue_clear_home(&mut io::stdout())?;
76 Ok(())
77 }
78
79 fn reset() -> Result<()> {
81 disable_raw_mode()?;
82 io::stdout().execute(LeaveAlternateScreen)?;
83 Ok(())
84 }
85
86 pub fn draw(
88 &mut self,
89 app: &mut App,
90 anim: &mut crate::animation::AnimationState,
91 ) -> Result<()> {
92 self.terminal.draw(|frame| ui::render(frame, app, anim))?;
93 Ok(())
94 }
95
96 pub fn force_redraw(&mut self) {
99 if let Err(e) = self.terminal.clear() {
100 debug!("[purple] Failed to clear terminal: {e}");
101 }
102 }
103}
104
105impl Drop for Tui {
106 fn drop(&mut self) {
107 let _ = Self::reset();
108 let _ = self.terminal.show_cursor();
109 }
110}
111
112fn queue_clear_home<W: io::Write>(w: &mut W) -> io::Result<()> {
117 w.queue(ResetColor)?;
118 w.queue(Clear(ClearType::All))?;
119 w.queue(MoveTo(0, 0))?;
120 w.flush()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn queue_clear_home_emits_reset_clear_then_home() {
129 let mut buf: Vec<u8> = Vec::new();
130 queue_clear_home(&mut buf).unwrap();
131 let s = String::from_utf8(buf).unwrap();
132 let reset_pos = s.find("\x1b[0m").expect("color-reset sequence present");
133 let clear_pos = s.find("\x1b[2J").expect("clear-all sequence present");
134 let home_pos = s.find("\x1b[1;1H").expect("cursor-home sequence present");
135 assert!(reset_pos < clear_pos, "color reset must run before clear");
136 assert!(clear_pos < home_pos, "clear must run before cursor home");
137 }
138}