use std::io::{self, Stdout, stdout};
use std::sync::Once;
use anyhow::Result;
use crossterm::{
ExecutableCommand, QueueableCommand,
cursor::MoveTo,
style::ResetColor,
terminal::{
Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode,
},
};
use ratatui::{Terminal, prelude::CrosstermBackend};
use log::debug;
use crate::app::App;
use crate::ui;
static PANIC_HOOK: Once = Once::new();
pub struct Tui {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl Tui {
pub fn new() -> Result<Self> {
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(Self { terminal })
}
pub fn enter(&mut self) -> Result<()> {
PANIC_HOOK.call_once(|| {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = Self::reset();
original_hook(panic_info);
}));
});
enable_raw_mode()?;
if let Err(e) = io::stdout().execute(EnterAlternateScreen) {
disable_raw_mode()?;
return Err(e.into());
}
if let Err(e) = self.terminal.hide_cursor() {
let _ = Self::reset();
return Err(e.into());
}
if let Err(e) = self.terminal.clear() {
let _ = Self::reset();
return Err(e.into());
}
Ok(())
}
pub fn exit(&mut self) -> Result<()> {
Self::reset()?;
self.terminal.show_cursor()?;
Ok(())
}
pub fn suspend(&mut self) -> Result<()> {
self.exit()?;
queue_clear_home(&mut io::stdout())?;
Ok(())
}
fn reset() -> Result<()> {
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
pub fn draw(
&mut self,
app: &mut App,
anim: &mut crate::animation::AnimationState,
) -> Result<()> {
self.terminal.draw(|frame| ui::render(frame, app, anim))?;
Ok(())
}
pub fn force_redraw(&mut self) {
if let Err(e) = self.terminal.clear() {
debug!("[purple] Failed to clear terminal: {e}");
}
}
}
impl Drop for Tui {
fn drop(&mut self) {
let _ = Self::reset();
let _ = self.terminal.show_cursor();
}
}
fn queue_clear_home<W: io::Write>(w: &mut W) -> io::Result<()> {
w.queue(ResetColor)?;
w.queue(Clear(ClearType::All))?;
w.queue(MoveTo(0, 0))?;
w.flush()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn queue_clear_home_emits_reset_clear_then_home() {
let mut buf: Vec<u8> = Vec::new();
queue_clear_home(&mut buf).unwrap();
let s = String::from_utf8(buf).unwrap();
let reset_pos = s.find("\x1b[0m").expect("color-reset sequence present");
let clear_pos = s.find("\x1b[2J").expect("clear-all sequence present");
let home_pos = s.find("\x1b[1;1H").expect("cursor-home sequence present");
assert!(reset_pos < clear_pos, "color reset must run before clear");
assert!(clear_pos < home_pos, "clear must run before cursor home");
}
}