AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Charm Bubbletea

skill-alxxpersonal-forge-charm-bubbletea · by alxxpersonal

Build terminal UIs in Go with Bubble Tea v2's Elm Architecture (Model/Update/View). Use when building Go TUI apps, tea.Model, tea.Cmd, Elm architecture, or terminal applications. NOT for pre-built TUI components (use bubbles).

No reviews yet
0 installs
30 views
0.0% view→install

Install

$ agentstack add skill-alxxpersonal-forge-charm-bubbletea

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-alxxpersonal-forge-charm-bubbletea)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Charm Bubbletea? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Bubble Tea v2

Build terminal UIs in Go using the Elm Architecture. Import as tea "charm.land/bubbletea/v2".

$ARGUMENTS context: If a specific component or pattern is requested, focus guidance on that area.

Quick Start

Minimal working program - a counter:

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 {
    return tea.NewView(fmt.Sprintf("Count: %d\n\nup/down to change, q to quit\n", m.count))
}

func main() {
    if _, err := tea.NewProgram(model{}).Run(); err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }
}

Core API Reference

The Elm Architecture

Bubble Tea follows the Elm Architecture - a unidirectional data flow pattern:

  1. Model holds all application state
  2. Update receives messages, returns updated model + optional command
  3. View renders the model to a string (called after every Update)
  4. Commands perform async I/O, returning messages back to Update

The runtime owns the loop. You never mutate state outside Update. You never call View yourself.

tea.Model Interface

type Model interface {
    Init() Cmd                      // initial command (return nil for none)
    Update(Msg) (Model, Cmd)        // handle messages, return new state + command
    View() View                     // render UI
}

tea.Cmd and tea.Msg

type Msg = any                      // messages can be any type
type Cmd func() Msg                 // commands are functions that return a message

A Cmd is a function that performs I/O and returns a Msg. Return nil for no command.

tea.View

// Create a view from a string
v := tea.NewView("Hello, World!")

// View struct fields (set after creation):
v.AltScreen = true                          // fullscreen mode
v.MouseMode = tea.MouseModeCellMotion       // enable mouse
v.ReportFocus = true                        // get FocusMsg/BlurMsg
v.Cursor = tea.NewCursor(x, y)             // show cursor at position
v.WindowTitle = "My App"                    // set terminal title
v.BackgroundColor = someColor               // set terminal bg
v.KeyboardEnhancements.ReportEventTypes = true // key release events

Program

p := tea.NewProgram(model{}, opts...)       // create program
m, err := p.Run()                           // run (blocks until quit)
p.Send(msg)                                 // send message from outside
p.Quit()                                    // quit from outside
p.Kill()                                    // force kill
p.Wait()                                    // block until shutdown

Program Options

tea.WithContext(ctx)                         // cancellable context
tea.WithInput(reader)                        // custom input (nil to disable)
tea.WithOutput(writer)                       // custom output
tea.WithFilter(func(Model, Msg) Msg)         // intercept/filter messages
tea.WithFPS(fps)                             // custom FPS (default 60, max 120)
tea.WithEnvironment([]string)                // custom env vars (SSH)
tea.WithWindowSize(w, h)                     // initial size (testing)
tea.WithColorProfile(profile)                // force color profile
tea.WithoutRenderer()                        // no TUI rendering
tea.WithoutSignalHandler()                   // handle signals yourself
tea.WithoutCatchPanics()                     // disable panic recovery

Built-in Messages

