use std::io;
use std::ops::{Deref, DerefMut};
use std::time::Duration;
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
pub type Term = Terminal<CrosstermBackend<io::Stdout>>;
#[must_use = "dropping the guard immediately restores the terminal — assign it to a variable"]
pub struct TerminalGuard {
terminal: Term,
}
impl Deref for TerminalGuard {
type Target = Term;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for TerminalGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
restore();
}
}
pub fn init() -> anyhow::Result<TerminalGuard> {
enable_raw_mode()?;
if let Err(e) = execute!(io::stdout(), EnterAlternateScreen) {
restore();
return Err(e.into());
}
match Terminal::new(CrosstermBackend::new(io::stdout())) {
Ok(terminal) => Ok(TerminalGuard { terminal }),
Err(e) => {
restore();
Err(e.into())
}
}
}
pub fn restore() {
let _ = execute!(io::stdout(), LeaveAlternateScreen);
let _ = disable_raw_mode();
}
pub trait TuiApp {
fn draw(&mut self, frame: &mut ratatui::Frame);
fn handle_key(&mut self, key: event::KeyEvent) -> anyhow::Result<bool>;
}
pub fn run_loop(terminal: &mut Term, poll_ms: u64, app: &mut dyn TuiApp) -> anyhow::Result<()> {
let timeout = Duration::from_millis(poll_ms);
loop {
terminal.draw(|f| app.draw(f))?;
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
break;
}
if !app.handle_key(key)? {
break;
}
}
}
}
Ok(())
}