Skip to main content

purple_ssh/
tui.rs

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    /// Enter TUI mode: panic hook (installed once), raw mode, alternate screen.
30    pub fn enter(&mut self) -> Result<()> {
31        // Install panic hook BEFORE enabling raw mode to ensure cleanup on panic
32        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    /// Exit TUI mode: restore terminal.
58    pub fn exit(&mut self) -> Result<()> {
59        Self::reset()?;
60        self.terminal.show_cursor()?;
61        Ok(())
62    }
63
64    /// Reset terminal to normal mode.
65    fn reset() -> Result<()> {
66        disable_raw_mode()?;
67        io::stdout().execute(LeaveAlternateScreen)?;
68        Ok(())
69    }
70
71    /// Draw the UI.
72    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    /// Force a full redraw on the next draw() call.
82    /// Use after external processes may have written to the terminal.
83    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}