FAQ

What is the difference between a library and a framework?

The terms library and framework are often used interchangeably in software development, but they serve different purposes and have distinct characteristics.

LibraryFramework
UsageA library is a collection of functions and procedures that a programmer can call in their application. The library provides specific functionality, but it’s the developer’s responsibility to explicitly call and use it.A framework is a pre-built structure or scaffold that developers build their application within. It provides a foundation, enforcing a particular way of creating an application.
Control FlowIn the case of a library, the control flow remains with the developer’s application. The developer chooses when and where to use the library.With a framework, the control flow is inverted. The framework decides the flow of control by providing places for the developer to plug in their own logic (often referred to as “Inversion of Control” or IoC).
NatureLibraries are passive in nature. They wait for the application’s code to invoke their methods.Frameworks are active and have a predefined flow of their own. The developer fills in specific pieces of the framework with their own code.
ExampleImagine you’re building a house. A library would be like a toolbox with tools (functions) that you can use at will. You decide when and where to use each tool.Using the house-building analogy, a framework would be like a prefabricated house where the main structure is already built. You’re tasked with filling in the interiors and decor, but you have to follow the design and architecture already provided by the prefabricated design.

What is the difference between a ratatui (a library) and a tui-realm (a framework)?

While ratatui provides tools (widgets) for building terminal UIs, it doesn’t dictate or enforce a specific way to structure your application. You need to decide how to best use the library in your particular context, giving you more flexibility.

In contrast, tui-realm might provide more guidelines and enforcements about how your application should be structured or how data flows through it. And, for the price of that freedom, you get more features out of the box with tui-realm and potentially lesser code in your application to do the same thing that you would with ratatui.

What is the difference between ratatui and cursive?

Cursive and Ratatui are both libraries that make TUIs easier to write. Both libraries are great! Both also work on linux, macOS and windows.

Cursive

Cursive uses a more declarative UI: the user defines the layout, then cursive handles the event loop. Cursive also handles most input (including mouse clicks), and forwards events to the currently focused view. User-code is more focused on “events” than on keyboard input. Cursive also supports different backends like ncurses, pancurses, termion, and crossterm.

One of cursive’s main features is its built-in event loop. You can easily attach callbacks to events like clicks or key presses, making it straightforward to handle user interactions.

use cursive::views::{Dialog, TextView};

fn main() {
    // Creates the cursive root - required for every application.
    let mut siv = cursive::default();

    // Creates a dialog with a single "Quit" button
    siv.add_layer(Dialog::around(TextView::new("Hello World!"))
                         .title("Cursive")
                         .button("Quit", |s| s.quit()));

    // Starts the event loop.
    siv.run();
}

Ratatui

In Ratatui, the user handles the event loop, the application state, and re-draws the entire UI on each iteration. It does not handle input and users have use another library (like crossterm). Ratatui supports Crossterm, termion, wezterm as backends.

use ratatui::{prelude::*, widgets::*};

fn init() -> Result<(), Box<dyn std::error::Error>> {
  crossterm::terminal::enable_raw_mode()?;
  crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?;
  Ok(())
}

fn exit() -> Result<(), Box<dyn std::error::Error>> {
  crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?;
  crossterm::terminal::disable_raw_mode()?;
  Ok(())
}

