Source-verified skills for the Charm terminal stack: Bubble Tea v2, Bubbles v2, Lip Gloss v2, Huh v2, Log v2, Glamour v2, plus the gum and vhs CLIs.
99
98%
Does it follow best practices?
Impact
100%
1.02xAverage score across 9 eval scenarios
Passed
No findings from the security scan
Render Markdown as styled terminal text in Go with Glamour v2, choosing a style for light or dark terminals, wrapping, custom styles, and a scrolling viewer inside Bubble Tea.
Import as "charm.land/glamour/v2"; styles live in
"charm.land/glamour/v2/styles" and the config types in
"charm.land/glamour/v2/ansi". Pinned and verified against v2.0.1. Go blocks
headed // examples/... are trimmed from
complete programs
that build against the pinned modules; // excerpt blocks are API sketches.
If the project's go.mod still requires github.com/charmbracelet/glamour
(pre-v2), the v2 APIs here do not apply. Use go doc against the
project's version and keep its import paths unless the user asks to
migrate.
"dark" or "light": outside Bubble Tea call
lipgloss.HasDarkBackground(os.Stdin, os.Stdout); inside, read
tea.BackgroundColorMsg.glamour.NewTermRenderer(glamour.WithStandardStyle(style), glamour.WithWordWrap(n))
and reuse it. Its error means an unknown style name or an unreadable
style path.r.Render(md) and handle the error.lipgloss.Print outside Bubble Tea; inside, hand the string to
a viewport or the view.lipgloss.Print; unexpected wrapping means the wrap width is too small
for the content (see Mistakes).See the complete render example:
// examples/charmed-glamour/render/main.go
out, err := glamour.Render(doc, "dark") // one shot with a named style
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
lipgloss.Print(out) // not fmt.Print
style := "dark"
if !lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
style = "light"
}
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(style),
glamour.WithWordWrap(60),
glamour.WithEmoji(),
)
if err != nil { // unknown style name or unreadable style path
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
out, err = r.Render("Rendered again at 60 columns :tada:")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
lipgloss.Print(out)Printing through Lip Gloss downsamples colors to what the terminal can show. Inside Bubble Tea, output goes through the program's renderer, which downsamples for you.
Built in style names: dark, light, pink, dracula, tokyo-night,
ascii (no color), notty. The options used most: WithWordWrap(n)
(default 80; 0 disables), WithEmoji() (:tada: becomes the character),
and WithStylePath(s) (a name or a path to a JSON file).
Read references/options.md for every renderer
option, GLAMOUR_STYLE environment configuration, RenderBytes, and the
buffered Write and Close API.
A style is an ansi.StyleConfig. Start from a built in config and edit, as
in the
custom style example:
// examples/charmed-glamour/custom/main.go
cfg := styles.DraculaStyleConfig // a value copy; safe to edit
cfg.Document.Margin = new(uint(0)) // new(value) needs Go 1.26 or later
cfg.H1.Prefix = ""
cfg.H1.BackgroundColor = new("#5A2D82")
cfg.H1.Color = new("#FFFFFF")
cfg.Code.StylePrimitive.Color = new("#C4A7F5")
cfg.Link.Underline = new(true)
r, err := glamour.NewTermRenderer(
glamour.WithStyles(cfg),
glamour.WithWordWrap(70),
)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}Read references/styles.md for the full list of StyleConfig blocks and primitives, the exported configs, and the JSON style format.
Render to a string, hand it to a viewport, and re-render when the width or
the theme changes. The complete
viewer example
defines model, minWrap, and the message handlers excerpted below.
From a repository checkout, run it in a terminal:
cd examples
go run ./charmed-glamour/viewer ../README.md// examples/charmed-glamour/viewer/main.go
func (m model) Init() tea.Cmd { return tea.RequestBackgroundColor }
func (m model) render() (string, error) {
style := "light"
if m.isDark {
style = "dark"
}
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle(style),
glamour.WithWordWrap(max(m.width-2, minWrap)), // 0 would disable wrapping
)
if err != nil {
return "", err
}
return r.Render(m.md)
}
// rerender is called from both message handlers below.
func (m *model) rerender() {
out, err := m.render()
if err != nil {
out = "render error: " + err.Error()
}
m.vp.SetContent(out)
}
case tea.BackgroundColorMsg:
m.isDark = msg.IsDark()
if m.ready { // the size may have arrived first
m.rerender()
}
case tea.WindowSizeMsg:
m.width = msg.Width
// (re)size the viewport, then:
m.rerender()WindowSizeMsg and BackgroundColorMsg arrive in either order, so both
handlers re-render; otherwise a dark terminal keeps the light rendering
until the next resize. Clamp the wrap width: WithWordWrap(0) turns
wrapping off, and a negative value is meaningless. Rendering is not free on
large documents; do it on resize, theme, and content change, not on every
frame.
| v1 | v2 |
|---|---|
github.com/charmbracelet/glamour | charm.land/glamour/v2 (and /ansi, /styles) |
glamour.WithAutoStyle() | removed; pass WithStandardStyle (see Mistakes) |
glamour.WithColorProfile(p) | removed; print with lipgloss.Print |
StylePrimitive.Overlined | removed |
fmt.Print(out) | lipgloss.Print(out) when not inside Bubble Tea |
Custom ansi.MarginWriter users must call Close(). See the
v2.0.1 upgrade guide
for migration details. Its background-detection example omits the input and
output arguments required by the pinned Lip Gloss version; use the call in
the render example.
After migrating, run your application's tests, fix failures, and rerun. Then do the terminal check from step 5 of the workflow. For a viewer, also resize the terminal and confirm that scrolling and background-color updates still work.
NewTermRenderer() does not select a named style by itself. Detect light
or dark yourself and pass WithStandardStyle explicitly, even where the
upgrade guide suggests an implicit dark style.fmt and getting wrong colors on a 256 color terminal.WithWordWrap smaller
than the content and being surprised by the wrapping. Tables wrap by default;
WithTableWrap(false) truncates overflowing cells with an ellipsis.