name: bubble-tea description: > 学习和复用 Charmbracelet Bubble Tea / Bubbles / Lip Gloss 的组件写法。 disable-model-invocation: true license: MIT
Bubble Tea 组件索引
本技能用于让 LLM 在编写或调整 Bubble Tea / Bubbles / Lip Gloss 代码前,先吸收本文件中的组件模型、生命周期接入点和常见坑;实现时按任务读取 snippets/ 中的 Go 片段作为可复用知识材料,而不是把片段机械复制成固定框架。
通用接入规则
- 先分发消息:退出、确认、窗口尺寸等应用级消息优先处理;但输入框、textarea 或 list 正在输入/过滤时,先让组件消费普通按键,避免全局快捷键抢输入。
- 子组件更新后必须写回:Bubbles 多为值类型,
component.Update(msg)返回的新组件值要写回 model,得到的tea.Cmd也要继续返回或合并。 - 副作用只放在
tea.Cmd:阻塞 I/O、轮询、等待、channel 监听都放进tea.Cmd;Cmd只返回tea.Msg让Update改状态,不要在Cmd里直接改 model;在Cmd中打开的资源也在Cmd内关闭。 - 正确返回命令:
Init/Update返回的是命令值本身,不要提前调用;需要参数时写返回tea.Cmd的工厂函数。spinner/progress/timer 等组件返回的后续 cmd 也不要丢。 View只渲染状态:不要在View里改 model、启动工作或读取外部资源。- 布局和命令编排:在
WindowSizeMsg中同步 viewport、list、progress 等依赖终端尺寸的组件;并发命令用tea.Batch,必须按顺序执行时用tea.Sequence。
根生命周期骨架
type model struct {
// 组件状态:input textinput.Model、items list.Model、viewport viewport.Model
// 业务状态:selectedPath string、loading bool、err error
}
func newModel() model {
return model{
// 创建组件;组件创建函数见 snippets
}
}
func (m model) Init() tea.Cmd {
// blink、tick、timer init、首次加载、终端能力请求等启动命令
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", "esc", "ctrl+c":
return m, tea.Quit
}
case tea.WindowSizeMsg:
// 同步依赖终端尺寸的组件宽高
}
// 转发给子组件,并把新组件值写回 m
// var cmd tea.Cmd
// m.input, cmd = m.input.Update(msg)
// return m, cmd
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView("")
// v.AltScreen = true
// v.MouseMode = tea.MouseModeCellMotion
// v.Cursor = ...
return v
}
组件与模式表
| 场景 | 字段 / 状态 | 代码片段 | 接入点 |
|---|---|---|---|
| Program 启动选项 / 事件过滤 | opts []tea.ProgramOption, filter func(tea.Model, tea.Msg) tea.Msg |
program.go: newProgram, preventQuitFilter, rendererOptions |
main: tea.NewProgram(newModel(), opts...); WithFilter: 拦截 tea.QuitMsg; daemon/非 TTY 可 WithoutRenderer |
| 单行输入 / 搜索框 | input textinput.Model |
inputs.go: newTextInput, updateTextInput, textInputCursor, textInputValue, setTextInputValue, setTextInputValueAtEnd |
Init: textinput.Blink; Update: 应用按键后转发; View: input.View();真实光标写入 tea.View.Cursor |
| 管道输入 / stdin 初值 | initial string, input textinput.Model |
inputs.go: stdinHasPipedInput, readTrimmedInput, setTextInputValueAtEnd |
main: 检测 stdin 并读取;newModel: SetValue 后 CursorEnd; Init: blink |
| 多字段表单 | focus int, inputs []textinput.Model |
inputs.go: newTextFields, moveFormFocus, updateTextFields, textFieldValues |
tab/up/down: 切焦点; 普通输入: 批量转发; 提交: 取值 |
| 自动建议 | textinput.Model + 建议状态 |
inputs.go: enableSuggestions, suggestionKeysEnabled; runtime.go: runWork, debounce, isCurrentDebounce |
输入变化递增 token;防抖消息先比对 token;结果消息更新 suggestions;建议快捷键按候选状态启停 |
| 多行输入 | area textarea.Model |
inputs.go: newTextarea, setTextareaDarkStyle, updateTextarea, resizeTextarea, textareaCursor |
Init: textarea.Blink,需要背景适配时加 tea.RequestBackgroundColor; WindowSizeMsg: resize; View: cursor |
| 动态多行输入 | area textarea.Model |
inputs.go: newDynamicTextarea, getTextareaMetrics |
初始化 DynamicHeight/MinHeight/MaxHeight/ShowLineNumbers; View: 可显示高度、行列、滚动百分比 |
| 原生轻量列表 / 多选 | cursor int, choices []string, selected map[int]struct{} |
raw_selection.go: moveCursorBounded, toggleIndexSelection, indexSelected, selectedCursorItem, renderChecklist |
项目确认页、一次性多选、小菜单可直接手写状态;up/down 或 j/k 移动;enter/space 切换;不需要过滤、分页或 delegate 时别急着上 list.Model |
| 列表 / 菜单 | items list.Model |
selection.go: optionItem, newOptionList, resizeList, updateList, selectedOption |
WindowSizeMsg: resize; Update: 转发; enter: 读选中项 |
| 列表动作 / 自定义 delegate | list.Model, delegateKeys, appKeys |
selection.go: listIsFiltering, setListChrome, newActionDelegate, addAdditionalFullHelpKeys, insertListItemWithStatus, removeSelectedListItemWithStatus |
过滤中跳过应用级快捷键;delegate UpdateFunc 处理 item 动作;状态提示用 NewStatusMessage |
| 交互式表格 | grid table.Model |
selection.go: newSelectableTable, updateSelectableTable, selectedTableRow |
enter: 读行; esc: focus/blur; 其他消息转发 |
| 静态表格 | *ltable.Table |
selection.go: newStaticTable, resizeStaticTable |
只展示;lipgloss/table 建议别名 ltable;复杂行列样式用 table 的 StyleFunc |
| 分页 | pager paginator.Model |
selection.go: newPaginator, updatePaginator, pageItems |
翻页消息转发;View: 渲染当前页和 pager.View();数据量变化后同步总页数 |
| 文件选择 | picker filepicker.Model, selectedPath string |
selection.go: newFilePicker, updateFilePicker |
Init: picker.Init(); 选择成功写路径;disabled file 显示错误并按需延迟清理 |
| 滚动内容 | viewport viewport.Model, ready bool |
display.go: newViewport, ensureViewport, resizeViewport, resizeViewportWithChrome, updateViewport, viewportFooter |
首次 WindowSizeMsg 后初始化;扣除 header/footer;键盘/鼠标滚动转发 |
| Chat / log 追加到底部 | messages []string, viewport viewport.Model, content string |
display.go: setViewportContentBottom, appendViewportMessage |
新消息后 SetContent + GotoBottom; resize 后重排内容再保持底部 |
| 快捷键帮助 | keys appKeys, help help.Model |
display.go: newAppKeys, ShortHelp, FullHelp, updateHelpView |
应用级按键用 key.Matches; 底部 help.View(keys); ? 消费后不要再触发业务动作 |
| 加载动画 | spin spinner.Model |
runtime.go: newSpinner, updateSpinner; shared.go: batch |
Init: spin.Tick; 每次动画 cmd 要返回,否则动画停止 |
| 进度条 | bar progress.Model |
runtime.go: newProgress, resizeProgress, setProgress, updateProgressFrame, completeProgressAfterPause |
业务消息更新 percent;FrameMsg 单独转发;WindowSizeMsg: 同步宽度;完成后可 pause 再 quit |
| 倒计时 / 秒表 | timer/stopwatch model, start/stop/reset keys | runtime.go: updateTimer, updateStopwatch |
启动命令放 Init; 暂停/继续/重置在应用层;按运行状态启停 key binding |
| 异步任务 / 防抖 / channel | 业务状态 + 消息类型 | runtime.go: runWork, every, debounce, isCurrentDebounce, waitFor; shared.go: sequence |
阻塞 I/O 包成 tea.Cmd; channel 事件回流主循环;连续监听要收到一次后再次返回 waitFor |
| 外部 goroutine 发消息 | p *tea.Program, 业务 resultMsg/progressMsg/errorMsg |
external.go: sendEvery, sendResult |
创建 program 后启动 worker;worker 调 p.Send(msg);Update 统一处理消息;退出时用 stop channel 收尾 |
| 外部进程 / 编辑器 | err error, 可选 altscreenActive bool |
external.go: execProcess, execEditor, externalResultMsg |
Update 中按键返回 tea.ExecProcess; 完成消息处理 err;交互式外部程序不要用普通 runWork |
| 挂起 / 恢复 | suspending bool |
terminal.go: requestSuspend, didResume |
ctrl+z: 返回 tea.Suspend; tea.ResumeMsg: 清理 suspend 状态、刷新视图 |
| 背景色自适应 / 终端能力 | dark bool, styles, capability 状态 |
terminal.go: requestBackgroundColor, backgroundIsDark, requestWindowSize, requestCapability, capabilityFromMsg |
Init: 请求背景色或能力;Update: BackgroundColorMsg 后用 msg.IsDark() 重建 styles |
| 键盘增强 / KeyRelease | supportsEventTypes bool |
terminal.go: keyboardEnhancementsFromMsg; shared.go: newTUIView |
View: ReportKeyReleases; Update: KeyboardEnhancementsMsg, KeyReleaseMsg |
| 焦点报告 | focused bool |
terminal.go: focusFromMsg; shared.go: newTUIView |
View: ReportFocus; Update: FocusMsg / BlurMsg |
| 鼠标事件 | mouseMode tea.MouseMode, hover/selection 状态 |
shared.go: newTUIView, mousePositionFromMsg, setViewMouseHandler |
View: MouseModeCellMotion 或 MouseModeAllMotion; Update: MouseMsg; 需要 view-local hit test 时用 OnMouse 转领域消息 |
| View 终端外观 | title, fg/bg/cursor color, flags | shared.go: viewOptions, newTUIView |
View: 设置 WindowTitle, ForegroundColor, BackgroundColor, Cursor, AltScreen, mouse/focus/keyboard flags |
| 临时输出 | 无或业务状态 | terminal.go: printStatus, printAndQuit |
Update: 返回 tea.Printf/tea.Println;退出前用 tea.Sequence(tea.Printf(...), tea.Quit) |
| 多视图 / 子模型组合 | mode, active child, 多个 child cmds |
shared.go: batch, sequence; 各组件 update* helpers |
Update: 转发给 active child;多个 cmd 用 batch; View: 渲染 active view;cursor 从 active child 透传 |
Reference Links
- Bubble Tea 官方 examples:https://github.com/charmbracelet/bubbletea/tree/main/examples