Token导航 LogoToken导航

Agent Skill · 待分类

bubbletea-docs(泡泡茶文档)

用于辅助文档、README、Markdown、说明文和内容稿件的整理与改写。它适合让 Agent 提炼结构、补齐章节、统一术语、检查链接或把零散材料整理成可读文档。使用时应保留项目已有事实、命令和路径,不要把未确认的信息写成确定结论;涉及对外文案时,还需要控制语气,避免过度营销或夸大能力。

bubbletea-docs 是待分类类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为执行命令;许可证信息还不明确,第三方扫描有提醒项,按安装量在待分类类的 11,209 个 Skill 里排第 9,348。

总安装 245周安装 10GitHub Stars 2下载 78核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:bubbletea-docs(泡泡茶文档)
来源仓库:https://github.com/quantmind-br/skills
仓库路径:skills/bubbletea-docs
安装命令:
npx skills add https://github.com/quantmind-br/skills --skill bubbletea-docs
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/quantmind-br/skills --skill bubbletea-docs

这个 Skill 是做什么的

这个 Skill 的分类还没最终确定,用法以来源仓库的说明为准。

用于辅助文档、README、Markdown 和内容稿件的整理与改写。适合提炼结构、补齐章节、统一术语或检查链接完整性。使用时应保留项目已有事实和路径,不虚构未确认的信息。涉及对外文案时需控制语气,避免过度营销或夸大能力。适用于 Codex、Claude、Cursor、Gemini CLI 等宿主环境。

来源仓库内路径:skills/bubbletea-docs

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/quantmind-br/skills --skill bubbletea-docs;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要执行命令,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

bubbletea-docs 怎么安装?

在终端执行 npx skills add https://github.com/quantmind-br/skills --skill bubbletea-docs。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

bubbletea-docs 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

bubbletea-docs 需要什么权限?

权限级别是执行命令。安装流程涉及命令执行,可能通过 npx skills add https://github.com/quantmind-br/skills --skill bubbletea-docs 联网下载 Skill 或依赖。用户安装前应确认命令来源、仓库内容和执行环境。

bubbletea-docs 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

bubbletea-docs 用的人多吗?

公开数据是累计安装 245 次,近一周安装 10 次,下载 78 次,GitHub 2 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

bubbletea-docs 安全吗?

第三方扫描有提醒项,说明存在需要人工确认的行为。安装前请打开源码看清它读了什么、联网到哪里。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。

先想清楚的地方

  • 需要执行命令,装到常用环境前要先评估影响面。
  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方扫描存在提醒项或未通过项,需要人工逐条确认。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理待分类类任务、又不想每次都手写提示词的人。

不太适合

  • 不能接受 Skill 在你机器上执行命令的人。
  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

待分类类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
infsh-cli

infsh CLI · Codex、Claude

4,374,576382权限未标注未明确未收录
ai-video-generation

AI 视频生成 · Codex、Claude

4,313,376382调用外部服务未明确有提醒
lark-openapi-explorer

云雀 OpenAPI 浏览器 · Codex、Claude

2,064,1209,011需要联网未明确有提醒
lark-workflow-standup-report

Lark 工作流程站立报告 · Codex、Claude

2,041,6328,978需要联网未明确通过
use-my-browser

使用我的浏览器 · Codex、Claude

1,848,49140操作浏览器未明确未通过
bubbletea-docs

泡泡茶文档 · 本页

2452执行命令未明确有提醒

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Bubble Tea TUI Framework Skill

Overview

Bubble Tea is a powerful TUI framework for Go based on The Elm Architecture. Every program has a Model (state) and three methods: Init() (initial command), Update() (handle messages), View() (render UI as string).

This skill covers the complete Charm ecosystem:

  • Bubble Tea - Core TUI framework
  • Bubbles - Reusable UI components
  • Lip Gloss - Terminal styling
  • Huh - Interactive forms

Installation

go get github.com/charmbracelet/bubbletea
go get github.com/charmbracelet/bubbles    # UI components
go get github.com/charmbracelet/lipgloss   # Styling
go get github.com/charmbracelet/huh        # Forms

Quick Start

package main

import (
    "fmt"
    "log"
    tea "github.com/charmbracelet/bubbletea"
)

type model struct {
    cursor   int
    choices  []string
    selected 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", " ":
            if _, ok := m.selected[m.cursor]; ok {
                delete(m.selected, m.cursor)
            } else {
                m.selected[m.cursor] = struct{}{}
            }
        }
    }
    return m, nil
}

