← Voltar para o blog

Bubble Tea Go (Golang): Interfaces de Terminal com TUI

Bubble Tea Go (Golang) cria interfaces de terminal com Model-Update-View. Tutorial v2: teclado, Cmd, AltScreen, Lip Gloss, testes e migração do v1.

Bubble Tea Go (também buscado como Bubble Tea Golang ou Bubbletea TUI) é o framework Charmbracelet para criar interfaces de terminal em Go: listas navegáveis, atalhos, formulários, progresso e telas atualizadas no lugar. Na versão 2, o pacote oficial é charm.land/bubbletea/v2 e a aplicação continua seguindo Model-Update-View: o modelo guarda estado, Update processa mensagens e View descreve o que o runtime deve exibir.

A recomendação direta é: use uma CLI convencional para comandos curtos, automação e pipelines; use Bubble Tea quando a interação faz parte do produto. Um deploy --env producao não precisa de uma TUI. Já um painel para escolher serviços, acompanhar jobs, inspecionar logs ou revisar migrações pode ficar muito mais útil com navegação interativa.

Atualizado em 28 de setembro de 2026: os exemplos deste tutorial usam Bubble Tea v2 (charm.land/bubbletea/v2, versão v2.0.10 no momento da revisão, publicada em 24 de setembro de 2026) e Lip Gloss v2 (charm.land/lipgloss/v2, v2.0.6). Além da mudança de módulo, de tea.KeyMsg para tea.KeyPressMsg e do retorno de View() de string para tea.View, o v2 tornou declarativas as configurações de terminal: tela cheia, mouse, cursor e título agora são campos da tea.View. As duas novas seções abaixo cobrem a View declarativa e a migração do v1, com base no guia oficial de upgrade para v2.

Neste tutorial de Bubble Tea Go para Golang, você vai construir a base de uma TUI em Go, entender Model, Update, View e Cmd, adicionar teclado, tarefas assíncronas, estilos com Lip Gloss e testes. Se ainda não conhece a estrutura de comandos e flags, leia também o tutorial de ferramentas CLI em Go, o guia de Cobra em Go e o de graceful shutdown em aplicações Go.

Bubble Tea, Cobra e uma CLI simples resolvem problemas diferentes

Antes de instalar uma dependência, escolha a experiência certa:

AbordagemMelhor paraVantagemPonto de atenção
flag da biblioteca padrãoUm comando pequenoZero dependência e API simplesPouca ergonomia para muitos subcomandos
CobraComandos, flags, help e completionsPadrão conhecido em CLIs GoNão cria uma interface interativa sozinho
Bubble TeaListas, formulários, dashboards e fluxos interativosEstado e eventos organizadosExige pensar em ciclo de atualização e terminal
Cobra + Bubble TeaProduto com modo scriptável e modo interativoCombina automação com boa UXPrecisa separar bem as duas entradas

Uma ferramenta de infraestrutura pode oferecer os dois modos:

minha-cli deploy --service api --env prod
minha-cli interactive

O primeiro funciona em CI e scripts. O segundo abre uma TUI para uma pessoa explorar serviços e confirmar ações. Essa separação preserva uma propriedade importante: toda operação crítica continua acessível sem interface interativa.

Como funciona Model-Update-View

Uma aplicação Bubble Tea gira em torno de três métodos:

  • Init() informa o primeiro comando assíncrono, se houver;
  • Update(msg) recebe uma mensagem e devolve o novo modelo mais um comando;
  • View() transforma o estado atual em uma tea.View, cujo campo Content contém o texto da interface.

O modelo não deve representar a tela como um conjunto de widgets mutáveis. Ele representa o estado do domínio da interface: itens, cursor, carregamento, erro, largura, altura e modo atual. A tela é uma consequência desse estado.

Primeira TUI com Bubble Tea Go: instalação e exemplo completo

Para executar este exemplo de Bubble Tea em Go (Golang), use Go 1.26.0 ou superior: esse é o requisito declarado no go.mod oficial do Bubble Tea v2.0.10. Confira seu toolchain com go version; se precisar instalar ou atualizar, siga o guia de primeiros passos e instalação do Go.

