218 lines
6.2 KiB
Rust
218 lines
6.2 KiB
Rust
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();
|
|
}
|
|
}
|