TUIs com ratatui
Crie interfaces de usuário interativas no terminal com ratatui, o fork mantido do tui-rs.
Receita
use crossterm::event::{self, Event, KeyCode};
use ratatui::{prelude::*, widgets::Paragraph};
fn main() -> anyhow::Result<()> {
// Inicializa o terminal usando o backend Crossterm direcionado para stderr.
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
// Limpa a tela do terminal.
terminal.clear()?;
loop {
// Desenha o conteúdo do terminal.
terminal.draw(|f| {
// Obtém a área disponível para desenho.
let area = f.area();
// Renderiza um widget de Parágrafo com o texto "Pressione q para sair".
f.render_widget(Paragraph::new("Pressione q para sair"), area);
})?;
// Lê um evento do teclado.
if let Event::Key(key) = event::read()? {
// Se a tecla 'q' for pressionada, sai do loop.
if key.code == KeyCode::Char('q') { break; }
}
}
// Mostra o cursor do terminal novamente.
terminal.show_cursor()?;
Ok(())
}Quando usar isso: Dashboards, visualizadores de logs ou assistentes onde CLIs baseadas em linhas são insuficientes.
Exemplo de Trabalho
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
layout::{Constraint, Direction, Layout},
prelude::*,
widgets::{Block, Borders, List, ListItem},
};
// Define a estrutura para o estado do aplicativo.
struct App {
items: Vec<String>,
selected: usize,
}
impl App {
// Método para avançar para o próximo item na lista.
fn next(&mut self) {
self.selected = (self.selected + 1) % self.items.len();
}
}
fn main() -> anyhow::Result<()> {
// Inicializa o estado do aplicativo com alguns itens.
let mut app = App {
items: vec!["alpha".into(), "beta".into(), "gamma".into()],
selected: 0,
};
// Inicializa o terminal usando o backend Crossterm direcionado para stderr.
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
loop {
// Desenha o conteúdo do terminal.
terminal.draw(|f| {
// Divide a área do terminal em chunks verticais.
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(1)])
.split(f.area());
// Mapeia os itens do aplicativo para ListItem, aplicando estilo ao item selecionado.
let items: Vec<ListItem> = app.items.iter()
.enumerate()
.map(|(i, s)| {
let style = if i == app.selected {
Style::default().bg(Color::Blue) // Estilo para o item selecionado
} else {
Style::default() // Estilo padrão
};
ListItem::new(s.as_str()).style(style)
})
.collect();
// Renderiza um widget de Lista com bordas e título.
f.render_widget(
List::new(items).block(Block::default().borders(Borders::ALL).title("Items")),
chunks[0], // Renderiza a lista no primeiro chunk
);
})?;
// Lê um evento do teclado.
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break, // Sai do loop se 'q' for pressionado
KeyCode::Down | KeyCode::Char('j') => app.next(), // Avança para o próximo item se 'Down' ou 'j' for pressionado
_ => {} // Ignora outras teclas
}
}
}
Ok(())
}O que isso demonstra:
- Layout divide a tela em regiões.
- Widget
Listcom destaque de seleção. - Loop de eventos lê a entrada do teclado via
crossterm. - Renderiza para
stderrpara manterstdoutlivre.
Mergulho Profundo
Arquitetura
- Backend -
CrosstermBackendgerencia o modo raw do terminal. - Árvore de Widgets - Compõe
Paragraph,Table,Chart,Tabs. - Loop de Eventos - Polling ou
event::read()bloqueante a cada frame. - Estado - Mantém o estado do aplicativo em uma struct;
drawé renderização pura.
Widgets Comuns
| Widget | Uso para |
|---|---|
List | Menus selecionáveis |
Table | Dados tabulares |
Block | Painéis com título e bordas |
Gauge | Visualização de progresso |
Armadilhas
- Esqueceu a limpeza do modo raw - O terminal permanece quebrado após um pânico. Correção: Use
ratatui::init()/restore()ou um guardaDrop. - Renderizando para stdout - Quebra o pipe e mistura com a saída de dados. Correção: Use
stderrcomo o stream de backend. - Loops de espera ativa (busy-wait) - 100% de CPU sem timeout de
poll. Correção: Useevent::poll(Duration::from_millis(100)). - Redimensionamento - O layout quebra ao redimensionar o terminal. Correção: Manipule
Event::Resizee redesenhe. - Suporte a mouse - Não habilitado por padrão. Correção: Habilite a captura de mouse do crossterm quando necessário.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
CLI baseada em linha + inquire | Prompts simples | Dashboards com múltiplos painéis |
dialoguer | Prompts de sim/não e seleção | Layouts complexos |
| UI Web | Gráficos ricos necessários | Ambientes de servidor apenas SSH |
FAQs
ratatui vs tui-rs?
ratatui é o fork ativamente mantido. Novos projetos devem usar ratatui.
Posso usar tokio com ratatui?
Sim. Execute o loop de eventos em uma thread bloqueante ou use tokio::select! com canais assíncronos para atualizações.
Como testar TUIs?
Extraia a renderização para funções que recebem Buffer e afirme o conteúdo da célula.
Gráficos e diagramas?
ratatui inclui Chart para sparklines e gráficos de linha. Para dados pesados, considere exportar para um navegador.
Múltiplas telas?
Use um enum para o estado do aplicativo (List, Detail, Help) e use match em draw.
Temas de cores?
Defina uma struct Theme com valores Style. Suporte claro/escuro via flag.
Suporte à área de transferência?
Integre arboard para copiar/colar fora do escopo básico da TUI.
Suporte no Windows?
crossterm suporta terminais do Windows. Teste no seu emulador de terminal alvo.
Embutindo em CLI existente?
Lance um subcomando TUI: meuapp tui entra em tela cheia; o modo padrão permanece baseado em linha.
Acessibilidade?
Forneça comandos não-TUI equivalentes (--plain, --json) para leitores de tela e scripts.
Relacionados
- Saída de Terminal - progresso e cor
- Noções Básicas de CLI - quando não usar uma TUI
- Relatório de Erros - erros fora do loop TUI
- Melhores Práticas de CLI - sempre ofereça modo não interativo
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.