package app import ( "strings" "sync" "github.com/charmbracelet/glamour" ) // defaultMarkdownWidth is the wrap width used when the caller doesn't yet know // the terminal width (e.g. a zero/negative value). 80 columns is a safe terminal // default that never blows up glamour's word-wrap. const defaultMarkdownWidth = 80 // markdownRenderers memoizes one glamour renderer per wrap width. A glamour // renderer bakes its word-wrap width in at construction, so we key the cache by // width and build lazily. Constructing a renderer is comparatively expensive // (it compiles a style + goldmark parser), hence the cache; the TUI renders the // same handful of widths over its lifetime. var ( markdownMu sync.Mutex markdownRenderers = map[int]*glamour.TermRenderer{} ) // rendererForWidth returns a cached dark-styled glamour renderer wrapped to w, // or nil if glamour could not build one (caller falls back to plain text). func rendererForWidth(w int) *glamour.TermRenderer { markdownMu.Lock() defer markdownMu.Unlock() if r, ok := markdownRenderers[w]; ok { return r } // Pin the "dark" standard style so output is deterministic and independent // of the ambient terminal's background detection (important for tests and // for headless/over-the-wire sessions). r, err := glamour.NewTermRenderer( glamour.WithStandardStyle("dark"), glamour.WithWordWrap(w), ) if err != nil { // Cache the failure as nil so we don't retry-and-fail every frame. markdownRenderers[w] = nil return nil } markdownRenderers[w] = r return r } // renderMarkdown renders content as terminal markdown (bold, italics, lists, // headings, code blocks/inline code) word-wrapped to width, styled for a dark // terminal. It is robust by construction: on any glamour error — or if the // renderer can't be built — it returns the original plain content rather than // crashing the TUI. Trailing whitespace glamour appends is trimmed so it doesn't // disrupt the surrounding layout. func renderMarkdown(content string, width int) string { if width < 1 { width = defaultMarkdownWidth } r := rendererForWidth(width) if r == nil { return content } out, err := r.Render(content) if err != nil { return content } // glamour pads block output with leading/trailing blank lines; strip them so // the rendered turn slots into the feed without extra gaps. out = strings.Trim(out, "\n") if strings.TrimSpace(out) == "" { // Nothing survived rendering (e.g. content was only whitespace) — prefer // the original so callers never lose the underlying text. return content } return out }