Skip to content

Commit

Permalink
feat: Add loading message
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Feb 19, 2024
1 parent c95c65c commit 4cd46b2
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 155 deletions.
11 changes: 10 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,16 @@ export default defineConfig({
{ label: "Tui", link: "/tutorials/crates-tui/tui" },
{ label: "Errors", link: "/tutorials/crates-tui/errors" },
{ label: "Events", link: "/tutorials/crates-tui/events" },
{ label: "App", link: "/tutorials/crates-tui/app" },
{
label: "App",
collapsed: true,
items: [
{ label: "Struct", link: "/tutorials/crates-tui/app" },
{ label: "Handle Events", link: "/tutorials/crates-tui/app_handle_event" },
{ label: "Handle Actions", link: "/tutorials/crates-tui/app_handle_action" },
{ label: "Draw", link: "/tutorials/crates-tui/app_draw" },
],
},
{
label: "crates_io_api Helper",
link: "/tutorials/crates-tui/crates_io_api_helper",
Expand Down
15 changes: 8 additions & 7 deletions code/crates-tui-tutorial-app/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::sync::{atomic::AtomicBool, Arc};

use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::prelude::*;
Expand Down Expand Up @@ -49,18 +47,21 @@ pub struct App {
// ANCHOR_END: app

impl App {
// ANCHOR: app_new
pub fn new() -> Self {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let loading_status = Arc::new(AtomicBool::default());
let search = SearchPage::new(tx.clone(), loading_status.clone());
let search_page = SearchPage::new(tx.clone());
let mode = Mode::default();
let quit = false;
Self {
rx,
tx,
mode: Mode::default(),
search_page: search,
quit: false,
mode,
search_page,
quit,
}
}
// ANCHOR_END: app_new

// ANCHOR: app_run
pub async fn run(
Expand Down
31 changes: 17 additions & 14 deletions code/crates-tui-tutorial-app/src/widgets/search_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crossterm::event::{Event as CrosstermEvent, KeyEvent};
use itertools::Itertools;
use ratatui::{
layout::{Constraint, Layout, Position},
widgets::StatefulWidget,
text::Line,
widgets::{StatefulWidget, Widget},
};
use tokio::sync::mpsc::UnboundedSender;
use tui_input::backend::crossterm::EventHandler;
Expand Down Expand Up @@ -38,10 +39,8 @@ pub struct SearchPage {
// ANCHOR_END: search_page

impl SearchPage {
pub fn new(
tx: UnboundedSender<Action>,
loading_status: Arc<AtomicBool>,
) -> Self {
pub fn new(tx: UnboundedSender<Action>) -> Self {
let loading_status = Arc::new(AtomicBool::default());
Self {
results: SearchResults::default(),
prompt: SearchPrompt::default(),
Expand All @@ -62,6 +61,10 @@ impl SearchPage {
self.results.scroll_next();
}

pub fn loading(&self) -> bool {
self.loading_status.load(Ordering::SeqCst)
}

// ANCHOR: prompt_methods
pub fn handle_key(&mut self, key: KeyEvent) {
self.prompt.input.handle_event(&CrosstermEvent::Key(key));
Expand All @@ -70,10 +73,6 @@ impl SearchPage {
pub fn cursor_position(&self) -> Option<Position> {
self.prompt.cursor_position()
}

pub fn submit_query(&mut self) {
self.reload_data();
}
// ANCHOR_END: prompt_methods

pub fn update_search_results(&mut self) {
Expand All @@ -84,13 +83,14 @@ impl SearchPage {
self.results.crates = crates;
}

pub fn reload_data(&mut self) {
self.prepare_reload();
// ANCHOR: submit
pub fn submit_query(&mut self) {
self.prepare_request();
let search_params = self.create_search_parameters();
self.request_search_results(search_params);
}

pub fn prepare_reload(&mut self) {
pub fn prepare_request(&mut self) {
self.results.select(None);
}

Expand Down Expand Up @@ -130,8 +130,7 @@ impl SearchPage {
}
// ANCHOR_END: request_search_results

// ANCHOR: mode_methods
// ANCHOR_END: mode_methods
// ANCHOR_END: submit
}

// ANCHOR: search_page_widget
Expand All @@ -148,6 +147,10 @@ impl StatefulWidget for SearchPageWidget {
buf: &mut ratatui::prelude::Buffer,
state: &mut Self::State,
) {
if state.loading() {
Line::from("Loading...").right_aligned().render(area, buf);
}

let prompt_height = 5;

let [main, prompt] = Layout::vertical([
Expand Down
138 changes: 17 additions & 121 deletions src/content/docs/tutorials/crates-tui/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,85 +51,31 @@ The variants of the `Action` enum we will be using are:
{{#include @code/crates-tui-tutorial-app/src/app.rs:action}}
```

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_handle_action}}
```

While this may seem like a lot more boilerplate at first, `Action` enums have a few advantages.

Firstly, they can be mapped from keypresses programmatically. For example, you can define a
configuration file that reads which keys are mapped to which `Action` like so:

```toml
[keymap]
"q" = "Quit"
"j" = "ScrollDown"
"k" = "ScrollUp"
```

Then you can add a new key configuration like so:

```rust
struct App {
...
// new field
keyconfig: HashMap<KeyCode, Action>
}
```

If you populate `keyconfig` with the contents of a user provided `toml` file, then you can figure
out which action to take by updating the `handle_action()` function:

```rust
fn handle_action(self: &App, event: Event) -> Action {
if let Event::Key(key) = event {
return self.keyconfig.get(key.code).unwrap_or(Action::None)
};
Action::None
}
```

For now all we need to know is we store both pairs of the `Action` channel in the `App`, i.e.
We will discuss `Action`s in more detail in a following subsection. For now all we need to know is
we store both pairs of the `Action` channel in the `App`, i.e.

- `tx`: Transmitter
- `rx`: Receiver

These pairs from the channel can be used sending and receiving actions. These pairs are created
using the `mpsc` channel, which stands for multiple producer single consumer channels.

```rust
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
```

This means that `tx` can be cloned many times but `rx` can only be moved. Practically, what this
means for our application is that we can pass around clones of the transmitter to any children of
the `App` struct and send `Action`s from anywhere in the `App`, and we will have a single `rx` here
in the root `App` struct that receives those `Action`s and acts on them. This allows us to organize
and struct our application in any way we please and propagate information up from child to parent
structs. As an illustration of this, we will store the state of the entire app in a `SearchPage`
struct, which will contain a `tx` clone used to decide how the app is going to behave.
Practically, what this means for our application is that we can pass around clones of the
transmitter to any children of the `App` struct and send `Action`s from anywhere in the `App`, and
we will have a single `rx` here in the root `App` struct that receives those `Action`s and acts on
them. This allows us to organize and struct our application in any way we please and propagate
information up from child to parent structs. As an illustration of this, we will store the state of
the entire app in a `SearchPage` struct, which will contain a `tx` clone used to decide how the app
is going to behave.

Because the `run` method has the following block of code:
We can construct an `App` instance like so:

```rust
while let Ok(action) = self.rx.try_recv() {
self.handle_action(action.clone(), &mut tui)?
.map(|action| self.tx.send(action));
impl App {
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_new}}
}
```

Any time the `rx` receiver receives an `Action` from _any_ `tx` transmitter, the application will
"handle the action" and the state of the application will update. This means you can send for
example a new variant `Action::Error(String)` from inside a tokio task to trigger a popup that shows
an error message associated with a failed task.

:::note[Homework]

Can you add a popup to the app that shows the error message received by the `Action::Error(String)`
variant?

:::

In `main`, we have call a `async` run method:

```rust
Expand All @@ -144,65 +90,15 @@ Let's implement that method now:

The two important parts of the `run` method are:

- `handle_event`
- `handle_action`
- [`handle_event`](./app_handle_event)
- [`handle_action`](./app_handle_action)

`events.next().await` returns an `Event` from the stream we implemented in the previous section.
That `event` is passed to the `handle_event` method. `handle_event` returns an `Option<Action>`
which we map to an `Action` that has to be acted on by the app:

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_handle_event}}
```

Most of the work in deciding which `Action` should be taken is done in `handle_key_event`.

For our application, we want to be able to:

**In search mode**:

1. Type any character into the search prompt
2. Hit Enter to submit a search query
3. Hit Esc to return focus to the results view

**In results mode**:

1. Use arrow keys to scroll
2. Use `/` to enter search mode
3. Use Esc to quit the application

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_handle_key_event}}
```

In `handle_action`, we map the `Action` back to the `app`'s corresponding method:

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_handle_action}}
```

Finally, to render the app, we render a stateful widget using `App` as the state.

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_draw}}
```

To do this, we define a singleton `struct` for the widget:

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_widget}}
```

And implement the `StatefulWidget` trait for this struct:

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_statefulwidget}}
```

Here's the full app for your reference:
Let's walk through these methods now.

<details>

<summary>Reference: <code>src/app.rs</code></summary>

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs}}
```
Expand Down
33 changes: 33 additions & 0 deletions src/content/docs/tutorials/crates-tui/app_draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: Draw
---

Finally, to render the app, we render a stateful widget using `App` as the state.

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_draw}}
```

To do this, we define a singleton `struct` for the widget:

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_widget}}
```

And implement the `StatefulWidget` trait for this struct:

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs:app_statefulwidget}}
```

Here's the full app.rs for your reference:

<details>

<summary>Copy the following into <code>src/app.rs</code></summary>

```rust
{{#include @code/crates-tui-tutorial-app/src/app.rs}}
```

</details>
Loading

0 comments on commit 4cd46b2

Please sign in to comment.