func (m model) View() string {
    s := "Choose:\n\n"
    for i, choice := range m.choices {
        cursor, checked := " ", " "
        if m.cursor == i { cursor = ">" }
        if _, ok := m.selected[i]; ok { checked = "x" }
        s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
    }
    return s + "\nq to quit.\n"
}

func main() {
    p := tea.NewProgram(model{
        choices:  []string{"Option A", "Option B", "Option C"},
        selected: make(map[int]struct{}),
    })
    if _, err := p.Run(); err != nil { log.Fatal(err) }
}

Core Concepts

Messages and Commands

// Custom messages
type tickMsg time.Time
type dataMsg struct{ data string }
type errMsg struct{ error }

// Command returns a message (runs async)
func fetchData() tea.Msg {
    resp, err := http.Get("https://api.example.com")
    if err != nil { return errMsg{err} }
    defer resp.Body.Close()
    // ... process response
    return dataMsg{data: "result"}
}

// Handle in Update
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case dataMsg:
        m.data = msg.data
    case errMsg:
        m.err = msg.error
    }
    return m, nil
}

// Start command in Init
func (m model) Init() tea.Cmd {
    return fetchData  // Runs async, sends message when done
}

Batch and Sequence Commands

// Parallel execution
return tea.Batch(cmd1, cmd2, cmd3)

// Sequential execution
return tea.Sequence(step1, step2, tea.Quit)

Window Size Handling

type model struct {
    width, height int
    viewport      viewport.Model
    ready         bool
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        m.width, m.height = msg.Width, msg.Height
        if !m.ready {
            m.viewport = viewport.New(msg.Width, msg.Height-4)
            m.viewport.SetContent(m.content)
            m.ready = true
        } else {
            m.viewport.Width = msg.Width
            m.viewport.Height = msg.Height - 4
        }
    }
    return m, nil
}

Common Patterns

Program Options

// Fullscreen
p := tea.NewProgram(m, tea.WithAltScreen())

// Mouse support
p := tea.NewProgram(m, tea.WithMouseCellMotion())

// Combined
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())

Key Bindings with bubbles/key

import "github.com/charmbracelet/bubbles/key"

type keyMap struct {
    Up   key.Binding
    Down key.Binding
    Quit key.Binding
}

var keys = keyMap{
    Up:   key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
    Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
    Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
}

// In Update
case tea.KeyMsg:
    switch {
    case key.Matches(msg, keys.Up):
        m.cursor--
    case key.Matches(msg, keys.Quit):
        return m, tea.Quit
    }

Multiple Views

type model struct {
    state int  // 0=menu, 1=detail
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch m.state {
    case 0: return m.updateMenu(msg)
    case 1: return m.updateDetail(msg)
    }
    return m, nil
}

func (m model) View() string {
    switch m.state {
    case 0: return m.menuView()
    case 1: return m.detailView()
    }
    return ""
}

Bubbles Components

Component Quick Reference

ComponentImportInitKey Method
Spinnerbubbles/spinnerspinner.New().Tick in Init
TextInputbubbles/textinputtextinput.New().Focus(), .Value()
TextAreabubbles/textareatextarea.New().Focus(), .Value()
Listbubbles/listlist.New(items, delegate, w, h).SelectedItem()
Tablebubbles/tabletable.New(opts...).SelectedRow()
Viewportbubbles/viewportviewport.New(w, h).SetContent(), .ScrollUp(), .ScrollDown()
Progressbubbles/progressprogress.New().SetPercent()
Helpbubbles/helphelp.New().View(keyMap)
FilePickerbubbles/filepickerfilepicker.New().DidSelectFile()
Timerbubbles/timertimer.New(duration).Toggle()
Stopwatchbubbles/stopwatchstopwatch.New().Elapsed()

Focus Management (Multiple Inputs)

type model struct {
    inputs     []textinput.Model
    focusIndex int
}

func (m *model) nextInput() tea.Cmd {
    m.inputs[m.focusIndex].Blur()
    m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
    return m.inputs[m.focusIndex].Focus()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "tab":
            return m, m.nextInput()
        }
    }
    // Update all inputs
    cmds := make([]tea.Cmd, len(m.inputs))
    for i := range m.inputs {
        m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
    }
    return m, tea.Batch(cmds...)
}