Crie uma pasta separada para o exemplo e inicialize o módulo:

mkdir contador-tui
cd contador-tui
go mod init exemplo.com/contador-tui
go get charm.land/bubbletea/[email protected]

Salve o código completo abaixo em main.go, dentro de contador-tui. Ele não precisa de Bubbles nem de Lip Gloss: o contador usa apenas Bubble Tea e a biblioteca padrão.

package main

import (
    "fmt"
    "os"

    tea "charm.land/bubbletea/v2"
)

type model struct {
    count int
}

func (m model) Init() tea.Cmd {
    return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        case "up", "k", "+":
            m.count++
        case "down", "j", "-":
            m.count--
        }
    }

    return m, nil
}

func (m model) View() tea.View {
    content := fmt.Sprintf(
        "Contador: %d\n\n↑/k aumenta • ↓/j diminui • q sai\n",
        m.count,
    )
    return tea.NewView(content)
}

func main() {
    program := tea.NewProgram(model{})
    if _, err := program.Run(); err != nil {
        fmt.Fprintln(os.Stderr, "erro ao executar TUI:", err)
        os.Exit(1)
    }
}

Na mesma pasta, organize as dependências, compile e execute em um terminal interativo:

go mod tidy
go build .
go run .

A tela inicial mostra Contador: 0. Pressione ↑ ou k para incrementar, ↓ ou j para decrementar e q ou ctrl+c para sair. go build . verifica a compilação sem abrir a interface; go run . inicia a TUI. Mantenha go.mod e go.sum no controle de versão para registrar as dependências do projeto.

Erros comuns ao instalar Bubbletea em Go

SintomaO que verificar
O Go informa que o módulo exige uma versão mais novaConfira go version: a versão v2.0.10 usada aqui exige Go 1.26.0 ou superior. A seleção automática de toolchain depende da configuração de GOTOOLCHAIN; consulte a documentação oficial de toolchains.
View() string não satisfaz tea.ModelVocê misturou código v1 com o import v2. Neste exemplo, a assinatura é View() tea.View e o retorno usa tea.NewView(...).
undefined: tea.WithAltScreenEssa opção é da API v1. No v2, defina v.AltScreen = true na View, como na seção de tela cheia.
O comando é executado em CI ou sem terminal interativoUse go build . para validar a compilação e teste a lógica chamando Update diretamente, como na seção de testes. Reserve a interação de teclado para um terminal.

As diferenças de API acima estão documentadas no guia oficial de migração para Bubble Tea v2.0.10. Os exemplos seguintes são trechos para evoluir esse contador, não arquivos completos independentes.

Quando uma tecla é pressionada, Bubble Tea v2 produz uma tea.KeyPressMsg. Update interpreta a mensagem, altera uma cópia do modelo e devolve o próximo estado. Em seguida, o runtime chama View novamente e renderiza a tea.View retornada.

Essa arquitetura lembra um event loop, mas evita espalhar leitura de teclado, mutação de estado e desenho da tela por várias goroutines. Para entender como isso se relaciona com o runtime da linguagem, consulte o guia de goroutines e concorrência em Go e o artigo sobre channels em produção.

Construindo uma lista navegável sem esconder o estado

Agora podemos representar tarefas e um cursor:

type task struct {
    Title string `json:"title"`
    Done  bool   `json:"done"`
}

type model struct {
    tasks  []task
    cursor int
    width  int
    height int
    err    error
}

O tratamento de teclado fica explícito:

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        m.width = msg.Width
        m.height = msg.Height

    case tea.KeyPressMsg:
        switch msg.String() {
        case "ctrl+c", "q":
            return m, tea.Quit

        case "up", "k":
            if m.cursor > 0 {
                m.cursor--
            }

        case "down", "j":
            if m.cursor < len(m.tasks)-1 {
                m.cursor++
            }

        case "enter", "space":
            if len(m.tasks) > 0 {
                m.tasks[m.cursor].Done = !m.tasks[m.cursor].Done
            }
        }
    }

    return m, nil
}

