Textual em Python: Criando Apps de Terminal com TUI
Aprenda a criar interfaces ricas no terminal com Textual em Python. Tutorial prático com widgets, CSS, layouts responsivos e exemplos de TUI interativa.
Aplicações de terminal não precisam ser feias. Com o Textual, você constrói interfaces ricas — com botões, tabelas, inputs, layouts responsivos e até CSS — direto no terminal. E tudo isso usando Python puro.
O Textual é um framework de TUI (Text User Interface) criado pela Textualize que transformou completamente o que é possível fazer no terminal. Com mais de 26 mil estrelas no GitHub e uma apresentação confirmada na PyCon US 2026 (“The Terminal is the New Browser”), o framework se consolidou como referência para interfaces de terminal em Python.
Se você já criou CLIs com Python usando Click ou Typer, o Textual é o próximo nível: em vez de texto sequencial, você tem uma aplicação interativa completa.
O que é o Textual?
O Textual é um framework Python para criar aplicações de terminal com interfaces gráficas sofisticadas. Ele se inspira no desenvolvimento web moderno e traz conceitos familiares:
- Widgets — componentes reutilizáveis como botões, inputs, tabelas e árvores
- CSS — estilização com uma versão adaptada de CSS (sim, CSS no terminal!)
- Layout — sistema de grid e dock para posicionar elementos
- Eventos — modelo orientado a eventos com handlers assíncronos
- Reatividade — atualização automática da interface quando dados mudam
O resultado são aplicações que parecem GUIs completas, mas rodam dentro do terminal — sem dependências de sistema, sem frameworks gráficos, sem instalação complexa.
Instalação
O Textual requer Python 3.8+ e pode ser instalado com qualquer gerenciador de pacotes. Se você usa o uv:
# Com uv
uv add "textual[dev]"
# Com pip
pip install "textual[dev]"
O extra [dev] inclui o comando textual com ferramentas de desenvolvimento, como o modo de live-reload para CSS.
Sua primeira aplicação Textual
Vamos criar uma aplicação mínima para entender a estrutura:
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class MinhaApp(App):
"""Uma aplicação Textual básica."""
CSS = """
Screen {
align: center middle;
}
#mensagem {
width: 60;
height: 5;
border: solid green;
content-align: center middle;
}
"""
def compose(self) -> ComposeResult:
yield Header()
yield Static("Bem-vindo ao Textual!", id="mensagem")
yield Footer()
if __name__ == "__main__":
app = MinhaApp()
app.run()
Execute com python app.py e você verá uma interface com cabeçalho, rodapé e uma mensagem centralizada com borda verde. Pressione Ctrl+C para sair.
O método compose() define a árvore de widgets (similar ao render() em React), e o atributo CSS estiliza os componentes.
Widgets principais
O Textual inclui dezenas de widgets prontos. Aqui estão os mais usados:
Input e Button
from textual.app import App, ComposeResult
from textual.widgets import Header, Input, Button, Static
from textual.containers import Vertical
class FormularioApp(App):
CSS = """
Vertical {
align: center middle;
width: 60;
height: auto;
margin: 2;
}
Input {
margin: 1 0;
}
Button {
margin: 1 0;
width: 100%;
}
#resultado {
margin: 1 0;
height: 3;
content-align: center middle;
color: $success;
}
"""
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
with Vertical():
yield Input(placeholder="Digite seu nome", id="nome")
yield Input(placeholder="Digite seu email", id="email")
yield Button("Enviar", variant="primary", id="enviar")
yield Static("", id="resultado")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "enviar":
nome = self.query_one("#nome", Input).value
email = self.query_one("#email", Input).value
resultado = self.query_one("#resultado", Static)
resultado.update(f"Cadastrado: {nome} ({email})")
if __name__ == "__main__":
FormularioApp().run()
O handler on_button_pressed captura cliques em qualquer botão. O método query_one busca widgets pelo seletor CSS — idêntico ao querySelector do JavaScript.
DataTable
Para exibir dados tabulares, o DataTable é o widget ideal:
from textual.app import App, ComposeResult
from textual.widgets import Header, DataTable
class TabelaApp(App):
CSS = """
DataTable {
height: 100%;
}
"""
def compose(self) -> ComposeResult:
yield Header()
yield DataTable()
def on_mount(self) -> None:
tabela = self.query_one(DataTable)
tabela.add_columns("Framework", "Tipo", "Estrelas GitHub")
dados = [
("Django", "Full-stack", "82k"),
("Flask", "Micro", "70k"),
("FastAPI", "Async API", "81k"),
("Textual", "TUI", "26k"),
("Streamlit", "Dashboard", "38k"),
]
tabela.add_rows(dados)
if __name__ == "__main__":
TabelaApp().run()
O DataTable suporta ordenação por coluna, seleção de linhas, scroll automático e temas personalizados.
CSS no terminal
Uma das funcionalidades mais surpreendentes do Textual é o suporte a CSS. Não é CSS completo da web, mas cobre o essencial:
/* arquivo: app.tcss */
Screen {
layout: grid;
grid-size: 2;
grid-gutter: 1;
padding: 1;
}
.card {
height: auto;
border: solid $primary;
padding: 1 2;
margin: 0 1;
}
.card:hover {
border: solid $accent;
background: $surface-lighten-1;
}
Button.primary {
dock: bottom;
margin: 1 0 0 0;
}
O Textual suporta variáveis de tema, pseudo-classes como :hover e :focus, layout grid, e até live-reload durante o desenvolvimento:
textual run --dev minha_app.py
Com live-reload, você edita o arquivo .tcss e vê as mudanças instantaneamente no terminal, sem reiniciar a aplicação. Isso torna a experiência de desenvolvimento muito parecida com desenvolvimento web.
Exemplo prático: monitor de sistema
Vamos criar algo mais completo — um monitor de sistema que atualiza em tempo real:
import psutil
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static, ProgressBar
from textual.containers import Horizontal, Vertical
from textual.timer import Timer
class MonitorWidget(Static):
"""Widget que exibe métricas do sistema."""
def __init__(self, titulo: str, **kwargs):
super().__init__(**kwargs)
self.titulo = titulo
def compose(self) -> ComposeResult:
yield Static(self.titulo, classes="titulo-metrica")
yield ProgressBar(total=100, show_eta=False)
yield Static("", classes="valor-metrica")
class MonitorApp(App):
CSS = """
Screen {
layout: grid;
grid-size: 2;
grid-gutter: 1;
padding: 1;
}
MonitorWidget {
height: auto;
border: solid $primary;
padding: 1;
}
.titulo-metrica {
text-style: bold;
color: $text;
margin-bottom: 1;
}
.valor-metrica {
margin-top: 1;
color: $text-muted;
}
"""
BINDINGS = [
("q", "quit", "Sair"),
("r", "refresh", "Atualizar"),
]
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield MonitorWidget("CPU", id="cpu")
yield MonitorWidget("Memória RAM", id="ram")
yield MonitorWidget("Disco", id="disco")
yield MonitorWidget("Swap", id="swap")
yield Footer()
def on_mount(self) -> None:
self.set_interval(2.0, self.atualizar_metricas)
self.atualizar_metricas()
def atualizar_metricas(self) -> None:
# CPU
cpu_percent = psutil.cpu_percent()
cpu_widget = self.query_one("#cpu")
cpu_widget.query_one(ProgressBar).update(progress=cpu_percent)
cpu_widget.query_one(".valor-metrica", Static).update(
f"{cpu_percent:.1f}%"
)
# RAM
mem = psutil.virtual_memory()
ram_widget = self.query_one("#ram")
ram_widget.query_one(ProgressBar).update(progress=mem.percent)
usado_gb = mem.used / (1024**3)
total_gb = mem.total / (1024**3)
ram_widget.query_one(".valor-metrica", Static).update(
f"{usado_gb:.1f} GB / {total_gb:.1f} GB ({mem.percent}%)"
)
# Disco
disco = psutil.disk_usage("/")
disco_widget = self.query_one("#disco")
disco_widget.query_one(ProgressBar).update(progress=disco.percent)
usado_gb = disco.used / (1024**3)
total_gb = disco.total / (1024**3)
disco_widget.query_one(".valor-metrica", Static).update(
f"{usado_gb:.0f} GB / {total_gb:.0f} GB ({disco.percent}%)"
)
# Swap
swap = psutil.swap_memory()
swap_widget = self.query_one("#swap")
swap_widget.query_one(ProgressBar).update(progress=swap.percent)
swap_widget.query_one(".valor-metrica", Static).update(
f"{swap.percent}%"
)
def action_refresh(self) -> None:
self.atualizar_metricas()
if __name__ == "__main__":
MonitorApp().run()
Esse monitor usa um grid 2x2, barras de progresso com cores temáticas, atualização automática a cada 2 segundos e atalhos de teclado. Tudo rodando no terminal.
Textual Web: do terminal para o navegador
Uma funcionalidade que destaca o Textual é o Textual Web — a capacidade de servir sua aplicação TUI como uma aplicação web:
textual serve minha_app.py
Esse comando inicia um servidor web que renderiza sua aplicação Textual no navegador. A mesma base de código funciona no terminal local e como aplicação web, sem mudanças.
Isso abre possibilidades interessantes: você pode criar dashboards internos, painéis de administração ou ferramentas de DevOps que funcionam tanto via SSH quanto via navegador.
Quando usar Textual vs outras opções?
A escolha depende do seu caso de uso:
| Cenário | Melhor opção |
|---|---|
| CLI simples com argumentos | Click / Typer |
| Dashboard de dados interativo | Streamlit |
| Aplicação de terminal rica | Textual |
| GUI desktop completa | PyQt / Tkinter |
| Interface web tradicional | Django ou FastAPI |
O Textual brilha quando você precisa de interatividade no terminal sem a complexidade de uma GUI desktop ou aplicação web. Ferramentas internas, painéis de monitoramento, interfaces de configuração e aplicações DevOps são os casos de uso ideais.
Boas práticas com Textual
Depois de explorar o framework, algumas dicas importantes:
- Separe CSS em arquivos
.tcss— use o live-reload para iterar rapidamente - Use containers (
Vertical,Horizontal,Grid) para organizar layouts - Widgets customizados — crie subclasses de
StaticouWidgetpara componentes reutilizáveis - Async handlers — o Textual usa asyncio, aproveite para operações não-bloqueantes
- Bindings — defina atalhos de teclado para ações frequentes
Se você já domina programacao orientada a objetos e tem familiaridade com decoradores, vai se sentir em casa com o modelo de composição do Textual.
Conclusão
O Textual prova que o terminal ainda é um ambiente poderoso para interfaces de usuário. Com CSS, widgets ricos, reatividade e a possibilidade de servir via web, ele preenche um espaço que antes não tinha solução elegante em Python.
Para projetos internos, ferramentas DevOps ou qualquer aplicação que precisa rodar onde há um terminal, o Textual é uma escolha produtiva e moderna. Se você quer criar interfaces mais tradicionais para a web, confira como Go pode complementar seus backends Python, ou explore como Kotlin é usado para aplicações Android que consomem APIs Python.
Equipe python.dev.br
Contribuidor do Python Brasil — Aprenda Python em Português