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