ghost-eye/src/tui.rs

218 lines
6.2 KiB
Rust
Raw Normal View History

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),
Oneshot,
Node,
}
pub struct Tui {
pub terminal: ratatui::Terminal<Backend<Stdout>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
pub frame_rate: f64,
pub tick_rate: f64,
pub mouse: bool,
pub paste: bool,
}
impl Tui {
pub fn new() -> Result<Self> {
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<Event>,
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 oneshot_node_interval = interval(Duration::from_secs_f64(86_400.0));
let mut fast_node_interval = interval(Duration::from_secs_f64(2.0));
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,
_ = oneshot_node_interval.tick() => Event::Oneshot,
_ = fast_node_interval.tick() => Event::Node,
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<Event> {
self.event_rx.recv().await
}
}
impl Deref for Tui {
type Target = ratatui::Terminal<Backend<Stdout>>;
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();
}
}