A mensagem tea.WindowSizeMsg é importante porque terminais mudam de tamanho. Guardar largura e altura no modelo permite adaptar tabelas, cortar texto e decidir quando exibir um painel lateral. Não presuma 80 colunas e não use largura fixa para conteúdo que pode crescer.

A View pode percorrer os itens:

func (m model) View() tea.View {
    var out strings.Builder
    out.WriteString("Tarefas do projeto\n\n")

    for i, item := range m.tasks {
        cursor := " "
        if i == m.cursor {
            cursor = ">"
        }

        mark := "[ ]"
        if item.Done {
            mark = "[x]"
        }

        fmt.Fprintf(&out, "%s %s %s\n", cursor, mark, item.Title)
    }

    out.WriteString("\n↑/↓ navega • espaço conclui • q sai\n")
    return tea.NewView(out.String())
}

strings.Builder evita concatenações repetidas e deixa a renderização legível. Para listas muito grandes, renderize apenas a janela visível ou use o componente list do projeto Bubbles.

Cmd: trabalho assíncrono sem bloquear Update

Update deve ser rápido. Não faça requisição HTTP, consulta longa ou leitura de arquivo diretamente dentro dele. Enquanto a função estiver bloqueada, a interface não processa teclado nem redesenha.

No Bubble Tea, uma operação assíncrona é representada por tea.Cmd, que é uma função que produz uma mensagem:

type tasksLoadedMsg []task

type loadFailedMsg struct {
    err error
}

func loadTasks(client *http.Client, url string) tea.Cmd {
    return func() tea.Msg {
        req, err := http.NewRequest(http.MethodGet, url, nil)
        if err != nil {
            return loadFailedMsg{err: err}
        }

        resp, err := client.Do(req)
        if err != nil {
            return loadFailedMsg{err: err}
        }
        defer resp.Body.Close()

        if resp.StatusCode != http.StatusOK {
            return loadFailedMsg{err: fmt.Errorf("status inesperado: %s", resp.Status)}
        }

        var tasks []task
        if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil {
            return loadFailedMsg{err: err}
        }
        return tasksLoadedMsg(tasks)
    }
}

O modelo pode começar carregando:

func (m model) Init() tea.Cmd {
    return loadTasks(m.client, m.endpoint)
}

E Update trata o resultado como qualquer outra mensagem:

case tasksLoadedMsg:
    m.loading = false
    m.tasks = []task(msg)

case loadFailedMsg:
    m.loading = false
    m.err = msg.err

Isso mantém uma regra saudável: efeitos produzem mensagens; mensagens alteram estado; View apenas renderiza. Se a operação precisa de timeout e cancelamento, configure um http.Client com timeout e propague context.Context na camada responsável. O guia de context, timeout e cancelamento em Go mostra os padrões para evitar chamadas penduradas.

Não atualize o modelo a partir de uma goroutine solta

Um antipadrão é iniciar uma goroutine em Update e modificar m.tasks ou outro campo diretamente. Além de introduzir corrida de dados, essa mutação ocorre fora do ciclo do framework e pode não gerar nova renderização.

Prefira retornar um Cmd. Se várias tarefas independentes devem começar juntas, use tea.Batch:

return m, tea.Batch(
    loadTasks(m.client, m.endpoint),
    checkVersion(m.versionClient),
)

Cada comando devolve sua própria mensagem. Se os resultados precisam de uma ordem rígida, encadeie operações em vez de depender da ordem de conclusão. Use também o race detector e a cheatsheet de debug durante o desenvolvimento:

go test -race ./...

Estilos com Lip Gloss

Bubble Tea não obriga uma biblioteca visual. Em v2, o método View() devolve uma tea.View cujo conteúdo pode incluir texto e sequências ANSI. Lip Gloss oferece uma API para cores, bordas, margens, padding, largura e alinhamento:

go get charm.land/lipgloss/v2

Defina estilos fora do fluxo de atualização:

var (
    titleStyle = lipgloss.NewStyle().
        Bold(true).
        Foreground(lipgloss.Color("86"))

    selectedStyle = lipgloss.NewStyle().
        Bold(true).
        Foreground(lipgloss.Color("229")).
        Background(lipgloss.Color("24")).
        Padding(0, 1)

    mutedStyle = lipgloss.NewStyle().
        Foreground(lipgloss.Color("241"))
)

