Loading...
Loading...
Build terminal UIs with Bubbletea, Bubbles, Lipgloss, and Huh. Use when creating TUI applications, interactive forms, styled terminal output, or when user mentions Bubbletea, Bubbles, Lipgloss, Huh, Charm, or TUI development.
npx skill4agent add yurifrl/cly charm-stacktype model struct {
cursor int
choices []string
selected map[int]struct{}
}func (m model) Init() tea.Cmd {
return nil // or return a command
}func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// Handle keyboard
}
return m, nil
}func (m model) View() string {
return "Hello, World!"
}User Input → Msg → Update → Model → View → Screen
↑ ↓
└────────── tea.Cmd ─────────────────┘package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
choices []string
cursor int
selected map[int]struct{}
}
func initialModel() model {
return model{
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
selected: make(map[int]struct{}),
}
}
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.KeyMsg:
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.choices)-1 {
m.cursor++
}
case "enter", " ":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
return m, nil
}
func (m model) View() string {
s := "What should we buy?\n\n"
for i, choice := range m.choices {
cursor := " "
if m.cursor == i {
cursor = ">"
}
checked := " "
if _, ok := m.selected[i]; ok {
checked = "x"
}
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
s += "\nPress q to quit.\n"
return s
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Error: %v", err)
os.Exit(1)
}
}func checkServer() tea.Msg {
// Do work
return statusMsg{online: true}
}
// In Update:
case tea.KeyMsg:
if msg.String() == "c" {
return m, checkServer // Execute command
}func fetchData() tea.Cmd {
return func() tea.Msg {
resp, err := http.Get("https://api.example.com/data")
if err != nil {
return errMsg{err}
}
return dataMsg{resp}
}
}return m, tea.Batch(
cmd1,
cmd2,
cmd3,
)type tickMsg time.Time
func tick() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
// In Init:
func (m model) Init() tea.Cmd {
return tick()
}
// In Update:
case tickMsg:
m.lastTick = time.Time(msg)
return m, tick() // Keep tickingtype model struct {
width int
height int
}
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
return m, nil
}
return m, nil
}
func (m model) View() string {
return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
"Centered text",
)
}p := tea.NewProgram(
initialModel(),
tea.WithAltScreen(), // Use alternate screen buffer
tea.WithMouseCellMotion(), // Enable mouse
)import "github.com/charmbracelet/bubbles/spinner"
type model struct {
spinner spinner.Model
}
func initialModel() model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{spinner: s}
}
func (m model) Init() tea.Cmd {
return m.spinner.Tick
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
func (m model) View() string {
return m.spinner.View() + " Loading..."
}spinner.Linespinner.Dotspinner.MiniDotspinner.Jumpspinner.Pulsespinner.Pointsspinner.Globespinner.Moonimport "github.com/charmbracelet/bubbles/textinput"
type model struct {
textInput textinput.Model
}
func initialModel() model {
ti := textinput.New()
ti.Placeholder = "Enter your name"
ti.Focus()
ti.CharLimit = 156
ti.Width = 20
return model{textInput: ti}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "enter":
name := m.textInput.Value()
// Use the name
return m, tea.Quit
}
}
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
func (m model) View() string {
return fmt.Sprintf(
"What's your name?\n\n%s\n\n%s",
m.textInput.View(),
"(esc to quit)",
)
}import "github.com/charmbracelet/bubbles/list"
type item struct {
title, desc string
}
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }
type model struct {
list list.Model
}
func initialModel() model {
items := []list.Item{
item{title: "Raspberry Pi", desc: "A small computer"},
item{title: "Arduino", desc: "Microcontroller"},
item{title: "ESP32", desc: "WiFi & Bluetooth"},
}
l := list.New(items, list.NewDefaultDelegate(), 0, 0)
l.Title = "Hardware"
return model{list: l}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.list.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
if msg.String() == "enter" {
selected := m.list.SelectedItem().(item)
// Use selected
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}import "github.com/charmbracelet/bubbles/table"
func initialModel() model {
columns := []table.Column{
{Title: "ID", Width: 4},
{Title: "Name", Width: 10},
{Title: "Status", Width: 10},
}
rows := []table.Row{
{"1", "Alice", "Active"},
{"2", "Bob", "Inactive"},
}
t := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(7),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
t.SetStyles(s)
return model{table: t}
}import "github.com/charmbracelet/bubbles/viewport"
type model struct {
viewport viewport.Model
content string
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport = viewport.New(msg.Width, msg.Height)
m.viewport.SetContent(m.content)
}
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
func (m model) View() string {
return m.viewport.View()
}import "github.com/charmbracelet/bubbles/progress"
type model struct {
progress progress.Model
percent float64
}
func initialModel() model {
return model{
progress: progress.New(progress.WithDefaultGradient()),
percent: 0.0,
}
}
func (m model) View() string {
return "\n" + m.progress.ViewAs(m.percent) + "\n\n"
}import "github.com/charmbracelet/lipgloss"
var (
// Define styles
titleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("170")).
Background(lipgloss.Color("235")).
Padding(0, 1)
errorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Bold(true)
successStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("42"))
)
// Use styles
title := titleStyle.Render("Hello")
err := errorStyle.Render("Error!")// ANSI 16 colors
lipgloss.Color("5") // magenta
// ANSI 256 colors
lipgloss.Color("86") // aqua
lipgloss.Color("201") // hot pink
// True color (hex)
lipgloss.Color("#0000FF") // blue
lipgloss.Color("#FF6B6B") // red
// Adaptive (light/dark)
lipgloss.AdaptiveColor{
Light: "236",
Dark: "248",
}style := lipgloss.NewStyle().
Width(50).
Height(10).
Padding(1, 2). // top/bottom, left/right
Margin(1, 2, 3, 4). // top, right, bottom, left
Align(lipgloss.Center)style := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("63")).
Padding(1, 2)
// Border types
lipgloss.NormalBorder()
lipgloss.RoundedBorder()
lipgloss.ThickBorder()
lipgloss.DoubleBorder()
lipgloss.HiddenBorder()
// Selective borders
style.BorderTop(true).
BorderLeft(true)// Horizontal
row := lipgloss.JoinHorizontal(
lipgloss.Top, // Alignment
box1, box2, box3,
)
// Vertical
col := lipgloss.JoinVertical(
lipgloss.Left,
box1, box2, box3,
)
// Positions: Top, Center, Bottom, Left, Right// Place in whitespace
centered := lipgloss.Place(
width, height,
lipgloss.Center, // horizontal
lipgloss.Center, // vertical
content,
)style := lipgloss.NewStyle().
Bold(true).
Italic(true).
Underline(true).
Strikethrough(true).
Blink(true).
Faint(true).
Reverse(true)import "github.com/charmbracelet/huh"
var (
burger string
toppings []string
name string
)
func runForm() error {
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Choose your burger").
Options(
huh.NewOption("Classic", "classic"),
huh.NewOption("Chicken", "chicken"),
huh.NewOption("Veggie", "veggie"),
).
Value(&burger),
huh.NewMultiSelect[string]().
Title("Toppings").
Options(
huh.NewOption("Lettuce", "lettuce"),
huh.NewOption("Tomato", "tomato"),
huh.NewOption("Cheese", "cheese"),
).
Limit(3).
Value(&toppings),
),
huh.NewGroup(
huh.NewInput().
Title("What's your name?").
Value(&name).
Validate(func(s string) error {
if s == "" {
return fmt.Errorf("name required")
}
return nil
}),
),
)
return form.Run()
}huh.NewInput().
Title("Username").
Placeholder("Enter username").
Value(&username)huh.NewText().
Title("Description").
CharLimit(400).
Value(&description)huh.NewSelect[string]().
Title("Pick one").
Options(
huh.NewOption("Option 1", "opt1"),
huh.NewOption("Option 2", "opt2"),
).
Value(&choice)huh.NewMultiSelect[string]().
Title("Pick several").
Options(...).
Limit(3).
Value(&choices)huh.NewConfirm().
Title("Are you sure?").
Affirmative("Yes").
Negative("No").
Value(&confirmed)form := huh.NewForm(...)
form.WithAccessible(true) // Screen reader friendlytype model struct {
form *huh.Form
}
func (m model) Init() tea.Cmd {
return m.form.Init()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
form, cmd := m.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
m.form = f
}
if m.form.State == huh.StateCompleted {
// Form done, get values
return m, tea.Quit
}
return m, cmd
}
func (m model) View() string {
if m.form.State == huh.StateCompleted {
return "Done!\n"
}
return m.form.View()
}modules/demo/spinner/
├── cmd.go # Register() and run()
└── spinner.go # Bubbletea modelpackage spinner
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
func Register(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "spinner",
Short: "Spinner demo",
RunE: run,
}
parent.AddCommand(cmd)
}
func run(cmd *cobra.Command, args []string) error {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
return err
}
return nil
}package spinner
import (
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type model struct {
spinner spinner.Model
}
func initialModel() model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{spinner: s}
}
func (m model) Init() tea.Cmd {
return m.spinner.Tick
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "q" {
return m, tea.Quit
}
}
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
func (m model) View() string {
return m.spinner.View() + " Loading...\n"
}type model struct {
loading bool
spinner spinner.Model
data []string
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "r" {
m.loading = true
return m, fetchData
}
case dataMsg:
m.loading = false
m.data = msg.data
return m, nil
}
if m.loading {
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
return m, nil
}
func (m model) View() string {
if m.loading {
return m.spinner.View() + " Loading data..."
}
return renderData(m.data)
}type model struct {
err error
}
type errMsg struct{ err error }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case errMsg:
m.err = msg.err
return m, nil
}
return m, nil
}
func (m model) View() string {
if m.err != nil {
return errorStyle.Render("Error: " + m.err.Error())
}
return normalView()
}type view int
const (
viewMenu view = iota
viewList
viewDetail
)
type model struct {
currentView view
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "1":
m.currentView = viewMenu
case "2":
m.currentView = viewList
case "3":
m.currentView = viewDetail
}
}
return m, nil
}
func (m model) View() string {
switch m.currentView {
case viewMenu:
return renderMenu()
case viewList:
return renderList()
case viewDetail:
return renderDetail()
}
return ""
}func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.counter++ // Modify copy
return m, nil
}func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.counter++ // Mutates original
return m, nil
}case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
return m, tea.Quit
}p := tea.NewProgram(
initialModel(),
tea.WithAltScreen(),
)type model struct {
spinner spinner.Model
textInput textinput.Model
list list.Model
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
cmds = append(cmds, cmd)
m.textInput, cmd = m.textInput.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}modules/demo/*/