Skip to content

Commit

Permalink
feat: Add visual keypress to gifs
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Feb 21, 2024
1 parent dedce34 commit ed4293f
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 57 deletions.
27 changes: 21 additions & 6 deletions code/crates-tui-tutorial-app/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::prelude::*;
use ratatui::{prelude::*, widgets::Paragraph};

use crate::{
events::{Event, Events},
Expand Down Expand Up @@ -59,10 +59,12 @@ struct AppWidget;
// ANCHOR: app
#[derive(Debug)]
pub struct App {
quit: bool,
last_key_event: Option<crossterm::event::KeyEvent>,
mode: Mode,

rx: tokio::sync::mpsc::UnboundedReceiver<Action>,
tx: tokio::sync::mpsc::UnboundedSender<Action>,
mode: Mode,
quit: bool,
search_page: SearchPage,
}
// ANCHOR_END: app
Expand All @@ -74,12 +76,14 @@ impl App {
let search_page = SearchPage::new(tx.clone());
let mode = Mode::default();
let quit = false;
let last_key_event = None;
Self {
quit,
last_key_event,
mode,
rx,
tx,
mode,
search_page,
quit,
}
}
// ANCHOR_END: app_new
Expand Down Expand Up @@ -113,6 +117,7 @@ impl App {
fn handle_event(&mut self, e: Event) -> Result<()> {
use crossterm::event::Event as CrosstermEvent;
if let Event::Crossterm(CrosstermEvent::Key(key)) = e {
self.last_key_event = Some(key);
self.handle_key(key)
};
Ok(())
Expand Down Expand Up @@ -205,8 +210,18 @@ impl StatefulWidget for AppWidget {
type State = App;

fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let [last_key_event, search_page] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(0)])
.areas(area);

if let Some(key) = state.last_key_event {
Paragraph::new(format!("last key event: {:?}", key.code))
.right_aligned()
.render(last_key_event, buf);
}

SearchPageWidget { mode: state.mode }.render(
area,
search_page,
buf,
&mut state.search_page,
);
Expand Down
19 changes: 16 additions & 3 deletions code/crates-tui-tutorial-app/src/bin/part-app-async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub enum Action {
// ANCHOR: app
pub struct App {
quit: bool,
last_key_event: Option<crossterm::event::KeyEvent>,
mode: Mode,

crates: Arc<Mutex<Vec<crates_io_api::Crate>>>, // new
Expand All @@ -155,9 +156,11 @@ impl App {
let table_state = TableState::default();
let prompt = Default::default();
let cursor_position = None;
let last_key_event = None;
Self {
quit,
mode,
last_key_event,
crates,
table_state,
prompt,
Expand Down Expand Up @@ -191,6 +194,7 @@ impl App {
match e {
Event::Render => self.draw(tui)?,
Event::Crossterm(CrosstermEvent::Key(key)) => {
self.last_key_event = Some(key);
match self.mode {
Mode::Prompt => match key.code {
Enter => self.submit_search_query(), // new
Expand Down Expand Up @@ -311,9 +315,12 @@ impl StatefulWidget for AppWidget {
type State = App;

fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let [results, prompt] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(5)])
.areas(area);
let [last_key_event, results, prompt] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(0),
Constraint::Length(5),
])
.areas(area);

let table = state.results();
StatefulWidget::render(table, results, buf, &mut state.table_state);
Expand All @@ -328,6 +335,12 @@ impl StatefulWidget for AppWidget {
buf,
);
state.update_prompt_cursor_state(prompt);

if let Some(key) = state.last_key_event {
Paragraph::new(format!("last key event: {:?}", key.code))
.right_aligned()
.render(last_key_event, buf);
}
}
}
// ANCHOR_END: app_statefulwidget
Expand Down
10 changes: 5 additions & 5 deletions code/crates-tui-tutorial-app/src/bin/part-app-basics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ impl App {
// ANCHOR: app_handle_event
fn handle_event(&mut self, e: Event, tui: &mut Tui) -> Result<()> {
use crossterm::event::Event as CrosstermEvent;
use crossterm::event::KeyCode::Esc;
use crossterm::event::KeyCode;
match e {
Event::Crossterm(CrosstermEvent::Key(key)) if key.code == Esc => {
self.quit()
}
Event::Crossterm(CrosstermEvent::Key(key)) => {
self.last_key_event = Some(key)
self.last_key_event = Some(key);
if key.code == KeyCode::Esc {
self.quit()
}
}
Event::Render => self.draw(tui)?,
_ => (),
Expand Down
37 changes: 24 additions & 13 deletions code/crates-tui-tutorial-app/src/bin/part-app-mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,21 @@ pub enum Mode {
// ANCHOR: app
pub struct App {
quit: bool,
frame_count: usize,
mode: Mode,
last_key_event: Option<crossterm::event::KeyEvent>,
mode: Mode, // new
}
// ANCHOR_END: app

impl App {
// ANCHOR: app_new
pub fn new() -> Self {
let quit = false;
let frame_count = 0;
let mode = Mode::default();
let last_key_event = None;
Self {
quit,
frame_count,
mode,
last_key_event,
}
}
// ANCHOR_END: app_new
Expand All @@ -63,12 +63,15 @@ impl App {
// ANCHOR: app_handle_event
fn handle_event(&mut self, e: Event, tui: &mut Tui) -> Result<()> {
use crossterm::event::Event as CrosstermEvent;
use crossterm::event::KeyCode::Esc;
use crossterm::event::KeyCode;
match e {
Event::Crossterm(CrosstermEvent::Key(key)) if key.code == Esc => {
match self.mode {
Mode::Prompt => self.switch_mode(Mode::Results),
Mode::Results => self.quit(),
Event::Crossterm(CrosstermEvent::Key(key)) => {
self.last_key_event = Some(key);
if key.code == KeyCode::Esc {
match self.mode {
Mode::Prompt => self.switch_mode(Mode::Results),
Mode::Results => self.quit(),
}
}
}
Event::Render => self.draw(tui)?,
Expand All @@ -81,7 +84,6 @@ impl App {
// ANCHOR: app_draw
fn draw(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
self.frame_count = frame.count();
frame.render_stateful_widget(AppWidget, frame.size(), self);
})?;
Ok(())
Expand Down Expand Up @@ -122,9 +124,12 @@ impl StatefulWidget for AppWidget {
type State = App;

fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let [results, prompt] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(5)])
.areas(area);
let [last_key_event, results, prompt] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(0),
Constraint::Length(5),
])
.areas(area);

let table = state.results();
Widget::render(table, results, buf);
Expand All @@ -138,6 +143,12 @@ impl StatefulWidget for AppWidget {
}),
buf,
);

if let Some(key) = state.last_key_event {
Paragraph::new(format!("last key event: {:?}", key.code))
.right_aligned()
.render(last_key_event, buf);
}
}
}
// ANCHOR_END: app_statefulwidget
Expand Down
28 changes: 19 additions & 9 deletions code/crates-tui-tutorial-app/src/bin/part-app-prototype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,32 +160,33 @@ pub enum Action {
// ANCHOR: app
pub struct App {
quit: bool,
frame_count: usize,
last_key_event: Option<crossterm::event::KeyEvent>,
mode: Mode,
tx: tokio::sync::mpsc::UnboundedSender<Action>,
rx: tokio::sync::mpsc::UnboundedReceiver<Action>,
crates: Arc<Mutex<Vec<crates_io_api::Crate>>>,
table_state: TableState,
prompt: tui_input::Input,
cursor_position: Option<Position>,

tx: tokio::sync::mpsc::UnboundedSender<Action>, // new
rx: tokio::sync::mpsc::UnboundedReceiver<Action>, // new
}
// ANCHOR_END: app

impl App {
// ANCHOR: app_new
pub fn new() -> Self {
let quit = false;
let frame_count = 0;
let mode = Mode::default();
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let crates = Default::default();
let table_state = TableState::default();
let prompt = Default::default();
let cursor_position = None;
let last_key_event = None;
Self {
quit,
frame_count,
mode,
last_key_event,
tx,
rx,
crates,
Expand Down Expand Up @@ -225,6 +226,7 @@ impl App {
fn handle_event(&mut self, e: Event) -> Result<()> {
use crossterm::event::Event as CrosstermEvent;
if let Event::Crossterm(CrosstermEvent::Key(key)) = e {
self.last_key_event = Some(key);
self.handle_key(key)
};
Ok(())
Expand Down Expand Up @@ -259,7 +261,6 @@ impl App {
// ANCHOR: app_draw
fn draw(&mut self, tui: &mut Tui) -> Result<()> {
tui.draw(|frame| {
self.frame_count = frame.count();
frame.render_stateful_widget(AppWidget, frame.size(), self);
self.update_cursor(frame);
})?;
Expand Down Expand Up @@ -357,9 +358,12 @@ impl StatefulWidget for AppWidget {
type State = App;

fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let [results, prompt] =
Layout::vertical([Constraint::Fill(0), Constraint::Length(5)])
.areas(area);
let [last_key_event, results, prompt] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(0),
Constraint::Length(5),
])
.areas(area);

let table = state.results();
StatefulWidget::render(table, results, buf, &mut state.table_state);
Expand All @@ -374,6 +378,12 @@ impl StatefulWidget for AppWidget {
buf,
);
state.update_prompt_cursor_state(prompt);

if let Some(key) = state.last_key_event {
Paragraph::new(format!("last key event: {:?}", key.code))
.right_aligned()
.render(last_key_event, buf);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion code/crates-tui-tutorial-app/src/widgets/search_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ impl StatefulWidget for SearchPageWidget {
state: &mut Self::State,
) {
if state.loading() {
Line::from("Loading...").right_aligned().render(area, buf);
Line::from("Loading...").render(area, buf);
}

let prompt_height = 5;
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/tutorials/crates-tui/crates-tui-demo-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/content/docs/tutorials/crates-tui/crates-tui-demo-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/content/docs/tutorials/crates-tui/crates-tui-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 8 additions & 6 deletions src/content/docs/tutorials/crates-tui/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
title: Crates TUI
---

In the previous tutorials, we had a purely sequential blocking application. However, there are times
when you may be interested in running IO operations or computations asynchronously in between
rendering frames.

This tutorial will lead you through creating an async TUI app that lists crates from crates.io based
on a user search request in an `async` manner.
In the previous tutorials, we were building a purely sequentially operational applications. However,
there are times when you may be interested in running IO operations or heavy computations in between
rendering frames. And when you do this, you don't want to block rendering. You can achieve a
consistent frame rate for rendering by running these blocking operations in a background thread or
task.

This tutorial will lead you through creating an `async` TUI app that lists crates from crates.io
based on a user search request in an `async` manner.

![](./crates-tui-demo-1.png)

Expand Down

0 comments on commit ed4293f

Please sign in to comment.