---
title: "Zig TUI: Apps de Terminal Rápidos com Estado, Teclas e Renderização"
url: "https://ziglang.com.br/artigos/zig-tui-terminal-apps/"
markdown_url: "https://ziglang.com.br/artigos/zig-tui-terminal-apps.MD"
description: "Como desenhar aplicações TUI em Zig para terminais: loop de eventos, estado, teclado, renderização incremental, logs, testes e distribuição multiplataforma."
date: "2026-05-26"
author: ""
---

# Zig TUI: Apps de Terminal Rápidos com Estado, Teclas e Renderização

Como desenhar aplicações TUI em Zig para terminais: loop de eventos, estado, teclado, renderização incremental, logs, testes e distribuição multiplataforma.


Uma boa ferramenta de terminal não precisa ser só uma lista de flags. Quando a pessoa precisa acompanhar jobs, navegar resultados, editar um plano curto, filtrar logs, aprovar passos ou observar métricas locais, uma **TUI em Zig** pode entregar uma experiência muito mais rápida que uma interface web pequena e muito mais confortável que comandos soltos.

TUI é _terminal user interface_: tela textual interativa, normalmente com atalhos de teclado, painéis, seleção, estado em memória e atualização incremental. Pense em `top`, `htop`, `lazygit`, `k9s`, clientes de banco, dashboards locais e assistentes de build. Zig entra bem nesse espaço porque gera binários previsíveis, inicializa rápido, conversa bem com APIs C de terminal e força uma arquitetura explícita de estado, buffers e erros.

Este guia mostra como desenhar uma aplicação TUI em Zig de forma pragmática: quando vale a pena, como separar estado e renderização, como lidar com teclas, modo raw, resize, logs, testes e distribuição. Para uma base anterior, veja também os guias de [criar CLI profissional em Zig](/artigos/zig-cli-aplicacao-linha-comando/), [ferramentas de desenvolvimento](/artigos/zig-devtools-produtividade/), [SQLite para ferramentas locais](/artigos/zig-sqlite-ferramentas-locais/) e [GitHub Actions para releases multiplataforma](/artigos/zig-github-actions-release-multiplataforma/).

## Quando uma TUI vale a pena

Uma TUI costuma valer quando a ferramenta tem pelo menos uma destas características:

- o usuário precisa ver estado mudando em tempo real;
- existe uma lista grande para filtrar, ordenar ou navegar;
- a próxima ação depende de comparação visual entre opções;
- há comandos perigosos que merecem confirmação clara;
- a ferramenta roda localmente, sem exigir login web;
- a latência de abrir navegador ou servidor local atrapalha;
- o público já vive no terminal.

Exemplos bons para Zig: painel de jobs de build, navegador de logs, explorador de banco SQLite local, cliente para fila interna, gerenciador de releases, cockpit de benchmarks, visualizador de traces pequenos ou aprovador de tarefas de um agente local.

Não use TUI para tudo. Se a pessoa só precisa rodar um comando uma vez, flags bastam. Se a equipe precisa colaborar em tempo real, uma UI web ou dashboard compartilhado pode ser melhor. Se acessibilidade com leitor de tela é requisito central, teste cedo: TUIs podem ser boas, mas exigem disciplina para não virar desenho visual impossível de navegar.

## Arquitetura mental: estado, eventos e renderização

O erro comum é misturar leitura de teclado, regra de negócio e desenho de tela no mesmo `while`. Funciona por uma tarde e vira um nó quando entram busca, paginação, resize, erro de rede e persistência.

Uma estrutura mais sustentável:

```text
src/
  main.zig
  app.zig        # estado global e reducer de eventos
  terminal.zig   # modo raw, leitura de teclas, tamanho da tela
  render.zig     # transforma estado em bytes ANSI
  data.zig       # arquivos, sqlite, api, comandos externos
  widgets.zig    # lista, barra de status, modal, input
```

O `main.zig` inicializa allocator, terminal e dados. O `app.zig` guarda o estado. O `terminal.zig` isola detalhes de plataforma. O `render.zig` recebe estado e dimensões e devolve bytes para escrever no stdout. Essa separação torna o núcleo testável sem abrir um terminal real.

Um estado mínimo pode ser assim:

```zig
const std = @import("std");

const View = enum { list, detail, help, confirm_quit };

const Item = struct {
    title: []const u8,
    status: []const u8,
};

const App = struct {
    allocator: std.mem.Allocator,
    view: View = .list,
    items: std.ArrayList(Item),
    selected: usize = 0,
    filter: std.ArrayList(u8),
    message: []const u8 = "",
    should_quit: bool = false,

    pub fn init(allocator: std.mem.Allocator) App {
        return .{
            .allocator = allocator,
            .items = std.ArrayList(Item).init(allocator),
            .filter = std.ArrayList(u8).init(allocator),
        };
    }

    pub fn deinit(self: *App) void {
        self.items.deinit();
        self.filter.deinit();
    }
};
```

A aplicação inteira passa a ser uma transformação: evento entra, estado muda, tela é renderizada.

## Loop de eventos simples

No começo, não invente concorrência. Faça um loop síncrono claro:

1. renderizar tela atual;
2. ler evento de teclado ou timer;
3. aplicar evento no estado;
4. repetir até `should_quit`.

```zig
while (!app.should_quit) {
    try renderer.draw(stdout.writer(), app, term.size);
    const event = try term.readEvent();
    try app.handle(event);
}
```

Depois, se precisar acompanhar processo externo ou rede, adicione uma fila de eventos: teclado, tick, resize, resultado de job. Zig ajuda porque você precisa explicitar ownership dos dados que cruzam essa fronteira.

## Teclas: normalize cedo

Terminal não entrega “tecla para cima” como uma abstração bonita. Ele entrega bytes e sequências ANSI. Normalize isso o mais cedo possível para o resto da aplicação não conhecer detalhes como `\x1b[A`.

```zig
const Key = union(enum) {
    char: u8,
    enter,
    escape,
    backspace,
    up,
    down,
    left,
    right,
    ctrl_c,
    unknown,
};
```

O parser de entrada lê bytes e devolve `Key`. O `app.handle` trabalha só com essa enum:

```zig
pub fn handle(self: *App, key: Key) !void {
    switch (key) {
        .char => |c| switch (c) {
            'q' => self.should_quit = true,
            'j' => self.moveDown(),
            'k' => self.moveUp(),
            '/' => self.view = .help,
            else => {},
        },
        .down => self.moveDown(),
        .up => self.moveUp(),
        .ctrl_c => self.should_quit = true,
        else => {},
    }
}
```

Esse desenho facilita remapear atalhos, testar comportamento e documentar ajuda.

## Modo raw e restauração segura

Para ler teclas sem esperar Enter, a TUI precisa colocar o terminal em modo raw. O cuidado mais importante é restaurar o terminal mesmo quando ocorre erro. Uma TUI que deixa o shell “quebrado” destrói confiança.

Em Unix, isso passa por `termios`. Em Zig, você pode envolver a chamada de sistema em um tipo com `enable` e `disable`, usando `defer` no `main`:

```zig
var terminal = try Terminal.init();
try terminal.enableRawMode();
defer terminal.disableRawMode() catch {};

try terminal.enterAlternateScreen();
defer terminal.leaveAlternateScreen() catch {};
```

Use tela alternativa quando a TUI ocupa o terminal inteiro. Para ferramentas pequenas, talvez seja melhor desenhar inline e preservar histórico. A decisão deve respeitar o contexto: dashboard interativo combina com tela alternativa; comando que imprime relatório não combina.

## Renderização: buffer antes de escrever

Escrever centenas de pequenos pedaços no stdout pode piscar e ficar lento. Monte um buffer por frame e escreva de uma vez. Isso também deixa testes mais fáceis: você compara string renderizada com um snapshot textual.

```zig
pub fn draw(writer: anytype, app: App, size: Size) !void {
    var buf = std.ArrayList(u8).init(app.allocator);
    defer buf.deinit();

    const w = buf.writer();
    try w.writeAll("\x1b[?25l"); // esconder cursor
    try w.writeAll("\x1b[H");    // cursor para topo

    try drawHeader(w, size, "zigjobs");
    try drawList(w, app, size);
    try drawStatus(w, size, app.message);

    try writer.writeAll(buf.items);
}
```

Evite redesenhar mais do que precisa no começo; mas não otimize cedo. Para telas pequenas, redesenhar o frame inteiro é simples e suficiente. Quando houver flicker, passe para renderização incremental: compare o frame anterior com o novo e escreva só linhas alteradas.

## Layout que cabe no terminal real

Terminais variam muito: 80x24 ainda existe; pane dividido no tmux é comum; fonte e emoji mudam largura. O layout precisa degradar bem.

Regras práticas:

- tenha uma largura mínima suportada e mostre mensagem se não couber;
- trunque texto longo com `…` ou `...` de forma previsível;
- não dependa de emoji para significado;
- reserve uma linha para status/erro;
- deixe a ajuda acessível por `?` ou `h`;
- mantenha atalhos visíveis nas telas críticas;
- trate resize como evento, não como acidente.

Para largura de caracteres Unicode, seja conservador. Se o público é brasileiro e o conteúdo tem acentos, teste com strings reais. Não deixe `ç`, `ã` e `é` quebrarem alinhamento de tabela.

## Dados: carregue pouco e mostre cedo

Uma TUI local precisa parecer instantânea. Se carregar tudo antes da primeira tela, o usuário acha que travou. Prefira:

1. abrir com estado “carregando”;
2. renderizar a estrutura da tela;
3. carregar dados em lote pequeno;
4. atualizar mensagem e lista.

Para ferramentas com SQLite local, a combinação é ótima: busca paginada, cache de resultados e filtros rápidos. O artigo sobre [Zig e SQLite](/artigos/zig-sqlite-ferramentas-locais/) cobre a persistência; na TUI, o importante é não bloquear o teclado durante consultas longas. Se a consulta pode demorar, rode em worker ou execute por página.

## Logs e mensagens de erro

Nunca misture logs de debug com a tela principal. Escrever no stderr enquanto a TUI controla o cursor bagunça a interface. Opções melhores:

- gravar logs em arquivo temporário;
- manter painel de eventos dentro da própria TUI;
- oferecer `--log-file caminho.log`;
- ao sair com erro, restaurar terminal e só então imprimir diagnóstico.

A mensagem de erro na barra inferior deve ser curta. Detalhes ficam em tela de ajuda, painel de logs ou arquivo.

## Confirmações para ações perigosas

Se a TUI executa delete, deploy, rollback, envio ou qualquer ação difícil de reverter, use confirmação explícita. Não basta perguntar “tem certeza?” com Enter como padrão.

Padrões melhores:

- modal com resumo da ação e alvo;
- exigir digitar uma palavra curta, como `deploy` ou `delete`;
- mostrar ambiente: `local`, `staging`, `produção`;
- registrar receipt local com timestamp e comando executado;
- permitir dry-run antes da execução real.

Isso é especialmente importante para ferramentas de operação. A TUI reduz atrito; por isso também precisa aumentar clareza.

## Testes sem terminal real

A separação estado/renderização paga dividendos em testes. Você pode testar:

- `handle(.down)` move seleção corretamente;
- filtro reduz lista sem perder índice;
- render de 80x24 contém atalhos essenciais;
- render de largura pequena trunca sem panic;
- erro de dados aparece na barra de status;
- `ctrl_c` marca `should_quit`.

Exemplo de teste de reducer:

```zig
test "down moves selection until last item" {
    var app = App.init(std.testing.allocator);
    defer app.deinit();

    try app.items.append(.{ .title = "build", .status = "ok" });
    try app.items.append(.{ .title = "test", .status = "fail" });

    try app.handle(.down);
    try std.testing.expectEqual(@as(usize, 1), app.selected);

    try app.handle(.down);
    try std.testing.expectEqual(@as(usize, 1), app.selected);
}
```

Para renderização, gere um frame e compare trechos importantes. Snapshot completo pode ser útil, mas tende a quebrar com ajustes pequenos de layout. Prefira asserts semânticos: título, seleção, mensagem de erro, ajuda.

## Distribuição multiplataforma

O grande atrativo de Zig para TUI é distribuir binários pequenos. Mas terminal é uma área em que plataforma importa.

Checklist de release:

- Linux x86_64 e aarch64;
- macOS Apple Silicon e Intel, se fizer sentido;
- Windows só se a camada de terminal foi testada no Windows Terminal;
- build Debug para desenvolvimento e ReleaseSafe/ReleaseFast para release;
- `--version` e `--help` fora da TUI;
- modo não interativo para scripts, quando possível;
- CI com smoke test que roda renderização sem TTY real.

O guia de [release multiplataforma com GitHub Actions](/artigos/zig-github-actions-release-multiplataforma/) mostra como organizar artefatos. Para TUI, inclua também uma gravação ou screenshot textual no README. Usuários decidem rápido se a ferramenta parece confiável.

## Exemplo de produto: cockpit de jobs Zig

Imagine uma ferramenta `zigjobs` para uma equipe que compila, testa e publica binários Zig. A TUI poderia ter:

- lista de jobs recentes;
- filtro por branch ou status;
- painel de detalhes com comando, duração e commit;
- tecla `r` para rerun local;
- tecla `l` para abrir logs;
- confirmação para publicar release;
- histórico em SQLite;
- exportação JSON para automação.

A CLI tradicional ainda existiria:

```bash
zigjobs list --status failed
zigjobs logs 123
zigjobs rerun 123 --dry-run
```

A TUI seria apenas a superfície interativa:

```bash
zigjobs tui
```

Esse arranjo é saudável: comandos scriptáveis por baixo, TUI por cima. Se a TUI quebrar em algum terminal, a ferramenta continua útil.

## Erros comuns em TUI Zig

Os problemas mais frequentes:

1. **Terminal não restaurado**: resolva com `defer` e testes manuais de crash.
2. **Estado global solto**: concentre no `App` e passe explicitamente.
3. **Renderização acoplada a dados externos**: render deve receber estado pronto.
4. **Atalhos invisíveis**: mostre ajuda e barra inferior.
5. **Sem fallback não interativo**: scripts precisam de comandos tradicionais.
6. **Logs quebrando a tela**: mande logs para arquivo ou painel.
7. **Unicode ignorado**: teste português e larguras reais.
8. **Ação perigosa fácil demais**: confirme com alvo e receipt.

## Conclusão

Zig é uma boa escolha para TUIs quando você quer ferramenta local, rápida, distribuível e previsível. A linguagem não transforma terminal em mágica; ela exige que você modele o problema: estado, eventos, renderização, erros e ownership. Essa disciplina é justamente o que mantém uma TUI pequena evoluindo sem virar um emaranhado.

Comece com uma CLI scriptável, adicione uma TUI apenas onde a interação visual economiza tempo e mantenha as partes separadas. O resultado pode ser uma ferramenta que abre instantaneamente, roda em várias plataformas e dá ao desenvolvedor uma sensação rara: controle direto, sem navegador, sem servidor e sem espera.
