mit einem Klick
go-bubbletea-patterns
Bubbletea Patterns Skill
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Menü
Bubbletea Patterns Skill
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Basierend auf der SOC-Berufsklassifikation
Diagnose Azedarach command latency and daemon/TUI performance with OpenTelemetry traces exported to a local Jaeger backend. Use when investigating slow `az` commands, TUI stalls, daemon RPC latency, missing spans, local Jaeger setup, or trace-driven performance regressions in this repository.
Review and integrate Azedarach issues that are in_review. Use when Codex is asked to run a reviewer/integrator session, sweep active issues that are ready for review, inspect worker evidence and diffs, return actionable findings to live or paused issue sessions, close accepted work, or get in_review issues integrated.
Review, consolidate, test, and harden Azedarach SQLite migrations before integration. Use for any migration, schema ensure/repair logic, trigger or index change, persistence-authority change, migration failure, or pre-merge review of a branch containing database changes. Also use when validating upgrades against real root-user or registered project databases.
Run an iterative code review and fix cycle until findings stabilize. Use when the user asks Codex to review code, review and fix, loop review/fix, address review findings, harden a change before handoff, or keep reviewing until repeated passes find no new actionable issues. Also use proactively before Codex declares coding work done, marks an issue in_review, says a change is ready, or hands off implementation that modified code, tests, build scripts, migrations, config, or developer tooling.
Go Testing Skill
Build and review production-grade Go logging and observability with `log/slog` and OpenTelemetry. Use when adding or refactoring logs, instrumenting request paths, defining event schemas, reducing log cost/cardinality, enforcing redaction/PII policy, correlating logs with traces, or reviewing Go services for observability gaps.
| name | go-bubbletea-patterns |
| description | Bubbletea Patterns Skill |
Version: 1.0 Purpose: Idiomatic Bubbletea patterns learned from Glow, Soft Serve, and community best practices
Bubbletea implements the Elm Architecture:
Key: The program sends messages (tea.Msg), Update handles them, returns (newModel, optionalCmd)
For non-trivial apps, use nested models with a top-level router:
type Model struct {
// Shared state accessible to all sub-models
common *CommonModel
// Sub-models (each implements tea.Model)
board *board.Model
detail *detail.Model
settings *settings.Model
overlays *overlay.Stack
// Current state for routing
state State
}
type CommonModel struct {
config *config.Config
width int
height int
styles *styles.Styles
program *tea.Program // For sending messages from goroutines
}
Key insight from Glow: Share common state via pointer to avoid duplication across sub-models.
func (m Model) Init() tea.Cmd {
return tea.Batch(
m.board.Init(),
m.detail.Init(),
m.settings.Init(),
loadInitialData, // Your custom init command
)
}
Pass ALL messages to relevant sub-models, not just "active" one:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
// Global handlers first (window size, quit, etc.)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.common.width = msg.Width
m.common.height = msg.Height
// Propagate to all sub-models
m.board.SetSize(msg.Width, msg.Height)
m.detail.SetSize(msg.Width, msg.Height)
return m, nil
case tea.KeyMsg:
// Handle quit regardless of state
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
}
// Route to active overlay first (if any)
if m.overlays.Current() != nil {
overlay, cmd := m.overlays.Current().Update(msg)
m.overlays.SetCurrent(overlay)
if cmd != nil {
cmds = append(cmds, cmd)
}
// Overlays may consume the message
if m.overlays.Current() != nil {
return m, tea.Batch(cmds...)
}
}
// Route to current view
switch m.state {
case StateBoard:
newBoard, cmd := m.board.Update(msg)
m.board = newBoard.(*board.Model)
cmds = append(cmds, cmd)
case StateDetail:
newDetail, cmd := m.detail.Update(msg)
m.detail = newDetail.(*detail.Model)
cmds = append(cmds, cmd)
}
return m, tea.Batch(cmds...)
}
// Command returning a message
func fetchTasks() tea.Cmd {
return func() tea.Msg {
return TasksLoadedMsg{tasks: getTasks()}
}
}
// Command with timeout
func fetchWithTimeout() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return TimeoutMsg{}
})
}
// Command from goroutine
func asyncWork() tea.Cmd {
ch := make(chan tea.Msg)
go func() {
defer close(ch)
ch <- WorkDoneMsg{result: doWork()}
}()
return func() tea.Msg {
return <-ch
}
}
// Batch commands
func (m Model) Init() tea.Cmd {
return tea.Batch(
m.board.Init(),
m.detail.Init(),
tea.Tick(time.Second, tickerTick), // Periodic
)
}
// In goroutine, send via program
func (s *Service) pollState(program *tea.Program) {
for {
time.Sleep(500 * time.Millisecond)
state := s.checkState()
program.Send(StateChangedMsg{state: state})
}
}
// In model Update, handle the message
case StateChangedMsg msg:
// Handle state change
func (m Model) View() string {
switch m.state {
case StateBoard:
return m.board.View()
case StateDetail:
return m.detail.View()
case StateSettings:
return m.settings.View()
default:
return ""
}
}
var (
// Define styles once (avoid reallocation in View())
baseStyle = lipgloss.NewStyle()
titleStyle = baseStyle.Bold(true).Foreground(lipgloss.Color("205"))
activeStyle = baseStyle.Background(lipgloss.Color("240"))
)
func (m Model) View() string {
return lipgloss.NewStyle().
Width(m.common.width).
Height(m.common.height).
Render(
lipgloss.Place(
m.common.width, m.common.height,
lipgloss.Center, lipgloss.Center,
m.content.View(),
),
)
}
var (
boxStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("63")).
Padding(1, 2)
)
func (m Model) View() string {
return boxStyle.Render(m.content)
}
type State int
const (
StateBoard State = iota
StateDetail
StateSettings
)
type State struct {
view ViewType
selectedItem int
isLoading bool
}
// Define custom message types (avoid collisions)
type TaskSelectedMsg struct {
id string
}
type TasksLoadedMsg struct {
tasks []Task
}
type StateChangedMsg struct {
state SessionState
}
| Message | Description |
|---|---|
tea.InitMsg | Program started (return initial cmd) |
tea.QuitMsg | Request program exit |
tea.WindowSizeMsg | Terminal size changed |
tea.KeyMsg | Key press event |
tea.MouseMsg | Mouse event (enable with tea.WithMouseCellMotion()) |
tea.TickMsg | Timer event (from tea.Tick) |
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":
m.cursor--
case "down", "j":
m.cursor++
case "enter":
return m, m.selectCurrent()
}
}
return m, nil
}
case tea.KeyMsg:
switch msg.Type {
case tea.KeyRunes:
// Any character key (letters, numbers, etc.)
case tea.KeyEnter, tea.KeySpace:
// Enter or Space
case tea.KeyUp, tea.KeyDown, tea.KeyLeft, tea.KeyRight:
// Arrow keys
case tea.KeyCtrlC, tea.KeyCtrlD:
// Ctrl combinations
}
case tea.KeyMsg:
if msg.Alt {
// Alt+key
}
import (
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
)
type Model struct {
list list.Model
input textinput.Model
content viewport.Model
}
func (m Model) Init() tea.Cmd {
m.list = list.New(..., list.NewDefaultDelegate())
m.input = textinput.New()
m.content = viewport.New(0, 0)
return nil
}
type Model struct {
focused focusedComponent
}
type focusedComponent int
const (
focusList focusedComponent = iota
focusInput
)
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "tab" {
// Cycle focus
m.focused = (m.focused + 1) % 2
}
}
// Update based on focus
switch m.focused {
case focusList:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
case focusInput:
var cmd tea.Cmd
m.input, cmd = m.input.Update(msg)
return m, cmd
}
}
// ❌ BAD: Reallocates styles every render
func (m Model) View() string {
return lipgloss.NewStyle().
Bold(true).
Render("Title")
}
// ✅ GOOD: Styles defined once
var titleStyle = lipgloss.NewStyle().Bold(true)
func (m Model) View() string {
return titleStyle.Render("Title")
}
// ❌ BAD: Renders entire content every frame
func (m Model) View() string {
return m.content
}
// ✅ GOOD: Only render what changed
func (m Model) View() string {
if m.needsRender {
m.needsRender = false
return m.content
}
return "" // Skip rendering
}
type ConfirmModel struct {
question string
confirmed bool
}
func (m ConfirmModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.String() == "y" {
m.confirmed = true
return m, tea.Quit
}
if msg.String() == "n" || msg.String() == "esc" {
return m, tea.Quit
}
}
return m, nil
}
func (m ConfirmModel) View() string {
return fmt.Sprintf("%s [y/n]", m.question)
}
type Model struct {
loading bool
content string
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case LoadStartMsg:
m.loading = true
case LoadDoneMsg:
m.loading = false
m.content = msg.data
}
return m, nil
}
func (m Model) View() string {
if m.loading {
return "Loading..."
}
return m.content
}
type Model 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
}
func (m Model) View() string {
if m.err != nil {
return lipgloss.NewStyle().
Foreground(lipgloss.Color("196")).
Render(fmt.Sprintf("Error: %v", m.err))
}
return m.content
}