| Message | When | |---------|------| | tea.KeyPressMsg | Key pressed. Use msg.String() to match (e.g. "ctrl+c", "enter", "a") | | tea.KeyReleaseMsg | Key released (needs keyboard enhancements) | | tea.WindowSizeMsg | Terminal resized. Fields: Width, Height. Sent on startup + resize | | tea.MouseClickMsg | Mouse click. Fields: X, Y, Button, Mod | | tea.MouseReleaseMsg | Mouse button released | | tea.MouseWheelMsg | Scroll wheel | | tea.MouseMotionMsg | Mouse moved (needs AllMotion mode) | | tea.FocusMsg | Terminal gained focus (needs ReportFocus) | | tea.BlurMsg | Terminal lost focus | | tea.PasteMsg | Bracketed paste. Field: Content | | tea.ColorProfileMsg | Terminal color profile on startup | | tea.BackgroundColorMsg | Response to RequestBackgroundColor. Has IsDark() | | tea.ResumeMsg | Program resumed after suspend | | tea.KeyboardEnhancementsMsg | Terminal keyboard capabilities |

Built-in Commands

| Command | What it does | |---------|-------------| | tea.Quit | Exit the program | | tea.Suspend | Suspend (ctrl+z behavior) | | tea.Interrupt | Interrupt (returns ErrInterrupted) | | tea.ClearScreen | Clear terminal | | tea.Batch(cmds...) | Run commands concurrently | | tea.Sequence(cmds...) | Run commands in order | | tea.Every(dur, fn) | Tick synced with system clock | | tea.Tick(dur, fn) | Tick from invocation time | | tea.Println(args...) | Print above TUI (persists across renders) | | tea.Printf(tmpl, args...) | Printf above TUI | | tea.SetClipboard(s) | Set system clipboard (OSC52) | | tea.ReadClipboard | Read system clipboard | | tea.RequestWindowSize | Query current window size | | tea.RequestBackgroundColor | Query terminal background color | | tea.ExecProcess(cmd, callback) | Run external process (e.g. editor) | | tea.Raw(seq) | Send raw escape sequence |

Key Handling

case tea.KeyPressMsg:
    switch msg.String() {
    case "ctrl+c", "q":        // string matching (most common)
        return m, tea.Quit
    case "up", "k":            // arrow keys have string names
    case "enter", "space":     // special keys
    case "ctrl+s":             // modifier combos
    case "a":                  // regular characters
    }

// Or use the Key struct for type-safe matching:
case tea.KeyPressMsg:
    key := msg.Key()
    switch key.Code {
    case tea.KeyEnter:          // typed constant
    case tea.KeyTab:
    case tea.KeyEsc:
    }
    // key.Mod for modifiers: tea.ModCtrl, tea.ModAlt, tea.ModShift
    // key.Text for printable characters

Mouse Handling

Enable mouse in View, handle in Update:

func (m model) View() tea.View {
    v := tea.NewView(m.render())
    v.MouseMode = tea.MouseModeCellMotion  // or tea.MouseModeAllMotion
    return v
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.MouseClickMsg:
        x, y := msg.X, msg.Y
        if msg.Button == tea.MouseLeft { /* handle click */ }
    case tea.MouseWheelMsg:
        if msg.Button == tea.MouseWheelUp { /* scroll up */ }
    case tea.MouseMsg:
        // catches all mouse events
        mouse := msg.Mouse()
    }
    return m, nil
}

Common Patterns

Pattern 1: Async I/O (HTTP, file, etc.)

Define a custom Msg type. Write a Cmd that does the I/O and returns it.

type statusMsg int
type errMsg struct{ error }

func checkServer() tea.Msg {
    res, err := http.Get("https://example.com")
    if err != nil {
        return errMsg{err}
    }
    defer res.Body.Close()
    return statusMsg(res.StatusCode)
}

func (m model) Init() tea.Cmd { return checkServer } // note: pass function, don't call it

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case statusMsg:
        m.status = int(msg)
        return m, tea.Quit
    case errMsg:
        m.err = msg.error
        return m, nil
    }
    return m, nil
}

Pattern 2: Ticking / Timers

tea.Tick and tea.Every send a single message. Re-dispatch to loop.

type tickMsg time.Time

func doTick() tea.Cmd {
    return tea.Tick(time.Second, func(t time.Time) tea.Msg {
        return tickMsg(t)
    })
}

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg.(type) {
    case tickMsg:
        m.elapsed++
        return m, doTick()  // re-dispatch to keep ticking
    }
    return m, nil
}

