Token导航 LogoToken导航TokenDH.com
前端设计敏感数据github未标认证来源可访问许可证需确认审计通过

charm-stack魅力堆栈

Agent Skill

charm-stack 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

802

周安装

51

GitHub Stars

公开资料未说明

下载量

420
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

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

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

命令行安装

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

skills.shnpx skills
npx skills add https://github.com/yurifrl/cly --skill charm-stack

简介

基于 Charm v2 技术栈构建终端用户界面(TUI)。

  • 支持 Bubble Tea、Bubbles、Lip Gloss 等组件库。
  • 采用 Elm 架构实现状态管理与交互逻辑。
  • 适用于 Go 语言开发的命令行工具场景。适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。
  • charm-stack 属于前端设计类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Charm Stack v2 TUI Development

Build terminal UIs using the Charm v2 stack (released Feb 2026):

  • Bubble Tea v2 — TUI framework (Elm Architecture)
  • Bubbles v2 — Reusable TUI components
  • Lip Gloss v2 — Terminal styling & layout
  • Huh v2 — Interactive forms & prompts

Installation

go get charm.land/bubbletea/v2
go get charm.land/bubbles/v2
go get charm.land/lipgloss/v2
go get charm.land/huh/v2
Import paths changed in v2 — use charm.land/ not github.com/charmbracelet/.

Your Role: TUI Architect

✅ Use Elm Architecture (Model-Update-View) ✅ Return tea.View from View() (not string) ✅ Use tea.KeyPressMsg (not tea.KeyMsg) ✅ Declare terminal features in View() (not commands) ✅ Use getter/setter methods on Bubbles components ✅ Use functional options for Bubbles constructors ✅ Follow CLY module patterns

❌ Do NOT return string from View() ❌ Do NOT use tea.EnterAltScreen commands ❌ Do NOT use tea.KeyMsg for key matching (use tea.KeyPressMsg) ❌ Do NOT use lipgloss.AdaptiveColor (removed — use LightDark) ❌ Do NOT set Bubbles fields directly (use SetWidth/SetHeight) ❌ Do NOT use old import paths (github.com/charmbracelet/...)


Core Architecture: The Elm Architecture

The Three Functions

// Model — application state
type model struct {
    cursor   int
    choices  []string
    selected map[int]struct{}
}

// Init — initial command
func (m model) Init() tea.Cmd {
    return nil
}

// Update — handle messages, return updated model + command
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "ctrl+c", "q":
            return m, tea.Quit
        }
    }
    return m, nil
}

// View — returns tea.View (NOT string)
func (m model) View() tea.View {
    s := "Hello, World!\n"
    return tea.NewView(s)
}

Message Flow

