use color_eyre::eyre::Result;
use futures::{FutureExt, StreamExt};
use ratatui::backend::CrosstermBackend as Backend;
use ratatui::crossterm::{
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, Event as CrosstermEvent,
KeyEvent, KeyEventKind, MouseEvent,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
use serde::{Deserialize, Serialize};
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
use tokio_util::sync::CancellationToken;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
pub fn new() -> Result<Self> {
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, frame_rate, tick_rate, mouse, paste })
pub fn tick_rate(mut self, tick_rate: f64) -> Self {
self.tick_rate = tick_rate;
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
self.frame_rate = frame_rate;
pub fn mouse(mut self, mouse: bool) -> Self {
pub fn paste(mut self, paste: bool) -> Self {
pub fn start(&mut self) {
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
self.cancellation_token = CancellationToken::new();
let _cancellation_token = self.cancellation_token.clone();
let _event_tx = self.event_tx.clone();
self.task = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut tick_interval = tokio::time::interval(tick_delay);
let mut render_interval = tokio::time::interval(render_delay);
_event_tx.send(Event::Init).unwrap();
let tick_delay = tick_interval.tick();
let render_delay = render_interval.tick();
let crossterm_event = reader.next().fuse();
_ = _cancellation_token.cancelled() => {
maybe_event = crossterm_event => {
CrosstermEvent::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Event::Key(key)).unwrap();
CrosstermEvent::Mouse(mouse) => {
_event_tx.send(Event::Mouse(mouse)).unwrap();
CrosstermEvent::Resize(x, y) => {
_event_tx.send(Event::Resize(x, y)).unwrap();
CrosstermEvent::FocusLost => {
_event_tx.send(Event::FocusLost).unwrap();
CrosstermEvent::FocusGained => {
_event_tx.send(Event::FocusGained).unwrap();
CrosstermEvent::Paste(s) => {
_event_tx.send(Event::Paste(s)).unwrap();
_event_tx.send(Event::Error).unwrap();
_event_tx.send(Event::Tick).unwrap();
_event_tx.send(Event::Render).unwrap();
pub fn stop(&self) -> Result<()> {
while !self.task.is_finished() {
std::thread::sleep(Duration::from_millis(1));
log::error!("Failed to abort task in 100 milliseconds for unknown reason");
pub fn enter(&mut self) -> Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
crossterm::execute!(std::io::stderr(), EnableMouseCapture)?;
crossterm::execute!(std::io::stderr(), EnableBracketedPaste)?;
pub fn exit(&mut self) -> Result<()> {
if crossterm::terminal::is_raw_mode_enabled()? {
crossterm::execute!(std::io::stderr(), DisableBracketedPaste)?;
crossterm::execute!(std::io::stderr(), DisableMouseCapture)?;
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
self.cancellation_token.cancel();
pub fn suspend(&mut self) -> Result<()> {
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?;
pub fn resume(&mut self) -> Result<()> {
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
fn deref(&self) -> &Self::Target {
fn deref_mut(&mut self) -> &mut Self::Target {