Na View, aplique o estilo de acordo com o estado:

line := fmt.Sprintf("%s %s", mark, item.Title)
if i == m.cursor {
    line = selectedStyle.Render(line)
}
out.WriteString(line + "\n")

Não use cor como único sinal. Mantenha >, [x], rótulos ou ícones textuais que comuniquem seleção e estado em terminais sem cor. Também ofereça uma saída não interativa, como --json ou --no-color, quando a ferramenta puder ser usada em scripts.

O próprio ecossistema Charmbracelet oferece componentes Bubbles para listas, tabelas, text inputs, spinners, viewport, progress e outros controles. Comece com os componentes de que realmente precisa. Uma TUI composta por dez abstrações antes de validar o fluxo fica tão difícil de manter quanto uma interface web superdimensionada.

Tela cheia, mouse e título agora são campos da View

Esta é a mudança conceitual mais importante do Bubble Tea v2 e a que mais gera dúvida em quem aprendeu com tutoriais antigos. No v1, recursos do terminal eram ligados de forma imperativa: tea.WithAltScreen() na criação do programa, tea.EnterAltScreen como comando dentro de Update, p.SetWindowTitle(...) no *Program. No v2, tudo isso virou campo da tea.View que você devolve em View().

func (m model) View() tea.View {
    var v tea.View
    v.SetContent(m.render())
    v.AltScreen = true
    v.MouseMode = tea.MouseModeCellMotion
    v.WindowTitle = "minha-cli • tarefas"
    return v
}

O resultado é que o estado visual do terminal deixa de ser um efeito colateral espalhado por NewProgram, Init e Update: ele passa a ser uma função do modelo, igual ao texto da tela. Alternar entre modo embutido e tela cheia vira uma condição comum:

func (m model) View() tea.View {
    var v tea.View
    v.SetContent(m.render())
    v.AltScreen = m.screen != screenList // detalhe e confirmação ocupam a tela toda
    return v
}

Os campos disponíveis na tea.View, segundo o guia oficial de upgrade:

CampoPara que serve
ContentO conteúdo renderizado (via SetContent() ou tea.NewView())
AltScreenEntra ou sai do buffer alternativo (tela cheia)
MouseModeMouseModeNone, MouseModeCellMotion ou MouseModeAllMotion
ReportFocusRecebe eventos de foco e perda de foco
DisableBracketedPasteModeDesliga o bracketed paste
WindowTitleDefine o título da janela do terminal
CursorPosição, formato, cor e piscar do cursor
ForegroundColor / BackgroundColorCores do terminal
ProgressBarBarra de progresso nativa do terminal
KeyboardEnhancementsSolicita recursos estendidos de teclado
OnMouseIntercepta eventos de mouse conforme o conteúdo

Na prática isso elimina uma classe inteira de bugs do v1: dois pontos do código disputando o mesmo recurso do terminal, a tela cheia ficando ligada depois de sair de um modal, ou o cursor sumindo após um erro. Como View é chamada a cada atualização, o estado declarado sempre vence.

Duas opções novas de programa ajudam nos testes: tea.WithColorProfile(p) fixa um perfil de cor e tea.WithWindowSize(w, h) define o tamanho inicial do terminal. As duas tornam a saída determinística em CI, o que combina com a seção de testes mais abaixo.

Migrando de Bubble Tea v1 para v2

Se você tem um projeto no github.com/charmbracelet/bubbletea, a migração é mecânica e costuma caber em um único commit. A tabela resume as trocas que o guia oficial lista como obrigatórias:

v1v2
github.com/charmbracelet/bubbleteacharm.land/bubbletea/v2
github.com/charmbracelet/lipglosscharm.land/lipgloss/v2
func View() stringfunc View() tea.View
case tea.KeyMsg:case tea.KeyPressMsg:
msg.Typemsg.Code (um rune)
msg.Runesmsg.Text (uma string)
msg.Altmsg.Mod (msg.Mod.Contains(tea.ModAlt))
case " ":case "space":
tea.WithAltScreen()view.AltScreen = true
tea.WithMouseCellMotion()view.MouseMode = tea.MouseModeCellMotion
tea.EnterAltScreen / tea.ExitAltScreenview.AltScreen = true / false
tea.SetWindowTitle("…")view.WindowTitle = "…"
p.Start() / p.StartReturningModel()p.Run()
tea.Sequentially(...)tea.Sequence(...)
tea.WindowSize()tea.RequestWindowSize

