Files
task-master/internal/ui/views/markdown.go
T
SkrueckenandCursor 24c1e146ec Add offline-first task-master TUI with tickets, board lists, and undo.
Charm-based CLI with JSON persistence, status/list filters, external editor editing, command-history undo/redo, and glamour markdown without AutoStyle OSC hangs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 10:21:48 +02:00

90 lines
1.9 KiB
Go

package views
import (
"sync"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/styles"
"github.com/muesli/termenv"
)
type mdCacheKey struct {
width int
src string
}
var (
mdMu sync.Mutex
mdRenderer *glamour.TermRenderer
mdWidth int
mdCache = map[mdCacheKey]string{}
)
// RenderMarkdown renders Markdown to ANSI using a cached glamour renderer.
//
// Important: we intentionally use a fixed dark style — NOT WithAutoStyle().
// AutoStyle calls termenv.HasDarkBackground(), which sends an OSC query to the
// terminal and waits up to ~5s for a reply. In Cursor/some PTYs that reply
// never arrives, so the TUI appears to hang on startup (first selected ticket
// with a description triggers the renderer in ensureVisible/View).
func RenderMarkdown(src string, width int) string {
if src == "" {
return ""
}
if width < 20 {
width = 20
}
key := mdCacheKey{width: width, src: src}
mdMu.Lock()
defer mdMu.Unlock()
if out, ok := mdCache[key]; ok {
return out
}
r, err := getRenderer(width)
if err != nil {
return src
}
out, err := r.Render(src)
if err != nil {
return src
}
for len(out) > 0 && (out[len(out)-1] == '\n' || out[len(out)-1] == ' ') {
out = out[:len(out)-1]
}
if len(mdCache) > 128 {
mdCache = map[mdCacheKey]string{}
}
mdCache[key] = out
return out
}
func getRenderer(width int) (*glamour.TermRenderer, error) {
if mdRenderer != nil && mdWidth == width {
return mdRenderer, nil
}
r, err := glamour.NewTermRenderer(
glamour.WithStyles(styles.DarkStyleConfig),
glamour.WithWordWrap(width),
glamour.WithColorProfile(termenv.ANSI256),
glamour.WithEmoji(),
)
if err != nil {
return nil, err
}
mdRenderer = r
mdWidth = width
return r, nil
}
// ClearMarkdownCache drops cached renders (e.g. after theme change).
func ClearMarkdownCache() {
mdMu.Lock()
defer mdMu.Unlock()
mdCache = map[mdCacheKey]string{}
mdRenderer = nil
mdWidth = 0
}