use std::{ io::{stdout, Stdout}, ops::{Deref, DerefMut}, time::Duration, }; use color_eyre::Result; use crossterm::{ cursor, event::{ DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind, MouseEvent, }, terminal::{EnterAlternateScreen, LeaveAlternateScreen}, }; use futures::{FutureExt, StreamExt}; use ratatui::backend::CrosstermBackend as Backend; use serde::{Serialize, Deserialize}; use tokio::{ sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, task::JoinHandle, time::interval, }; use tokio_util::sync::CancellationToken; use tracing::error; #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Event { Init, Quit, Error, Closed, Tick, Render, FocusGained, FocusLost, Paste(String), Key(KeyEvent), Mouse(MouseEvent), Resize(u16, u16), Node, FastNode, Runtime, } pub struct Tui { pub terminal: ratatui::Terminal>, pub task: JoinHandle<()>, pub cancellation_token: CancellationToken, pub event_rx: UnboundedReceiver, pub event_tx: UnboundedSender, pub frame_rate: f64, pub tick_rate: f64, pub mouse: bool, pub paste: bool, } impl Tui { pub fn new() -> Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); Ok(Self { terminal: ratatui::Terminal::new(Backend::new(stdout()))?, task: tokio::spawn(async {}), cancellation_token: CancellationToken::new(), event_rx, event_tx, frame_rate: 60.0, tick_rate: 4.0, mouse: false, paste: false, }) } pub fn start(&mut self) { self.cancel(); self.cancellation_token = CancellationToken::new(); let event_loop = Self::event_loop( self.event_tx.clone(), self.cancellation_token.clone(), self.tick_rate, self.frame_rate, ); self.task = tokio::spawn(async { event_loop.await; }); } async fn event_loop( event_tx: UnboundedSender, cancellation_token: CancellationToken, tick_rate: f64, frame_rate: f64, ) { let mut event_stream = EventStream::new(); let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate)); let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate)); let mut normal_node_interval = interval(Duration::from_secs_f64(9.0)); let mut fast_node_interval = interval(Duration::from_secs_f64(9.0)); let mut runtime_interval = interval(Duration::from_secs_f64(1.69)); event_tx .send(Event::Init) .expect("failed to send init event"); loop { let event = tokio::select! { _ = cancellation_token.cancelled() => { break; }, _ = tick_interval.tick() => Event::Tick, _ = render_interval.tick() => Event::Render, _ = normal_node_interval.tick() => Event::Node, _ = fast_node_interval.tick() => Event::FastNode, _ = runtime_interval.tick() => Event::Runtime, crossterm_event = event_stream.next().fuse() => match crossterm_event { Some(Ok(event)) => match event { CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key), CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse), CrosstermEvent::Resize(x, y) => Event::Resize(x, y), CrosstermEvent::FocusLost => Event::FocusLost, CrosstermEvent::FocusGained => Event::FocusGained, CrosstermEvent::Paste(s) => Event::Paste(s), _ => continue, } Some(Err(_)) => Event::Error, None => break, }, }; if event_tx.send(event).is_err() { break; } } cancellation_token.cancel(); } pub fn stop(&self) -> Result<()> { self.cancel(); let mut counter = 0; while !self.task.is_finished() { std::thread::sleep(Duration::from_millis(1)); counter += 1; if counter > 50 { self.task.abort(); } if counter > 100 { error!("failed to abort task in 100 milliseconds for unknown reason"); break; } } Ok(()) } pub fn enter(&mut self) -> Result<()> { crossterm::terminal::enable_raw_mode()?; crossterm::execute!(stdout(), EnterAlternateScreen, cursor::Hide)?; if self.mouse { crossterm::execute!(stdout(), EnableMouseCapture)?; } if self.paste { crossterm::execute!(stdout(), EnableBracketedPaste)?; } self.start(); Ok(()) } pub fn exit(&mut self) -> Result<()> { self.stop()?; if crossterm::terminal::is_raw_mode_enabled()? { self.flush()?; if self.paste { crossterm::execute!(stdout(), DisableBracketedPaste)?; } if self.mouse { crossterm::execute!(stdout(), DisableMouseCapture)?; } crossterm::execute!(stdout(), LeaveAlternateScreen, cursor::Show)?; crossterm::terminal::disable_raw_mode()?; } Ok(()) } pub fn cancel(&self) { self.cancellation_token.cancel(); } pub fn suspend(&mut self) -> Result<()> { self.exit()?; #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; Ok(()) } pub async fn next_event(&mut self) -> Option { self.event_rx.recv().await } } impl Deref for Tui { type Target = ratatui::Terminal>; fn deref(&self) -> &Self::Target { &self.terminal } } impl DerefMut for Tui { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.terminal } } impl Drop for Tui { fn drop(&mut self) { self.exit().unwrap(); } }