0
# Command System
1
2
Asynchronous operations and side effects through commands, including built-in commands for timers, batching, external process execution, and custom async operations.
3
4
## Capabilities
5
6
### Command Fundamentals
7
8
Commands represent I/O operations that run asynchronously and return messages.
9
10
```go { .api }
11
/**
12
* Cmd represents an I/O operation that returns a message
13
* Commands are returned from Init() and Update() to perform side effects
14
* Return nil for no-op commands
15
*/
16
type Cmd func() Msg
17
18
// Core message interface that all messages implement
19
type Msg interface{}
20
```
21
22
Commands are the primary way to perform side effects in Bubble Tea:
23
24
```go
25
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
26
switch msg := msg.(type) {
27
case tea.KeyMsg:
28
if msg.String() == "r" {
29
// Return a command to refresh data
30
return m, m.fetchDataCmd()
31
}
32
case dataFetchedMsg:
33
m.data = msg.data
34
return m, nil
35
}
36
return m, nil
37
}
38
39
func (m model) fetchDataCmd() tea.Cmd {
40
return func() tea.Msg {
41
data := fetchDataFromAPI() // This runs in a goroutine
42
return dataFetchedMsg{data: data}
43
}
44
}
45
```
46
47
### Command Batching
48
49
Execute multiple commands concurrently with no ordering guarantees.
50
51
```go { .api }
52
/**
53
* Batch executes commands concurrently with no ordering guarantees
54
* Use for independent operations that can run simultaneously
55
* @param cmds - Variable number of commands to execute
56
* @returns Single command that runs all provided commands
57
*/
58
func Batch(cmds ...Cmd) Cmd
59
60
/**
61
* BatchMsg is sent when batch execution completes
62
* You typically don't need to handle this directly
63
*/
64
type BatchMsg []Cmd
65
```
66
67
**Usage Example:**
68
69
```go
70
func (m model) Init() tea.Cmd {
71
return tea.Batch(
72
m.loadUserCmd(),
73
m.loadSettingsCmd(),
74
m.checkUpdatesCmd(),
75
)
76
}
77
78
func (m model) loadUserCmd() tea.Cmd {
79
return func() tea.Msg {
80
user := loadUser()
81
return userLoadedMsg{user}
82
}
83
}
84
85
func (m model) loadSettingsCmd() tea.Cmd {
86
return func() tea.Msg {
87
settings := loadSettings()
88
return settingsLoadedMsg{settings}
89
}
90
}
91
```
92
93
### Command Sequencing
94
95
Execute commands one at a time in specified order.
96
97
```go { .api }
98
/**
99
* Sequence executes commands one at a time in order
100
* Each command waits for the previous to complete
101
* @param cmds - Commands to execute in sequence
102
* @returns Single command that runs commands sequentially
103
*/
104
func Sequence(cmds ...Cmd) Cmd
105
```
106
107
**Usage Example:**
108
109
```go
110
func (m model) saveAndExit() tea.Cmd {
111
return tea.Sequence(
112
m.saveFileCmd(),
113
m.showSavedMessageCmd(),
114
tea.Quit(),
115
)
116
}
117
118
func (m model) saveFileCmd() tea.Cmd {
119
return func() tea.Msg {
120
err := saveFile(m.filename, m.content)
121
return fileSavedMsg{err: err}
122
}
123
}
124
125
func (m model) showSavedMessageCmd() tea.Cmd {
126
return func() tea.Msg {
127
return showMessageMsg{text: "File saved!"}
128
}
129
}
130
```
131
132
### Timer Commands
133
134
Create time-based operations for animations, polling, and scheduled events.
135
136
```go { .api }
137
/**
138
* Tick creates a timer that fires once after the specified duration
139
* Timer starts when the command is executed (not when created)
140
* @param d - Duration to wait before sending message
141
* @param fn - Function that returns message when timer fires
142
* @returns Command that sends timed message
143
*/
144
func Tick(d time.Duration, fn func(time.Time) Msg) Cmd
145
146
/**
147
* Every creates a timer that syncs with the system clock
148
* Useful for synchronized ticking (e.g., every minute on the minute)
149
* @param duration - Duration for timer alignment
150
* @param fn - Function that returns message when timer fires
151
* @returns Command that sends system-clock-aligned message
152
*/
153
func Every(duration time.Duration, fn func(time.Time) Msg) Cmd
154
```
155
156
**Usage Examples:**
157
158
```go
159
// Simple tick timer
160
type tickMsg time.Time
161
162
func (m model) Init() tea.Cmd {
163
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
164
return tickMsg(t)
165
})
166
}
167
168
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
169
switch msg.(type) {
170
case tickMsg:
171
// Update every second
172
m.counter++
173
return m, tea.Tick(time.Second, func(t time.Time) tea.Msg {
174
return tickMsg(t)
175
})
176
}
177
return m, nil
178
}
179
180
// System clock alignment
181
func (m model) startClockSync() tea.Cmd {
182
return tea.Every(time.Minute, func(t time.Time) tea.Msg {
183
return clockUpdateMsg{time: t}
184
})
185
}
186
```
187
188
### Window and Terminal Commands
189
190
Commands for controlling terminal and window properties.
191
192
```go { .api }
193
/**
194
* SetWindowTitle sets the terminal window title
195
* @param title - New title for the terminal window
196
* @returns Command to set window title
197
*/
198
func SetWindowTitle(title string) Cmd
199
200
/**
201
* WindowSize queries the current terminal size
202
* Delivers results via WindowSizeMsg
203
* Note: Size messages are automatically sent on start and resize
204
* @returns Command to query terminal dimensions
205
*/
206
func WindowSize() Cmd
207
```
208
209
**Usage Example:**
210
211
```go
212
func (m model) Init() tea.Cmd {
213
return tea.Batch(
214
tea.SetWindowTitle("My App - " + m.filename),
215
tea.WindowSize(),
216
)
217
}
218
219
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
220
switch msg := msg.(type) {
221
case tea.WindowSizeMsg:
222
m.width = msg.Width
223
m.height = msg.Height
224
return m, nil
225
}
226
return m, nil
227
}
228
```
229
230
### External Process Execution
231
232
Execute external processes and programs while suspending the TUI.
233
234
```go { .api }
235
/**
236
* ExecCallback is called when command execution completes
237
* @param error - Error from command execution (nil if successful)
238
* @returns Message to send to update function
239
*/
240
type ExecCallback func(error) Msg
241
242
/**
243
* ExecCommand interface for executable commands
244
* Implemented by exec.Cmd and custom command types
245
*/
246
type ExecCommand interface {
247
Run() error
248
SetStdin(io.Reader)
249
SetStdout(io.Writer)
250
SetStderr(io.Writer)
251
}
252
253
/**
254
* Exec executes arbitrary I/O in blocking fashion
255
* Program is suspended while command runs
256
* @param c - Command implementing ExecCommand interface
257
* @param fn - Callback function for completion notification
258
* @returns Command that blocks and executes external process
259
*/
260
func Exec(c ExecCommand, fn ExecCallback) Cmd
261
262
/**
263
* ExecProcess executes an *exec.Cmd in blocking fashion
264
* Convenience wrapper around Exec for standard commands
265
* @param c - Standard library exec.Cmd
266
* @param fn - Callback for completion (can be nil)
267
* @returns Command that executes external process
268
*/
269
func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd
270
```
271
272
**Usage Examples:**
273
274
```go
275
import "os/exec"
276
277
type editorFinishedMsg struct {
278
err error
279
}
280
281
func (m model) openEditor() tea.Cmd {
282
cmd := exec.Command("vim", m.filename)
283
return tea.ExecProcess(cmd, func(err error) tea.Msg {
284
return editorFinishedMsg{err: err}
285
})
286
}
287
288
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
289
switch msg := msg.(type) {
290
case tea.KeyMsg:
291
if msg.String() == "e" {
292
return m, m.openEditor()
293
}
294
case editorFinishedMsg:
295
if msg.err != nil {
296
m.status = "Editor failed: " + msg.err.Error()
297
} else {
298
m.status = "File edited successfully"
299
return m, m.reloadFileCmd()
300
}
301
return m, nil
302
}
303
return m, nil
304
}
305
306
// Simple execution without callback
307
func (m model) runGitStatus() tea.Cmd {
308
cmd := exec.Command("git", "status")
309
return tea.ExecProcess(cmd, nil)
310
}
311
```
312
313
## Custom Command Patterns
314
315
### HTTP Requests
316
317
Common pattern for making HTTP requests:
318
319
```go
320
type httpResponseMsg struct {
321
status int
322
body []byte
323
err error
324
}
325
326
func fetchURL(url string) tea.Cmd {
327
return func() tea.Msg {
328
resp, err := http.Get(url)
329
if err != nil {
330
return httpResponseMsg{err: err}
331
}
332
defer resp.Body.Close()
333
334
body, err := io.ReadAll(resp.Body)
335
return httpResponseMsg{
336
status: resp.StatusCode,
337
body: body,
338
err: err,
339
}
340
}
341
}
342
343
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
344
switch msg := msg.(type) {
345
case httpResponseMsg:
346
if msg.err != nil {
347
m.error = msg.err.Error()
348
return m, nil
349
}
350
m.data = string(msg.body)
351
return m, nil
352
}
353
return m, nil
354
}
355
```
356
357
### File I/O Operations
358
359
Pattern for file operations:
360
361
```go
362
type fileReadMsg struct {
363
filename string
364
content []byte
365
err error
366
}
367
368
type fileWriteMsg struct {
369
filename string
370
err error
371
}
372
373
func readFileCmd(filename string) tea.Cmd {
374
return func() tea.Msg {
375
content, err := os.ReadFile(filename)
376
return fileReadMsg{
377
filename: filename,
378
content: content,
379
err: err,
380
}
381
}
382
}
383
384
func writeFileCmd(filename string, content []byte) tea.Cmd {
385
return func() tea.Msg {
386
err := os.WriteFile(filename, content, 0644)
387
return fileWriteMsg{
388
filename: filename,
389
err: err,
390
}
391
}
392
}
393
```
394
395
### Periodic Tasks
396
397
Pattern for recurring operations:
398
399
```go
400
type pollMsg time.Time
401
402
func startPolling(interval time.Duration) tea.Cmd {
403
return tea.Tick(interval, func(t time.Time) tea.Msg {
404
return pollMsg(t)
405
})
406
}
407
408
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
409
switch msg.(type) {
410
case pollMsg:
411
// Do periodic work
412
cmd := m.checkStatusCmd()
413
414
// Schedule next poll
415
nextPoll := startPolling(30 * time.Second)
416
417
return m, tea.Batch(cmd, nextPoll)
418
}
419
return m, nil
420
}
421
```
422
423
### Conditional Commands
424
425
Pattern for conditional command execution:
426
427
```go
428
func conditionalCmd(condition bool, cmd tea.Cmd) tea.Cmd {
429
if condition {
430
return cmd
431
}
432
return nil
433
}
434
435
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
436
switch msg := msg.(type) {
437
case tea.KeyMsg:
438
if msg.String() == "s" {
439
return m, conditionalCmd(
440
m.hasUnsavedChanges,
441
m.saveFileCmd(),
442
)
443
}
444
}
445
return m, nil
446
}
447
```
448
449
### Long-Running Tasks with Progress
450
451
Pattern for tasks that provide progress updates:
452
453
```go
454
type progressMsg struct {
455
percent int
456
status string
457
}
458
459
type completedMsg struct {
460
result interface{}
461
err error
462
}
463
464
func longRunningTask() tea.Cmd {
465
return func() tea.Msg {
466
// This would typically be in a separate goroutine
467
// sending progress updates via a channel
468
469
for i := 0; i <= 100; i += 10 {
470
// In real implementation, send progress via program.Send()
471
time.Sleep(100 * time.Millisecond)
472
}
473
474
return completedMsg{result: "Task complete!"}
475
}
476
}
477
```
478
479
## Deprecated Functions
480
481
```go { .api }
482
/**
483
* Sequentially executes commands but returns first non-nil message
484
* Deprecated: Use Sequence instead for better behavior
485
*/
486
func Sequentially(cmds ...Cmd) Cmd
487
```