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```