In the previous counter app, we had a purely sequential blocking application. There are times when
you may be interested in running IO operations or compute asynchronously.
For this tutorial, we will build a single file version of an async TUI using
tokio. This tutorial section is a simplified version of the
ratatui-async-template project.
Let’s take the single file multiple function example from the counter app from earlier:
// Hover on this codeblock and click "Show hidden lines" in the top right to see the full codeuse color_eyre::eyre::Result;
use crossterm::{
event::{self, Event::Key, KeyCode::Char},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::{CrosstermBackend, Terminal},
widgets::Paragraph,
};
pubtypeFrame<'a> = ratatui::Frame<'a, CrosstermBackend<std::io::Stderr>>;
fnstartup() -> Result<()> {
enable_raw_mode()?;
execute!(std::io::stderr(), EnterAlternateScreen)?;
Ok(())
}
fnshutdown() -> Result<()> {
execute!(std::io::stderr(), LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
// App statestructApp {
counter: i64,
should_quit: bool,
}
// App ui render functionfnui(app: &App, f: &mut Frame<'_>) {
f.render_widget(Paragraph::new(format!("Counter: {}", app.counter)), f.size());
}
// App update functionfnupdate(app: &mut App) -> Result<()> {
if event::poll(std::time::Duration::from_millis(250))? {
iflet Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press {
match key.code {
Char('j') => app.counter += 1,
Char('k') => app.counter -= 1,
Char('q') => app.should_quit = true,
_ => {},
}
}
}
}
Ok(())
}
fnrun() -> Result<()> {
// ratatui terminalletmut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
// application stateletmut app = App { counter: 0, should_quit: false };
loop {
// application update update(&mut app)?;
// application render t.draw(|f| {
ui(&app, f);
})?;
// application exitif app.should_quit {
break;
}
}
Ok(())
}
fnmain() -> Result<()> {
// setup terminal
startup()?;
let result = run();
// teardown terminal before unwrapping Result of app run
shutdown()?;
result?;
Ok(())
}
Tokio is an asynchronous runtime for the Rust programming language. It provides the building blocks
needed for writing network applications. We recommend you read the
Tokio documentation to learn more.
For the setup for this section of the tutorial, we are going to make just one change. We are going
to make our main function a tokio entry point.
// Hover on this codeblock and click "Show hidden lines" in the top right to see the full codeuse color_eyre::eyre::Result;
use crossterm::{
event::{self, Event::Key, KeyCode::Char},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::{CrosstermBackend, Terminal},
widgets::Paragraph,
};
pubtypeFrame<'a> = ratatui::Frame<'a, CrosstermBackend<std::io::Stderr>>;
fnstartup() -> Result<()> {
enable_raw_mode()?;
execute!(std::io::stderr(), EnterAlternateScreen)?;
Ok(())
}
fnshutdown() -> Result<()> {
execute!(std::io::stderr(), LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
// App statestructApp {
counter: i64,
should_quit: bool,
}
// App ui render functionfnui(app: &App, f: &mut Frame<'_>) {
f.render_widget(Paragraph::new(format!("Counter: {}", app.counter)), f.size());
}
// App update functionfnupdate(app: &mut App) -> Result<()> {
if event::poll(std::time::Duration::from_millis(250))? {
iflet Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press {
match key.code {
Char('j') => app.counter += 1,
Char('k') => app.counter -= 1,
Char('q') => app.should_quit = true,
_ => {},
}
}
}
}
Ok(())
}
fnrun() -> Result<()> {
// ratatui terminalletmut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
// application stateletmut app = App { counter: 0, should_quit: false };
loop {
// application update update(&mut app)?;
// application render t.draw(|f| {
ui(&app, f);
})?;
// application exitif app.should_quit {
break;
}
}
Ok(())
}
#[tokio::main]asyncfnmain() -> Result<()> {
// setup terminal
startup()?;
let result = run();
// teardown terminal before unwrapping Result of app run
shutdown()?;
result?;
Ok(())
}
Adding this #[tokio::main] macro allows us to spawn tokio tasks within main. At the moment,
there are no async functions other than main and we are not using .await anywhere yet. We will
change that in the following sections. But first, we let us introduce the Action enum.