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 statestruct App { counter: i64, should_quit: bool,}
// App ui render functionfn ui(app: &App, f: &mut Frame) { f.render_widget(Paragraph::new(format!("Counter: {}", app.counter)), f.size());}
// App update functionfn 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: