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