tea.Every syncs with the system clock (wall-clock aligned ticks). tea.Tick starts from invocation.

Pattern 3: Composing Child Components

Child components follow the same Model/Update/View pattern. Parent delegates messages.

type parentModel struct {
    spinner spinner.Model
    input   textinput.Model
    focus   int
}

func (m parentModel) Init() tea.Cmd {
    return tea.Batch(m.spinner.Tick, m.input.Focus())
}

func (m parentModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    var cmds []tea.Cmd

    // Always update spinner (it needs ticks regardless of focus)
    var cmd tea.Cmd
    m.spinner, cmd = m.spinner.Update(msg)
    cmds = append(cmds, cmd)

    // Only update focused component for key events
    switch msg.(type) {
    case tea.KeyPressMsg:
        if m.focus == 0 {
            m.input, cmd = m.input.Update(msg)
            cmds = append(cmds, cmd)
        }
    }

    return m, tea.Batch(cmds...)
}

func (m parentModel) View() tea.View {
    return tea.NewView(m.spinner.View() + "\n" + m.input.View())
}

Pattern 4: External Messages via Channel

Use p.Send() to push messages from goroutines, or use a channel-based Cmd.

// Channel-based approach (preferred for subscription-like behavior)
func waitForActivity(sub chan resultMsg) tea.Cmd {
    return func() tea.Msg {
        return "`

Each bubble follows the same Init/Update/View pattern. See Pattern 3 above.

### Huh (Forms)

```go
import "charm.land/huh/v2"

// Huh can run standalone or embed in a bubbletea program
form := huh.NewForm(
    huh.NewGroup(
        huh.NewInput().Title("Name").Value(&name),
    ),
)

Common Mistakes

  1. Calling a Cmd instead of passing it. Init returns tea.Cmd, not tea.Msg. Write return checkServer not return checkServer(). The runtime calls the function.
  1. Forgetting to return a Cmd from tick handlers. tea.Tick and tea.Every fire once. You must return a new tick command in your Update handler to keep ticking.
  1. Mutating the model outside Update. The Elm Architecture requires all state changes go through Update. Don't share model pointers with goroutines. Use p.Send() or channel-based Cmds to communicate.
  1. Not handling WindowSizeMsg. It's sent on startup and every resize. If you do any layout math, store the dimensions and use them in View.
  1. Using fmt.Println for output. stdout is owned by the TUI. Use tea.LogToFile() for debug logging. Use tea.Println() / tea.Printf() to print above the TUI.
  1. Returning tea.Batch() with nil commands. This is safe - tea.Batch filters nil commands - but returning a plain nil is cleaner when you have no commands.
  1. AltScreen in v2 is a View property, not a Program option. Set v.AltScreen = true in your View method. Same for mouse mode - set v.MouseMode in View.
  1. Blocking in Update. Update must return quickly. Any I/O (HTTP calls, file reads, sleeps) belongs in a Cmd, not directly in Update.
  1. Not handling ctrl+c. The terminal is in raw mode - ctrl+c won't kill your app automatically. Always match "ctrl+c" in your KeyPressMsg handler and return tea.Quit or tea.Interrupt.

Checklist

  • [ ] Model implements Init() tea.Cmd, Update(tea.Msg) (tea.Model, tea.Cmd), View() tea.View
  • [ ] View returns tea.NewView(s), not a raw string
  • [ ] ctrl+c / q handling exists in Update
  • [ ] WindowSizeMsg handled if doing any layout
  • [ ] All I/O in Cmds, never in Update
  • [ ] Tick commands re-dispatched in Update handler
  • [ ] Child component updates collected with tea.Batch(cmds...)
  • [ ] No fmt.Println - use tea.LogToFile for debugging
  • [ ] AltScreen and MouseMode set in View, not as Program options

Reference

For full message/type/constant listings, see [references/api.md](references/api.md).

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.