TUI apps com Ratatui: interfaces no terminal
1. Introdução ao Ratatui e TUI em Rust
Terminal User Interfaces (TUIs) oferecem uma alternativa elegante para aplicações que não necessitam de interfaces gráficas completas. Em Rust, o ecossistema para construção de TUIs é maduro, com destaque para o Ratatui — um fork ativo do popular tui-rs. Diferente de bibliotecas como Cursive (orientada a callbacks) ou termion (baixo nível), o Ratatui adota uma abordagem reativa e declarativa, similar a frameworks web modernos.
Para iniciar, crie um novo projeto:
cargo new meu-app-tui --name meu_app_tui
cd meu-app-tui
Adicione as dependências no Cargo.toml:
[dependencies]
ratatui = "0.26"
crossterm = "0.27"
O crossterm será responsável pelo gerenciamento do terminal (modo raw, eventos de teclado).
2. Primeiros Passos: Seu Primeiro App com Ratatui
Vamos construir um app mínimo que exibe "Olá, Ratatui!":
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Alignment, Rect},
widgets::{Block, Borders, Paragraph},
Frame, Terminal,
};
use std::io::{stdout, Write};
fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = run_app(&mut terminal);
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn std::error::Error>> {
loop {
terminal.draw(|f| {
let area = f.size();
let block = Block::default()
.title("Meu App")
.borders(Borders::ALL);
let paragraph = Paragraph::new("Olá, Ratatui!")
.block(block)
.alignment(Alignment::Center);
f.render_widget(paragraph, area);
})?;
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
A estrutura é simples: um loop de eventos que alterna entre renderização (terminal.draw) e captura de entrada. O widget Paragraph é envolvido por um Block com bordas.
3. Layout e Organização Visual
Para criar interfaces mais complexas, use o Layout com Constraint:
use ratatui::layout::{Constraint, Direction, Layout};
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn std::error::Error>> {
loop {
terminal.draw(|f| {
let area = f.size();
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(area);
let left_block = Block::default()
.title("Painel Lateral")
.borders(Borders::ALL);
let right_block = Block::default()
.title("Área Principal")
.borders(Borders::ALL);
f.render_widget(left_block, chunks[0]);
f.render_widget(right_block, chunks[1]);
})?;
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
Constraint permite distribuir espaço usando porcentagens, proporções (Ratio), tamanhos fixos (Length) ou mínimo/máximo (Min, Max). É possível aninhar layouts para criar grids complexos.
4. Widgets Interativos e Navegação
Lista de seleção
use ratatui::widgets::{List, ListItem, ListState};
let items = vec!["Item A", "Item B", "Item C"];
let list_items: Vec<ListItem> = items.iter().map(|i| ListItem::new(*i)).collect();
let list = List::new(list_items).block(Block::default().title("Menu").borders(Borders::ALL));
let mut list_state = ListState::default();
list_state.select(Some(0));
f.render_stateful_widget(list, chunks[0], &mut list_state);
Tabela
use ratatui::widgets::{Row, Table};
let rows = vec![
Row::new(vec!["1", "Alice", "Engenharia"]),
Row::new(vec!["2", "Bob", "Design"]),
];
let table = Table::new(rows)
.header(Row::new(vec!["ID", "Nome", "Departamento"]).style(Style::default().add_modifier(Modifier::BOLD)))
.block(Block::default().title("Funcionários").borders(Borders::ALL))
.widths(&[Constraint::Length(5), Constraint::Length(15), Constraint::Length(15)]);
Abas (Tabs)
use ratatui::widgets::{Tabs, TabsState};
let titles = vec!["Home", "Config", "Ajuda"];
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL))
.highlight_style(Style::default().bg(Color::Blue));
let mut tabs_state = TabsState::new(vec!["Home", "Config", "Ajuda"]);
tabs_state.selected = 0;
f.render_widget(tabs, chunks[0]);
5. Input do Usuário e Eventos
O crossterm captura eventos de forma não bloqueante:
use crossterm::event::{poll, read, Event, KeyCode, KeyEvent};
use std::time::Duration;
fn handle_input(state: &mut AppState) -> Result<(), Box<dyn std::error::Error>> {
if poll(Duration::from_millis(100))? {
match read()? {
Event::Key(KeyEvent { code, .. }) => match code {
KeyCode::Up => state.focus_previous(),
KeyCode::Down => state.focus_next(),
KeyCode::Enter => state.select_current(),
KeyCode::Char('q') => state.quit = true,
_ => {}
},
Event::Resize(_, _) => state.needs_redraw = true,
_ => {}
}
}
Ok(())
}
Para gerenciar múltiplas telas ou contextos, use um enum de estado:
enum Focus {
Menu,
Content,
Help,
}
struct AppState {
focus: Focus,
selected_item: usize,
quit: bool,
}
6. Estado, Atualização e Ciclo de Vida
O padrão MVU (Model-View-Update) simplifica o gerenciamento:
struct Model {
counter: i32,
items: Vec<String>,
}
enum Message {
Increment,
Decrement,
AddItem(String),
Quit,
}
fn update(model: &mut Model, msg: Message) {
match msg {
Message::Increment => model.counter += 1,
Message::Decrement => model.counter -= 1,
Message::AddItem(name) => model.items.push(name),
Message::Quit => std::process::exit(0),
}
}
fn view<B: Backend>(f: &mut Frame<B>, model: &Model) {
// Renderização baseada no modelo
}
Para atualizações em tempo real, use std::thread::sleep ou timers:
use std::time::{Duration, Instant};
let start = Instant::now();
loop {
let elapsed = start.elapsed().as_secs();
// Atualiza modelo com base no tempo
terminal.draw(|f| render_with_time(f, elapsed))?;
std::thread::sleep(Duration::from_millis(50));
}
7. Estilização e Temas
O Style permite controle fino sobre cores e modificadores:
use ratatui::style::{Color, Modifier, Style};
let title_style = Style::default()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::BOLD);
let selected_style = Style::default()
.fg(Color::White)
.bg(Color::Blue);
let error_style = Style::default()
.fg(Color::Red)
.add_modifier(Modifier::RAPID_BLINK);
Para temas customizados, crie uma struct centralizada:
struct Theme {
primary: Style,
secondary: Style,
error: Style,
success: Style,
}
impl Default for Theme {
fn default() -> Self {
Self {
primary: Style::default().fg(Color::Cyan),
secondary: Style::default().fg(Color::Magenta),
error: Style::default().fg(Color::Red),
success: Style::default().fg(Color::Green),
}
}
}
O Ratatui detecta automaticamente a capacidade de cores do terminal (16, 256 ou truecolor) usando crossterm::style::terminal_supports_colors.
8. Boas Práticas e Publicação
Testes com TestBackend
use ratatui::backend::TestBackend;
#[test]
fn test_render() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|f| {
let paragraph = Paragraph::new("Teste");
f.render_widget(paragraph, f.size());
}).unwrap();
let buffer = terminal.backend().buffer();
// Verificar conteúdo do buffer
assert_eq!(buffer.get(0, 0).symbol, "T");
}
Tratamento de redimensionamento
O Event::Resize(width, height) permite reagir a mudanças de tamanho. Em loops de renderização, use f.size() sempre para obter as dimensões atuais.
Dicas de performance
- Use
render_widgetem vez derender_stateful_widgetquando não há estado interno. - Evite alocações desnecessárias no loop de renderização — pré-calcule listas e estilos.
- Para listas grandes, considere
Listcom scroll virtual (apenas itens visíveis).
Referências
- Documentação oficial do Ratatui — API completa com exemplos de todos os widgets e layouts.
- Ratatui Book (GitHub) — Guia prático com tutoriais passo a passo para iniciantes.
- Crossterm Documentation — Documentação do gerenciador de terminal usado nos exemplos.
- Awesome Rust TUI — Lista curada de projetos e bibliotecas TUI em Rust.
- Construindo um editor de texto TUI em Rust (blog) — Série de artigos que demonstra na prática conceitos avançados de TUI com Ratatui.