Component Composition

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

    // Update all components and collect commands
    m.spinner, cmd = m.spinner.Update(msg)
    cmds = append(cmds, cmd)

    m.textInput, cmd = m.textInput.Update(msg)
    cmds = append(cmds, cmd)

    m.viewport, cmd = m.viewport.Update(msg)
    cmds = append(cmds, cmd)

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

Spinner Example

s := spinner.New()
s.Spinner = spinner.Dot  // Line, Dot, MiniDot, Jump, Pulse, Points, Globe, Moon, Monkey
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))

// In Init(): return m.spinner.Tick
// In Update(): handle spinner.TickMsg

List with Custom Items

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 }  // Required for filtering

items := []list.Item{item{"One", "Description"}}
l := list.New(items, list.NewDefaultDelegate(), 30, 10)
l.Title = "Select Item"

Table

columns := []table.Column{
    {Title: "ID", Width: 10},
    {Title: "Name", Width: 20},
}
rows := []table.Row{
    {"1", "Alice"},
    {"2", "Bob"},
}
t := table.New(
    table.WithColumns(columns),
    table.WithRows(rows),
    table.WithFocused(true),
    table.WithHeight(7),
)

Lip Gloss Styling

Basic Styling

import "github.com/charmbracelet/lipgloss"

var style = lipgloss.NewStyle().
    Bold(true).
    Foreground(lipgloss.Color("205")).
    Background(lipgloss.Color("#7D56F4")).
    Border(lipgloss.RoundedBorder()).
    Padding(1, 2)

output := style.Render("Hello, World!")

Colors

// Hex colors
lipgloss.Color("#FF00FF")

// ANSI 256 colors
lipgloss.Color("205")

// Adaptive colors (auto light/dark background)
lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}

// Complete color profiles
lipgloss.CompleteColor{
    TrueColor: "#FF00FF",
    ANSI256:   "205",
    ANSI:      "5",
}

Layout Composition

// Horizontal layout
left := lipgloss.NewStyle().Width(20).Render("Left")
right := lipgloss.NewStyle().Width(20).Render("Right")
row := lipgloss.JoinHorizontal(lipgloss.Top, left, right)

// Vertical layout
header := "Header"
body := "Body content"
footer := "Footer"
page := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)

// Center content in box
centered := lipgloss.Place(80, 24, lipgloss.Center, lipgloss.Center, content)

Frame Size for Calculations

// Account for padding/border/margin in calculations
h, v := docStyle.GetFrameSize()
m.list.SetSize(m.width-h, m.height-v)

// Dynamic content height
headerH := lipgloss.Height(m.header())
footerH := lipgloss.Height(m.footer())
m.viewport.Height = m.height - headerH - footerH

Border Styles

lipgloss.NormalBorder()   // Standard box
lipgloss.RoundedBorder()  // Rounded corners
lipgloss.ThickBorder()    // Thick lines
lipgloss.DoubleBorder()   // Double lines
lipgloss.HiddenBorder()   // Invisible (for spacing)

Huh Forms

Quick Start

import "github.com/charmbracelet/huh"

var name string
var confirmed bool

form := huh.NewForm(
    huh.NewGroup(
        huh.NewInput().
            Title("What's your name?").
            Value(&name),

        huh.NewConfirm().
            Title("Ready to proceed?").
            Value(&confirmed),
    ),
)

err := form.Run()
if err != nil {
    if err == huh.ErrUserAborted {
        fmt.Println("Cancelled")
        return
    }
    log.Fatal(err)
}

Field Types (GENERIC TYPES REQUIRED)

// Input - single line text
huh.NewInput().Title("Name").Value(&name)

// Text - multi-line
huh.NewText().Title("Bio").Lines(5).Value(&bio)

// Select - MUST specify type
huh.NewSelect[string]().
    Title("Choose").
    Options(
        huh.NewOption("Option A", "a"),
        huh.NewOption("Option B", "b"),
    ).
    Value(&choice)

// MultiSelect - MUST specify type
huh.NewMultiSelect[string]().
    Title("Choose many").
    Options(huh.NewOptions("A", "B", "C")...).
    Limit(2).
    Value(&choices)

// Confirm
huh.NewConfirm().
    Title("Sure?").
    Affirmative("Yes").
    Negative("No").
    Value(&confirmed)