User Input → Msg → Update → Model → View → Screen
    ↑                                    ↓
    └────────── tea.Cmd ─────────────────┘
  • Messages are immutable events
  • Update returns new model (don't mutate)
  • Commands run async, generate more messages
  • View is a pure function of model state

Bubble Tea v2

Basic Program

package main

import (
    "fmt"
    "os"

    tea "charm.land/bubbletea/v2"
)

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.KeyPressMsg:
        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", "space":
            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() tea.View {
    s := "What should we buy at the market?\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 tea.NewView(s)
}

func main() {
    p := tea.NewProgram(initialModel())
    if _, err := p.Run(); err != nil {
        fmt.Printf("Error: %v", err)
        os.Exit(1)
    }
}

Declarative View (v2 Key Change)

In v2, View() returns tea.View instead of string. Terminal features are declared on the view struct — no more imperative commands:

func (m model) View() tea.View {
    v := tea.NewView(m.renderContent())

    // Declare terminal features (replaces tea.EnterAltScreen, tea.EnableMouseCellMotion, etc.)
    v.AltScreen = true
    v.MouseMode = tea.MouseModeCellMotion
    v.ReportFocus = true
    v.WindowTitle = "My App"

    // Real cursor support
    if m.showCursor {
        v.Cursor = &tea.Cursor{
            Position: tea.Position{X: m.cursorX, Y: m.cursorY},
            Shape:    tea.CursorBlock,
            Blink:    true,
            Color:    lipgloss.Green,
        }
    }

    // Keyboard enhancements (progressive, supported terminals only)
    v.KeyboardEnhancements.ReportEventTypes = true // key release events

    return v
}

View struct fields:

type View struct {
    Content                   string
    OnMouse                   func(msg MouseMsg) Cmd
    Cursor                    *Cursor
    BackgroundColor           color.Color
    ForegroundColor           color.Color
    WindowTitle               string
    ProgressBar               *ProgressBar
    AltScreen                 bool
    ReportFocus               bool
    DisableBracketedPasteMode bool
    MouseMode                 MouseMode
    KeyboardEnhancements      KeyboardEnhancements
}

Key Messages (v2 Breaking Change)

Keys are now split into tea.KeyPressMsg and tea.KeyReleaseMsg. Use msg.String() for matching:

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 "space":
            // Space bar returns "space" (not " ")
        case "shift+enter":
            // New! Not possible in v1
        case "ctrl+alt+super+enter":
            // Full modifier support
        }
    case tea.KeyReleaseMsg:
        // Key release events (requires keyboard enhancements)
    }
    return m, nil
}

Key struct fields:

  • key.Code — replaces key.Type
  • key.Text — replaces key.Runes
  • key.Mod — replaces separate Alt bool
  • key.BaseCode — key on US PC-101 layout (handy for international keyboards)
  • key.IsRepeat — auto-repeating held key
  • key.Keystroke() — always includes modifier info (e.g. "ctrl+shift+alt+a")

Mouse Messages (v2 Breaking Change)

Split into specific types:

case tea.MouseClickMsg:
    if msg.Button == tea.MouseLeft {
        // Left click
    }
case tea.MouseReleaseMsg:
    // Mouse button released
case tea.MouseWheelMsg:
    // Scroll
case tea.MouseMotionMsg:
    // Mouse moved

Mouse mode is set declaratively in View():

func (m model) View() tea.View {
    v := tea.NewView("Move that mouse!")
    v.MouseMode = tea.MouseModeAllMotion
    return v
}

Paste Messages

case tea.PasteMsg:
    m.text += msg.Content
case tea.PasteStartMsg:
    // Paste started
case tea.PasteEndMsg:
    // Paste ended

Commands

// One-off message (function returning tea.Msg)
func checkServer() tea.Msg {
    res, err := http.Get("https://example.com")
    if err != nil {
        return errMsg{err}
    }
    return statusMsg(res.StatusCode)
}

// Usage in Update:
return m, checkServer // pass the function directly

// Command returning a Cmd (for wrapping)
func fetchData() tea.Cmd {
    return func() tea.Msg {
        // async work...
        return dataMsg{data}
    }
}

// Ticking (timers)
type tickMsg time.Time
func tick() tea.Cmd {
    return tea.Tick(time.Second, func(t time.Time) tea.Msg {
        return tickMsg(t)
    })
}

// Batch commands
return m, tea.Batch(cmdA, cmdB, cmdC)

// Print above the TUI
return m, tea.Println("Status: done")

// Clipboard (v2 new!)
return m, tea.SetClipboard("copied text")
return m, tea.ReadClipboard()
// Listen for: case tea.ClipboardMsg:

Background Color Detection

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

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.BackgroundColorMsg:
        m.isDark = msg.IsDark()
        m.styles = newStyles(msg.IsDark())
    }
    return m, nil
}

Environment Variables (v2 new!)

Bubble Tea sends tea.EnvMsg at startup — essential for SSH apps:

case tea.EnvMsg:
    m.term = msg.Getenv("TERM")

Color Profile Detection

case tea.ColorProfileMsg:
    m.colorProfile = msg.Profile

Keyboard Enhancements Detection

