use std::io;
use std::panic;
use std::time::{Duration, Instant};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent},
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use crate::ui::terminal::Event;
pub struct Tui {
terminal: Terminal<CrosstermBackend<io::Stdout>>,
tick_rate: Duration,
last_tick: Instant,
}
impl Tui {
pub fn new() -> anyhow::Result<Self> {
terminal::enable_raw_mode()
.map_err(|e| anyhow::anyhow!("Failed to enable raw mode: {}", e))?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
.map_err(|e| anyhow::anyhow!("Failed to initialize terminal: {}", e))?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)
.map_err(|e| anyhow::anyhow!("Failed to create terminal: {}", e))?;
terminal
.clear()
.map_err(|e| anyhow::anyhow!("Failed to clear terminal: {}", e))?;
terminal
.hide_cursor()
.map_err(|e| anyhow::anyhow!("Failed to hide cursor: {}", e))?;
let tick_rate = Duration::from_millis(100);
Ok(Self {
terminal,
tick_rate,
last_tick: Instant::now(),
})
}
fn restore_terminal(&mut self) -> anyhow::Result<()> {
self.terminal.show_cursor()?;
self.terminal.clear()?;
terminal::disable_raw_mode()?;
execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
Ok(())
}
pub fn init_panic_hook() {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = terminal::disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
original_hook(panic_info);
}));
}
pub fn exit(&mut self) -> anyhow::Result<()> {
self.restore_terminal()?;
Ok(())
}
pub fn draw<F>(&mut self, render_fn: F) -> anyhow::Result<()>
where
F: FnOnce(&mut ratatui::Frame<'_>),
{
self.terminal.draw(render_fn)?;
Ok(())
}
pub fn next_event(&mut self) -> anyhow::Result<Event> {
let timeout = self
.tick_rate
.checked_sub(self.last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
match event::read()? {
CrosstermEvent::Key(key) => Ok(Event::Key(key)),
CrosstermEvent::Resize(width, height) => Ok(Event::Resize(width, height)),
_ => {
if self.last_tick.elapsed() >= self.tick_rate {
self.last_tick = Instant::now();
Ok(Event::Tick)
} else {
self.next_event()
}
}
}
} else {
if self.last_tick.elapsed() >= self.tick_rate {
self.last_tick = Instant::now();
Ok(Event::Tick)
} else {
self.next_event()
}
}
}
}
impl Drop for Tui {
fn drop(&mut self) {
let _ = self.restore_terminal();
}
}