// FilePicker
huh.NewFilePicker().
    Title("Select file").
    AllowedTypes([]string{".go", ".md"}).
    Value(&filepath)

Validation

huh.NewInput().
    Title("Email").
    Value(&email).
    Validate(func(s string) error {
        if !strings.Contains(s, "@") {
            return errors.New("invalid email")
        }
        return nil
    })

Dynamic Forms (OptionsFunc/TitleFunc)

var country string
var state string

huh.NewSelect[string]().
    Value(&state).
    TitleFunc(func() string {
        if country == "Canada" { return "Province" }
        return "State"
    }, &country).  // Recompute when country changes
    OptionsFunc(func() []huh.Option[string] {
        return huh.NewOptions(getStatesFor(country)...)
    }, &country)

Bubble Tea Integration

type 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 completed - access values
        name := m.form.GetString("name")
        return m, tea.Quit
    }

    return m, cmd
}

func (m Model) View() string {
    if m.form.State == huh.StateCompleted {
        return "Done!"
    }
    return m.form.View()
}

Themes

form.WithTheme(huh.ThemeCharm())      // Default pink/purple
form.WithTheme(huh.ThemeDracula())    // Dark purple/pink
form.WithTheme(huh.ThemeCatppuccin()) // Pastel
form.WithTheme(huh.ThemeBase16())     // Muted
form.WithTheme(huh.ThemeBase())       // Minimal

Spinner for Loading

import "github.com/charmbracelet/huh/spinner"

err := spinner.New().
    Title("Processing...").
    Action(func() {
        time.Sleep(2 * time.Second)
        processData()
    }).
    Run()

Common Gotchas

Bubble Tea Core

  1. Blocking in Update/View: Never block. Use commands for I/O: // BAD time.Sleep(time.Second) // Blocks event loop! // GOOD return m, func() tea.Msg {time.Sleep(time.Second) return doneMsg{}}
  2. Goroutines modifying model: Race condition! Use commands instead: // BAD go func() {m.data = fetch()}() // Race! // GOOD return m, func() tea.Msg {return dataMsg{fetch()}}
  3. Viewport before WindowSizeMsg: Initialize after receiving dimensions: if!m.ready {m.viewport = viewport.New(msg.Width, msg.Height) m.ready = true}
  4. Using receiver methods incorrectly: Only use func (m *model) for internal helpers. Model interface methods must use value receivers func (m model).
  5. Startup commands via Init: Don't use tea.EnterAltScreen in Init. Use tea.WithAltScreen() option instead.
  6. Messages not in order: tea.Batch results arrive in ANY order. Use tea.Sequence when order matters.

Bubbles Components

  1. Spinner not animating: Must return m.spinner.Tick from Init() and handle spinner.TickMsg in Update.
  2. TextInput not accepting input: Must call .Focus() before input accepts keystrokes.
  3. Component updates not reflected: Always reassign after Update: m.spinner, cmd = m.spinner.Update(msg) // Must reassign!
  4. Multiple components losing commands: Use tea.Batch(cmds...) to combine all commands.
  5. List items not filtering: Must implement FilterValue() string on items.
  6. Table not responding: Must set table.WithFocused(true) or call t.Focus().

Lip Gloss

  1. Style method has no effect: Styles are immutable, must reassign: // BAD style.Bold(true) // Result discarded! // GOOD style = style.Bold(true)
  2. Alignment not working: Requires explicit Width: style:= lipgloss.NewStyle().Width(40).Align(lipgloss.Center)
  3. Width calculation wrong: Use lipgloss.Width() not len() for unicode.
  4. Layout arithmetic errors: Use style.GetFrameSize() to account for padding/border/margin.