case tea.KeyboardEnhancementsMsg:
    if msg.SupportsKeyDisambiguation() {
        // Terminal supports enhanced key input
    }

Program Options

p := tea.NewProgram(model,
    tea.WithWindowSize(80, 24),           // For testing
    tea.WithColorProfile(profile),        // Manual color profile
    tea.WithEnvironment(envs),            // SSH: pass client envs
    tea.WithInput(ttyIn),                 // Custom input
    tea.WithOutput(ttyOut),               // Custom output
)

// Open TTY manually (useful when stdin/stdout are piped)
ttyIn, ttyOut, err := tea.OpenTTY()

Debugging

if len(os.Getenv("DEBUG")) > 0 {
    f, err := tea.LogToFile("debug.log", "debug")
    if err != nil {
        fmt.Println("fatal:", err)
        os.Exit(1)
    }
    defer f.Close()
}

Bubbles v2

All Bubbles updated for v2 with consistent patterns:

Import Paths

import "charm.land/bubbles/v2/spinner"
import "charm.land/bubbles/v2/textinput"
import "charm.land/bubbles/v2/textarea"
import "charm.land/bubbles/v2/list"
import "charm.land/bubbles/v2/table"
import "charm.land/bubbles/v2/viewport"
import "charm.land/bubbles/v2/progress"
import "charm.land/bubbles/v2/help"
import "charm.land/bubbles/v2/paginator"
import "charm.land/bubbles/v2/filepicker"
import "charm.land/bubbles/v2/timer"
import "charm.land/bubbles/v2/stopwatch"
import "charm.land/bubbles/v2/cursor"
import "charm.land/bubbles/v2/key"

v2 Universal Changes

  1. Functional options for constructors: // v1: viewport.New(80, 24) vp:= viewport.New(viewport.WithWidth(80), viewport.WithHeight(24)) // v1: stopwatch.NewWithInterval(500ms) sw:= stopwatch.New(stopwatch.WithInterval(500 * time.Millisecond)) // v1: timer.NewWithInterval(30s, 100ms) t:= timer.New(30*time.Second, timer.WithInterval(100*time.Millisecond))
  2. Getters/setters instead of exported fields: // v1: vp.Width = 40 / fmt.Println(vp.Width) vp.SetWidth(40) fmt.Println(vp.Width())
  3. DefaultKeyMap is a function: // v1: textinput.DefaultKeyMap (variable) km:= textinput.DefaultKeyMap() // function km:= textarea.DefaultKeyMap() km:= paginator.DefaultKeyMap()
  4. Light/dark styles: h:= help.New() h.Styles = help.DefaultStyles(isDark) // pass bool h.Styles = help.DefaultDarkStyles() // or pick directly h.Styles = help.DefaultLightStyles() // Same pattern for: list, textarea, textinput

Spinner

import "charm.land/bubbles/v2/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() // v2: use Model.Tick() not 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() tea.View {
    return tea.NewView(m.spinner.View() + " Loading...")
}

Text Input

import "charm.land/bubbles/v2/textinput"

type model struct {
    input textinput.Model
}

func initialModel() model {
    ti := textinput.New()
    ti.Placeholder = "Type something..."
    ti.Focus()
    ti.SetWidth(40) // v2: setter method
    return model{input: ti}
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "enter":
            // process m.input.Value()
        case "ctrl+c":
            return m, tea.Quit
        }
    }
    var cmd tea.Cmd
    m.input, cmd = m.input.Update(msg)
    return m, cmd
}

func (m model) View() tea.View {
    return tea.NewView(m.input.View())
}

v2 style changes for textinput:

// v1: ti.PromptStyle, ti.TextStyle, ti.PlaceholderStyle, ti.CompletionStyle
// v2: grouped under Styles struct
styles := textinput.DefaultStyles(isDark)
ti.SetStyles(styles)
// Access: styles.Focused.Prompt, styles.Focused.Text, etc.

Text Area

