โ– humdrum codex / glint v1.0.2
license AGPL-3.0
2.5 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
// Package preview renders the current buffer through Glamour into a scrollable,
// read-only viewport โ€” the full glow read experience, markup concealed.
package preview

import (
	"os"

	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/glamour"
)

// Model wraps a Glamour renderer and a viewport.
type Model struct {
	vp     viewport.Model
	style  string
	width  int
	height int
}

// New returns a preview using the given Glamour style (builtin name or a path
// to a style JSON file).
func New(style string) *Model {
	return &Model{
		vp:     viewport.New(0, 0),
		style:  style,
		width:  80,
		height: 24,
	}
}

// SetSize resizes the viewport.
func (m *Model) SetSize(w, h int) {
	m.width = w
	if h < 1 {
		h = 1
	}
	m.height = h
	m.vp.Width = w
	m.vp.Height = h
}

// SetStyle changes the glamour style used by the next Render.
func (m *Model) SetStyle(s string) { m.style = s }

// Render runs markdown through Glamour and loads it into the viewport.
func (m *Model) Render(markdown string) error {
	r, err := m.renderer()
	if err != nil {
		return err
	}
	out, err := r.Render(markdown)
	if err != nil {
		return err
	}
	m.vp.SetContent(out)
	m.vp.GotoTop()
	return nil
}

// knownStyles maps glamour's known builtin style names.
var knownStyles = map[string]bool{
	"ascii":       true,
	"dark":        true,
	"light":       true,
	"dracula":     true,
	"tokyo-night": true,
	"notty":       true,
	"pink":        true,
}

// renderer builds a Glamour renderer, treating m.style as a file path when it
// exists on disk and as a builtin style name otherwise. Falls back to "dark" if
// the style is neither an existing file nor a known builtin name.
func (m *Model) renderer() (*glamour.TermRenderer, error) {
	width := m.width
	if width < 1 {
		width = 80
	}
	opts := []glamour.TermRendererOption{glamour.WithWordWrap(width)}

	// If it exists as a file, use it as a path
	if _, err := os.Stat(m.style); err == nil {
		opts = append(opts, glamour.WithStylePath(m.style))
	} else if knownStyles[m.style] {
		// If it's a known builtin name, use it
		opts = append(opts, glamour.WithStandardStyle(m.style))
	} else {
		// Otherwise, fall back to "dark"
		opts = append(opts, glamour.WithStandardStyle("dark"))
	}
	return glamour.NewTermRenderer(opts...)
}

// Update forwards scroll keys to the viewport.
func (m *Model) Update(msg tea.Msg) tea.Cmd {
	var cmd tea.Cmd
	m.vp, cmd = m.vp.Update(msg)
	return cmd
}

// View renders the viewport.
func (m *Model) View() string { return m.vp.View() }