โ– humdrum codex / glint v1.0.2
license AGPL-3.0
4.3 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// 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"
	"github.com/charmbracelet/glamour/ansi"
	"github.com/charmbracelet/glamour/styles"
)

// paintCanvasBackground rewrites a glamour style so every block sits on the
// theme background (and drops glamour's document margin), so code blocks,
// inline code, and tables don't show their own darker panels in the preview.
// The chroma config is deep-copied so glamour's shared global isn't mutated.
func paintCanvasBackground(cfg *ansi.StyleConfig, hex string) {
	var zero uint
	cfg.Document.Margin = &zero
	if hex == "" {
		return
	}
	bg := hex
	cfg.Document.BackgroundColor = &bg
	cfg.Code.BackgroundColor = &bg
	cfg.CodeBlock.BackgroundColor = &bg
	if cfg.CodeBlock.Chroma != nil {
		chroma := *cfg.CodeBlock.Chroma
		chroma.Background.BackgroundColor = &bg
		cfg.CodeBlock.Chroma = &chroma
	}
	cfg.Table.BackgroundColor = &bg
}

// Model wraps a Glamour renderer and a viewport.
type Model struct {
	vp     viewport.Model
	style  string
	bg     string // theme background hex, so the preview matches the canvas
	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,
	}
}

// SetBackground sets the document background (a hex like "#100F0F") so the
// glamour preview blends into the themed canvas instead of showing glamour's
// own panel color.
func (m *Model) SetBackground(hex string) { m.bg = hex }

// 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
}

// fileStyle reports whether s names an existing style file on disk.
func fileStyle(s string) bool {
	if s == "" {
		return false
	}
	_, err := os.Stat(s)
	return err == 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)}

	switch {
	case fileStyle(m.style):
		// An explicit style file wins.
		opts = append(opts, glamour.WithStylePath(m.style))
	case m.style == "light" || m.style == "dark" || m.style == "":
		// The theme-driven styles: take glamour's base config but paint the
		// document with the theme background (and drop glamour's own margin) so
		// the preview blends seamlessly into the canvas.
		cfg := styles.DarkStyleConfig
		if m.style == "light" {
			cfg = styles.LightStyleConfig
		}
		paintCanvasBackground(&cfg, m.bg)
		opts = append(opts, glamour.WithStyles(cfg))
	case knownStyles[m.style]:
		// A user-chosen named style keeps its own look.
		opts = append(opts, glamour.WithStandardStyle(m.style))
	default:
		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() }

// Style returns the current glamour style (used in tests).
func (m *Model) Style() string { return m.style }