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 "bytes"
14 "context"
15 "errors"
16 "fmt"
17 "image/color"
18 "io"
19 "log"
20 "os"
21 "os/signal"
22 "runtime/debug"
23 "strconv"
24 "strings"
25 "sync"
26 "sync/atomic"
27 "syscall"
28 "time"
29
30 "github.com/charmbracelet/colorprofile"
31 uv "github.com/charmbracelet/ultraviolet"
32 "github.com/charmbracelet/x/ansi"
33 "github.com/charmbracelet/x/term"
34 "github.com/lucasb-eyer/go-colorful"
35 "golang.org/x/sync/errgroup"
36)
37
38// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
39var ErrProgramPanic = errors.New("program experienced a panic")
40
41// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
42var ErrProgramKilled = errors.New("program was killed")
43
44// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT
45// signal, or when it receives a [InterruptMsg].
46var ErrInterrupted = errors.New("program was interrupted")
47
48// Msg contain data from the result of a IO operation. Msgs trigger the update
49// function and, henceforth, the UI.
50type Msg = uv.Event
51
52// Model contains the program's state as well as its core functions.
53type Model interface {
54 // Init is the first function that will be called. It returns an optional
55 // initial command. To not perform an initial command return nil.
56 Init() Cmd
57
58 // Update is called when a message is received. Use it to inspect messages
59 // and, in response, update the model and/or send a command.
60 Update(Msg) (Model, Cmd)
61}
62
63// ViewModel is an optional interface that can be implemented by the main model
64// to provide a view. If the main model does not implement a view interface,
65// the program won't render anything.
66type ViewModel interface {
67 // View renders the program's UI, which is just a string. The view is
68 // rendered after every Update.
69 View() string
70}
71
72// ViewableModel is an optional interface that can be implemented by the main
73// model to provide a view that can be composed of multiple layers. If the
74// main model does not implement a view interface, the program won't render
75// anything.
76type ViewableModel interface {
77 // View returns a [View] that contains the layers to be rendered. The
78 // layers are rendered based on their z-index, with the lowest z-index
79 // rendered first and the highest z-index rendered last. If some layers
80 // have the same z-index, they are rendered in the order they were added to
81 // the view.
82 // The cursor is optional, if it's nil the cursor will be hidden.
83 View() View
84}
85
86// Buffer represents a terminal cell buffer that defines the current state of
87// the terminal screen.
88type Buffer = uv.Buffer
89
90// Screen represents a read writable canvas that can be used to render
91// components on the terminal screen.
92type Screen = uv.Screen
93
94// Rectangle represents a rectangular area with two points: the top left corner
95// and the bottom right corner. It is used to define the area where components
96// will be rendered on the terminal screen.
97type Rectangle = uv.Rectangle
98
99// Layer represents a drawable component on a [Screen].
100type Layer interface {
101 // Draw renders the component on the given [Screen] within the specified
102 // [Rectangle]. The component should draw itself within the bounds of the
103 // rectangle, which is defined by the top left corner (x0, y0) and the
104 // bottom right corner (x1, y1).
105 Draw(s Screen, r Rectangle)
106}
107
108// Hittable is an interface that can be implemented by a [Layer] to test
109// whether a layer was hit by a mouse event.
110type Hittable interface {
111 // Hit tests the layer against the given position. If the position is
112 // inside the layer, it returns the layer ID that was hit. If no
113 // layer was hit, it returns an empty string.
114 Hit(x, y int) string
115}
116
117// NewView is a helper function to create a new [View] with the given string or
118// [Layer].
119func NewView(s any) View {
120 var view View
121 switch v := s.(type) {
122 case string:
123 view.Layer = uv.NewStyledString(v)
124 case fmt.Stringer:
125 view.Layer = uv.NewStyledString(v.String())
126 case Layer:
127 view.Layer = v
128 default:
129 view.Layer = uv.NewStyledString(fmt.Sprintf("%v", v))
130 }
131 return view
132}
133
134// View represents a terminal view that can be composed of multiple layers.
135// It can also contain a cursor that will be rendered on top of the layers.
136type View struct {
137 Layer Layer
138 Cursor *Cursor
139 BackgroundColor color.Color
140 ForegroundColor color.Color
141 WindowTitle string
142}
143
144// Cursor represents a cursor on the terminal screen.
145type Cursor struct {
146 // Position is a [Position] that determines the cursor's position on the
147 // screen relative to the top left corner of the frame.
148 Position
149
150 // Color is a [color.Color] that determines the cursor's color.
151 Color color.Color
152
153 // Shape is a [CursorShape] that determines the cursor's shape.
154 Shape CursorShape
155
156 // Blink is a boolean that determines whether the cursor should blink.
157 Blink bool
158}
159
160// NewCursor returns a new cursor with the default settings and the given
161// position.
162func NewCursor(x, y int) *Cursor {
163 return &Cursor{
164 Position: Position{X: x, Y: y},
165 Color: nil,
166 Shape: CursorBlock,
167 Blink: true,
168 }
169}
170
171// CursorModel is an optional interface that can be implemented by the main
172// model to provide a view that manages the cursor. If the main model does not
173// implement a view interface, the program won't render anything.
174type CursorModel interface {
175 // View renders the program's UI, which is just a string. The view is
176 // rendered after every Update. The cursor is optional, if it's nil the
177 // cursor will be hidden.
178 // Use [NewCursor] to quickly create a cursor for a given position with
179 // default styles.
180 View() (string, *Cursor)
181}
182
183// Cmd is an IO operation that returns a message when it's complete. If it's
184// nil it's considered a no-op. Use it for things like HTTP requests, timers,
185// saving and loading from disk, and so on.
186//
187// Note that there's almost never a reason to use a command to send a message
188// to another part of your program. That can almost always be done in the
189// update function.
190type Cmd func() Msg
191
192type inputType int
193
194const (
195 defaultInput inputType = iota
196 ttyInput
197 customInput
198)
199
200// String implements the stringer interface for [inputType]. It is inteded to
201// be used in testing.
202func (i inputType) String() string {
203 return [...]string{
204 "default input",
205 "tty input",
206 "custom input",
207 }[i]
208}
209
210// Options to customize the program during its initialization. These are
211// generally set with ProgramOptions.
212//
213// The options here are treated as bits.
214type startupOptions int16
215
216func (s startupOptions) has(option startupOptions) bool {
217 return s&option != 0
218}
219
220const (
221 withAltScreen startupOptions = 1 << iota
222 withMouseCellMotion
223 withMouseAllMotion
224 withoutSignalHandler
225 // Catching panics is incredibly useful for restoring the terminal to a
226 // usable state after a panic occurs. When this is set, Bubble Tea will
227 // recover from panics, print the stack trace, and disable raw mode. This
228 // feature is on by default.
229 withoutCatchPanics
230 withoutBracketedPaste
231 withReportFocus
232 withKittyKeyboard
233 withModifyOtherKeys
234 withWindowsInputMode
235 withColorProfile
236 withGraphemeClustering
237 withoutKeyEnhancements
238)
239
240// channelHandlers manages the series of channels returned by various processes.
241// It allows us to wait for those processes to terminate before exiting the
242// program.
243type channelHandlers struct {
244 handlers []chan struct{}
245 mu sync.RWMutex
246}
247
248// Adds a channel to the list of handlers. We wait for all handlers to terminate
249// gracefully on shutdown.
250func (h *channelHandlers) add(ch chan struct{}) {
251 h.mu.Lock()
252 h.handlers = append(h.handlers, ch)
253 h.mu.Unlock()
254}
255
256// shutdown waits for all handlers to terminate.
257func (h *channelHandlers) shutdown() {
258 var wg sync.WaitGroup
259
260 h.mu.RLock()
261 defer h.mu.RUnlock()
262
263 for _, ch := range h.handlers {
264 wg.Add(1)
265 go func(ch chan struct{}) {
266 <-ch
267 wg.Done()
268 }(ch)
269 }
270 wg.Wait()
271}
272
273// Program is a terminal user interface.
274type Program struct {
275 initialModel Model
276
277 // handlers is a list of channels that need to be waited on before the
278 // program can exit.
279 handlers channelHandlers
280
281 // Configuration options that will set as the program is initializing,
282 // treated as bits. These options can be set via various ProgramOptions.
283 startupOptions startupOptions
284
285 // startupTitle is the title that will be set on the terminal when the
286 // program starts.
287 startupTitle string
288
289 inputType inputType
290
291 // externalCtx is a context that was passed in via WithContext, otherwise defaulting
292 // to ctx.Background() (in case it was not), the internal context is derived from it.
293 externalCtx context.Context
294
295 // ctx is the programs's internal context for signalling internal teardown.
296 // It is built and derived from the externalCtx in NewProgram().
297 ctx context.Context
298 cancel context.CancelFunc
299
300 msgs chan Msg
301 errs chan error
302 finished chan struct{}
303 shutdownOnce sync.Once
304
305 profile colorprofile.Profile // the terminal color profile
306
307 // where to send output, this will usually be os.Stdout.
308 output io.Writer
309 outputBuf bytes.Buffer // buffer used to queue commands to be sent to the output
310
311 // ttyOutput is null if output is not a TTY.
312 ttyOutput term.File
313 previousOutputState *term.State
314 renderer renderer
315
316 // the environment variables for the program, defaults to os.Environ().
317 environ uv.Environ
318 // the program's logger for debugging.
319 logger uv.Logger
320
321 // where to read inputs from, this will usually be os.Stdin.
322 input io.Reader
323 mu sync.Mutex
324 // ttyInput is null if input is not a TTY.
325 ttyInput term.File
326 previousTtyInputState *term.State
327 inputReader *uv.TerminalReader
328 traceInput bool // true if input should be traced
329 readLoopDone chan struct{}
330 mouseMode bool // indicates whether we should enable mouse on Windows
331
332 // modes keeps track of terminal modes that have been enabled or disabled.
333 modes ansi.Modes
334 ignoreSignals uint32
335
336 filter func(Model, Msg) Msg
337
338 // fps is the frames per second we should set on the renderer, if
339 // applicable,
340 fps int
341
342 // ticker is the ticker that will be used to write to the renderer.
343 ticker *time.Ticker
344
345 // once is used to stop the renderer.
346 once sync.Once
347
348 // rendererDone is used to stop the renderer.
349 rendererDone chan struct{}
350
351 // stores the requested keyboard enhancements.
352 requestedEnhancements KeyboardEnhancements
353 // activeEnhancements stores the active keyboard enhancements read from the
354 // terminal.
355 activeEnhancements KeyboardEnhancements
356
357 // When a program is suspended, the terminal state is saved and the program
358 // is paused. This saves the terminal colors state so they can be restored
359 // when the program is resumed.
360 setBg, setFg, setCc color.Color
361 lastBgColor, lastFgColor, lastCursorColor color.Color
362 lastWindowTitle string
363
364 // Initial window size. Mainly used for testing.
365 width, height int
366
367 // whether to use hard tabs to optimize cursor movements
368 useHardTabs bool
369 // whether to use backspace to optimize cursor movements
370 useBackspace bool
371}
372
373// Quit is a special command that tells the Bubble Tea program to exit.
374func Quit() Msg {
375 return QuitMsg{}
376}
377
378// QuitMsg signals that the program should quit. You can send a [QuitMsg] with
379// [Quit].
380type QuitMsg struct{}
381
382// Suspend is a special command that tells the Bubble Tea program to suspend.
383func Suspend() Msg {
384 return SuspendMsg{}
385}
386
387// SuspendMsg signals the program should suspend.
388// This usually happens when ctrl+z is pressed on common programs, but since
389// bubbletea puts the terminal in raw mode, we need to handle it in a
390// per-program basis.
391//
392// You can send this message with [Suspend()].
393type SuspendMsg struct{}
394
395// ResumeMsg can be listen to to do something once a program is resumed back
396// from a suspend state.
397type ResumeMsg struct{}
398
399// InterruptMsg signals the program should suspend.
400// This usually happens when ctrl+c is pressed on common programs, but since
401// bubbletea puts the terminal in raw mode, we need to handle it in a
402// per-program basis.
403//
404// You can send this message with [Interrupt()].
405type InterruptMsg struct{}
406
407// Interrupt is a special command that tells the Bubble Tea program to
408// interrupt.
409func Interrupt() Msg {
410 return InterruptMsg{}
411}
412
413// NewProgram creates a new Program.
414func NewProgram(model Model, opts ...ProgramOption) *Program {
415 p := &Program{
416 initialModel: model,
417 msgs: make(chan Msg),
418 rendererDone: make(chan struct{}),
419 modes: ansi.Modes{},
420 }
421
422 // Apply all options to the program.
423 for _, opt := range opts {
424 opt(p)
425 }
426
427 // A context can be provided with a ProgramOption, but if none was provided
428 // we'll use the default background context.
429 if p.externalCtx == nil {
430 p.externalCtx = context.Background()
431 }
432 // Initialize context and teardown channel.
433 p.ctx, p.cancel = context.WithCancel(p.externalCtx)
434
435 // if no output was set, set it to stdout
436 if p.output == nil {
437 p.output = os.Stdout
438 }
439
440 // if no environment was set, set it to os.Environ()
441 if p.environ == nil {
442 p.environ = os.Environ()
443 }
444
445 if p.fps < 1 {
446 p.fps = defaultFPS
447 } else if p.fps > maxFPS {
448 p.fps = maxFPS
449 }
450
451 tracePath, traceOk := os.LookupEnv("TEA_TRACE")
452 if traceOk && len(tracePath) > 0 {
453 // We have a trace filepath.
454 if f, err := os.OpenFile(tracePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666); err == nil {
455 p.logger = log.New(f, "bubbletea: ", log.LstdFlags|log.Lshortfile)
456 }
457 }
458
459 return p
460}
461
462func (p *Program) handleSignals() chan struct{} {
463 ch := make(chan struct{})
464
465 // Listen for SIGINT and SIGTERM.
466 //
467 // In most cases ^C will not send an interrupt because the terminal will be
468 // in raw mode and ^C will be captured as a keystroke and sent along to
469 // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
470 // caught here.
471 //
472 // SIGTERM is sent by unix utilities (like kill) to terminate a process.
473 go func() {
474 sig := make(chan os.Signal, 1)
475 signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
476 defer func() {
477 signal.Stop(sig)
478 close(ch)
479 }()
480
481 for {
482 select {
483 case <-p.ctx.Done():
484 return
485
486 case s := <-sig:
487 if atomic.LoadUint32(&p.ignoreSignals) == 0 {
488 switch s {
489 case syscall.SIGINT:
490 p.msgs <- InterruptMsg{}
491 default:
492 p.msgs <- QuitMsg{}
493 }
494 return
495 }
496 }
497 }
498 }()
499
500 return ch
501}
502
503// handleResize handles terminal resize events.
504func (p *Program) handleResize() chan struct{} {
505 ch := make(chan struct{})
506
507 if p.ttyOutput != nil {
508 // Listen for window resizes.
509 go p.listenForResize(ch)
510 } else {
511 close(ch)
512 }
513
514 return ch
515}
516
517// handleCommands runs commands in a goroutine and sends the result to the
518// program's message channel.
519func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
520 ch := make(chan struct{})
521
522 go func() {
523 defer close(ch)
524
525 for {
526 select {
527 case <-p.ctx.Done():
528 return
529
530 case cmd := <-cmds:
531 if cmd == nil {
532 continue
533 }
534
535 // Don't wait on these goroutines, otherwise the shutdown
536 // latency would get too large as a Cmd can run for some time
537 // (e.g. tick commands that sleep for half a second). It's not
538 // possible to cancel them so we'll have to leak the goroutine
539 // until Cmd returns.
540 go func() {
541 // Recover from panics.
542 if !p.startupOptions.has(withoutCatchPanics) {
543 defer func() {
544 if r := recover(); r != nil {
545 p.recoverFromPanic(r)
546 }
547 }()
548 }
549
550 msg := cmd() // this can be long.
551 p.Send(msg)
552 }()
553 }
554 }
555 }()
556
557 return ch
558}
559
560// eventLoop is the central message loop. It receives and handles the default
561// Bubble Tea messages, update the model and triggers redraws.
562func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
563 for {
564 select {
565 case <-p.ctx.Done():
566 return model, nil
567
568 case err := <-p.errs:
569 return model, err
570
571 case msg := <-p.msgs:
572 msg = p.translateInputEvent(msg)
573
574 // Filter messages.
575 if p.filter != nil {
576 msg = p.filter(model, msg)
577 }
578 if msg == nil {
579 continue
580 }
581
582 // Handle special internal messages.
583 switch msg := msg.(type) {
584 case QuitMsg:
585 return model, nil
586
587 case InterruptMsg:
588 return model, ErrInterrupted
589
590 case SuspendMsg:
591 if suspendSupported {
592 p.suspend()
593 }
594
595 case CapabilityMsg:
596 switch msg {
597 case "RGB", "Tc":
598 if p.profile != colorprofile.TrueColor {
599 p.profile = colorprofile.TrueColor
600 go p.Send(ColorProfileMsg{p.profile})
601 }
602 }
603
604 case MouseMsg:
605 for _, m := range p.renderer.hit(msg) {
606 go p.Send(m) // send hit messages
607 }
608
609 case modeReportMsg:
610 switch msg.Mode {
611 case ansi.GraphemeClusteringMode:
612 // 1 means mode is set (see DECRPM).
613 p.modes[ansi.GraphemeClusteringMode] = msg.Value
614 }
615
616 case enableModeMsg:
617 mode := p.modes.Get(msg.Mode)
618 if mode.IsSet() {
619 break
620 }
621
622 p.modes.Set(msg.Mode)
623
624 switch msg.Mode {
625 case ansi.AltScreenSaveCursorMode:
626 p.renderer.enterAltScreen()
627 case ansi.TextCursorEnableMode:
628 p.renderer.showCursor()
629 case ansi.GraphemeClusteringMode:
630 // We store the state of grapheme clustering after we enable it
631 // and get a response in the eventLoop.
632 p.execute(ansi.SetGraphemeClusteringMode + ansi.RequestGraphemeClusteringMode)
633 default:
634 p.execute(ansi.SetMode(msg.Mode))
635 }
636
637 case disableModeMsg:
638 mode := p.modes.Get(msg.Mode)
639 if mode.IsReset() {
640 break
641 }
642
643 p.modes.Reset(msg.Mode)
644
645 switch msg.Mode {
646 case ansi.AltScreenSaveCursorMode:
647 p.renderer.exitAltScreen()
648 case ansi.TextCursorEnableMode:
649 p.renderer.hideCursor()
650 default:
651 p.execute(ansi.ResetMode(msg.Mode))
652 }
653
654 case enableMouseCellMotionMsg:
655 p.enableMouse(false)
656
657 case enableMouseAllMotionMsg:
658 p.enableMouse(true)
659
660 case disableMouseMotionMsg:
661 p.disableMouse()
662
663 case readClipboardMsg:
664 p.execute(ansi.RequestSystemClipboard)
665
666 case setClipboardMsg:
667 p.execute(ansi.SetSystemClipboard(string(msg)))
668
669 case readPrimaryClipboardMsg:
670 p.execute(ansi.RequestPrimaryClipboard)
671
672 case setPrimaryClipboardMsg:
673 p.execute(ansi.SetPrimaryClipboard(string(msg)))
674
675 case setBackgroundColorMsg:
676 // The renderer handles flushing the color to the terminal.
677 p.lastBgColor = msg.Color
678
679 case setForegroundColorMsg:
680 // The renderer handles flushing the color to the terminal.
681 p.lastFgColor = msg.Color
682
683 case setCursorColorMsg:
684 // The renderer handles flushing the color to the terminal.
685 p.lastCursorColor = msg.Color
686
687 case backgroundColorMsg:
688 p.execute(ansi.RequestBackgroundColor)
689
690 case foregroundColorMsg:
691 p.execute(ansi.RequestForegroundColor)
692
693 case cursorColorMsg:
694 p.execute(ansi.RequestCursorColor)
695
696 case KeyboardEnhancementsMsg:
697 p.activeEnhancements.kittyFlags = msg.kittyFlags
698 p.activeEnhancements.modifyOtherKeys = msg.modifyOtherKeys
699
700 case enableKeyboardEnhancementsMsg:
701 if p.startupOptions.has(withoutKeyEnhancements) {
702 break
703 }
704
705 if isWindows() {
706 // We use the Windows Console API which supports keyboard
707 // enhancements.
708 // Send an empty message to tell the user we support
709 // keyboard enhancements on Windows.
710 go p.Send(KeyboardEnhancementsMsg{})
711 break
712 }
713
714 var ke KeyboardEnhancements
715 for _, e := range msg {
716 e(&ke)
717 }
718
719 p.requestedEnhancements.kittyFlags |= ke.kittyFlags
720 if ke.modifyOtherKeys > p.requestedEnhancements.modifyOtherKeys {
721 p.requestedEnhancements.modifyOtherKeys = ke.modifyOtherKeys
722 }
723
724 p.requestKeyboardEnhancements()
725
726 case disableKeyboardEnhancementsMsg:
727 if p.startupOptions.has(withoutKeyEnhancements) {
728 break
729 }
730
731 if isWindows() {
732 // We use the Windows Console API which supports keyboard
733 // enhancements.
734 break
735 }
736
737 if p.activeEnhancements.modifyOtherKeys > 0 {
738 p.execute(ansi.ResetModifyOtherKeys)
739 p.activeEnhancements.modifyOtherKeys = 0
740 p.requestedEnhancements.modifyOtherKeys = 0
741 }
742 if p.activeEnhancements.kittyFlags > 0 {
743 p.execute(ansi.DisableKittyKeyboard)
744 p.activeEnhancements.kittyFlags = 0
745 p.requestedEnhancements.kittyFlags = 0
746 }
747
748 case execMsg:
749 // NB: this blocks.
750 p.exec(msg.cmd, msg.fn)
751
752 case terminalVersion:
753 p.execute(ansi.RequestNameVersion)
754
755 case requestCapabilityMsg:
756 p.execute(ansi.RequestTermcap(string(msg)))
757
758 case BatchMsg:
759 for _, cmd := range msg {
760 select {
761 case <-p.ctx.Done():
762 return model, nil
763 case cmds <- cmd:
764 }
765 }
766 continue
767
768 case sequenceMsg:
769 go func() {
770 // Execute commands one at a time, in order.
771 for _, cmd := range msg {
772 if cmd == nil {
773 continue
774 }
775
776 switch msg := cmd().(type) {
777 case BatchMsg:
778 g, _ := errgroup.WithContext(p.ctx)
779 for _, cmd := range msg {
780 cmd := cmd
781 g.Go(func() error {
782 p.Send(cmd())
783 return nil
784 })
785 }
786
787 _ = g.Wait() // wait for all commands from batch msg to finish
788 continue
789 case sequenceMsg:
790 for _, cmd := range msg {
791 p.Send(cmd())
792 }
793 default:
794 p.Send(msg)
795 }
796 }
797 }()
798
799 case setWindowTitleMsg:
800 p.renderer.setWindowTitle(p.lastWindowTitle)
801 p.lastWindowTitle = string(msg)
802
803 case WindowSizeMsg:
804 p.renderer.resize(msg.Width, msg.Height)
805
806 case windowSizeMsg:
807 go p.checkResize()
808
809 case requestCursorPosMsg:
810 p.execute(ansi.RequestCursorPositionReport)
811
812 case RawMsg:
813 p.execute(fmt.Sprint(msg.Msg))
814
815 case printLineMessage:
816 p.renderer.insertAbove(msg.messageBody)
817
818 case repaintMsg:
819 p.renderer.repaint()
820
821 case clearScreenMsg:
822 p.renderer.clearScreen()
823
824 case ColorProfileMsg:
825 p.renderer.setColorProfile(msg.Profile)
826 }
827
828 var cmd Cmd
829 model, cmd = model.Update(msg) // run update
830
831 select {
832 case <-p.ctx.Done():
833 return model, nil
834 case cmds <- cmd: // process command (if any)
835 }
836
837 p.render(model) // render view
838 }
839 }
840}
841
842// hasView returns true if the model has a view.
843func hasView(model Model) (ok bool) {
844 switch model.(type) {
845 case ViewModel, CursorModel, ViewableModel:
846 ok = true
847 }
848 return
849}
850
851// render renders the given view to the renderer.
852func (p *Program) render(model Model) {
853 var view View
854 switch model := model.(type) {
855 case ViewModel, CursorModel:
856 var frame string
857 switch model := model.(type) {
858 case ViewModel:
859 frame = model.View()
860 case CursorModel:
861 frame, view.Cursor = model.View()
862 }
863 view.Layer = uv.NewStyledString(frame)
864 view.BackgroundColor = p.lastBgColor
865 view.ForegroundColor = p.lastFgColor
866 view.WindowTitle = p.lastWindowTitle
867 if view.Cursor != nil && p.lastCursorColor != nil {
868 view.Cursor.Color = p.lastCursorColor
869 }
870 case ViewableModel:
871 view = model.View()
872 }
873 if p.renderer != nil {
874 p.renderer.render(view) // send view to renderer
875 }
876}
877
878// Run initializes the program and runs its event loops, blocking until it gets
879// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
880// Returns the final model.
881func (p *Program) Run() (returnModel Model, returnErr error) {
882 p.handlers = channelHandlers{}
883 cmds := make(chan Cmd)
884 p.errs = make(chan error, 1)
885
886 p.finished = make(chan struct{})
887 defer func() {
888 close(p.finished)
889 }()
890
891 defer p.cancel()
892
893 switch p.inputType {
894 case defaultInput:
895 p.input = os.Stdin
896
897 // The user has not set a custom input, so we need to check whether or
898 // not standard input is a terminal. If it's not, we open a new TTY for
899 // input. This will allow things to "just work" in cases where data was
900 // piped in or redirected to the application.
901 //
902 // To disable input entirely pass nil to the [WithInput] program option.
903 f, isFile := p.input.(term.File)
904 if !isFile {
905 break
906 }
907 if term.IsTerminal(f.Fd()) {
908 break
909 }
910
911 f, err := openInputTTY()
912 if err != nil {
913 return p.initialModel, err
914 }
915 defer f.Close() //nolint:errcheck
916 p.input = f
917
918 case ttyInput:
919 // Open a new TTY, by request
920 f, err := openInputTTY()
921 if err != nil {
922 return p.initialModel, err
923 }
924 defer f.Close() //nolint:errcheck
925 p.input = f
926
927 case customInput:
928 // (There is nothing extra to do.)
929 }
930
931 // Handle signals.
932 if !p.startupOptions.has(withoutSignalHandler) {
933 p.handlers.add(p.handleSignals())
934 }
935
936 // Recover from panics.
937 if !p.startupOptions.has(withoutCatchPanics) {
938 defer func() {
939 if r := recover(); r != nil {
940 returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic)
941 p.recoverFromPanic(r)
942 }
943 }()
944 }
945
946 // Check if output is a TTY before entering raw mode, hiding the cursor and
947 // so on.
948 if err := p.initTerminal(); err != nil {
949 return p.initialModel, err
950 }
951
952 // Get the initial window size.
953 resizeMsg := WindowSizeMsg{Width: p.width, Height: p.height}
954 if p.ttyOutput != nil {
955 // Set the initial size of the terminal.
956 w, h, err := term.GetSize(p.ttyOutput.Fd())
957 if err != nil {
958 return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
959 }
960
961 resizeMsg.Width, resizeMsg.Height = w, h
962 }
963
964 if p.renderer == nil { //nolint:nestif
965 if hasView(p.initialModel) {
966 stdr, ok := os.LookupEnv("TEA_STANDARD_RENDERER")
967 if has, _ := strconv.ParseBool(stdr); ok && has {
968 p.renderer = newRenderer(p.output)
969 } else {
970 // If no renderer is set use the cursed one.
971 p.renderer = newCursedRenderer(
972 p.output,
973 p.environ,
974 resizeMsg.Width,
975 resizeMsg.Height,
976 p.useHardTabs,
977 p.useBackspace,
978 p.ttyInput == nil,
979 p.logger,
980 )
981 }
982 } else {
983 // If the model has no view we don't need a renderer.
984 p.renderer = &nilRenderer{}
985 }
986 }
987
988 // Get the color profile and send it to the program.
989 if !p.startupOptions.has(withColorProfile) {
990 p.profile = colorprofile.Detect(p.output, p.environ)
991 }
992
993 // Set the color profile on the renderer and send it to the program.
994 p.renderer.setColorProfile(p.profile)
995 go p.Send(ColorProfileMsg{p.profile})
996
997 // Send the initial size to the program.
998 go p.Send(resizeMsg)
999 p.renderer.resize(resizeMsg.Width, resizeMsg.Height)
1000
1001 // Send the environment variables used by the program.
1002 go p.Send(EnvMsg(p.environ))
1003
1004 // Init the input reader and initial model.
1005 model := p.initialModel
1006 if p.input != nil {
1007 if err := p.initInputReader(false); err != nil {
1008 return model, err
1009 }
1010 }
1011
1012 // Hide the cursor before starting the renderer. This is handled by the
1013 // renderer so we don't need to write the sequence here.
1014 p.modes.Reset(ansi.TextCursorEnableMode)
1015 p.renderer.hideCursor()
1016
1017 // Honor program startup options.
1018 if p.startupTitle != "" {
1019 p.execute(ansi.SetWindowTitle(p.startupTitle))
1020 }
1021 if p.startupOptions&withAltScreen != 0 {
1022 // Enter alternate screen mode. This is handled by the renderer so we
1023 // don't need to write the sequence here.
1024 p.modes.Set(ansi.AltScreenSaveCursorMode)
1025 p.renderer.enterAltScreen()
1026 }
1027 if p.startupOptions&withoutBracketedPaste == 0 {
1028 p.execute(ansi.SetBracketedPasteMode)
1029 p.modes.Set(ansi.BracketedPasteMode)
1030 }
1031 if p.startupOptions&withGraphemeClustering != 0 {
1032 p.execute(ansi.SetGraphemeClusteringMode)
1033 p.execute(ansi.RequestGraphemeClusteringMode)
1034 // We store the state of grapheme clustering after we query it and get
1035 // a response in the eventLoop.
1036 }
1037
1038 // Enable mouse mode.
1039 cellMotion := p.startupOptions&withMouseCellMotion != 0
1040 allMotion := p.startupOptions&withMouseAllMotion != 0
1041 if cellMotion || allMotion {
1042 p.enableMouse(allMotion)
1043 }
1044
1045 if p.startupOptions&withReportFocus != 0 {
1046 p.execute(ansi.SetFocusEventMode)
1047 p.modes.Set(ansi.FocusEventMode)
1048 }
1049
1050 if !p.startupOptions.has(withoutKeyEnhancements) {
1051 if !isWindows() {
1052 // Enable unambiguous keys using whichever protocol the terminal prefer.
1053 p.requestedEnhancements.kittyFlags |= ansi.KittyDisambiguateEscapeCodes
1054 if p.requestedEnhancements.modifyOtherKeys == 0 {
1055 p.requestedEnhancements.modifyOtherKeys = 1 // mode 1
1056 }
1057 // We use the Windows Console API which supports keyboard
1058 // enhancements.
1059 p.requestKeyboardEnhancements()
1060 } else {
1061 // Send an empty message to tell the user we support
1062 // keyboard enhancements on Windows.
1063 go p.Send(KeyboardEnhancementsMsg{})
1064 }
1065 }
1066
1067 // Start the renderer.
1068 p.startRenderer()
1069
1070 // Initialize the program.
1071 initCmd := model.Init()
1072 if initCmd != nil {
1073 ch := make(chan struct{})
1074 p.handlers.add(ch)
1075
1076 go func() {
1077 defer close(ch)
1078
1079 select {
1080 case cmds <- initCmd:
1081 case <-p.ctx.Done():
1082 }
1083 }()
1084 }
1085
1086 // Render the initial view.
1087 p.render(model)
1088
1089 // Handle resize events.
1090 p.handlers.add(p.handleResize())
1091
1092 // Process commands.
1093 p.handlers.add(p.handleCommands(cmds))
1094
1095 // Run event loop, handle updates and draw.
1096 var err error
1097 model, err = p.eventLoop(model, cmds)
1098
1099 if err == nil && len(p.errs) > 0 {
1100 err = <-p.errs // Drain a leftover error in case eventLoop crashed.
1101 }
1102
1103 killed := p.externalCtx.Err() != nil || p.ctx.Err() != nil || err != nil
1104 if killed {
1105 if err == nil && p.externalCtx.Err() != nil {
1106 // Return also as context error the cancellation of an external context.
1107 // This is the context the user knows about and should be able to act on.
1108 err = fmt.Errorf("%w: %w", ErrProgramKilled, p.externalCtx.Err())
1109 } else if err == nil && p.ctx.Err() != nil {
1110 // Return only that the program was killed (not the internal mechanism).
1111 // The user does not know or need to care about the internal program context.
1112 err = ErrProgramKilled
1113 } else {
1114 // Return that the program was killed and also the error that caused it.
1115 err = fmt.Errorf("%w: %w", ErrProgramKilled, err)
1116 }
1117 } else {
1118 // Graceful shutdown of the program (not killed):
1119 // Ensure we rendered the final state of the model.
1120 p.render(model)
1121 }
1122
1123 // Restore terminal state.
1124 p.shutdown(killed)
1125
1126 return model, err
1127}
1128
1129// Send sends a message to the main update function, effectively allowing
1130// messages to be injected from outside the program for interoperability
1131// purposes.
1132//
1133// If the program hasn't started yet this will be a blocking operation.
1134// If the program has already been terminated this will be a no-op, so it's safe
1135// to send messages after the program has exited.
1136func (p *Program) Send(msg Msg) {
1137 select {
1138 case <-p.ctx.Done():
1139 case p.msgs <- msg:
1140 }
1141}
1142
1143// Quit is a convenience function for quitting Bubble Tea programs. Use it
1144// when you need to shut down a Bubble Tea program from the outside.
1145//
1146// If you wish to quit from within a Bubble Tea program use the Quit command.
1147//
1148// If the program is not running this will be a no-op, so it's safe to call
1149// if the program is unstarted or has already exited.
1150func (p *Program) Quit() {
1151 p.Send(Quit())
1152}
1153
1154// Kill stops the program immediately and restores the former terminal state.
1155// The final render that you would normally see when quitting will be skipped.
1156// [program.Run] returns a [ErrProgramKilled] error.
1157func (p *Program) Kill() {
1158 p.shutdown(true)
1159}
1160
1161// Wait waits/blocks until the underlying Program finished shutting down.
1162func (p *Program) Wait() {
1163 <-p.finished
1164}
1165
1166// execute writes the given sequence to the program output.
1167func (p *Program) execute(seq string) {
1168 _, _ = p.outputBuf.WriteString(seq)
1169}
1170
1171// flush flushes the output buffer to the program output.
1172func (p *Program) flush() error {
1173 if p.outputBuf.Len() == 0 {
1174 return nil
1175 }
1176 if p.logger != nil {
1177 p.logger.Printf("output: %q", p.outputBuf.String())
1178 }
1179 _, err := p.output.Write(p.outputBuf.Bytes())
1180 p.outputBuf.Reset()
1181 if err != nil {
1182 return fmt.Errorf("error writing to output: %w", err)
1183 }
1184 return nil
1185}
1186
1187// shutdown performs operations to free up resources and restore the terminal
1188// to its original state.
1189func (p *Program) shutdown(kill bool) {
1190 p.shutdownOnce.Do(func() {
1191 p.cancel()
1192
1193 // Wait for all handlers to finish.
1194 p.handlers.shutdown()
1195
1196 // Check if the cancel reader has been setup before waiting and closing.
1197 if p.inputReader != nil {
1198 // Wait for input loop to finish.
1199 if p.inputReader.Cancel() {
1200 if !kill {
1201 p.waitForReadLoop()
1202 }
1203 }
1204 _ = p.inputReader.Close()
1205 }
1206
1207 if p.renderer != nil {
1208 p.stopRenderer(kill)
1209 }
1210
1211 _ = p.restoreTerminalState()
1212 })
1213}
1214
1215// recoverFromPanic recovers from a panic, prints the stack trace, and restores
1216// the terminal to a usable state.
1217func (p *Program) recoverFromPanic(r any) {
1218 select {
1219 case p.errs <- ErrProgramPanic:
1220 default:
1221 }
1222 p.cancel() // Just in case a previous shutdown has failed.
1223 p.shutdown(true)
1224 // We use "\r\n" to ensure the output is formatted even when restoring the
1225 // terminal does not work or when raw mode is still active.
1226 rec := strings.ReplaceAll(fmt.Sprintf("%s", r), "\n", "\r\n")
1227 fmt.Fprintf(os.Stderr, "Caught panic:\r\n\r\n%s\r\n\r\nRestoring terminal...\r\n\r\n", rec)
1228 stack := strings.ReplaceAll(fmt.Sprintf("%s\n", debug.Stack()), "\n", "\r\n")
1229 fmt.Fprint(os.Stderr, stack)
1230 if v, err := strconv.ParseBool(os.Getenv("TEA_DEBUG")); err == nil && v {
1231 f, err := os.Create(fmt.Sprintf("bubbletea-panic-%d.log", time.Now().Unix()))
1232 if err == nil {
1233 defer f.Close() //nolint:errcheck
1234 fmt.Fprintln(f, rec) //nolint:errcheck
1235 fmt.Fprintln(f) //nolint:errcheck
1236 fmt.Fprintln(f, stack) //nolint:errcheck
1237 }
1238 }
1239}
1240
1241// ReleaseTerminal restores the original terminal state and cancels the input
1242// reader. You can return control to the Program with RestoreTerminal.
1243func (p *Program) ReleaseTerminal() error {
1244 return p.releaseTerminal(false)
1245}
1246
1247func (p *Program) releaseTerminal(reset bool) error {
1248 atomic.StoreUint32(&p.ignoreSignals, 1)
1249 if p.inputReader != nil {
1250 p.inputReader.Cancel()
1251 }
1252
1253 p.waitForReadLoop()
1254
1255 if p.renderer != nil {
1256 p.stopRenderer(false)
1257 if reset {
1258 p.renderer.reset()
1259 }
1260 }
1261
1262 return p.restoreTerminalState()
1263}
1264
1265// RestoreTerminal reinitializes the Program's input reader, restores the
1266// terminal to the former state when the program was running, and repaints.
1267// Use it to reinitialize a Program after running ReleaseTerminal.
1268func (p *Program) RestoreTerminal() error {
1269 atomic.StoreUint32(&p.ignoreSignals, 0)
1270
1271 if err := p.initTerminal(); err != nil {
1272 return err
1273 }
1274 if err := p.initInputReader(false); err != nil {
1275 return err
1276 }
1277 if p.modes.IsReset(ansi.AltScreenSaveCursorMode) {
1278 // entering alt screen already causes a repaint.
1279 go p.Send(repaintMsg{})
1280 }
1281
1282 p.startRenderer()
1283 if p.modes.IsSet(ansi.BracketedPasteMode) {
1284 p.execute(ansi.SetBracketedPasteMode)
1285 }
1286 if p.activeEnhancements.modifyOtherKeys != 0 {
1287 p.execute(ansi.KeyModifierOptions(4, p.activeEnhancements.modifyOtherKeys)) //nolint:mnd
1288 }
1289 if p.activeEnhancements.kittyFlags != 0 {
1290 p.execute(ansi.PushKittyKeyboard(p.activeEnhancements.kittyFlags))
1291 }
1292 if p.modes.IsSet(ansi.FocusEventMode) {
1293 p.execute(ansi.SetFocusEventMode)
1294 }
1295 if p.modes.IsSet(ansi.ButtonEventMouseMode) || p.modes.IsSet(ansi.AnyEventMouseMode) {
1296 if p.startupOptions&withMouseCellMotion != 0 {
1297 p.execute(ansi.SetButtonEventMouseMode)
1298 p.execute(ansi.SetSgrExtMouseMode)
1299 } else if p.startupOptions&withMouseAllMotion != 0 {
1300 p.execute(ansi.SetAnyEventMouseMode)
1301 p.execute(ansi.SetSgrExtMouseMode)
1302 }
1303 }
1304 if p.modes.IsSet(ansi.GraphemeClusteringMode) {
1305 p.execute(ansi.SetGraphemeClusteringMode)
1306 }
1307
1308 // Restore terminal colors.
1309 if p.setBg != nil {
1310 c, ok := colorful.MakeColor(p.setBg)
1311 if ok {
1312 p.execute(ansi.SetBackgroundColor(c.Hex()))
1313 }
1314 }
1315 if p.setFg != nil {
1316 c, ok := colorful.MakeColor(p.setFg)
1317 if ok {
1318 p.execute(ansi.SetForegroundColor(c.Hex()))
1319 }
1320 }
1321 if p.setCc != nil {
1322 c, ok := colorful.MakeColor(p.setCc)
1323 if ok {
1324 p.execute(ansi.SetCursorColor(c.Hex()))
1325 }
1326 }
1327
1328 // If the output is a terminal, it may have been resized while another
1329 // process was at the foreground, in which case we may not have received
1330 // SIGWINCH. Detect any size change now and propagate the new size as
1331 // needed.
1332 go p.checkResize()
1333
1334 // Flush queued commands.
1335 return p.flush()
1336}
1337
1338// Println prints above the Program. This output is unmanaged by the program
1339// and will persist across renders by the Program.
1340//
1341// If the altscreen is active no output will be printed.
1342func (p *Program) Println(args ...any) {
1343 p.msgs <- printLineMessage{
1344 messageBody: fmt.Sprint(args...),
1345 }
1346}
1347
1348// Printf prints above the Program. It takes a format template followed by
1349// values similar to fmt.Printf. This output is unmanaged by the program and
1350// will persist across renders by the Program.
1351//
1352// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
1353// its own line.
1354//
1355// If the altscreen is active no output will be printed.
1356func (p *Program) Printf(template string, args ...any) {
1357 p.msgs <- printLineMessage{
1358 messageBody: fmt.Sprintf(template, args...),
1359 }
1360}
1361
1362// startRenderer starts the renderer.
1363func (p *Program) startRenderer() {
1364 framerate := time.Second / time.Duration(p.fps)
1365 if p.ticker == nil {
1366 p.ticker = time.NewTicker(framerate)
1367 } else {
1368 // If the ticker already exists, it has been stopped and we need to
1369 // reset it.
1370 p.ticker.Reset(framerate)
1371 }
1372
1373 // Since the renderer can be restarted after a stop, we need to reset
1374 // the done channel and its corresponding sync.Once.
1375 p.once = sync.Once{}
1376
1377 // Start the renderer.
1378 go func() {
1379 for {
1380 select {
1381 case <-p.rendererDone:
1382 p.ticker.Stop()
1383 return
1384
1385 case <-p.ticker.C:
1386 _ = p.flush()
1387 _ = p.renderer.flush(p)
1388 }
1389 }
1390 }()
1391}
1392
1393// stopRenderer stops the renderer.
1394// If kill is true, the renderer will be stopped immediately without flushing
1395// the last frame.
1396func (p *Program) stopRenderer(kill bool) {
1397 // Stop the renderer before acquiring the mutex to avoid a deadlock.
1398 p.once.Do(func() {
1399 p.rendererDone <- struct{}{}
1400 })
1401
1402 if !kill {
1403 // flush locks the mutex
1404 _ = p.renderer.flush(p)
1405 }
1406
1407 _ = p.renderer.close()
1408}
1409
1410// requestKeyboardEnhancements tries to enable keyboard enhancements and read
1411// the active keyboard enhancements from the terminal.
1412func (p *Program) requestKeyboardEnhancements() {
1413 if p.requestedEnhancements.modifyOtherKeys > 0 {
1414 p.execute(ansi.KeyModifierOptions(4, p.requestedEnhancements.modifyOtherKeys)) //nolint:mnd
1415 p.execute(ansi.QueryModifyOtherKeys)
1416 }
1417 if p.requestedEnhancements.kittyFlags > 0 {
1418 p.execute(ansi.PushKittyKeyboard(p.requestedEnhancements.kittyFlags))
1419 p.execute(ansi.RequestKittyKeyboard)
1420 }
1421}
1422
1423// enableMouse enables mouse events on the terminal. When all is true, it will
1424// enable [ansi.AnyEventMouseMode], otherwise, it will use
1425// [ansi.ButtonEventMouseMode].
1426// Note this has no effect on Windows since we use the Windows Console API.
1427func (p *Program) enableMouse(all bool) {
1428 if isWindows() {
1429 // XXX: This is used to enable mouse mode on Windows. We need
1430 // to reinitialize the cancel reader to get the mouse events to
1431 // work.
1432 if !p.mouseMode {
1433 p.mouseMode = true
1434 if p.inputReader != nil {
1435 // Only reinitialize if the input reader has been initialized.
1436 _ = p.initInputReader(true)
1437 }
1438 }
1439 }
1440
1441 if all {
1442 p.execute(ansi.SetAnyEventMouseMode + ansi.SetSgrExtMouseMode)
1443 p.modes.Set(ansi.AnyEventMouseMode, ansi.SgrExtMouseMode)
1444 } else {
1445 p.execute(ansi.SetButtonEventMouseMode + ansi.SetSgrExtMouseMode)
1446 p.modes.Set(ansi.ButtonEventMouseMode, ansi.SgrExtMouseMode)
1447 }
1448}
1449
1450// disableMouse disables mouse events on the terminal.
1451// Note this has no effect on Windows since we use the Windows Console API.
1452func (p *Program) disableMouse() {
1453 if isWindows() {
1454 // XXX: On Windows, mouse mode is enabled on the input reader
1455 // level. We need to instruct the input reader to stop reading
1456 // mouse events.
1457 if p.mouseMode {
1458 p.mouseMode = false
1459 if p.inputReader != nil {
1460 // Only reinitialize if the input reader has been initialized.
1461 _ = p.initInputReader(true)
1462 }
1463 }
1464 }
1465
1466 var modes []ansi.Mode
1467 if p.modes.IsSet(ansi.AnyEventMouseMode) {
1468 modes = append(modes, ansi.AnyEventMouseMode)
1469 }
1470 if p.modes.IsSet(ansi.ButtonEventMouseMode) {
1471 modes = append(modes, ansi.ButtonEventMouseMode)
1472 }
1473 if len(modes) > 0 {
1474 modes = append(modes, ansi.SgrExtMouseMode)
1475 for _, m := range modes {
1476 // We could combine all of these modes into one single sequence,
1477 // but we're being cautious here for terminals that might not support
1478 // that format i.e. `CSI ? 10003 ; 1006 l`.
1479 p.execute(ansi.ResetMode(m))
1480 p.modes.Reset(m)
1481 }
1482 }
1483}