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
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 noneWhat 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.
Updatechanges the model and returns follow-up commands.Viewrenders 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.goKeep 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/appIf 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.Cmdfunctions and keepUpdatedeterministic. - If output wraps poorly, test at narrow widths and keep
Viewformatting responsive to available space when the model tracks dimensions.
Next steps
- Open the Stack Builder.
- Read the Go ecosystem docs.