Guides/ Go

Go CLI with Bubble Tea

Create a Go terminal application with Bubble Tea using Better Fullstack's Go CLI project scaffolding.

Updated 2026-05-12

goclibubbletea

Use this stack when you want a Go terminal UI instead of a web API.

npm create better-fullstack@latest my-go-cli -- \
  --ecosystem go \
  --go-web-framework none \
  --go-orm none \
  --go-api none \
  --go-cli bubbletea \
  --go-logging none \
  --go-auth none

What this creates

  • A Go CLI project.
  • Bubble Tea as the terminal UI option.
  • No web framework, ORM, gRPC, or auth add-on.

Generated shape

This stack creates an interactive terminal app, not a server. Bubble Tea organizes the program around a model, messages, updates, and views.

The main loop is simple:

  • The model holds current application state.
  • Messages describe input, ticks, or command results.
  • Update changes the model and returns follow-up commands.
  • View renders the current model as terminal text.

Representative file tree

my-go-cli/
  go.mod
  cmd/
    app/
      main.go
  internal/
    tui/
      model.go
      update.go
      view.go

Keep side effects in Bubble Tea commands where possible. That makes the model and view easier to test without running a real terminal.

Model and update examples

A Bubble Tea model can start with just enough state to render and react to input:

package tui

type Model struct {
	Items    []string
	Selected int
	Quitting bool
}

func NewModel() Model {
	return Model{
		Items: []string{"Build", "Test", "Deploy"},
	}
}

The update function owns state transitions:

package tui

import tea "github.com/charmbracelet/bubbletea"

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "q", "ctrl+c":
			m.Quitting = true
			return m, tea.Quit
		case "j", "down":
			if len(m.Items) > 0 {
				m.Selected = (m.Selected + 1) % len(m.Items)
			}
		case "k", "up":
			if len(m.Items) > 0 {
				m.Selected = (m.Selected - 1 + len(m.Items)) % len(m.Items)
			}
		}
	}

	return m, nil
}

The view should render from state without performing work:

package tui

import "strings"

func (m Model) View() string {
	if m.Quitting {
		return ""
	}

	var b strings.Builder
	b.WriteString("Tasks\n\n")

	for i, item := range m.Items {
		cursor := " "
		if i == m.Selected {
			cursor = ">"
		}
		b.WriteString(cursor + " " + item + "\n")
	}

	b.WriteString("\nq: quit  j/k: move\n")
	return b.String()
}

Compatibility notes

CLI projects should keep server-only categories set to none unless they are intentionally hybrid tools.

Keep --go-cli bubbletea with --go-web-framework none, --go-orm none, and --go-api none for a terminal-first app. If the CLI later talks to an API, keep that client in a separate package and call it through commands instead of from View.

When to choose it

Choose Bubble Tea for terminal dashboards, developer tools, and command-line interfaces where interactivity matters.

It is a good fit for tools that need selection, progress, forms, or live status without building a browser UI.

Testing and deployment notes

Unit test the model transitions directly:

go test ./...

For distribution, build a single binary and document supported environment variables or config files:

go build ./cmd/app

If the generated project uses a different command path, follow the path in its cmd/ directory.

Troubleshooting

  • If key presses do nothing, verify the focused terminal is sending the key sequence Bubble Tea receives.
  • If the screen leaves behind stale output, check program startup and shutdown options for alternate-screen behavior.
  • If tests are hard to write, move side effects into tea.Cmd functions and keep Update deterministic.
  • If output wraps poorly, test at narrow widths and keep View formatting responsive to available space when the model tracks dimensions.

Next steps