This simple async template will create the following project structure:
src/
├── app.rs -> holds the state and application logic
├── event.rs -> handles the terminal events (key press, mouse click, resize, etc.)
├── handler.rs -> handles the key press events and updates the application
├── lib.rs -> module definitions
├── main.rs -> entry-point
├── tui.rs -> initializes/exits the terminal interface
└── ui.rs -> renders the widgets / UI
This is identical to the [simple] template but has async
events out of the box with tokio
and
crossterm
's EventStream
.
Here's a diff
if you use as reference if want to convert your own code to async
:
./Cargo.toml
--- ./simple/Cargo.toml 2023-12-15 11:45:40
+++ ./simple-async/Cargo.toml 2024-01-14 05:33:25
@@ -6,5 +6,8 @@
edition = "2021"
[dependencies]
-crossterm = "0.27.0"
-ratatui = "0.24.0"
+crossterm = { version = "0.27.0", features = ["event-stream"] }
+futures = "0.3.30"
+ratatui = "0.25.0"
+tokio = { version = "1.35.1", features = ["full"] }
./src/event.rs
--- ./simple/src/event.rs 2024-01-06 22:25:37
+++ ./simple-async/src/event.rs 2024-01-14 05:42:04
@@ -1,8 +1,10 @@
+use std::time::Duration;
+
+use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
+use futures::{FutureExt, StreamExt};
+use tokio::sync::mpsc;
+
use crate::app::AppResult;
-use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
-use std::sync::mpsc;
-use std::thread;
-use std::time::{Duration, Instant};
/// Terminal events.
#[derive(Clone, Copy, Debug)]
@@ -22,46 +24,53 @@
#[derive(Debug)]
pub struct EventHandler {
/// Event sender channel.
- sender: mpsc::Sender<Event>,
+ sender: mpsc::UnboundedSender<Event>,
/// Event receiver channel.
- receiver: mpsc::Receiver<Event>,
+ receiver: mpsc::UnboundedReceiver<Event>,
/// Event handler thread.
- handler: thread::JoinHandle<()>,
+ handler: tokio::task::JoinHandle<()>,
}
impl EventHandler {
/// Constructs a new instance of [`EventHandler`].
pub fn new(tick_rate: u64) -> Self {
let tick_rate = Duration::from_millis(tick_rate);
- let (sender, receiver) = mpsc::channel();
- let handler = {
- let sender = sender.clone();
- thread::spawn(move || {
- let mut last_tick = Instant::now();
+ let (sender, receiver) = mpsc::unbounded_channel();
+ let _sender = sender.clone();
+ let handler = tokio::spawn(async move {
+ let mut reader = crossterm::event::EventStream::new();
+ let mut tick = tokio::time::interval(tick_rate);
loop {
- let timeout = tick_rate
- .checked_sub(last_tick.elapsed())
- .unwrap_or(tick_rate);
-
- if event::poll(timeout).expect("failed to poll new events") {
- match event::read().expect("unable to read event") {
- CrosstermEvent::Key(e) => sender.send(Event::Key(e)),
- CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)),
- CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)),
- CrosstermEvent::FocusGained => Ok(()),
- CrosstermEvent::FocusLost => Ok(()),
- CrosstermEvent::Paste(_) => unimplemented!(),
+ let tick_delay = tick.tick();
+ let crossterm_event = reader.next().fuse();
+ tokio::select! {
+ _ = tick_delay => {
+ _sender.send(Event::Tick).unwrap();
}
- .expect("failed to send terminal event")
+ Some(Ok(evt)) = crossterm_event => {
+ match evt {
+ CrosstermEvent::Key(key) => {
+ if key.kind == crossterm::event::KeyEventKind::Press {
+ _sender.send(Event::Key(key)).unwrap();
}
-
- if last_tick.elapsed() >= tick_rate {
- sender.send(Event::Tick).expect("failed to send tick event");
- last_tick = Instant::now();
+ },
+ CrosstermEvent::Mouse(mouse) => {
+ _sender.send(Event::Mouse(mouse)).unwrap();
+ },
+ CrosstermEvent::Resize(x, y) => {
+ _sender.send(Event::Resize(x, y)).unwrap();
+ },
+ CrosstermEvent::FocusLost => {
+ },
+ CrosstermEvent::FocusGained => {
+ },
+ CrosstermEvent::Paste(_) => {
+ },
}
}
- })
};
+ }
+ });
Self {
sender,
receiver,
@@ -73,7 +82,13 @@
///
/// This function will always block the current thread if
/// there is no data available and it's possible for more data to be sent.
- pub fn next(&self) -> AppResult<Event> {
- Ok(self.receiver.recv()?)
+ pub async fn next(&mut self) -> AppResult<Event> {
+ self.receiver
+ .recv()
+ .await
+ .ok_or(Box::new(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "This is an IO error",
+ )))
}
}
./src/main.rs
diff -bur ./simple/src/main.rs ./simple-async/src/main.rs
--- ./simple/src/main.rs 2023-12-15 11:45:41
+++ ./simple-async/src/main.rs 2024-01-14 05:36:37
@@ -6,7 +6,8 @@
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
-fn main() -> AppResult<()> {
+#[tokio::main]
+async fn main() -> AppResult<()> {
// Create an application.
let mut app = App::new();
@@ -22,7 +23,7 @@
// Render the user interface.
tui.draw(&mut app)?;
// Handle events.
- match tui.events.next()? {
+ match tui.events.next().await? {
Event::Tick => app.tick(),
Event::Key(key_event) => handle_key_events(key_event, &mut app)?,
Event::Mouse(_) => {}