Três detalhes que costumam passar despercebidos e quebram comportamento em silêncio:

  • Barra de espaço. msg.String() agora devolve "space", não " ". Um case " ": compila normalmente e simplesmente para de funcionar. Este é o erro mais comum da migração.
  • Colar texto. Eventos de paste deixaram de chegar como tea.KeyMsg com a flag Paste e viraram tea.PasteMsg (com msg.Content), tea.PasteStartMsg e tea.PasteEndMsg.
  • Mouse. tea.MouseMsg virou uma interface: pegue as coordenadas com msg.Mouse() e trate os tipos específicos tea.MouseClickMsg, tea.MouseReleaseMsg, tea.MouseWheelMsg e tea.MouseMotionMsg. As constantes também mudaram, por exemplo tea.MouseButtonLeft virou tea.MouseLeft.

Um roteiro seguro de migração:

  1. atualize os imports e rode go mod tidy;
  2. corrija a assinatura de View() e devolva tea.NewView(...);
  3. troque tea.KeyMsg por tea.KeyPressMsg e revise cada case de tecla, com atenção ao "space";
  4. mova opções e comandos de terminal para campos da tea.View;
  5. rode go build ./... e go vet ./..., que apontam a maior parte do resto;
  6. execute a suíte de testes de Update antes de abrir a TUI manualmente.

Não há pressa em migrar uma ferramenta estável que já está em produção com v1, mas projetos novos devem começar direto no v2: é onde ficam a View declarativa, o teclado estendido e as opções determinísticas de teste.

Organizando uma TUI que cresceu

O exemplo de arquivo único é ótimo para aprender, mas um produto real precisa separar responsabilidades. Uma estrutura possível:

cmd/minha-cli/
  main.go
internal/tui/
  model.go
  update.go
  view.go
  keys.go
  styles.go
  messages.go
internal/tasks/
  service.go
  repository.go

A divisão não precisa ser dogmática. O objetivo é manter:

  • tipos de mensagens próximos ao domínio que representam;
  • efeitos externos em serviços testáveis;
  • atalhos centralizados e documentados;
  • estilos separados da regra de negócio;
  • Update como coordenador, não como arquivo de duas mil linhas.

Para múltiplas telas, use um campo de modo:

type screen int

const (
    screenList screen = iota
    screenDetails
    screenConfirm
)

Update delega conforme a tela atual e View renderiza o componente correspondente. Outra abordagem é manter submodelos, por exemplo list.Model, textinput.Model e viewport.Model, cada um recebendo a mensagem e devolvendo o próprio comando.

Evite criar uma goroutine por componente ou uma arquitetura de microserviços dentro da TUI. A aplicação continua sendo um processo Go com uma máquina de estados. O artigo de Clean Architecture em Go sem overengineering ajuda a encontrar uma divisão proporcional ao tamanho do produto.

Teclas, ajuda e ações destrutivas

Atalhos previsíveis melhoram muito a usabilidade:

  • setas e j/k para navegação;
  • / para busca;
  • enter para abrir ou confirmar uma ação reversível;
  • esc para voltar;
  • ? para ajuda;
  • q para sair quando o foco não está em um campo de texto;
  • ctrl+c como saída de emergência.

Ações destrutivas precisam de confirmação explícita. Não associe d diretamente a apagar um recurso em produção. Abra uma tela de confirmação, mostre o alvo completo e, para operações críticas, exija a digitação do nome. A interface bonita não reduz a necessidade de idempotência, autorização, logs e validação no backend.

Em ferramentas que disparam deploys, publique progresso como mensagens e ofereça cancelamento. Para processos locais, trate sinais e encerre subprocessos. Para operações remotas, diferencie “parar de acompanhar” de “cancelar o trabalho no servidor”; são ações diferentes.

