Criando interfaces TUI em Rust com Ratatui
Olá hoje quero compartilhar um pouco minha jornada aprendendo a lib Ratatui para criar TUIs com Rust, mas afinal que diabos é TUI?
O que é uma TUI?
TUI (Text User Interface) é um tipo de interface gráfica que permite a interação com o usuário por meio de texto em um ambiente de terminal ou console. Diferentemente de uma GUI (Graphical User Interface), que utiliza botões, janelas e ícones visuais, as TUIs se baseiam em caracteres de texto para criar interfaces interativas, como menus, caixas de diálogo, e áreas de texto.
Características principais:
- Baseado em texto: Em vez de elementos gráficos, TUIs utilizam caracteres ASCII/Unicode para compor a interface.
- Alta eficiência: TUIs são leves e funcionam bem em ambientes com poucos recursos ou sem suporte gráfico.
- Interatividade: Embora simples, TUIs permitem que os usuários naveguem e interajam com as funcionalidades do software através do teclado.
- Facilidade de automação: TUIs são frequentemente preferidas em ambientes de programação e administração de sistemas por serem fáceis de automatizar com scripts.
Exemplos de uso de TUIs:
- Vim e Emacs: Editores de texto populares com interfaces TUI.
- Ranger: Um gerenciador de arquivos que usa TUI para navegação por diretórios.
Comparação com CLI:
Embora pareçam similares, uma TUI difere de uma CLI (Command Line Interface). Na CLI, o usuário interage diretamente digitando comandos, enquanto na TUI, há uma interface visual onde o usuário navega entre opções ou edita texto.
TUI
Ratatui é uma biblioteca para criação de interfaces de usuário baseadas em texto (TUI) na linguagem de programação Rust. Ela é uma versão aprimorada e mantida da biblioteca TUI-rs, com o objetivo de fornecer uma maneira eficiente e fácil de construir TUIs no terminal.
Principais Recursos:
Flexibilidade e Composição:
O Ratatui oferece layout flexível e ferramentas para organizar componentes como painéis, caixas de texto, tabelas, gráficos, e barras de progresso, com base nas suas necessidades.
Ele usa um sistema de layouts declarativos, permitindo que você divida sua interface em blocos reutilizáveis e bem organizados.
Estilização e Temas:
A biblioteca suporta estilos customizáveis para textos e widgets, permitindo a aplicação de cores, modificadores como negrito, sublinhado e itálico, além de temas personalizados.
Com isso, você pode criar interfaces que se adaptam à estética desejada para a aplicação no terminal.
Performance:
Escrito em Rust, o Ratatui é muito eficiente em termos de uso de memória e processamento, o que o torna adequado para aplicações de linha de comando rápidas e reativas.
Integração com Crossterm:
O Ratatui funciona em conjunto com o Crossterm, uma biblioteca que oferece controle sobre o terminal (como captura de eventos de teclado e controle de entrada/saída de terminal) de maneira cross-platform (suporta Windows, Linux e macOS).
Suporte a Eventos:
Além do controle de layout e estilo, o Ratatui permite a captura de eventos de teclado e mouse, tornando possível a criação de interfaces interativas, como editores de texto, navegadores de arquivos, e clientes de chat. Uma outra coisa legal é que é possivel capturar evento do mouse também.
Desafio
Então me desafiei durante 30 dias para fazer um TUI para tomar notas, o popular editor de texto.
No meio do desafio precisei dividir em 15 dias uma pausa e depois 15 dias, então não foi 30 dias seguidos, também inclui ele no meu plano de estudos tomando o lugar temporariamente do estudo da bevy engine.
Como inseri esse desafio dentro do meu plano de estudos esses 30 dias eu investi mais ou menos 8 horas de estudo e testes para chegar no projeto final. Uma das coisas importantes como era um desafio exploratório eu não busquei a melhor sintaxe mas, conseguir fazer as coisas funcionarem mesmo não estando bem estruturado.
Uma coisa importante é que fora CLI eu nunca fiz outro tipo de aplicação desktop (Isso se você ignorar meus projetos em delphi e vb6) então já foi um desafio em si entender como trabalhar algumas coisas e estruturar minhas funções.
Outro ponto interessante que devo levar pro meus estudos de jogos com a bevy, é a questão de interfaces gráficas, a estruturação delas e como remover e sobrepor essa interfaces de modo a ficar funcional.
No final cheguei em algo bem simples, queria criar um navegador de arquivos mas, me limitei em apenas mostrar o diretório atual que está aberto meu editor.
Bom se quiser ver o projeto pode acessar pelo link abaixo, aqui vou atentar num exemplo mais básico:
https://github.com/jonatasoli/ratanotes
Criando um projeto básico
Primeiro no nosso main precisamos inicar nosso ratatui, criando uma variável mutável. Além disso a saída da nossa função principal vai ser um io:Result
.
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
}
Como funciona a questão de trabalhar com uma TUI, eu preciso criar uma função que vai executar dentro do meu terminal e desenhar minhas telas pra isso preciso criar uma função chamada run
como a interface vai sofrendo alterações ou seja ela vai abrir novas janelas ou imprimir alguma coisa dinamicamente ele precisar ser um mut
. Uma coisa adicional é que nossa função run preicsa receber nosso terminal que foi criado na main para poder ser manipulado e ele tem o tipo DefaultTerminal
.
fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
}
Agora precisamos "desenhar" nossa interface, o Ratatui usa o esquema de widgets, como estamos fazendo um exemplo simples, vamos desenhar um paragrafo e colocar algumas bordas e dentro do paragrafo vamos colocar um texto simples. Como disse antes a ideia de ter uma interface é ela ser dinâmica por conta disso precisamos deixar ela dentro de um loop.
fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Olá a nossa super interface! (aperte 'q' pra sair)")
.white()
.on_blue();
frame.render_widget(greeting, frame.area());
})?;
}
}
Agora temos uma tela, porém para sair teriamos que forçar o encerramento da aplicação por conta disso vamos criar uma variável que captura os eventos do teclado e caso o usuário aperte q
ele vai encerrar a aplicação.
fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Olá a nossa super interface! (aperte 'q' pra sair)")
.white()
.on_blue();
frame.render_widget(greeting, frame.area());
})?;
if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
Agora precisamos chamar nossa função run dentro da nossa função principal main
.
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
run(terminal);
}
Mas, se rodarmos assim vamos ter alguns problemas por que nosso terminal não está sendo limpo para inciar nosso app além disso quando terminar eu gostaria de voltar o terminal da forma que ele estava antes. Ai precisamos chamar as funções clear
e restore
entre as chamadas do meu run, mas ainda sim preciso retornar o resultado do meu run, então vamos criar uma variável app_result
para ser retornado na main
.
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
terminal.clear()?;
let app_result = run(terminal);
ratatui::restore();
app_result
}
Certo se rodarmos agora estará tudo funcionando, e nosso projeto fica assim:
use std::io;
use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind},
style::Stylize,
widgets::Paragraph,
DefaultTerminal,
};
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
terminal.clear()?;
let app_result = run(terminal);
ratatui::restore();
app_result
}
fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
loop {
terminal.draw(|frame| {
let greeting = Paragraph::new("Hello Ratatui! (press 'q' to quit)")
.white()
.on_blue();
frame.render_widget(greeting, frame.area());
})?;
if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
}
Bom esse foi o resultado da minha jornada aprendendo o Ratatui e espero que tenha gostado do conteúdo, eu quero fazer um vídeo nas próximas semanas explicando esse processo se você quiser uma explicação mostrando como faço para alternar entre salvar um arquivo e abrir um arquivo e voltar pra interface de edição não deixa de pedir lá nos comentários do vídeo =).
Por hoje é isso e até mais pessoal.