AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Charm Glamour

skill-alxxpersonal-forge-charm-glamour · by alxxpersonal

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).

No reviews yet
0 installs
16 views
0.0% view→install

Install

$ agentstack add skill-alxxpersonal-forge-charm-glamour

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-alxxpersonal-forge-charm-glamour)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Charm Glamour? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 WithWordWrap to match your output width minus borders/padding
  • [ ] Use lipgloss.Print() for proper color downsampling on real terminals
  • [ ] For bubbletea: re-render on WindowSizeMsg with updated wrap width
  • [ ] Handle errors from NewTermRenderer and Render (malformed styles, etc.)
  • [ ] For env-based config: use WithEnvironmentConfig() or RenderWithEnvironmentConfig()
  • [ ] 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.