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>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user