Huh Forms

  1. Generic types required: Select and MultiSelect MUST have type parameter: // BAD - won't compile huh.NewSelect() // GOOD huh.NewSelect[string]()
  2. Value takes pointer: Always pass pointer: .Value(&myVar) not .Value(myVar).
  3. OptionsFunc not updating: Must pass binding variable: .OptionsFunc(fn, &country) // Recomputes when country changes
  4. Don't use Placeholder() in huh forms: Causes rendering bugs. Put examples in Description instead: // BAD huh.NewInput().Placeholder("example@email.com") // GOOD huh.NewInput().Description("e.g. example@email.com")
  5. Loop variable closure capture: Capture explicitly in loops: for _, name:= range items {currentName:= name // Capture! huh.NewInput().Value(&configs[currentName].Field)}
  6. Don't intercept Enter before form: Let huh handle Enter for navigation.

Debugging

// Log to file (stdout is the TUI)
if os.Getenv("DEBUG") != "" {
    f, _ := tea.LogToFile("debug.log", "app")
    defer f.Close()
}
log.Println("Debug message")

Best Practices

  1. Keep Update/View fast - The event loop blocks on these
  2. Use tea.Cmd for all I/O - HTTP, file, database operations
  3. Use tea.Batch for parallel - Multiple independent commands
  4. Use tea.Sequence for ordered - Commands that must run in order
  5. Store window dimensions - Handle tea.WindowSizeMsg, update components
  6. Initialize viewport after WindowSizeMsg - Dimensions aren't available at start
  7. Use value receivers - func (m model) Update not func (m *model) Update
  8. Define styles as package variables - Reuse instead of creating in loops
  9. Use AdaptiveColor - For light/dark terminal support
  10. Handle ErrUserAborted - Graceful Ctrl+C handling in huh forms

Additional Resources

Essential Imports

import (
    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/lipgloss"
    "github.com/charmbracelet/lipgloss/table"  // Static tables
    "github.com/charmbracelet/lipgloss/tree"   // Hierarchical data
    "github.com/charmbracelet/bubbles/spinner"
    "github.com/charmbracelet/bubbles/textinput"
    "github.com/charmbracelet/bubbles/textarea"
    "github.com/charmbracelet/bubbles/list"
    "github.com/charmbracelet/bubbles/table"   // Interactive tables
    "github.com/charmbracelet/bubbles/viewport"
    "github.com/charmbracelet/bubbles/help"
    "github.com/charmbracelet/bubbles/key"
    "github.com/charmbracelet/bubbles/filepicker"
    "github.com/charmbracelet/bubbles/progress"
    "github.com/charmbracelet/bubbles/timer"
    "github.com/charmbracelet/bubbles/stopwatch"
    "github.com/charmbracelet/huh"
    "github.com/charmbracelet/huh/spinner"
    "github.com/charmbracelet/wish"            // SSH TUI server
    "github.com/lrstanley/bubblezone"          // Mouse zones
)

Lip Gloss Tree (Hierarchical Data)

Render tree structures with customizable enumerators:

import "github.com/charmbracelet/lipgloss/tree"

t := tree.Root("Root").
    Child("Child 1").
    Child(
        tree.Root("Child 2").
            Child("Grandchild 1").
            Child("Grandchild 2"),
    ).
    Child("Child 3")

// Custom enumerator
t.Enumerator(tree.RoundedEnumerator)  // ├── └──
t.Enumerator(tree.BulletEnumerator)   // • bullets
t.Enumerator(tree.NumberedEnumerator) // 1. 2. 3.

// Custom styling
t.EnumeratorStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99")))
t.ItemStyle(lipgloss.NewStyle().Bold(true))

fmt.Println(t)

Lip Gloss Table (Static Rendering)

For high-performance static tables (reports, logs). Use bubbles/table for interactive selection.

import "github.com/charmbracelet/lipgloss/table"

t := table.New().
    Border(lipgloss.NormalBorder()).
    BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))).
    Headers("NAME", "AGE", "CITY").
    Row("Alice", "30", "NYC").
    Row("Bob", "25", "LA").
    Wrap(true).  // Enable text wrapping
    StyleFunc(func(row, col int) lipgloss.Style {
        if row == table.HeaderRow {
            return lipgloss.NewStyle().Bold(true)
        }
        return lipgloss.NewStyle()
    })

fmt.Println(t)

When to use which table:

  • lipgloss/table: Static rendering, reports, logs, non-interactive
  • bubbles/table: Interactive selection, keyboard navigation, focused rows

Custom Component Pattern (Sub-Model)

Create reusable Bubble Tea components by exposing Init, Update, View:

// counter.go - Reusable component
package counter

import tea "github.com/charmbracelet/bubbletea"

type Model struct {
    Count int
    Min   int
    Max   int
}

func New(min, max int) Model {
    return Model{Min: min, Max: max}
}

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

func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "+", "=":
            if m.Count < m.Max { m.Count++ }
        case "-":
            if m.Count > m.Min { m.Count-- }
        }
    }
    return m, nil
}

func (m Model) View() string {
    return fmt.Sprintf("Count: %d", m.Count)
}