import "charm.land/bubbles/v2/textarea"

ta := textarea.New()
ta.Placeholder = "Write something..."
ta.Focus()

// v2 style changes:
// v1: ta.FocusedStyle / ta.BlurredStyle
// v2: ta.Styles.Focused / ta.Styles.Blurred
styles := textarea.DefaultStyles(isDark)

// v2 new methods:
ta.Column()           // current column
ta.ScrollYOffset()    // scroll position
ta.MoveToBeginning()  // jump to start
ta.MoveToEnd()        // jump to end

// v2: SetCursor renamed
// v1: ta.SetCursor(col)
ta.SetCursorColumn(col) // v2

List

import "charm.land/bubbles/v2/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 }

func initialModel() model {
    items := []list.Item{
        item{title: "Raspberry Pi", desc: "A tiny computer"},
        item{title: "Arduino", desc: "A microcontroller"},
    }
    // v2: DefaultStyles takes isDark bool
    l := list.New(items, list.NewDefaultDelegate(), 20, 14)
    l.Title = "My Items"
    return model{list: l}
}

Table

import "charm.land/bubbles/v2/table"

columns := []table.Column{
    {Title: "Name", Width: 20},
    {Title: "Age", Width: 5},
}
rows := []table.Row{
    {"Alice", "25"},
    {"Bob", "30"},
}
t := table.New(
    table.WithColumns(columns),
    table.WithRows(rows),
    table.WithFocused(true),
)
// v2: use getter/setter for width/height
t.SetWidth(40)
t.SetHeight(10)

Viewport

import "charm.land/bubbles/v2/viewport"

// v2: functional options
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
vp.SetContent("long content here...")

// v2 new: horizontal scrolling (arrow keys)
// v2 new: soft wrapping
vp.SoftWrap = true

// v2 new: line number gutter
vp.LeftGutterFunc = func(info viewport.GutterContext) string {
    if info.Soft {
        return "     | "
    }
    if info.Index >= info.TotalLines {
        return "   ~ | "
    }
    return fmt.Sprintf("%4d | ", info.Index+1)
}

// v2 new: regex highlighting
vp.SetHighlights(regexp.MustCompile("error").FindAllStringIndex(vp.GetContent(), -1))
vp.HighlightNext()
vp.HighlightPrevious()
vp.ClearHighlights()

// v2 new: per-line styling
vp.StyleLineFunc = func(line int, content string) string {
    // return styled content
}

// v2: HighPerformanceRendering removed (no longer needed)

Progress

import "charm.land/bubbles/v2/progress"

// v2: complete color API overhaul
p := progress.New(
    progress.WithColors(lipgloss.Color("#FF0000"), lipgloss.Color("#0000FF")), // gradient
    progress.WithScaled(true),           // scale blend to filled portion
    progress.WithDefaultBlend(),         // v2 replaces WithDefaultGradient()
)
p.SetWidth(40) // v2: setter

// Single color:
p := progress.New(progress.WithColors(lipgloss.Color("#FF7CCB")))

// Dynamic coloring:
p := progress.New(progress.WithColorFunc(func(total, current float64) color.Color {
    // return computed color
}))

Help

import "charm.land/bubbles/v2/help"
import "charm.land/bubbles/v2/key"

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

func (k keyMap) ShortHelp() []key.Binding {
    return []key.Binding{k.Up, k.Down, k.Quit}
}
func (k keyMap) FullHelp() [][]key.Binding {
    return [][]key.Binding{{k.Up, k.Down}, {k.Quit}}
}

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")),
}

h := help.New()
h.SetWidth(80)                       // v2: setter
h.Styles = help.DefaultStyles(isDark) // v2: light/dark aware

Key Bindings

import "charm.land/bubbles/v2/key"

// Define bindings
binding := key.NewBinding(
    key.WithKeys("k", "up"),
    key.WithHelp("↑/k", "move up"),
)

