use color_eyre::eyre::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use futures::StreamExt;
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, Stdout};
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
task::JoinHandle,
time::{self, Duration},
};
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone)]
pub enum Event {
Crossterm(CrosstermEvent),
Tick,
Render,
Resize(u16, u16),
}
pub struct Tui {
pub terminal: Terminal<CrosstermBackend<Stdout>>,
task: JoinHandle<()>,
cancellation_token: CancellationToken,
event_rx: UnboundedReceiver<Event>,
_event_tx: UnboundedSender<Event>,
#[allow(dead_code)]
frame_rate: f64,
#[allow(dead_code)]
tick_rate: f64,
}
impl Tui {
pub fn new() -> Result<Self> {
let frame_rate = 60.0; let tick_rate = 4.0;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = {
let event_tx = event_tx.clone();
let cancellation_token = cancellation_token.clone();
let tick_duration = Duration::from_secs_f64(1.0 / tick_rate);
let render_duration = Duration::from_secs_f64(1.0 / frame_rate);
tokio::spawn(async move {
let mut reader = EventStream::new();
let mut tick_interval = time::interval(tick_duration);
let mut render_interval = time::interval(render_duration);
loop {
tokio::select! {
biased;
_ = cancellation_token.cancelled() => {
break;
}
maybe_event = reader.next() => {
match maybe_event {
Some(Ok(evt)) => {
if let CrosstermEvent::Resize(w, h) = evt {
let _ = event_tx.send(Event::Resize(w, h));
}
let _ = event_tx.send(Event::Crossterm(evt));
}
Some(Err(_)) => {}
None => break,
}
}
_ = tick_interval.tick() => {
let _ = event_tx.send(Event::Tick);
}
_ = render_interval.tick() => {
let _ = event_tx.send(Event::Render);
}
}
}
})
};
let terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
Ok(Self {
terminal,
task,
cancellation_token,
event_rx,
_event_tx: event_tx,
frame_rate,
tick_rate,
})
}
pub fn enter(&mut self) -> Result<()> {
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
io::stdout().execute(EnableMouseCapture)?;
self.terminal.hide_cursor()?;
self.terminal.clear()?;
Ok(())
}
pub fn exit(&mut self) -> Result<()> {
self.terminal.show_cursor()?;
io::stdout().execute(DisableMouseCapture)?;
io::stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
pub fn cancel(&self) {
self.cancellation_token.cancel();
}
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
}
#[allow(dead_code)]
pub fn frame_rate(&self) -> f64 {
self.frame_rate
}
#[allow(dead_code)]
pub fn tick_rate(&self) -> f64 {
self.tick_rate
}
}
impl Drop for Tui {
fn drop(&mut self) {
self.cancel();
let _ = self.exit();
self.task.abort();
}
}