feat: live rendered markdown preview in style init TUI

Show a rendered markdown snippet using glamour with the selected theme's colors.
Preview updates in real-time as user navigates themes with arrow keys.
Removes redundant color swatches in favor of actual rendered output.
This commit is contained in:
britz 2026-06-13 17:36:16 +05:30
commit 07a89d7356

View file

@ -11,6 +11,7 @@ import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
@ -108,8 +109,11 @@ type model struct {
result *result
quitting bool
help help.Model
preview string
}
const sampleMD = "# Hello World\n\nThis is **bold** and *italic* text. Here is `inline code`.\n\n> A blockquote with style\n\n```\n$ glow README.md\n```\n\n[Link to somewhere](https://example.com)"
type result struct {
styleJSON []byte
themeName string
@ -119,6 +123,27 @@ func (m model) Init() tea.Cmd {
return nil
}
func (m model) renderPreview(t theme) string {
data, err := json.MarshalIndent(buildStyle(t.Doc, t.H1, t.H1Bg, t.H1Bold, t.H2, t.H3, t.H6,
t.Code, t.CodeBg, t.CodeBlock, t.Link, t.LinkUnder, t.HR, t.BqToken), "", " ")
if err != nil {
return " (preview unavailable)"
}
r, err := glamour.NewTermRenderer(
glamour.WithStylesFromJSONBytes(data),
glamour.WithWordWrap(60),
)
if err != nil {
return " (preview unavailable)"
}
defer r.Close()
out, err := r.Render(sampleMD)
if err != nil {
return " (preview unavailable)"
}
return strings.TrimRight(out, "\n")
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
@ -134,15 +159,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.cursor < 0 {
m.cursor = len(m.themes) - 1
}
m.preview = m.renderPreview(m.themes[m.cursor])
case key.Matches(msg, keys.Down):
m.cursor++
if m.cursor >= len(m.themes) {
m.cursor = 0
}
m.preview = m.renderPreview(m.themes[m.cursor])
case key.Matches(msg, keys.Random):
rt := randomTheme()
m.themes = append(m.themes[:len(m.themes)-1], rt)
m.cursor = len(m.themes) - 1
m.preview = m.renderPreview(m.themes[m.cursor])
case key.Matches(msg, keys.Enter):
t := m.themes[m.cursor]
data, _ := json.MarshalIndent(buildStyle(t.Doc, t.H1, t.H1Bg, t.H1Bold, t.H2, t.H3, t.H6,
@ -160,8 +188,6 @@ func (m model) View() string {
return ""
}
t := m.themes[m.cursor]
title := lipStyles.title.Render("Pick a theme")
divider := strings.Repeat("─", 70)
@ -178,29 +204,15 @@ func (m model) View() string {
}
themeList := lipgloss.JoinVertical(lipgloss.Left, listRows...)
swatch := func(label, color string) string {
block := lipgloss.NewStyle().Background(lipgloss.Color(color)).Render(" ")
return lipStyles.swatchLabel.Render(fmt.Sprintf("%s %s", block, label))
}
swatchRow := fmt.Sprintf(
" %s %s %s %s %s %s",
swatch("Text", t.Doc),
swatch("H1", t.H1),
swatch("H1 Bg", t.H1Bg),
swatch("H2", t.H2),
swatch("Code", t.Code),
swatch("Link", t.Link),
)
helpView := m.help.View(keys)
content := fmt.Sprintf(
"%s\n%s\n\n%s\n\n %s\n %s\n\n%s\n",
"%s\n%s\n\n%s\n\n %s\n%s\n\n%s\n",
title,
divider,
themeList,
lipStyles.previewTitle.Render("Colors"),
swatchRow,
lipStyles.previewTitle.Render("Preview"),
m.preview,
helpView,
)
@ -217,8 +229,9 @@ func runStyleInitTUI() error {
}
m := model{
themes: themes,
help: help.New(),
themes: themes,
help: help.New(),
preview: (&model{}).renderPreview(themes[0]),
}
p := tea.NewProgram(m, tea.WithAltScreen())