// Match in Update
case tea.KeyPressMsg:
    switch {
    case key.Matches(msg, keys.Up):
        // handle up
    case key.Matches(msg, keys.Down):
        // handle down
    }

Lip Gloss v2

Import

import "charm.land/lipgloss/v2"

Styles

var style = lipgloss.NewStyle().
    Bold(true).
    Italic(true).
    Foreground(lipgloss.Color("#FAFAFA")).
    Background(lipgloss.Color("#7D56F4")).
    PaddingTop(2).
    PaddingLeft(4).
    Width(22)

// IMPORTANT: Use lipgloss.Println for auto color downsampling (standalone)
// Inside Bubble Tea, downsampling is automatic
lipgloss.Println(style.Render("Hello, kitty"))

Colors (v2 Change: colors are now image/color.Color)

// True color
lipgloss.Color("#FF0000")

// ANSI 256
lipgloss.Color("86")

// ANSI 16
lipgloss.Color("5")

// Named constants
lipgloss.Red
lipgloss.Green
lipgloss.BrightCyan

// RGB
lipgloss.RGBColor{R: 255, G: 0, B: 0}

Adaptive Colors (v2 Breaking Change)

lipgloss.AdaptiveColor is removed. Use LightDark:

// With Bubble Tea (recommended):
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.BackgroundColorMsg:
        m.styles = newStyles(msg.IsDark())
    }
    return m, nil
}

func newStyles(isDark bool) styles {
    lightDark := lipgloss.LightDark(isDark)
    return styles{
        title: lipgloss.NewStyle().Foreground(lightDark(
            lipgloss.Color("#333333"),  // light bg
            lipgloss.Color("#FAFAFA"),  // dark bg
        )),
    }
}

// Standalone:
hasDarkBG, _ := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
lightDark := lipgloss.LightDark(hasDarkBG)
myColor := lightDark(lipgloss.Color("#333"), lipgloss.Color("#FFF"))

// Quick migration with compat package:
import "charm.land/lipgloss/v2/compat"
color := compat.AdaptiveColor{
    Light: lipgloss.Color("#f1f1f1"),
    Dark:  lipgloss.Color("#cccccc"),
}

Color Utilities

dark := lipgloss.Darken(c, 0.5)
light := lipgloss.Lighten(c, 0.35)
comp := lipgloss.Complementary(c)
alpha := lipgloss.Alpha(c, 0.2)

// 1D gradient
colors := lipgloss.Blend1D(10, color1, color2)

// 2D gradient with rotation
colors := lipgloss.Blend2D(80, 24, 45.0, color1, color2, color3)

Borders

var style = lipgloss.NewStyle().
    BorderStyle(lipgloss.RoundedBorder()).
    BorderForeground(lipgloss.Color("63")).
    BorderTop(true).
    BorderBottom(true).
    BorderLeft(true).
    BorderRight(true)

// Gradient borders (v2 new!)
s := lipgloss.NewStyle().
    Border(lipgloss.RoundedBorder()).
    BorderForegroundBlend(lipgloss.Color("#FF0000"), lipgloss.Color("#0000FF"))

// Border types: NormalBorder, RoundedBorder, ThickBorder, DoubleBorder,
//               HiddenBorder, MarkdownBorder, ASCIIBorder

Underline Styles (v2 new!)

s := lipgloss.NewStyle().
    UnderlineStyle(lipgloss.UnderlineCurly).
    UnderlineColor(lipgloss.Color("#FF0000"))
// Styles: UnderlineNone, UnderlineSingle, UnderlineDouble,
//         UnderlineCurly, UnderlineDotted, UnderlineDashed

Hyperlinks (v2 new!)

s := lipgloss.NewStyle().
    Foreground(lipgloss.Color("#7B2FBE")).
    Hyperlink("https://charm.land")
lipgloss.Println(s.Render("Visit Charm"))

Layout

