SKILL.md

  1---
  2name: charm-glamour
  3description: "Render markdown to styled ANSI terminal output in Go with glamour v2. Use when rendering markdown programmatically in Go, glamour, terminal markdown rendering, or styled markdown output. NOT for viewing markdown files in terminal (use glow)."
  4---
  5
  6# glamour - Terminal Markdown Rendering
  7
  8`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.
  9
 10## Quick Start
 11
 12```bash
 13go get charm.land/glamour/v2@latest
 14```
 15
 16### One-liner
 17
 18```go
 19import "charm.land/glamour/v2"
 20
 21out, err := glamour.Render("# Hello\n\nSome **bold** text.", "dark")
 22fmt.Print(out)
 23```
 24
 25### With renderer (reusable)
 26
 27```go
 28r, err := glamour.NewTermRenderer(
 29    glamour.WithStandardStyle("dark"),
 30    glamour.WithWordWrap(80),
 31)
 32if err != nil {
 33    log.Fatal(err)
 34}
 35
 36out, err := r.Render(markdown)
 37fmt.Print(out)
 38```
 39
 40## Core API
 41
 42### Package-level functions
 43
 44| Function | Description |
 45|---|---|
 46| `Render(in, stylePath string) (string, error)` | One-shot render with a style name or file path |
 47| `RenderBytes(in []byte, stylePath string) ([]byte, error)` | Same but bytes in/out |
 48| `RenderWithEnvironmentConfig(in string) (string, error)` | Uses `GLAMOUR_STYLE` env var, defaults to `"dark"` |
 49
 50### TermRenderer
 51
 52Created via `NewTermRenderer(options ...TermRendererOption)`. Reusable for multiple renders.
 53
 54**Methods:**
 55
 56| Method | Description |
 57|---|---|
 58| `Render(in string) (string, error)` | Render markdown string |
 59| `RenderBytes(in []byte) ([]byte, error)` | Render markdown bytes |
 60| `Write(b []byte) (int, error)` | Implements `io.Writer`, buffer markdown input |
 61| `Close() error` | Flush buffered input, call before `Read` |
 62| `Read(b []byte) (int, error)` | Implements `io.Reader`, read rendered output |
 63
 64**io.ReadWriter pattern** (streaming):
 65
 66```go
 67r, _ := glamour.NewTermRenderer(glamour.WithWordWrap(80))
 68r.Write([]byte("# Streamed\n\nContent here."))
 69r.Close()
 70
 71rendered, _ := io.ReadAll(r)
 72fmt.Print(string(rendered))
 73```
 74
 75### Options
 76
 77| Option | Description |
 78|---|---|
 79| `WithStandardStyle(name string)` | Use a built-in style by name |
 80| `WithStylePath(path string)` | Style name OR path to JSON file |
 81| `WithStyles(cfg ansi.StyleConfig)` | Programmatic style struct |
 82| `WithStylesFromJSONBytes(b []byte)` | Parse style from JSON bytes |
 83| `WithStylesFromJSONFile(path string)` | Load style from JSON file |
 84| `WithEnvironmentConfig()` | Use `GLAMOUR_STYLE` env var |
 85| `WithWordWrap(width int)` | Word wrap width (default: 80) |
 86| `WithTableWrap(wrap bool)` | Wrap table content (default: true). False truncates with ellipsis |
 87| `WithInlineTableLinks(inline bool)` | Render links inline in tables instead of footer list |
 88| `WithPreservedNewLines()` | Keep newlines instead of reflowing |
 89| `WithEmoji()` | Enable `:emoji_code:` rendering |
 90| `WithBaseURL(url string)` | Resolve relative URLs against this base |
 91| `WithChromaFormatter(fmt string)` | Set Chroma formatter for code blocks |
 92| `WithOptions(opts ...TermRendererOption)` | Combine multiple options |
 93
 94### Built-in styles
 95
 96| Constant | String | Use case |
 97|---|---|---|
 98| `styles.DarkStyle` | `"dark"` | Dark terminal backgrounds (default) |
 99| `styles.LightStyle` | `"light"` | Light terminal backgrounds |
100| `styles.DraculaStyle` | `"dracula"` | Dracula color scheme |
101| `styles.TokyoNightStyle` | `"tokyo-night"` | Tokyo Night color scheme |
102| `styles.PinkStyle` | `"pink"` | Pink accent theme |
103| `styles.AsciiStyle` | `"ascii"` | ASCII-only, no unicode box chars |
104| `styles.NoTTYStyle` | `"notty"` | No ANSI codes at all, plain text |
105
106Each has a corresponding `StyleConfig` variable: `styles.DarkStyleConfig`, `styles.LightStyleConfig`, etc.
107
108## Common Patterns
109
110### Custom style (programmatic)
111
112Start from a built-in config and modify fields. Style fields use pointers for optional values.
113
114```go
115import (
116    "charm.land/glamour/v2"
117    "charm.land/glamour/v2/ansi"
118    "charm.land/glamour/v2/styles"
119)
120
121func boolPtr(b bool) *bool    { return &b }
122func strPtr(s string) *string { return &s }
123func uintPtr(u uint) *uint    { return &u }
124
125func customRenderer() (*glamour.TermRenderer, error) {
126    style := styles.DarkStyleConfig
127
128    // Custom H1: green text, no background
129    style.H1 = ansi.StyleBlock{
130        StylePrimitive: ansi.StylePrimitive{
131            Color:  strPtr("34"),
132            Bold:   boolPtr(true),
133            Prefix: "# ",
134        },
135    }
136
137    // Wider margins
138    style.Document.Margin = uintPtr(4)
139
140    // Custom code block theme
141    style.CodeBlock.Theme = "monokai"
142
143    return glamour.NewTermRenderer(
144        glamour.WithStyles(style),
145        glamour.WithWordWrap(100),
146    )
147}
148```
149
150### Custom style (JSON file)
151
152```json
153{
154    "document": {
155        "color": "252",
156        "margin": 2,
157        "block_prefix": "\n",
158        "block_suffix": "\n"
159    },
160    "heading": {
161        "color": "39",
162        "bold": true,
163        "block_suffix": "\n"
164    },
165    "h1": {
166        "color": "228",
167        "background_color": "63",
168        "bold": true,
169        "prefix": " ",
170        "suffix": " "
171    },
172    "h2": {
173        "prefix": "## "
174    },
175    "code_block": {
176        "theme": "dracula",
177        "margin": 2
178    },
179    "link": {
180        "color": "123",
181        "underline": true
182    },
183    "strong": {
184        "bold": true
185    },
186    "emph": {
187        "italic": true
188    }
189}
190```
191
192```go
193r, err := glamour.NewTermRenderer(
194    glamour.WithStylesFromJSONFile("./my-style.json"),
195    glamour.WithWordWrap(80),
196)
197```
198
199### StyleConfig structure reference
200
201```
202StyleConfig
203  Document, BlockQuote, Paragraph     -> StyleBlock
204  List                                -> StyleList (StyleBlock + LevelIndent)
205  Heading, H1-H6                     -> StyleBlock
206  Text, Emph, Strong, Strikethrough  -> StylePrimitive
207  HorizontalRule                     -> StylePrimitive (use Format for custom rule)
208  Item, Enumeration                  -> StylePrimitive (BlockPrefix for bullet char)
209  Task                               -> StyleTask (Ticked/Unticked strings)
210  Link, LinkText                     -> StylePrimitive
211  Image, ImageText                   -> StylePrimitive
212  Code                               -> StyleBlock (inline code)
213  CodeBlock                          -> StyleCodeBlock (Theme + Chroma)
214  Table                              -> StyleTable (separators)
215  DefinitionList/Term/Description    -> StyleBlock/StylePrimitive
216  HTMLBlock, HTMLSpan                 -> StyleBlock
217
218StyleBlock
219  Indent *uint, IndentToken *string, Margin *uint
220  + StylePrimitive (all fields below)
221
222StylePrimitive
223  Color, BackgroundColor  *string    // ANSI color number or hex "#RRGGBB"
224  Bold, Italic, Underline *bool
225  CrossedOut, Faint       *bool
226  Inverse, Conceal, Blink *bool
227  Upper, Lower, Title     *bool      // text transform
228  Prefix, Suffix          string     // per-line prefix/suffix
229  BlockPrefix, BlockSuffix string    // before/after entire block
230  Format                  string     // Go template, e.g. link format
231```
232
233### Color downsampling with lipgloss (v2)
234
235Glamour 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.
236
237```go
238import (
239    "charm.land/glamour/v2"
240    "charm.land/lipgloss/v2"
241)
242
243r, _ := glamour.NewTermRenderer(glamour.WithWordWrap(80))
244out, _ := r.Render(markdown)
245
246// lipgloss detects terminal capabilities and downsamples
247lipgloss.Print(out)
248```
249
250Alternative with `colorprofile` for explicit control:
251
252```go
253import "github.com/charmbracelet/colorprofile"
254
255w := colorprofile.NewWriter(os.Stdout, os.Environ())
256fmt.Fprintf(w, "%s", out)
257```
258
259### Detect terminal background for style selection
260
261```go
262import "charm.land/lipgloss/v2"
263
264style := "dark"
265if !lipgloss.HasDarkBackground() {
266    style = "light"
267}
268r, _ := glamour.NewTermRenderer(glamour.WithStandardStyle(style))
269```
270
271### Environment-based style
272
273```bash
274export GLAMOUR_STYLE=dracula
275# or a file path:
276export GLAMOUR_STYLE=/path/to/custom.json
277```
278
279```go
280// Picks up GLAMOUR_STYLE, falls back to "dark"
281out, err := glamour.RenderWithEnvironmentConfig(markdown)
282
283// Or with a renderer:
284r, err := glamour.NewTermRenderer(glamour.WithEnvironmentConfig())
285```
286
287## Integration
288
289### Bubbletea viewport (scrollable markdown)
290
291```go
292import (
293    "charm.land/glamour/v2"
294    tea "charm.land/bubbletea/v2"
295    "charm.land/bubbles/v2/viewport"
296)
297
298type model struct {
299    viewport viewport.Model
300    content  string
301}
302
303func initialModel(markdown string) model {
304    r, _ := glamour.NewTermRenderer(
305        glamour.WithStandardStyle("dark"),
306        glamour.WithWordWrap(78), // viewport width minus padding
307    )
308    rendered, _ := r.Render(markdown)
309
310    vp := viewport.New(viewport.WithWidth(80), viewport.WithHeight(24))
311    vp.SetContent(rendered)
312
313    return model{viewport: vp, content: rendered}
314}
315
316func (m model) Init() tea.Cmd {
317    return nil
318}
319
320func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
321    var cmd tea.Cmd
322    m.viewport, cmd = m.viewport.Update(msg)
323    return m, cmd
324}
325
326func (m model) View() string {
327    return m.viewport.View()
328}
329```
330
331Key point: set `WithWordWrap` to viewport width minus any horizontal padding/margin. Re-render when terminal resizes.
332
333### Re-render on resize
334
335```go
336func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
337    switch msg := msg.(type) {
338    case tea.WindowSizeMsg:
339        m.viewport.SetWidth(msg.Width)
340        m.viewport.SetHeight(msg.Height)
341
342        r, _ := glamour.NewTermRenderer(
343            glamour.WithStandardStyle("dark"),
344            glamour.WithWordWrap(msg.Width - 2),
345        )
346        rendered, _ := r.Render(m.rawMarkdown)
347        m.viewport.SetContent(rendered)
348    }
349
350    var cmd tea.Cmd
351    m.viewport, cmd = m.viewport.Update(msg)
352    return m, cmd
353}
354```
355
356### Lipgloss styled container around rendered markdown
357
358```go
359import "charm.land/lipgloss/v2"
360
361border := lipgloss.NewStyle().
362    Border(lipgloss.RoundedBorder()).
363    Padding(1, 2)
364
365r, _ := glamour.NewTermRenderer(
366    glamour.WithStandardStyle("dark"),
367    glamour.WithWordWrap(76), // account for border + padding (2 border + 4 padding = 6)
368)
369rendered, _ := r.Render(markdown)
370
371fmt.Println(border.Render(rendered))
372```
373
374## Common Mistakes
375
376**Using `WithAutoStyle()` or `WithColorProfile()`** - Removed in v2. Use `WithStandardStyle("dark")` and `lipgloss.Print()` for color handling.
377
378**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.
379
380**Creating a new renderer per render when unnecessary** - `TermRenderer` is reusable. Create once, call `Render()` many times.
381
382**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)`.
383
384**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)`.
385
386**Import path still on v1** - v2 uses `charm.land/glamour/v2`, not `github.com/charmbracelet/glamour`.
387
388**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 }`.
389
390**Using `WithStandardStyle` with a file path** - `WithStandardStyle` only accepts built-in style names. For file paths, use `WithStylePath` or `WithStylesFromJSONFile`.
391
392## Checklist
393
394- [ ] Import `charm.land/glamour/v2` (not the old github path)
395- [ ] Pick a style: built-in name, JSON file, or programmatic `StyleConfig`
396- [ ] Set `WithWordWrap` to match your output width minus borders/padding
397- [ ] Use `lipgloss.Print()` for proper color downsampling on real terminals
398- [ ] For bubbletea: re-render on `WindowSizeMsg` with updated wrap width
399- [ ] Handle errors from `NewTermRenderer` and `Render` (malformed styles, etc.)
400- [ ] For env-based config: use `WithEnvironmentConfig()` or `RenderWithEnvironmentConfig()`
401- [ ] Test with `"notty"` style for CI/non-terminal environments