Como testar Update e View

A arquitetura torna Update testável sem abrir um terminal. Um teste de navegação pode criar um modelo e enviar uma tecla:

func TestUpdateMovesCursorDown(t *testing.T) {
    initial := model{
        tasks: []task{
            {Title: "primeira"},
            {Title: "segunda"},
        },
    }

    updated, cmd := initial.Update(tea.KeyPressMsg{Code: tea.KeyDown})
    got := updated.(model)

    if cmd != nil {
        t.Fatal("comando inesperado")
    }
    if got.cursor != 1 {
        t.Fatalf("cursor = %d; queríamos 1", got.cursor)
    }
}

No v2, teste também a barra de espaço: segundo o guia oficial de migração, msg.String() devolve "space", embora o código da tecla continue sendo ' '. O teste abaixo verifica os dois atalhos de conclusão usados na lista e evita que um case " " antigo passe despercebido:

func TestUpdateTogglesTask(t *testing.T) {
    tests := []struct {
        name string
        key  tea.KeyPressMsg
    }{
        {name: "enter", key: tea.KeyPressMsg{Code: tea.KeyEnter}},
        {name: "space", key: tea.KeyPressMsg{Code: tea.KeySpace, Text: " "}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            initial := model{tasks: []task{{Title: "revisar PR"}}}
            updated, cmd := initial.Update(tt.key)
            got := updated.(model)

            if cmd != nil {
                t.Fatal("comando inesperado")
            }
            if !got.tasks[0].Done {
                t.Fatalf("tecla %q não concluiu a tarefa", tt.key.String())
            }

            updated, _ = got.Update(tt.key)
            if updated.(model).tasks[0].Done {
                t.Fatal("a segunda pressão deveria desmarcar a tarefa")
            }
        })
    }
}

Coloque os testes em um arquivo model_test.go no mesmo pacote do modelo, com imports de testing, strings e tea "charm.land/bubbletea/v2" conforme os exemplos usados. Cada subteste cria sua própria lista: isso evita compartilhar o slice de tarefas entre os casos.

Teste também limites: seta para cima no primeiro item, lista vazia, remoção do último item e resize para largura pequena. Para uma mensagem de domínio:

func TestTasksLoadedReplacesLoadingState(t *testing.T) {
    initial := model{loading: true}
    msg := tasksLoadedMsg{{Title: "revisar PR"}}

    updated, _ := initial.Update(msg)
    got := updated.(model)

    if got.loading {
        t.Fatal("loading deveria ser false")
    }
    if len(got.tasks) != 1 {
        t.Fatalf("len(tasks) = %d; queríamos 1", len(got.tasks))
    }
}

Para View, prefira asserções sobre conteúdo importante a snapshots gigantes e frágeis. No v2, View() retorna uma struct, não uma string: examine view.Content, o campo documentado na API oficial de tea.View.

func TestViewShowsTask(t *testing.T) {
    view := model{tasks: []task{{Title: "revisar PR"}}}.View()
    if !strings.Contains(view.Content, "revisar PR") {
        t.Fatal("a tarefa não apareceu na tela")
    }
}

Snapshots são úteis para layouts estáveis, mas códigos ANSI, largura e pequenas mudanças de espaçamento podem gerar muito ruído. Separe uma função de renderização sem cor ou normalize ANSI antes da comparação.

Combine esses testes com table-driven tests em Go para cobrir várias teclas e estados sem duplicar a estrutura da suíte. Depois execute go test ./..., go vet ./... e sua política de Staticcheck e golangci-lint.

Checklist para uma TUI de produção

Antes de distribuir o binário, verifique:

  • existe modo não interativo para automação, quando aplicável;
  • Update não executa I/O bloqueante;
  • efeitos externos retornam mensagens por tea.Cmd;
  • resize do terminal não quebra a tela;
  • lista vazia, loading e erro têm estados visíveis;
  • teclado funciona sem depender apenas de cor;
  • ações destrutivas exigem confirmação;
  • erros vão para uma área da UI ou para logs, não misturados ao desenho;
  • chamadas de rede têm timeout e cancelamento;
  • Update e transições principais têm testes;
  • o binário é testado em Linux, macOS e Windows, se esses sistemas forem suportados;
  • versões de dependências e processo de release estão fixados.