// Join horizontally/vertically
lipgloss.JoinHorizontal(lipgloss.Bottom, blockA, blockB)
lipgloss.JoinVertical(lipgloss.Center, blockA, blockB)

// Place in whitespace
lipgloss.Place(80, 24, lipgloss.Center, lipgloss.Center, content)
lipgloss.PlaceHorizontal(80, lipgloss.Center, content)
lipgloss.PlaceVertical(24, lipgloss.Bottom, content)

// Measure
w, h := lipgloss.Size(block)
width := lipgloss.Width(block)
height := lipgloss.Height(block)

// Wrap (preserves ANSI styles and hyperlinks)
wrapped := lipgloss.Wrap(styledText, 40, " ")

Compositing (v2 new!)

// Layer-based compositing
a := lipgloss.NewLayer(pickles).X(4).Y(2).Z(1)
b := lipgloss.NewLayer(bitterMelon).X(22).Y(1)
c := lipgloss.NewLayer(sriracha).X(11).Y(7)
output := compositor.Compose(a, b, c).Render()

Tables (lipgloss sub-package)

import "charm.land/lipgloss/v2/table"

t := table.New().
    Border(lipgloss.RoundedBorder()).
    BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))).
    StyleFunc(func(row, col int) lipgloss.Style {
        switch {
        case row == table.HeaderRow:
            return headerStyle
        case row%2 == 0:
            return evenRowStyle
        default:
            return oddRowStyle
        }
    }).
    Headers("LANGUAGE", "FORMAL", "INFORMAL").
    Rows(rows...)

lipgloss.Println(t)

Lists and Trees (lipgloss sub-packages)

import "charm.land/lipgloss/v2/list"
import "charm.land/lipgloss/v2/tree"

// List
l := list.New("A", "B", "C").
    Enumerator(list.Roman).
    EnumeratorStyle(enumStyle).
    ItemStyle(itemStyle)

// Tree
t := tree.Root(".").
    Child("macOS", "Linux", "BSD").
    Enumerator(tree.RoundedEnumerator)

lipgloss.Println(l)
lipgloss.Println(t)

Printing (v2 Change: use lipgloss writers for standalone)

// Inside Bubble Tea: downsampling is automatic, just use View()
// Standalone: use lipgloss print functions (drop-in fmt replacements)
lipgloss.Print(s)
lipgloss.Println(s)
lipgloss.Printf("hello %s", styled)
lipgloss.Fprint(os.Stderr, s)
lipgloss.Sprint(s)  // render to variable

Huh v2 (Forms)

Import

import "charm.land/huh/v2"

Standalone Form

var name string
var confirmed bool

form := huh.NewForm(
    huh.NewGroup(
        huh.NewInput().
            Title("What's your name?").
            Value(&name).
            Validate(func(s string) error {
                if s == "" {
                    return errors.New("name required")
                }
                return nil
            }),

        huh.NewConfirm().
            Title("Continue?").
            Value(&confirmed),
    ),
)

err := form.Run()

Field Types

// Input (single line)
huh.NewInput().Title("Name").Placeholder("Enter name...").Value(&name)

// Text (multi line)
huh.NewText().Title("Description").CharLimit(400).Value(&desc)

// Select
huh.NewSelect[string]().
    Title("Pick one").
    Options(
        huh.NewOption("Option A", "a"),
        huh.NewOption("Option B", "b"),
    ).
    Value(&choice)

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

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

Single Field Shorthand

var name string
huh.NewInput().Title("Name?").Value(&name).Run()

Dynamic Forms

huh.NewSelect[string]().
    Value(&state).
    TitleFunc(func() string {
        if country == "US" { return "State" }
        return "Province"
    }, &country).
    OptionsFunc(func() []huh.Option[string] {
        return huh.NewOptions(fetchStates(country)...)
    }, &country)

Huh in Bubble Tea

type model struct {
    form *huh.Form
}

