Install
$ agentstack add skill-alxxpersonal-forge-charm-glamour ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
glamour - Terminal Markdown Rendering
charm.land/glamour/v2 renders markdown to styled ANSI output. Built on goldmark, supports GFM (tables, task lists, strikethrough), syntax highlighting via Chroma, emoji, and fully customizable stylesheets.
Quick Start
go get charm.land/glamour/v2@latest
One-liner
import "charm.land/glamour/v2"
out, err := glamour.Render("# Hello\n\nSome **bold** text.", "dark")
fmt.Print(out)
With renderer (reusable)
r, err := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(80),
)
if err != nil {
log.Fatal(err)
}
out, err := r.Render(markdown)
fmt.Print(out)
Core API
Package-level functions
| Function | Description | |---|---| | Render(in, stylePath string) (string, error) | One-shot render with a style name or file path | | RenderBytes(in []byte, stylePath string) ([]byte, error) | Same but bytes in/out | | RenderWithEnvironmentConfig(in string) (string, error) | Uses GLAMOUR_STYLE env var, defaults to "dark" |
TermRenderer
Created via NewTermRenderer(options ...TermRendererOption). Reusable for multiple renders.
Methods:
| Method | Description | |---|---| | Render(in string) (string, error) | Render markdown string | | RenderBytes(in []byte) ([]byte, error) | Render markdown bytes | | Write(b []byte) (int, error) | Implements io.Writer, buffer markdown input | | Close() error | Flush buffered input, call before Read | | Read(b []byte) (int, error) | Implements io.Reader, read rendered output |
io.ReadWriter pattern (streaming):
r, _ := glamour.NewTermRenderer(glamour.WithWordWrap(80))
r.Write([]byte("# Streamed\n\nContent here."))
r.Close()
rendered, _ := io.ReadAll(r)
fmt.Print(string(rendered))
Options
| Option | Description | |---|---| | WithStandardStyle(name string) | Use a built-in style by name | | WithStylePath(path string) | Style name OR path to JSON file | | WithStyles(cfg ansi.StyleConfig) | Programmatic style struct | | WithStylesFromJSONBytes(b []byte) | Parse style from JSON bytes | | WithStylesFromJSONFile(path string) | Load style from JSON file | | WithEnvironmentConfig() | Use GLAMOUR_STYLE env var | | WithWordWrap(width int) | Word wrap width (default: 80) | | WithTableWrap(wrap bool) | Wrap table content (default: true). False truncates with ellipsis | | WithInlineTableLinks(inline bool) | Render links inline in tables instead of footer list | | WithPreservedNewLines() | Keep newlines instead of reflowing | | WithEmoji() | Enable :emoji_code: rendering | | WithBaseURL(url string) | Resolve relative URLs against this base | | WithChromaFormatter(fmt string) | Set Chroma formatter for code blocks | | WithOptions(opts ...TermRendererOption) | Combine multiple options |
Built-in styles
| Constant | String | Use case | |---|---|---| | styles.DarkStyle | "dark" | Dark terminal backgrounds (default) | | styles.LightStyle | "light" | Light terminal backgrounds | | styles.DraculaStyle | "dracula" | Dracula color scheme | | styles.TokyoNightStyle | "tokyo-night" | Tokyo Night color scheme | | styles.PinkStyle | "pink" | Pink accent theme | | styles.AsciiStyle | "ascii" | ASCII-only, no unicode box chars | | styles.NoTTYStyle | "notty" | No ANSI codes at all, plain text |
Each has a corresponding StyleConfig variable: styles.DarkStyleConfig, styles.LightStyleConfig, etc.
Common Patterns
Custom style (programmatic)
Start from a built-in config and modify fields. Style fields use pointers for optional values.
import (
"charm.land/glamour/v2"
"charm.land/glamour/v2/ansi"
"charm.land/glamour/v2/styles"
)
func boolPtr(b bool) *bool { return &b }
func strPtr(s string) *string { return &s }
func uintPtr(u uint) *uint { return &u }
func customRenderer() (*glamour.TermRenderer, error) {
style := styles.DarkStyleConfig
// Custom H1: green text, no background
style.H1 = ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: strPtr("34"),
Bold: boolPtr(true),
Prefix: "# ",
},
}
// Wider margins
style.Document.Margin = uintPtr(4)
// Custom code block theme
style.CodeBlock.Theme = "monokai"
return glamour.NewTermRenderer(
glamour.WithStyles(style),
glamour.WithWordWrap(100),
)
}
Custom style (JSON file)
{
"document": {
"color": "252",
"margin": 2,
"block_prefix": "\n",
"block_suffix": "\n"
},
"heading": {
"color": "39",
"bold": true,
"block_suffix": "\n"
},
"h1": {
"color": "228",
"background_color": "63",
"bold": true,
"prefix": " ",
"suffix": " "
},
"h2": {
"prefix": "## "
},
"code_block": {
"theme": "dracula",
"margin": 2
},
"link": {
"color": "123",
"underline": true
},
"strong": {
"bold": true
},
"emph": {
"italic": true
}
}
r, err := glamour.NewTermRenderer(
glamour.WithStylesFromJSONFile("./my-style.json"),
glamour.WithWordWrap(80),
)
StyleConfig structure reference
StyleConfig
Document, BlockQuote, Paragraph -> StyleBlock
List -> StyleList (StyleBlock + LevelIndent)
Heading, H1-H6 -> StyleBlock
Text, Emph, Strong, Strikethrough -> StylePrimitive
HorizontalRule -> StylePrimitive (use Format for custom rule)
Item, Enumeration -> StylePrimitive (BlockPrefix for bullet char)
Task -> StyleTask (Ticked/Unticked strings)
Link, LinkText -> StylePrimitive
Image, ImageText -> StylePrimitive
Code -> StyleBlock (inline code)
CodeBlock -> StyleCodeBlock (Theme + Chroma)
Table -> StyleTable (separators)
DefinitionList/Term/Description -> StyleBlock/StylePrimitive
HTMLBlock, HTMLSpan -> StyleBlock
StyleBlock
Indent *uint, IndentToken *string, Margin *uint
+ StylePrimitive (all fields below)
StylePrimitive
Color, BackgroundColor *string // ANSI color number or hex "#RRGGBB"
Bold, Italic, Underline *bool
CrossedOut, Faint *bool
Inverse, Conceal, Blink *bool
Upper, Lower, Title *bool // text transform
Prefix, Suffix string // per-line prefix/suffix
BlockPrefix, BlockSuffix string // before/after entire block
Format string // Go template, e.g. link format
Color downsampling with lipgloss (v2)
Glamour v2 is "pure" - same input always gives same output. It does NOT auto-detect terminal color capabilities. Use lipgloss to downsample colors for the actual terminal.
import (
"charm.land/glamour/v2"
"charm.land/lipgloss/v2"
)
r, _ := glamour.NewTermRenderer(glamour.WithWordWrap(80))
out, _ := r.Render(markdown)
// lipgloss detects terminal capabilities and downsamples
lipgloss.Print(out)
Alternative with colorprofile for explicit control:
import "github.com/charmbracelet/colorprofile"
w := colorprofile.NewWriter(os.Stdout, os.Environ())
fmt.Fprintf(w, "%s", out)
Detect terminal background for style selection
import "charm.land/lipgloss/v2"
style := "dark"
if !lipgloss.HasDarkBackground() {
style = "light"
}
r, _ := glamour.NewTermRenderer(glamour.WithStandardStyle(style))
Environment-based style
export GLAMOUR_STYLE=dracula
# or a file path:
export GLAMOUR_STYLE=/path/to/custom.json
// Picks up GLAMOUR_STYLE, falls back to "dark"
out, err := glamour.RenderWithEnvironmentConfig(markdown)
// Or with a renderer:
r, err := glamour.NewTermRenderer(glamour.WithEnvironmentConfig())
Integration
Bubbletea viewport (scrollable markdown)
import (
"charm.land/glamour/v2"
tea "charm.land/bubbletea/v2"
"charm.land/bubbles/v2/viewport"
)
type model struct {
viewport viewport.Model
content string
}
func initialModel(markdown string) model {
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(78), // viewport width minus padding
)
rendered, _ := r.Render(markdown)
vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
vp.SetContent(rendered)
return model{viewport: vp, content: rendered}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
func (m model) View() string {
return m.viewport.View()
}
Key point: set WithWordWrap to viewport width minus any horizontal padding/margin. Re-render when terminal resizes.
Re-render on resize
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport.SetWidth(msg.Width)
m.viewport.SetHeight(msg.Height)
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(msg.Width - 2),
)
rendered, _ := r.Render(m.rawMarkdown)
m.viewport.SetContent(rendered)
}
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}
Lipgloss styled container around rendered markdown
import "charm.land/lipgloss/v2"
border := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Padding(1, 2)
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(76), // account for border + padding (2 border + 4 padding = 6)
)
rendered, _ := r.Render(markdown)
fmt.Println(border.Render(rendered))
Common Mistakes
Using WithAutoStyle() or WithColorProfile() - Removed in v2. Use WithStandardStyle("dark") and lipgloss.Print() for color handling.
Not accounting for margin/padding in word wrap - If you wrap a glamour-rendered block in a lipgloss container with padding/border, subtract that width from the word wrap value or text will overflow.
Creating a new renderer per render when unnecessary - TermRenderer is reusable. Create once, call Render() many times.
Using fmt.Print instead of lipgloss.Print - If colors look wrong on some terminals, you need color downsampling. Use lipgloss.Print(out) instead of fmt.Print(out).
Forgetting Close() when using Write/Read pattern - After writing markdown via r.Write(), you must call r.Close() before reading with r.Read() or io.ReadAll(r).
Import path still on v1 - v2 uses charm.land/glamour/v2, not github.com/charmbracelet/glamour.
Setting style fields directly instead of via pointer - Color, Bold, Italic, etc. are pointer types. Use helper functions like func boolPtr(b bool) *bool { return &b }.
Using WithStandardStyle with a file path - WithStandardStyle only accepts built-in style names. For file paths, use WithStylePath or WithStylesFromJSONFile.
Checklist
- [ ] Import
charm.land/glamour/v2(not the old github path) - [ ] Pick a style: built-in name, JSON file, or programmatic
StyleConfig - [ ] Set
WithWordWrapto match your output width minus borders/padding - [ ] Use
lipgloss.Print()for proper color downsampling on real terminals - [ ] For bubbletea: re-render on
WindowSizeMsgwith updated wrap width - [ ] Handle errors from
NewTermRendererandRender(malformed styles, etc.) - [ ] For env-based config: use
WithEnvironmentConfig()orRenderWithEnvironmentConfig() - [ ] Test with
"notty"style for CI/non-terminal environments
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: alxxpersonal
- Source: alxxpersonal/forge
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.