cookbook.md

  1# Integration Cookbook
  2
  3Working multi-library code examples. Import paths verified against go.mod.
  4
  5## 1. Standard TUI App (bubbletea + lipgloss + bubbles)
  6
  7A list with styled header and footer. The three core libraries working together.
  8
  9```go
 10package main
 11
 12import (
 13	"fmt"
 14	"os"
 15
 16	tea "charm.land/bubbletea/v2"
 17	"charm.land/bubbles/v2/list"
 18	"charm.land/lipgloss/v2"
 19)
 20
 21var (
 22	titleStyle = lipgloss.NewStyle().
 23		Bold(true).
 24		Foreground(lipgloss.Color("#FAFAFA")).
 25		Background(lipgloss.Color("#7D56F4")).
 26		Padding(0, 1)
 27
 28	statusStyle = lipgloss.NewStyle().
 29		Foreground(lipgloss.Color("241"))
 30)
 31
 32type item string
 33
 34func (i item) FilterValue() string { return string(i) }
 35func (i item) Title() string       { return string(i) }
 36func (i item) Description() string { return "" }
 37
 38type model struct {
 39	list   list.Model
 40	width  int
 41	height int
 42}
 43
 44func initialModel() model {
 45	items := []list.Item{
 46		item("Buy groceries"),
 47		item("Clean the house"),
 48		item("Write some Go"),
 49		item("Touch grass"),
 50	}
 51	l := list.New(items, list.NewDefaultDelegate(), 40, 15)
 52	l.Title = "Tasks"
 53	return model{list: l}
 54}
 55
 56func (m model) Init() tea.Cmd { return nil }
 57
 58func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 59	switch msg := msg.(type) {
 60	case tea.KeyPressMsg:
 61		if msg.String() == "q" || msg.String() == "ctrl+c" {
 62			return m, tea.Quit
 63		}
 64	case tea.WindowSizeMsg:
 65		m.width = msg.Width
 66		m.height = msg.Height
 67		m.list.SetSize(msg.Width, msg.Height-2)
 68	}
 69
 70	var cmd tea.Cmd
 71	m.list, cmd = m.list.Update(msg)
 72	return m, cmd
 73}
 74
 75func (m model) View() tea.View {
 76	header := titleStyle.Render("My App")
 77	body := m.list.View()
 78	footer := statusStyle.Render("q to quit")
 79	v := tea.NewView(fmt.Sprintf("%s\n%s\n%s", header, body, footer))
 80	v.AltScreen = true
 81	return v
 82}
 83
 84func main() {
 85	if _, err := tea.NewProgram(initialModel()).Run(); err != nil {
 86		fmt.Println("Error:", err)
 87		os.Exit(1)
 88	}
 89}
 90```
 91
 92## 2. Forms Embedded in TUI (bubbletea + huh)
 93
 94A bubbletea app that shows a huh form, then displays results.
 95
 96```go
 97package main
 98
 99import (
100	"fmt"
101	"os"
102
103	tea "charm.land/bubbletea/v2"
104	"charm.land/huh/v2"
105	"charm.land/lipgloss/v2"
106)
107
108type state int
109
110const (
111	stateForm state = iota
112	stateDone
113)
114
115type model struct {
116	form  *huh.Form
117	state state
118	name  string
119	lang  string
120}
121
122func newModel() model {
123	var name, lang string
124
125	form := huh.NewForm(
126		huh.NewGroup(
127			huh.NewInput().
128				Key("name").
129				Title("Your name").
130				Value(&name),
131			huh.NewSelect[string]().
132				Key("lang").
133				Title("Favorite language").
134				Options(huh.NewOptions("Go", "Rust", "Python", "TypeScript")...).
135				Value(&lang),
136		),
137	)
138
139	return model{form: form, name: name, lang: lang}
140}
141
142func (m model) Init() tea.Cmd {
143	return m.form.Init()
144}
145
146func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
147	switch msg := msg.(type) {
148	case tea.KeyPressMsg:
149		if msg.String() == "ctrl+c" {
150			return m, tea.Quit
151		}
152	}
153
154	form, cmd := m.form.Update(msg)
155	if f, ok := form.(*huh.Form); ok {
156		m.form = f
157	}
158
159	if m.form.State == huh.StateCompleted {
160		m.state = stateDone
161		m.name = m.form.GetString("name")
162		m.lang = m.form.GetString("lang")
163		return m, tea.Quit
164	}
165
166	return m, cmd
167}
168
169func (m model) View() tea.View {
170	if m.state == stateDone {
171		result := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")).
172			Render(fmt.Sprintf("%s loves %s!", m.name, m.lang))
173		return tea.NewView(result + "\n")
174	}
175	return tea.NewView(m.form.View())
176}
177
178func main() {
179	if _, err := tea.NewProgram(newModel()).Run(); err != nil {
180		fmt.Println("Error:", err)
181		os.Exit(1)
182	}
183}
184```
185
186## 3. Scrollable Markdown Viewer (bubbletea + glamour + viewport)
187
188Renders markdown with glamour, displays in a scrollable viewport.
189
190```go
191package main
192
193import (
194	"fmt"
195	"os"
196
197	tea "charm.land/bubbletea/v2"
198	"charm.land/bubbles/v2/viewport"
199	"charm.land/glamour/v2"
200	"charm.land/lipgloss/v2"
201)
202
203const sampleMD = `# Welcome
204
205This is a **scrollable** markdown viewer built with:
206
207- Bubble Tea (framework)
208- Glamour (markdown rendering)
209- Bubbles viewport (scrolling)
210
211## Features
212
213Scroll with j/k or arrow keys. Press q to quit.
214
215> "The terminal is the ultimate UI." - someone, probably
216
217## Code Example
218
219` + "```go" + `
220fmt.Println("Hello from glamour!")
221` + "```" + `
222
223More content here to make it scrollable...
224`
225
226type model struct {
227	viewport    viewport.Model
228	rawMarkdown string
229	ready       bool
230}
231
232func (m model) Init() tea.Cmd { return nil }
233
234func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
235	switch msg := msg.(type) {
236	case tea.KeyPressMsg:
237		if msg.String() == "q" || msg.String() == "ctrl+c" {
238			return m, tea.Quit
239		}
240	case tea.WindowSizeMsg:
241		if !m.ready {
242			m.viewport = viewport.New(
243				viewport.WithWidth(msg.Width),
244				viewport.WithHeight(msg.Height-2),
245			)
246			m.ready = true
247		} else {
248			m.viewport.SetWidth(msg.Width)
249			m.viewport.SetHeight(msg.Height - 2)
250		}
251
252		r, _ := glamour.NewTermRenderer(
253			glamour.WithStandardStyle("dark"),
254			glamour.WithWordWrap(msg.Width-4),
255		)
256		rendered, _ := r.Render(m.rawMarkdown)
257		m.viewport.SetContent(rendered)
258	}
259
260	var cmd tea.Cmd
261	m.viewport, cmd = m.viewport.Update(msg)
262	return m, cmd
263}
264
265func (m model) View() tea.View {
266	if !m.ready {
267		return tea.NewView("Loading...")
268	}
269
270	header := lipgloss.NewStyle().Bold(true).
271		Foreground(lipgloss.Color("205")).
272		Render("Markdown Viewer")
273	footer := lipgloss.NewStyle().
274		Foreground(lipgloss.Color("241")).
275		Render(fmt.Sprintf("scroll: %d%%", int(m.viewport.ScrollPercent()*100)))
276
277	v := tea.NewView(header + "\n" + m.viewport.View() + "\n" + footer)
278	v.AltScreen = true
279	return v
280}
281
282func main() {
283	m := model{rawMarkdown: sampleMD}
284	if _, err := tea.NewProgram(m).Run(); err != nil {
285		fmt.Println("Error:", err)
286		os.Exit(1)
287	}
288}
289```
290
291## 4. Animated Transitions (bubbletea + harmonica)
292
293Spring-animated horizontal bar that follows a target position.
294
295```go
296package main
297
298import (
299	"fmt"
300	"math"
301	"os"
302	"strings"
303	"time"
304
305	tea "charm.land/bubbletea/v2"
306	"charm.land/lipgloss/v2"
307	"github.com/charmbracelet/harmonica"
308)
309
310const fps = 60
311
312type frameMsg time.Time
313
314func animate() tea.Cmd {
315	return tea.Tick(time.Second/fps, func(t time.Time) tea.Msg {
316		return frameMsg(t)
317	})
318}
319
320type model struct {
321	spring harmonica.Spring
322	x      float64
323	xVel   float64
324	target float64
325	width  int
326}
327
328func (m model) Init() tea.Cmd { return animate() }
329
330func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
331	switch msg := msg.(type) {
332	case tea.KeyPressMsg:
333		switch msg.String() {
334		case "q", "ctrl+c":
335			return m, tea.Quit
336		case "left":
337			m.target = float64(m.width) * 0.2
338		case "right":
339			m.target = float64(m.width) * 0.8
340		case "space":
341			m.target = float64(m.width) * 0.5
342		}
343	case tea.WindowSizeMsg:
344		m.width = msg.Width
345		m.target = float64(msg.Width) * 0.5
346	case frameMsg:
347		m.x, m.xVel = m.spring.Update(m.x, m.xVel, m.target)
348
349		if math.Abs(m.x-m.target) < 0.01 && math.Abs(m.xVel) < 0.01 {
350			return m, nil // stop animating when settled
351		}
352		return m, animate()
353	}
354	return m, nil
355}
356
357func (m model) View() tea.View {
358	if m.width == 0 {
359		return tea.NewView("")
360	}
361
362	pos := int(m.x)
363	if pos < 0 {
364		pos = 0
365	}
366	if pos >= m.width {
367		pos = m.width - 1
368	}
369
370	bar := lipgloss.NewStyle().
371		Foreground(lipgloss.Color("205")).
372		Bold(true).
373		Render("@")
374
375	line := strings.Repeat(" ", pos) + bar
376	help := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).
377		Render("\nleft/right/space to move, q to quit")
378
379	return tea.NewView(line + help)
380}
381
382func main() {
383	m := model{
384		spring: harmonica.NewSpring(harmonica.FPS(fps), 7.0, 0.35),
385	}
386	if _, err := tea.NewProgram(m).Run(); err != nil {
387		fmt.Println("Error:", err)
388		os.Exit(1)
389	}
390}
391```
392
393## 5. Scripted Demo Recording (gum + vhs)
394
395A VHS tape that demos a gum-powered script.
396
397```tape
398Output demo.gif
399
400Set FontSize 14
401Set Width 1000
402Set Height 500
403Set Theme "Catppuccin Frappe"
404Set WindowBar Colorful
405Set TypingSpeed 0.06
406Set Framerate 30
407
408Require gum
409
410# Show a styled banner
411Type `gum style --foreground 212 --border rounded --padding "1 2" "Deploy Helper"`
412Enter
413Sleep 1s
414
415# Choose environment
416Type `ENV=$(gum choose "staging" "production")`
417Enter
418Sleep 500ms
419Down
420Sleep 300ms
421Enter
422Sleep 1s
423
424# Confirm
425Type `gum confirm "Deploy to $ENV?" && echo "Deploying..."  || echo "Cancelled"`
426Enter
427Sleep 500ms
428Enter
429Sleep 2s
430```