fn centered_rect(r: Rect, percent_x: u16, percent_y: u16) -> Rect {
  let popup_layout = Layout::default()
    .direction(Direction::Vertical)
    .constraints([
      Constraint::Percentage((100 - percent_y) / 2),
      Constraint::Percentage(percent_y),
      Constraint::Percentage((100 - percent_y) / 2),
    ])
    .split(r);

  Layout::default()
    .direction(Direction::Horizontal)
    .constraints([
      Constraint::Percentage((100 - percent_x) / 2),
      Constraint::Percentage(percent_x),
      Constraint::Percentage((100 - percent_x) / 2),
    ])
    .split(popup_layout[1])[1]
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
  init()?;

  let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

  loop {
    terminal.draw(|f| {
      let rect = centered_rect(f.size(), 35, 35);
      f.render_widget(
        Paragraph::new("Hello World!\n\n\n'q' to quit")
          .block(
            Block::default().title(block::Title::from("Ratatui").alignment(Alignment::Center)).borders(Borders::all()),
          )
          .alignment(Alignment::Center),
        rect,
      );
    })?;

    if crossterm::event::poll(std::time::Duration::from_millis(250))? {
      if let crossterm::event::Event::Key(key) = crossterm::event::read()? {
        if key.code == crossterm::event::KeyCode::Char('q') {
          break;
        }
      }
    }
  }
  exit()?;

  Ok(())
}

You may have to write more code but you get precise control over exact UI you want to display with Ratatui.

Can you change font size in a terminal using ratatui?

ratatui itself doesn’t control the terminal’s font size. ratatui renders content based on the size and capabilities of the terminal it’s running in. If you want to change the font size, you’ll need to adjust the settings of your terminal emulator.

However, changing this setting in your terminal emulator will only change the font size for you while you are developing your ratatui based application.

When a user zooms in and out using terminal shortcuts, that will typically change the font size in their terminal. You typically will not know what the terminal font size is ahead of time.

However, you can know the current terminal size (i.e. columns and rows). Additionally, when zooming in and out ratatui applications will see a terminal resize event that will contain the new columns and rows. You should ensure your ratatui application can handle these changes gracefully.

You can detect changes in the terminal’s size by listening for terminal resize events from the backend of your choice and you can adjust your application layout as needed.

For example, here’s how you might do it in crossterm:

    match crossterm::terminal::read() {
        Ok(evt) => {
            match evt {
                crossterm::event::Event::Resize(x, y) => {
                    // handle resize event here
                },
                _ => {}
            }
        }
    }

Tip

Since this can happen on the user end without your control, this means that you’ll have to design your ratatui based terminal user interface application to display content well in a number of different terminal sizes.

ratatui does support various styles, including bold, italic, underline, and more, and while this doesn’t change the font size, it does provide you with the capability to emphasize or de-emphasize text content in your application.

Additionally you can use figlet or tui-big-text to display text content across multiple lines. Here’s an example using tui-big-text:

tui-big-text

Can you use multiple terminal.draw() calls consequently?

You cannot use terminal.draw() multiple times in the same main loop.

Because Ratatui uses a double buffer rendering technique, writing code like this will NOT render all three widgets:

  loop {
    terminal.draw(|f| {
      f.render_widget(widget1, f.size());
    })?;
    terminal.draw(|f| {
      f.render_widget(widget2, f.size());
    })?;
    terminal.draw(|f| {
      f.render_widget(widget3, f.size());
    })?;
    // handle events
    // manage state
  }

You want to write the code like this instead:

  loop {
    terminal.draw(|f| {
      f.render_widget(widget1, f.size());
      f.render_widget(widget2, f.size());
      f.render_widget(widget3, f.size());
    })?;
    // handle events
    // manage state
  }

Should I use stdout or stderr?

When using crossterm, application developers have the option of rendering to stdout or stderr.

let mut t = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
// OR
let mut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

Both of these will work fine for normal purposes. The question you have to ask if how would you like your application to behave in non-TTY environments.

For example, if you run ratatui-application | grep foo with stdout, your application won’t render anything to the screen and there would be no indication of anything going wrong. With stderr the application will still render a TUI.

With stdout:

  • Every app needs to add code to check if the output is a TTY and do something different based on the result
  • App can’t write a result to the user that can be passed in a pipeline, e.g. my-select-some-value-app | grep foo
  • Tends to be what most command line applications do by default.

With stderr:

  • No special setup necessary in order to run in a pipe command
  • Unconventional and that might subvert users expectations