func newModel() model {
    return model{
        form: huh.NewForm(
            huh.NewGroup(
                huh.NewSelect[string]().
                    Key("class").
                    Options(huh.NewOptions("Warrior", "Mage", "Rogue")...).
                    Title("Choose your class"),
            ),
        ),
    }
}

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
    }
    return m, cmd
}

func (m model) View() tea.View {
    if m.form.State == huh.StateCompleted {
        class := m.form.GetString("class")
        return tea.NewView(fmt.Sprintf("You chose: %s", class))
    }
    return tea.NewView(m.form.View())
}

Themes & Accessibility

form.WithTheme(huh.ThemeDracula())  // Charm, Dracula, Catppuccin, Base16, Default
form.WithAccessible(os.Getenv("ACCESSIBLE") != "")

Spinner

import "charm.land/huh/v2/spinner"

err := spinner.New().
    Title("Processing...").
    Action(doWork).
    Run()

CLY Module Integration

Simple TUI Module

package mymodule

import (
    "fmt"
    "github.com/spf13/cobra"
    tea "charm.land/bubbletea/v2"
    "charm.land/lipgloss/v2"
)

func Register(parent *cobra.Command) {
    cmd := &cobra.Command{
        Use:   "mymodule",
        Short: "My TUI module",
        RunE:  run,
    }
    parent.AddCommand(cmd)
}

func run(cmd *cobra.Command, args []string) error {
    p := tea.NewProgram(initialModel())
    _, err := p.Run()
    return err
}

type model struct {
    // state
}

func initialModel() model {
    return model{}
}

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
        }
    }
    return m, nil
}

func (m model) View() tea.View {
    return tea.NewView("Hello from my module!")
}

Full-screen TUI Module

func (m model) View() tea.View {
    v := tea.NewView(m.renderContent())
    v.AltScreen = true
    return v
}

Using Shared Styles

import "github.com/yurifrl/cly/pkg/style"

fmt.Println(style.TitleStyle.Render("Title"))
fmt.Println(style.GreenStyle.Render("Success"))

v1 to v2 Migration Quick Reference

v1v2
import tea "github.com/charmbracelet/bubbletea"import tea "charm.land/bubbletea/v2"
import "github.com/charmbracelet/bubbles/..."import "charm.land/bubbles/v2/..."
import "github.com/charmbracelet/lipgloss"import "charm.land/lipgloss/v2"
import "github.com/charmbracelet/huh"import "charm.land/huh/v2"
View() stringView() tea.View
return "content"return tea.NewView("content")
tea.KeyMsgtea.KeyPressMsg / tea.KeyReleaseMsg
key.Type, key.Runeskey.Code, key.Text
msg.Alt (bool)key.Mod
tea.MouseMsgtea.MouseClickMsg / tea.MouseWheelMsg / etc.
tea.EnterAltScreen (Cmd)v.AltScreen = true (View)
tea.EnableMouseCellMotion (Cmd)v.MouseMode = tea.MouseModeCellMotion (View)
lipgloss.AdaptiveColor{Light, Dark}lipgloss.LightDark(isDark)(light, dark)
fmt.Println(styled)lipgloss.Println(styled)
viewport.New(w, h)viewport.New(viewport.WithWidth(w), viewport.WithHeight(h))
vp.Width = 40vp.SetWidth(40)
vp.Width (field)vp.Width() (method)
textinput.DefaultKeyMap (var)textinput.DefaultKeyMap() (func)
spinner.Tick() (pkg func)m.spinner.Tick() (method)
viewport.HighPerformanceRenderingRemoved (not needed)
ti.PromptStylestyles.Focused.Prompt
ta.FocusedStyleta.Styles.Focused
progress.WithGradient(a, b)progress.WithColors(a, b)
progress.WithDefaultGradient()progress.WithDefaultBlend()
space key returns " "space key returns "space"

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.59%
按下载量换算149

Claude

30.5%
按下载量换算128

Cursor

20.51%
按下载量换算86

Gemini CLI

9.38%
按下载量换算39

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills