Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: simple-async template as a alternative for simple ✨ #43

Merged
merged 1 commit into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
```
kdheepak marked this conversation as resolved.
Show resolved Hide resolved
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.
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:
kdheepak marked this conversation as resolved.
Show resolved Hide resolved

**`./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