use crossterm::{
event::{EnableFocusChange, EventStream},
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use futures_util::{future::Either, stream, Stream};
use ratatui::Frame;
use std::io::{self, IsTerminal};
#[cfg(not(feature = "integration"))]
mod backend;
#[cfg(not(feature = "integration"))]
pub use backend::{new_backend, TerminalBackend};
#[cfg(feature = "integration")]
mod integration_backend;
#[cfg(feature = "integration")]
pub use integration_backend::{new_backend, Buffer, TerminalBackend};
pub struct Terminal {
backend: ratatui::Terminal<TerminalBackend>,
}
impl Terminal {
pub fn new() -> anyhow::Result<Self> {
let backend = new_backend();
Ok(Terminal::with(ratatui::Terminal::new(backend)?))
}
pub fn with(backend: ratatui::Terminal<TerminalBackend>) -> Self {
Self { backend }
}
pub fn init(&mut self) -> io::Result<()> {
terminal::enable_raw_mode()?;
crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableFocusChange)?;
let panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic| {
Self::restore_backend().expect("Failed to reset terminal");
panic_hook(panic);
}));
self.backend.hide_cursor()?;
self.backend.clear()?;
Ok(())
}
pub fn restore(&mut self) -> io::Result<()> {
Self::restore_backend()?;
self.backend.show_cursor()?;
Ok(())
}
fn restore_backend() -> io::Result<()> {
terminal::disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
pub fn render<F>(&mut self, f: F) -> anyhow::Result<()>
where
F: FnOnce(&mut Frame),
{
self.backend.draw(f)?;
Ok(())
}
pub fn force_redraw(&mut self) {
self.backend.clear().unwrap();
}
#[cfg(feature = "integration")]
pub fn buffer(&self) -> &Buffer {
self.backend.backend().buffer()
}
}
pub fn event_stream() -> impl Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin {
let is_terminal = std::io::stdout().is_terminal();
if is_terminal {
Either::Left(EventStream::new())
} else {
Either::Right(stream::empty())
}
}