Para distribuir em múltiplas plataformas, o guia de GoReleaser, checksums e SBOM mostra como gerar artefatos reproduzíveis. Se a TUI chama APIs, aplique também logging estruturado com slog e não exiba tokens, headers ou respostas sensíveis na tela.

Quando não usar Bubble Tea

Bubble Tea não é automaticamente melhor porque a tela se move. Evite uma TUI quando:

  • o comando só recebe argumentos e imprime uma resposta;
  • a saída precisa ser consumida por jq, pipes ou outro programa;
  • um formulário web seria mais acessível e compartilhável;
  • a equipe não quer manter comportamento específico de terminal;
  • a interação esconde uma operação que deveria ser declarativa e reproduzível.

Nesses casos, uma CLI com saída estável, códigos de retorno corretos e formatos json ou text é uma solução superior. A melhor ferramenta de terminal é aquela que respeita tanto a pessoa usando o teclado quanto o script executando às três da manhã.

Perguntas frequentes

O que é Bubble Tea em Go?

Bubble Tea Go (Bubble Tea Golang) é o framework Charmbracelet para interfaces de terminal baseado em Model-Update-View. O modelo guarda o estado, Update processa teclado e mensagens, e View descreve a tela em uma tea.View. Ele é útil para listas, seletores, dashboards, formulários e fluxos interativos.

Bubble Tea substitui Cobra?

Não. Cobra organiza comandos, subcomandos e flags. Bubble Tea organiza uma interface interativa. Eles podem ser usados separadamente ou no mesmo binário: Cobra recebe minha-cli interactive, por exemplo, e esse subcomando inicia o programa Bubble Tea.

Preciso usar Lip Gloss?

Não. Você pode renderizar strings simples e ainda ter uma TUI funcional. Lip Gloss ajuda a controlar estilos, bordas e layout, mas deve melhorar a leitura sem esconder informação de terminais sem cor.

É possível testar sem abrir um terminal?

Sim. A maior parte da lógica pode ser testada enviando mensagens diretamente para Update e verificando o modelo resultante. View pode ser testada por trechos essenciais ou snapshots normalizados. Testes end-to-end em terminal ficam reservados para os fluxos mais importantes.

Como deixar a TUI em tela cheia no v2?

Defina v.AltScreen = true na tea.View devolvida por View(). A opção tea.WithAltScreen() e os comandos tea.EnterAltScreen e tea.ExitAltScreen foram removidos no v2. O mesmo vale para mouse (v.MouseMode), título da janela (v.WindowTitle) e cursor (v.Cursor).

O que mais quebra ao migrar do v1 para o v2?

A barra de espaço. msg.String() passou a devolver "space" em vez de " ", então um case " ": continua compilando e para de responder. Depois disso vêm os eventos de colar (tea.PasteMsg) e o mouse, que virou interface com tipos de mensagem específicos.

Quando uma TUI vale a pena?

Ela vale a pena quando navegação, exploração ou acompanhamento em tempo real fazem parte da tarefa. Para comandos atômicos e automação, prefira uma CLI convencional. Se os dois públicos existem, ofereça uma operação scriptável e um modo interativo sobre a mesma camada de aplicação.

Próximos passos

Comece pequeno: implemente uma lista com cursor, loading, erro e uma ação. Depois extraia o acesso externo para um serviço, transforme efeitos em Cmd, teste as transições e só então adicione componentes visuais. A documentação e os exemplos oficiais estão nos repositórios do Bubble Tea, Bubbles e Lip Gloss, com a API publicada também no pkg.go.dev de Bubble Tea v2.

Para continuar no ecossistema de ferramentas Go, veja projetos de Go para portfólio, o tutorial de go:embed para incluir arquivos no binário e as vagas de Go no Brasil. Uma TUI pequena, bem testada e distribuída como binário multiplataforma é um excelente projeto para demonstrar domínio de interfaces, concorrência, testes e experiência de desenvolvedor.