// parent.go - Using the component
type parentModel struct {
    counter counter.Model
}

func (m parentModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    var cmd tea.Cmd
    m.counter, cmd = m.counter.Update(msg)
    return m, cmd
}

ProgramContext Pattern (Production)

Share global state across components without prop drilling:

// context.go
type ProgramContext struct {
    Config     *Config
    Theme      *Theme
    Width      int
    Height     int
    StartTask  func(Task) tea.Cmd  // Callback for background tasks
}

// model.go
type Model struct {
    ctx       *ProgramContext
    sidebar   sidebar.Model
    main      main.Model
    tasks     map[string]Task
    spinner   spinner.Model
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        m.ctx.Width = msg.Width
        m.ctx.Height = msg.Height
        // Sync all components
        m.sidebar.SetHeight(msg.Height)
        m.main.SetSize(msg.Width - sidebarWidth, msg.Height)
    }
    return m, nil
}

// Initialize context with task callback
func NewModel(cfg *Config) Model {
    ctx := &ProgramContext{Config: cfg}
    m := Model{ctx: ctx, tasks: make(map[string]Task)}

    ctx.StartTask = func(t Task) tea.Cmd {
        m.tasks[t.ID] = t
        return m.spinner.Tick
    }

    return m
}

Testing with teatest

import (
    "testing"
    "time"
    tea "github.com/charmbracelet/bubbletea"
    "github.com/charmbracelet/x/exp/teatest"
)

func TestModel(t *testing.T) {
    m := NewModel()
    tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(80, 24))

    // Send key presses
    tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}})
    tm.Send(tea.KeyMsg{Type: tea.KeyEnter})

    // Wait for specific output
    teatest.WaitFor(t, tm.Output(), func(bts []byte) bool {
        return strings.Contains(string(bts), "Selected")
    }, teatest.WithDuration(time.Second))

    // Check final state
    tm.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}})
    tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second))

    final := tm.FinalModel(t).(Model)
    if final.selected != "expected" {
        t.Errorf("expected 'expected', got %q", final.selected)
    }
}

SSH Integration with Wish

Serve TUI apps over SSH:

import (
    "github.com/charmbracelet/wish"
    "github.com/charmbracelet/wish/bubbletea"
    "github.com/charmbracelet/ssh"
)

func main() {
    s, err := wish.NewServer(
        wish.WithAddress(":2222"),
        wish.WithHostKeyPath(".ssh/term_info_ed25519"),
        wish.WithMiddleware(
            bubbletea.Middleware(teaHandler),
        ),
    )
    if err != nil { log.Fatal(err) }

    log.Println("Starting SSH server on :2222")
    if err := s.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
    pty, _, _ := s.Pty()
    m := NewModel(pty.Window.Width, pty.Window.Height)
    return m, []tea.ProgramOption{tea.WithAltScreen()}
}

Mouse Zones with bubblezone

Define clickable regions without coordinate math:

import zone "github.com/lrstanley/bubblezone"

type model struct {
    zone *zone.Manager
}

func newModel() model {
    return model{zone: zone.New()}
}

func (m model) View() string {
    // Wrap clickable elements
    button1 := m.zone.Mark("btn1", "[Button 1]")
    button2 := m.zone.Mark("btn2", "[Button 2]")

    return lipgloss.JoinHorizontal(lipgloss.Top, button1, " ", button2)
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.MouseMsg:
        // Check which zone was clicked
        if m.zone.Get("btn1").InBounds(msg) {
            // Button 1 clicked
        }
        if m.zone.Get("btn2").InBounds(msg) {
            // Button 2 clicked
        }
    }
    return m, nil
}

// Wrap program with zone.NewGlobal() for simpler API
func main() {
    zone.NewGlobal()
    p := tea.NewProgram(newModel(), tea.WithMouseCellMotion())
    p.Run()
}

平台分布

Codex

39.44%
按下载量换算31

Claude

29.14%
按下载量换算23

Cursor

19.22%
按下载量换算15

Gemini CLI

8.46%
按下载量换算7

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

执行命令

安装流程涉及命令执行,可能通过 npx skills add https://github.com/quantmind-br/skills --skill bubbletea-docs 联网下载 Skill 或依赖。用户安装前应确认命令来源、仓库内容和执行环境。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在提醒或未通过项,安装前请自行复核。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills