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.
Library | Framework | |
---|---|---|
Usage | A 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 Flow | In 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). |
Nature | Libraries 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. |
Example | Imagine 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
},
_ => {}
}
}
}
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
:
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