Skip to content

Multiple Functions

In this section, we will walk through the process of refactoring the application to set ourselves up better for bigger projects. Not all of these changes are ratatui specific, and are generally good coding practices to follow.

We are still going to keep everything in one file for this section, but we are going to split the previous functionality into separate functions.

Organizing imports

The first thing you might consider doing is reorganizing imports with qualified names.

use crossterm::{
event::{self, Event::Key, KeyCode::Char},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::{CrosstermBackend, Terminal, Frame},
widgets::Paragraph,
};

Typedefs and Type Aliases

By defining custom types and aliases, we can simplify our code and make it more expressive.

type Err = Box<dyn std::error::Error>;
type Result<T> = std::result::Result<T, Err>;

App struct

By defining an App struct, we can encapsulate our application state and make it more structured.

struct App {
counter: i64,
should_quit: bool,
}
  • counter holds the current value of our counter.
  • should_quit is a flag that indicates whether the application should exit its main loop.

Breaking up main()

We can extract significant parts of the main() function into separate smaller functions, e.g. startup(), shutdown(), ui(), update(), run().

startup() is responsible for initializing the terminal.

fn startup() -> Result<()> {
enable_raw_mode()?;
execute!(std::io::stderr(), EnterAlternateScreen)?;
Ok(())
}

shutdown() cleans up the terminal.

fn shutdown() -> Result<()> {
execute!(std::io::stderr(), LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}

ui() handles rendering of our application state.

fn ui(app: &App, f: &mut Frame) {
f.render_widget(Paragraph::new(format!("Counter: {}", app.counter)), f.size());
}

update() processes user input and updates our application state.

fn update(app: &mut App) -> Result<()> {
if event::poll(std::time::Duration::from_millis(250))? {
if let 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(())
}

run() contains our main application loop.

fn run() -> Result<()> {
// ratatui terminal
let mut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
// application state
let mut app = App { counter: 0, should_quit: false };
loop {
// application render
t.draw(|f| {
ui(&app, f);
})?;
// application update
update(&mut app)?;
// application exit
if app.should_quit {
break;
}
}
Ok(())
}

Each function now has a specific task, making our main application logic more organized and easier to follow.

fn main() -> Result<()> {
startup()?;
let status = run();
shutdown()?;
status?;
Ok(())
}

Conclusion

By making our code more organized, modular, and readable, we not only make it easier for others to understand and work with but also set the stage for future enhancements and extensions.

Here’s the full code for reference:

use anyhow::Result;
use crossterm::{
event::{self, Event::Key, KeyCode::Char},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
prelude::{CrosstermBackend, Terminal, Frame},
widgets::Paragraph,
};
fn startup() -> Result<()> {
enable_raw_mode()?;
execute!(std::io::stderr(), EnterAlternateScreen)?;
Ok(())
}
fn shutdown() -> Result<()> {
execute!(std::io::stderr(), LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
// App state
struct App {
counter: i64,
should_quit: bool,
}
// App ui render function
fn ui(app: &App, f: &mut Frame) {
f.render_widget(Paragraph::new(format!("Counter: {}", app.counter)), f.size());
}
// App update function
fn update(app: &mut App) -> Result<()> {
if event::poll(std::time::Duration::from_millis(250))? {
if let 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(())
}
fn run() -> Result<()> {
// ratatui terminal
let mut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
// application state
let mut app = App { counter: 0, should_quit: false };
loop {
// application update
update(&mut app)?;
// application render
t.draw(|f| {
ui(&app, f);
})?;
// application exit
if app.should_quit {
break;
}
}
Ok(())
}
fn main() -> Result<()> {
// setup terminal
startup()?;
let result = run();
// teardown terminal before unwrapping Result of app run
shutdown()?;
result?;
Ok(())
}

Here’s a flow chart representation of the various steps in the program:

No KeyPress
KeyPress Received
Yes
No
Main: Run
Main: Poll KeyPress
Main: Update App
Main: Check should_quit?
Main: Break Loop
Main: Start
Main: End
Draw