Skip to content

Commit

Permalink
feat: simple-async template as an alternative for simple ✨ (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Jan 14, 2024
1 parent 975f5cf commit 4fd82be
Show file tree
Hide file tree
Showing 84 changed files with 575 additions and 20 deletions.
27 changes: 25 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
21 changes: 5 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,11 @@ This repository contains templates for bootstrapping a Rust
cargo generate ratatui-org/ratatui-template
```

3. Choose either the [Simple](#simple-template) or [Async](./async/README.md) template.

## Simple 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
```
3. Choose one of the following templates:

- [Simple](./simple/README.md)
- [Simple Async](./simple-async/README.md)
- [Component](./component/README.md)

## See also

Expand Down
2 changes: 1 addition & 1 deletion cargo-generate.toml
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.
1 change: 1 addition & 0 deletions simple-async/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
12 changes: 12 additions & 0 deletions simple-async/Cargo.toml
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"] }
183 changes: 183 additions & 0 deletions simple-async/README.md
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(_) => {}

```
49 changes: 49 additions & 0 deletions simple-async/src/app.rs
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;
}
}
}
Loading

0 comments on commit 4fd82be

Please sign in to comment.