-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: simple-async template as an alternative for simple ✨
- Loading branch information
Showing
84 changed files
with
575 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,7 +29,7 @@ jobs: | |
cd ${{ runner.temp }}/$PROJECT_NAME | ||
cargo check --tests | ||
build-async: | ||
build-simple-async: | ||
runs-on: ubuntu-latest | ||
env: | ||
PROJECT_NAME: ratatui-github-example | ||
|
@@ -40,7 +40,30 @@ jobs: | |
uses: cargo-generate/[email protected] | ||
with: | ||
name: ${{ env.PROJECT_NAME }} | ||
template: async | ||
template: simple-async | ||
- name: Install Rust toolchain | ||
uses: dtolnay/rust-toolchain@stable | ||
- name: Cargo check | ||
# we need to move the generated project to a temp folder, away from the template project | ||
# otherwise `cargo` runs would fail | ||
# see https://github.com/rust-lang/cargo/issues/9922 | ||
run: | | ||
mv $PROJECT_NAME ${{ runner.temp }}/ | ||
cd ${{ runner.temp }}/$PROJECT_NAME | ||
cargo check --tests | ||
build-component: | ||
runs-on: ubuntu-latest | ||
env: | ||
PROJECT_NAME: ratatui-github-example | ||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
- name: Run cargo generate | ||
uses: cargo-generate/[email protected] | ||
with: | ||
name: ${{ env.PROJECT_NAME }} | ||
template: component | ||
template_values_file: .github/workflows/template.toml | ||
- name: Install Rust toolchain | ||
uses: dtolnay/rust-toolchain@stable | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
# configuration for https://cargo-generate.github.io/cargo-generate/ | ||
|
||
[template] | ||
sub_templates = ["simple", "async"] | ||
sub_templates = ["simple", "simple-async", "component"] |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
[package] | ||
name = "{{project-name}}" | ||
version = "0.1.0" | ||
authors = ["{{authors}}"] | ||
license = "MIT" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
crossterm = { version = "0.27.0", features = ["event-stream"] } | ||
futures = "0.3.30" | ||
ratatui = "0.25.0" | ||
tokio = { version = "1.35.1", features = ["full"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
## Simple Async template | ||
|
||
The simple template will create the following project structure: | ||
|
||
```text | ||
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 the exact diff if you want to convert your own code to async: | ||
|
||
**`./Cargo.toml`** | ||
|
||
```diff | ||
--- ./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"] } | ||
+tokio-util = "0.7.10" | ||
``` | ||
|
||
**`./src/event.rs`** | ||
|
||
```diff | ||
--- ./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 | ||
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(_) => {} | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
use std::error; | ||
|
||
/// Application result type. | ||
pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>; | ||
|
||
/// Application. | ||
#[derive(Debug)] | ||
pub struct App { | ||
/// Is the application running? | ||
pub running: bool, | ||
/// counter | ||
pub counter: u8, | ||
} | ||
|
||
impl Default for App { | ||
fn default() -> Self { | ||
Self { | ||
running: true, | ||
counter: 0, | ||
} | ||
} | ||
} | ||
|
||
impl App { | ||
/// Constructs a new instance of [`App`]. | ||
pub fn new() -> Self { | ||
Self::default() | ||
} | ||
|
||
/// Handles the tick event of the terminal. | ||
pub fn tick(&self) {} | ||
|
||
/// Set running to false to quit the application. | ||
pub fn quit(&mut self) { | ||
self.running = false; | ||
} | ||
|
||
pub fn increment_counter(&mut self) { | ||
if let Some(res) = self.counter.checked_add(1) { | ||
self.counter = res; | ||
} | ||
} | ||
|
||
pub fn decrement_counter(&mut self) { | ||
if let Some(res) = self.counter.checked_sub(1) { | ||
self.counter = res; | ||
} | ||
} | ||
} |
Oops, something went wrong.