main
1// Package tea provides a framework for building rich terminal user interfaces
2// based on the paradigms of The Elm Architecture. It's well-suited for simple
3// and complex terminal applications, either inline, full-window, or a mix of
4// both. It's been battle-tested in several large projects and is
5// production-ready.
6//
7// A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials
8//
9// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples
10package tea
11
12import (
13 "context"
14 "errors"
15 "fmt"
16 "io"
17 "os"
18 "os/signal"
19 "runtime"
20 "runtime/debug"
21 "sync"
22 "sync/atomic"
23 "syscall"
24
25 "github.com/charmbracelet/x/term"
26 "github.com/muesli/cancelreader"
27 "golang.org/x/sync/errgroup"
28)
29
30// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
31var ErrProgramKilled = errors.New("program was killed")
32
33// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT
34// signal, or when it receives a [InterruptMsg].
35var ErrInterrupted = errors.New("program was interrupted")
36
37// Msg contain data from the result of a IO operation. Msgs trigger the update
38// function and, henceforth, the UI.
39type Msg interface{}
40
41// Model contains the program's state as well as its core functions.
42type Model interface {
43 // Init is the first function that will be called. It returns an optional
44 // initial command. To not perform an initial command return nil.
45 Init() Cmd
46
47 // Update is called when a message is received. Use it to inspect messages
48 // and, in response, update the model and/or send a command.
49 Update(Msg) (Model, Cmd)
50
51 // View renders the program's UI, which is just a string. The view is
52 // rendered after every Update.
53 View() string
54}
55
56// Cmd is an IO operation that returns a message when it's complete. If it's
57// nil it's considered a no-op. Use it for things like HTTP requests, timers,
58// saving and loading from disk, and so on.
59//
60// Note that there's almost never a reason to use a command to send a message
61// to another part of your program. That can almost always be done in the
62// update function.
63type Cmd func() Msg
64
65type inputType int
66
67const (
68 defaultInput inputType = iota
69 ttyInput
70 customInput
71)
72
73// String implements the stringer interface for [inputType]. It is inteded to
74// be used in testing.
75func (i inputType) String() string {
76 return [...]string{
77 "default input",
78 "tty input",
79 "custom input",
80 }[i]
81}
82
83// Options to customize the program during its initialization. These are
84// generally set with ProgramOptions.
85//
86// The options here are treated as bits.
87type startupOptions int16
88
89func (s startupOptions) has(option startupOptions) bool {
90 return s&option != 0
91}
92
93const (
94 withAltScreen startupOptions = 1 << iota
95 withMouseCellMotion
96 withMouseAllMotion
97 withANSICompressor
98 withoutSignalHandler
99 // Catching panics is incredibly useful for restoring the terminal to a
100 // usable state after a panic occurs. When this is set, Bubble Tea will
101 // recover from panics, print the stack trace, and disable raw mode. This
102 // feature is on by default.
103 withoutCatchPanics
104 withoutBracketedPaste
105 withReportFocus
106)
107
108// channelHandlers manages the series of channels returned by various processes.
109// It allows us to wait for those processes to terminate before exiting the
110// program.
111type channelHandlers []chan struct{}
112
113// Adds a channel to the list of handlers. We wait for all handlers to terminate
114// gracefully on shutdown.
115func (h *channelHandlers) add(ch chan struct{}) {
116 *h = append(*h, ch)
117}
118
119// shutdown waits for all handlers to terminate.
120func (h channelHandlers) shutdown() {
121 var wg sync.WaitGroup
122 for _, ch := range h {
123 wg.Add(1)
124 go func(ch chan struct{}) {
125 <-ch
126 wg.Done()
127 }(ch)
128 }
129 wg.Wait()
130}
131
132// Program is a terminal user interface.
133type Program struct {
134 initialModel Model
135
136 // handlers is a list of channels that need to be waited on before the
137 // program can exit.
138 handlers channelHandlers
139
140 // Configuration options that will set as the program is initializing,
141 // treated as bits. These options can be set via various ProgramOptions.
142 startupOptions startupOptions
143
144 // startupTitle is the title that will be set on the terminal when the
145 // program starts.
146 startupTitle string
147
148 inputType inputType
149
150 ctx context.Context
151 cancel context.CancelFunc
152
153 msgs chan Msg
154 errs chan error
155 finished chan struct{}
156
157 // where to send output, this will usually be os.Stdout.
158 output io.Writer
159 // ttyOutput is null if output is not a TTY.
160 ttyOutput term.File
161 previousOutputState *term.State
162 renderer renderer
163
164 // the environment variables for the program, defaults to os.Environ().
165 environ []string
166
167 // where to read inputs from, this will usually be os.Stdin.
168 input io.Reader
169 // ttyInput is null if input is not a TTY.
170 ttyInput term.File
171 previousTtyInputState *term.State
172 cancelReader cancelreader.CancelReader
173 readLoopDone chan struct{}
174
175 // was the altscreen active before releasing the terminal?
176 altScreenWasActive bool
177 ignoreSignals uint32
178
179 bpWasActive bool // was the bracketed paste mode active before releasing the terminal?
180 reportFocus bool // was focus reporting active before releasing the terminal?
181
182 filter func(Model, Msg) Msg
183
184 // fps is the frames per second we should set on the renderer, if
185 // applicable,
186 fps int
187
188 // mouseMode is true if the program should enable mouse mode on Windows.
189 mouseMode bool
190}
191
192// Quit is a special command that tells the Bubble Tea program to exit.
193func Quit() Msg {
194 return QuitMsg{}
195}
196
197// QuitMsg signals that the program should quit. You can send a [QuitMsg] with
198// [Quit].
199type QuitMsg struct{}
200
201// Suspend is a special command that tells the Bubble Tea program to suspend.
202func Suspend() Msg {
203 return SuspendMsg{}
204}
205
206// SuspendMsg signals the program should suspend.
207// This usually happens when ctrl+z is pressed on common programs, but since
208// bubbletea puts the terminal in raw mode, we need to handle it in a
209// per-program basis.
210//
211// You can send this message with [Suspend()].
212type SuspendMsg struct{}
213
214// ResumeMsg can be listen to to do something once a program is resumed back
215// from a suspend state.
216type ResumeMsg struct{}
217
218// InterruptMsg signals the program should suspend.
219// This usually happens when ctrl+c is pressed on common programs, but since
220// bubbletea puts the terminal in raw mode, we need to handle it in a
221// per-program basis.
222//
223// You can send this message with [Interrupt()].
224type InterruptMsg struct{}
225
226// Interrupt is a special command that tells the Bubble Tea program to
227// interrupt.
228func Interrupt() Msg {
229 return InterruptMsg{}
230}
231
232// NewProgram creates a new Program.
233func NewProgram(model Model, opts ...ProgramOption) *Program {
234 p := &Program{
235 initialModel: model,
236 msgs: make(chan Msg),
237 }
238
239 // Apply all options to the program.
240 for _, opt := range opts {
241 opt(p)
242 }
243
244 // A context can be provided with a ProgramOption, but if none was provided
245 // we'll use the default background context.
246 if p.ctx == nil {
247 p.ctx = context.Background()
248 }
249 // Initialize context and teardown channel.
250 p.ctx, p.cancel = context.WithCancel(p.ctx)
251
252 // if no output was set, set it to stdout
253 if p.output == nil {
254 p.output = os.Stdout
255 }
256
257 // if no environment was set, set it to os.Environ()
258 if p.environ == nil {
259 p.environ = os.Environ()
260 }
261
262 return p
263}
264
265func (p *Program) handleSignals() chan struct{} {
266 ch := make(chan struct{})
267
268 // Listen for SIGINT and SIGTERM.
269 //
270 // In most cases ^C will not send an interrupt because the terminal will be
271 // in raw mode and ^C will be captured as a keystroke and sent along to
272 // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
273 // caught here.
274 //
275 // SIGTERM is sent by unix utilities (like kill) to terminate a process.
276 go func() {
277 sig := make(chan os.Signal, 1)
278 signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
279 defer func() {
280 signal.Stop(sig)
281 close(ch)
282 }()
283
284 for {
285 select {
286 case <-p.ctx.Done():
287 return
288
289 case s := <-sig:
290 if atomic.LoadUint32(&p.ignoreSignals) == 0 {
291 switch s {
292 case syscall.SIGINT:
293 p.msgs <- InterruptMsg{}
294 default:
295 p.msgs <- QuitMsg{}
296 }
297 return
298 }
299 }
300 }
301 }()
302
303 return ch
304}
305
306// handleResize handles terminal resize events.
307func (p *Program) handleResize() chan struct{} {
308 ch := make(chan struct{})
309
310 if p.ttyOutput != nil {
311 // Get the initial terminal size and send it to the program.
312 go p.checkResize()
313
314 // Listen for window resizes.
315 go p.listenForResize(ch)
316 } else {
317 close(ch)
318 }
319
320 return ch
321}
322
323// handleCommands runs commands in a goroutine and sends the result to the
324// program's message channel.
325func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
326 ch := make(chan struct{})
327
328 go func() {
329 defer close(ch)
330
331 for {
332 select {
333 case <-p.ctx.Done():
334 return
335
336 case cmd := <-cmds:
337 if cmd == nil {
338 continue
339 }
340
341 // Don't wait on these goroutines, otherwise the shutdown
342 // latency would get too large as a Cmd can run for some time
343 // (e.g. tick commands that sleep for half a second). It's not
344 // possible to cancel them so we'll have to leak the goroutine
345 // until Cmd returns.
346 go func() {
347 // Recover from panics.
348 if !p.startupOptions.has(withoutCatchPanics) {
349 defer p.recoverFromPanic()
350 }
351
352 msg := cmd() // this can be long.
353 p.Send(msg)
354 }()
355 }
356 }
357 }()
358
359 return ch
360}
361
362func (p *Program) disableMouse() {
363 p.renderer.disableMouseCellMotion()
364 p.renderer.disableMouseAllMotion()
365 p.renderer.disableMouseSGRMode()
366}
367
368// eventLoop is the central message loop. It receives and handles the default
369// Bubble Tea messages, update the model and triggers redraws.
370func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
371 for {
372 select {
373 case <-p.ctx.Done():
374 return model, nil
375
376 case err := <-p.errs:
377 return model, err
378
379 case msg := <-p.msgs:
380 // Filter messages.
381 if p.filter != nil {
382 msg = p.filter(model, msg)
383 }
384 if msg == nil {
385 continue
386 }
387
388 // Handle special internal messages.
389 switch msg := msg.(type) {
390 case QuitMsg:
391 return model, nil
392
393 case InterruptMsg:
394 return model, ErrInterrupted
395
396 case SuspendMsg:
397 if suspendSupported {
398 p.suspend()
399 }
400
401 case clearScreenMsg:
402 p.renderer.clearScreen()
403
404 case enterAltScreenMsg:
405 p.renderer.enterAltScreen()
406
407 case exitAltScreenMsg:
408 p.renderer.exitAltScreen()
409
410 case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
411 switch msg.(type) {
412 case enableMouseCellMotionMsg:
413 p.renderer.enableMouseCellMotion()
414 case enableMouseAllMotionMsg:
415 p.renderer.enableMouseAllMotion()
416 }
417 // mouse mode (1006) is a no-op if the terminal doesn't support it.
418 p.renderer.enableMouseSGRMode()
419
420 // XXX: This is used to enable mouse mode on Windows. We need
421 // to reinitialize the cancel reader to get the mouse events to
422 // work.
423 if runtime.GOOS == "windows" && !p.mouseMode {
424 p.mouseMode = true
425 p.initCancelReader(true) //nolint:errcheck
426 }
427
428 case disableMouseMsg:
429 p.disableMouse()
430
431 // XXX: On Windows, mouse mode is enabled on the input reader
432 // level. We need to instruct the input reader to stop reading
433 // mouse events.
434 if runtime.GOOS == "windows" && p.mouseMode {
435 p.mouseMode = false
436 p.initCancelReader(true) //nolint:errcheck
437 }
438
439 case showCursorMsg:
440 p.renderer.showCursor()
441
442 case hideCursorMsg:
443 p.renderer.hideCursor()
444
445 case enableBracketedPasteMsg:
446 p.renderer.enableBracketedPaste()
447
448 case disableBracketedPasteMsg:
449 p.renderer.disableBracketedPaste()
450
451 case enableReportFocusMsg:
452 p.renderer.enableReportFocus()
453
454 case disableReportFocusMsg:
455 p.renderer.disableReportFocus()
456
457 case execMsg:
458 // NB: this blocks.
459 p.exec(msg.cmd, msg.fn)
460
461 case BatchMsg:
462 for _, cmd := range msg {
463 cmds <- cmd
464 }
465 continue
466
467 case sequenceMsg:
468 go func() {
469 // Execute commands one at a time, in order.
470 for _, cmd := range msg {
471 if cmd == nil {
472 continue
473 }
474
475 msg := cmd()
476 if batchMsg, ok := msg.(BatchMsg); ok {
477 g, _ := errgroup.WithContext(p.ctx)
478 for _, cmd := range batchMsg {
479 cmd := cmd
480 g.Go(func() error {
481 p.Send(cmd())
482 return nil
483 })
484 }
485
486 //nolint:errcheck
487 g.Wait() // wait for all commands from batch msg to finish
488 continue
489 }
490
491 p.Send(msg)
492 }
493 }()
494
495 case setWindowTitleMsg:
496 p.SetWindowTitle(string(msg))
497
498 case windowSizeMsg:
499 go p.checkResize()
500 }
501
502 // Process internal messages for the renderer.
503 if r, ok := p.renderer.(*standardRenderer); ok {
504 r.handleMessages(msg)
505 }
506
507 var cmd Cmd
508 model, cmd = model.Update(msg) // run update
509 cmds <- cmd // process command (if any)
510 p.renderer.write(model.View()) // send view to renderer
511 }
512 }
513}
514
515// Run initializes the program and runs its event loops, blocking until it gets
516// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
517// Returns the final model.
518func (p *Program) Run() (Model, error) {
519 p.handlers = channelHandlers{}
520 cmds := make(chan Cmd)
521 p.errs = make(chan error)
522 p.finished = make(chan struct{}, 1)
523
524 defer p.cancel()
525
526 switch p.inputType {
527 case defaultInput:
528 p.input = os.Stdin
529
530 // The user has not set a custom input, so we need to check whether or
531 // not standard input is a terminal. If it's not, we open a new TTY for
532 // input. This will allow things to "just work" in cases where data was
533 // piped in or redirected to the application.
534 //
535 // To disable input entirely pass nil to the [WithInput] program option.
536 f, isFile := p.input.(term.File)
537 if !isFile {
538 break
539 }
540 if term.IsTerminal(f.Fd()) {
541 break
542 }
543
544 f, err := openInputTTY()
545 if err != nil {
546 return p.initialModel, err
547 }
548 defer f.Close() //nolint:errcheck
549 p.input = f
550
551 case ttyInput:
552 // Open a new TTY, by request
553 f, err := openInputTTY()
554 if err != nil {
555 return p.initialModel, err
556 }
557 defer f.Close() //nolint:errcheck
558 p.input = f
559
560 case customInput:
561 // (There is nothing extra to do.)
562 }
563
564 // Handle signals.
565 if !p.startupOptions.has(withoutSignalHandler) {
566 p.handlers.add(p.handleSignals())
567 }
568
569 // Recover from panics.
570 if !p.startupOptions.has(withoutCatchPanics) {
571 defer p.recoverFromPanic()
572 }
573
574 // If no renderer is set use the standard one.
575 if p.renderer == nil {
576 p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps)
577 }
578
579 // Check if output is a TTY before entering raw mode, hiding the cursor and
580 // so on.
581 if err := p.initTerminal(); err != nil {
582 return p.initialModel, err
583 }
584
585 // Honor program startup options.
586 if p.startupTitle != "" {
587 p.renderer.setWindowTitle(p.startupTitle)
588 }
589 if p.startupOptions&withAltScreen != 0 {
590 p.renderer.enterAltScreen()
591 }
592 if p.startupOptions&withoutBracketedPaste == 0 {
593 p.renderer.enableBracketedPaste()
594 }
595 if p.startupOptions&withMouseCellMotion != 0 {
596 p.renderer.enableMouseCellMotion()
597 p.renderer.enableMouseSGRMode()
598 } else if p.startupOptions&withMouseAllMotion != 0 {
599 p.renderer.enableMouseAllMotion()
600 p.renderer.enableMouseSGRMode()
601 }
602
603 // XXX: Should we enable mouse mode on Windows?
604 // This needs to happen before initializing the cancel and input reader.
605 p.mouseMode = p.startupOptions&withMouseCellMotion != 0 || p.startupOptions&withMouseAllMotion != 0
606
607 if p.startupOptions&withReportFocus != 0 {
608 p.renderer.enableReportFocus()
609 }
610
611 // Start the renderer.
612 p.renderer.start()
613
614 // Initialize the program.
615 model := p.initialModel
616 if initCmd := model.Init(); initCmd != nil {
617 ch := make(chan struct{})
618 p.handlers.add(ch)
619
620 go func() {
621 defer close(ch)
622
623 select {
624 case cmds <- initCmd:
625 case <-p.ctx.Done():
626 }
627 }()
628 }
629
630 // Render the initial view.
631 p.renderer.write(model.View())
632
633 // Subscribe to user input.
634 if p.input != nil {
635 if err := p.initCancelReader(false); err != nil {
636 return model, err
637 }
638 }
639
640 // Handle resize events.
641 p.handlers.add(p.handleResize())
642
643 // Process commands.
644 p.handlers.add(p.handleCommands(cmds))
645
646 // Run event loop, handle updates and draw.
647 model, err := p.eventLoop(model, cmds)
648 killed := p.ctx.Err() != nil || err != nil
649 if killed && err == nil {
650 err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err())
651 }
652 if err == nil {
653 // Ensure we rendered the final state of the model.
654 p.renderer.write(model.View())
655 }
656
657 // Restore terminal state.
658 p.shutdown(killed)
659
660 return model, err
661}
662
663// StartReturningModel initializes the program and runs its event loops,
664// blocking until it gets terminated by either [Program.Quit], [Program.Kill],
665// or its signal handler. Returns the final model.
666//
667// Deprecated: please use [Program.Run] instead.
668func (p *Program) StartReturningModel() (Model, error) {
669 return p.Run()
670}
671
672// Start initializes the program and runs its event loops, blocking until it
673// gets terminated by either [Program.Quit], [Program.Kill], or its signal
674// handler.
675//
676// Deprecated: please use [Program.Run] instead.
677func (p *Program) Start() error {
678 _, err := p.Run()
679 return err
680}
681
682// Send sends a message to the main update function, effectively allowing
683// messages to be injected from outside the program for interoperability
684// purposes.
685//
686// If the program hasn't started yet this will be a blocking operation.
687// If the program has already been terminated this will be a no-op, so it's safe
688// to send messages after the program has exited.
689func (p *Program) Send(msg Msg) {
690 select {
691 case <-p.ctx.Done():
692 case p.msgs <- msg:
693 }
694}
695
696// Quit is a convenience function for quitting Bubble Tea programs. Use it
697// when you need to shut down a Bubble Tea program from the outside.
698//
699// If you wish to quit from within a Bubble Tea program use the Quit command.
700//
701// If the program is not running this will be a no-op, so it's safe to call
702// if the program is unstarted or has already exited.
703func (p *Program) Quit() {
704 p.Send(Quit())
705}
706
707// Kill stops the program immediately and restores the former terminal state.
708// The final render that you would normally see when quitting will be skipped.
709// [program.Run] returns a [ErrProgramKilled] error.
710func (p *Program) Kill() {
711 p.shutdown(true)
712}
713
714// Wait waits/blocks until the underlying Program finished shutting down.
715func (p *Program) Wait() {
716 <-p.finished
717}
718
719// shutdown performs operations to free up resources and restore the terminal
720// to its original state.
721func (p *Program) shutdown(kill bool) {
722 p.cancel()
723
724 // Wait for all handlers to finish.
725 p.handlers.shutdown()
726
727 // Check if the cancel reader has been setup before waiting and closing.
728 if p.cancelReader != nil {
729 // Wait for input loop to finish.
730 if p.cancelReader.Cancel() {
731 if !kill {
732 p.waitForReadLoop()
733 }
734 }
735 _ = p.cancelReader.Close()
736 }
737
738 if p.renderer != nil {
739 if kill {
740 p.renderer.kill()
741 } else {
742 p.renderer.stop()
743 }
744 }
745
746 _ = p.restoreTerminalState()
747 if !kill {
748 p.finished <- struct{}{}
749 }
750}
751
752// recoverFromPanic recovers from a panic, prints the stack trace, and restores
753// the terminal to a usable state.
754func (p *Program) recoverFromPanic() {
755 if r := recover(); r != nil {
756 p.shutdown(true)
757 fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
758 debug.PrintStack()
759 }
760}
761
762// ReleaseTerminal restores the original terminal state and cancels the input
763// reader. You can return control to the Program with RestoreTerminal.
764func (p *Program) ReleaseTerminal() error {
765 atomic.StoreUint32(&p.ignoreSignals, 1)
766 if p.cancelReader != nil {
767 p.cancelReader.Cancel()
768 }
769
770 p.waitForReadLoop()
771
772 if p.renderer != nil {
773 p.renderer.stop()
774 p.altScreenWasActive = p.renderer.altScreen()
775 p.bpWasActive = p.renderer.bracketedPasteActive()
776 p.reportFocus = p.renderer.reportFocus()
777 }
778
779 return p.restoreTerminalState()
780}
781
782// RestoreTerminal reinitializes the Program's input reader, restores the
783// terminal to the former state when the program was running, and repaints.
784// Use it to reinitialize a Program after running ReleaseTerminal.
785func (p *Program) RestoreTerminal() error {
786 atomic.StoreUint32(&p.ignoreSignals, 0)
787
788 if err := p.initTerminal(); err != nil {
789 return err
790 }
791 if err := p.initCancelReader(false); err != nil {
792 return err
793 }
794 if p.altScreenWasActive {
795 p.renderer.enterAltScreen()
796 } else {
797 // entering alt screen already causes a repaint.
798 go p.Send(repaintMsg{})
799 }
800 if p.renderer != nil {
801 p.renderer.start()
802 }
803 if p.bpWasActive {
804 p.renderer.enableBracketedPaste()
805 }
806 if p.reportFocus {
807 p.renderer.enableReportFocus()
808 }
809
810 // If the output is a terminal, it may have been resized while another
811 // process was at the foreground, in which case we may not have received
812 // SIGWINCH. Detect any size change now and propagate the new size as
813 // needed.
814 go p.checkResize()
815
816 return nil
817}
818
819// Println prints above the Program. This output is unmanaged by the program
820// and will persist across renders by the Program.
821//
822// If the altscreen is active no output will be printed.
823func (p *Program) Println(args ...interface{}) {
824 p.msgs <- printLineMessage{
825 messageBody: fmt.Sprint(args...),
826 }
827}
828
829// Printf prints above the Program. It takes a format template followed by
830// values similar to fmt.Printf. This output is unmanaged by the program and
831// will persist across renders by the Program.
832//
833// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
834// its own line.
835//
836// If the altscreen is active no output will be printed.
837func (p *Program) Printf(template string, args ...interface{}) {
838 p.msgs <- printLineMessage{
839 messageBody: fmt.Sprintf(template, args...),
840 }
841}