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 // ttyInput is null if input is not a TTY.
324 ttyInput term.File
325 previousTtyInputState *term.State
326 inputReader *uv.TerminalReader
327 readLoopDone chan struct{}
328 mouseMode bool // indicates whether we should enable mouse on Windows
329
330 // modes keeps track of terminal modes that have been enabled or disabled.
331 modes ansi.Modes
332 ignoreSignals uint32
333
334 filter func(Model, Msg) Msg
335
336 // fps is the frames per second we should set on the renderer, if
337 // applicable,
338 fps int
339
340 // ticker is the ticker that will be used to write to the renderer.
341 ticker *time.Ticker
342
343 // once is used to stop the renderer.
344 once sync.Once
345
346 // rendererDone is used to stop the renderer.
347 rendererDone chan struct{}
348
349 // stores the requested keyboard enhancements.
350 requestedEnhancements KeyboardEnhancements
351 // activeEnhancements stores the active keyboard enhancements read from the
352 // terminal.
353 activeEnhancements KeyboardEnhancements
354
355 // When a program is suspended, the terminal state is saved and the program
356 // is paused. This saves the terminal colors state so they can be restored
357 // when the program is resumed.
358 setBg, setFg, setCc color.Color
359 lastBgColor, lastFgColor, lastCursorColor color.Color
360 lastWindowTitle string
361
362 // Initial window size. Mainly used for testing.
363 width, height int
364
365 // whether to use hard tabs to optimize cursor movements
366 useHardTabs bool
367 // whether to use backspace to optimize cursor movements
368 useBackspace bool
369}
370
371// Quit is a special command that tells the Bubble Tea program to exit.
372func Quit() Msg {
373 return QuitMsg{}
374}
375
376// QuitMsg signals that the program should quit. You can send a [QuitMsg] with
377// [Quit].
378type QuitMsg struct{}
379
380// Suspend is a special command that tells the Bubble Tea program to suspend.
381func Suspend() Msg {
382 return SuspendMsg{}
383}
384
385// SuspendMsg signals the program should suspend.
386// This usually happens when ctrl+z is pressed on common programs, but since
387// bubbletea puts the terminal in raw mode, we need to handle it in a
388// per-program basis.
389//
390// You can send this message with [Suspend()].
391type SuspendMsg struct{}
392
393// ResumeMsg can be listen to to do something once a program is resumed back
394// from a suspend state.
395type ResumeMsg struct{}
396
397// InterruptMsg signals the program should suspend.
398// This usually happens when ctrl+c is pressed on common programs, but since
399// bubbletea puts the terminal in raw mode, we need to handle it in a
400// per-program basis.
401//
402// You can send this message with [Interrupt()].
403type InterruptMsg struct{}
404
405// Interrupt is a special command that tells the Bubble Tea program to
406// interrupt.
407func Interrupt() Msg {
408 return InterruptMsg{}
409}
410
411// NewProgram creates a new Program.
412func NewProgram(model Model, opts ...ProgramOption) *Program {
413 p := &Program{
414 initialModel: model,
415 msgs: make(chan Msg),
416 rendererDone: make(chan struct{}),
417 modes: ansi.Modes{},
418 }
419
420 // Apply all options to the program.
421 for _, opt := range opts {
422 opt(p)
423 }
424
425 // A context can be provided with a ProgramOption, but if none was provided
426 // we'll use the default background context.
427 if p.externalCtx == nil {
428 p.externalCtx = context.Background()
429 }
430 // Initialize context and teardown channel.
431 p.ctx, p.cancel = context.WithCancel(p.externalCtx)
432
433 // if no output was set, set it to stdout
434 if p.output == nil {
435 p.output = os.Stdout
436 }
437
438 // if no environment was set, set it to os.Environ()
439 if p.environ == nil {
440 p.environ = os.Environ()
441 }
442
443 if p.fps < 1 {
444 p.fps = defaultFPS
445 } else if p.fps > maxFPS {
446 p.fps = maxFPS
447 }
448
449 tracePath, traceOk := os.LookupEnv("TEA_TRACE")
450 if traceOk && len(tracePath) > 0 {
451 // We have a trace filepath.
452 if f, err := os.OpenFile(tracePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666); err == nil {
453 p.logger = log.New(f, "bubbletea: ", log.LstdFlags|log.Lshortfile)
454 }
455 }
456
457 return p
458}
459
460func (p *Program) handleSignals() chan struct{} {
461 ch := make(chan struct{})
462
463 // Listen for SIGINT and SIGTERM.
464 //
465 // In most cases ^C will not send an interrupt because the terminal will be
466 // in raw mode and ^C will be captured as a keystroke and sent along to
467 // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
468 // caught here.
469 //
470 // SIGTERM is sent by unix utilities (like kill) to terminate a process.
471 go func() {
472 sig := make(chan os.Signal, 1)
473 signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
474 defer func() {
475 signal.Stop(sig)
476 close(ch)
477 }()
478
479 for {
480 select {
481 case <-p.ctx.Done():
482 return
483
484 case s := <-sig:
485 if atomic.LoadUint32(&p.ignoreSignals) == 0 {
486 switch s {
487 case syscall.SIGINT:
488 p.msgs <- InterruptMsg{}
489 default:
490 p.msgs <- QuitMsg{}
491 }
492 return
493 }
494 }
495 }
496 }()
497
498 return ch
499}
500
501// handleResize handles terminal resize events.
502func (p *Program) handleResize() chan struct{} {
503 ch := make(chan struct{})
504
505 if p.ttyOutput != nil {
506 // Listen for window resizes.
507 go p.listenForResize(ch)
508 } else {
509 close(ch)
510 }
511
512 return ch
513}
514
515// handleCommands runs commands in a goroutine and sends the result to the
516// program's message channel.
517func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
518 ch := make(chan struct{})
519
520 go func() {
521 defer close(ch)
522
523 for {
524 select {
525 case <-p.ctx.Done():
526 return
527
528 case cmd := <-cmds:
529 if cmd == nil {
530 continue
531 }
532
533 // Don't wait on these goroutines, otherwise the shutdown
534 // latency would get too large as a Cmd can run for some time
535 // (e.g. tick commands that sleep for half a second). It's not
536 // possible to cancel them so we'll have to leak the goroutine
537 // until Cmd returns.
538 go func() {
539 // Recover from panics.
540 if !p.startupOptions.has(withoutCatchPanics) {
541 defer func() {
542 if r := recover(); r != nil {
543 p.recoverFromPanic(r)
544 }
545 }()
546 }
547
548 msg := cmd() // this can be long.
549 p.Send(msg)
550 }()
551 }
552 }
553 }()
554
555 return ch
556}
557
558// eventLoop is the central message loop. It receives and handles the default
559// Bubble Tea messages, update the model and triggers redraws.
560func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
561 for {
562 select {
563 case <-p.ctx.Done():
564 return model, nil
565
566 case err := <-p.errs:
567 return model, err
568
569 case msg := <-p.msgs:
570 msg = p.translateInputEvent(msg)
571
572 // Filter messages.
573 if p.filter != nil {
574 msg = p.filter(model, msg)
575 }
576 if msg == nil {
577 continue
578 }
579
580 // Handle special internal messages.
581 switch msg := msg.(type) {
582 case QuitMsg:
583 return model, nil
584
585 case InterruptMsg:
586 return model, ErrInterrupted
587
588 case SuspendMsg:
589 if suspendSupported {
590 p.suspend()
591 }
592
593 case CapabilityMsg:
594 switch msg {
595 case "RGB", "Tc":
596 if p.profile != colorprofile.TrueColor {
597 p.profile = colorprofile.TrueColor
598 go p.Send(ColorProfileMsg{p.profile})
599 }
600 }
601
602 case MouseMsg:
603 for _, m := range p.renderer.hit(msg) {
604 go p.Send(m) // send hit messages
605 }
606
607 case modeReportMsg:
608 switch msg.Mode {
609 case ansi.GraphemeClusteringMode:
610 // 1 means mode is set (see DECRPM).
611 p.modes[ansi.GraphemeClusteringMode] = msg.Value
612 }
613
614 case enableModeMsg:
615 mode := p.modes.Get(msg.Mode)
616 if mode.IsSet() {
617 break
618 }
619
620 p.modes.Set(msg.Mode)
621
622 switch msg.Mode {
623 case ansi.AltScreenSaveCursorMode:
624 p.renderer.enterAltScreen()
625 // Main and alternate screen have their own Kitty keyboard
626 // stack. We need to request keyboard enhancements again
627 // when entering/exiting the alternate screen.
628 p.requestKeyboardEnhancements()
629 case ansi.TextCursorEnableMode:
630 p.renderer.showCursor()
631 case ansi.GraphemeClusteringMode:
632 // We store the state of grapheme clustering after we enable it
633 // and get a response in the eventLoop.
634 p.execute(ansi.SetGraphemeClusteringMode + ansi.RequestGraphemeClusteringMode)
635 default:
636 p.execute(ansi.SetMode(msg.Mode))
637 }
638
639 case disableModeMsg:
640 mode := p.modes.Get(msg.Mode)
641 if mode.IsReset() {
642 break
643 }
644
645 p.modes.Reset(msg.Mode)
646
647 switch msg.Mode {
648 case ansi.AltScreenSaveCursorMode:
649 p.renderer.exitAltScreen()
650 // Main and alternate screen have their own Kitty keyboard
651 // stack. We need to request keyboard enhancements again
652 // when entering/exiting the alternate screen.
653 p.requestKeyboardEnhancements()
654 case ansi.TextCursorEnableMode:
655 p.renderer.hideCursor()
656 default:
657 p.execute(ansi.ResetMode(msg.Mode))
658 }
659
660 case enableMouseCellMotionMsg:
661 p.enableMouse(false)
662
663 case enableMouseAllMotionMsg:
664 p.enableMouse(true)
665
666 case disableMouseMotionMsg:
667 p.disableMouse()
668
669 case readClipboardMsg:
670 p.execute(ansi.RequestSystemClipboard)
671
672 case setClipboardMsg:
673 p.execute(ansi.SetSystemClipboard(string(msg)))
674
675 case readPrimaryClipboardMsg:
676 p.execute(ansi.RequestPrimaryClipboard)
677
678 case setPrimaryClipboardMsg:
679 p.execute(ansi.SetPrimaryClipboard(string(msg)))
680
681 case setBackgroundColorMsg:
682 // The renderer handles flushing the color to the terminal.
683 p.lastBgColor = msg.Color
684
685 case setForegroundColorMsg:
686 // The renderer handles flushing the color to the terminal.
687 p.lastFgColor = msg.Color
688
689 case setCursorColorMsg:
690 // The renderer handles flushing the color to the terminal.
691 p.lastCursorColor = msg.Color
692
693 case backgroundColorMsg:
694 p.execute(ansi.RequestBackgroundColor)
695
696 case foregroundColorMsg:
697 p.execute(ansi.RequestForegroundColor)
698
699 case cursorColorMsg:
700 p.execute(ansi.RequestCursorColor)
701
702 case KeyboardEnhancementsMsg:
703 p.activeEnhancements.kittyFlags = msg.kittyFlags
704 p.activeEnhancements.modifyOtherKeys = msg.modifyOtherKeys
705
706 case enableKeyboardEnhancementsMsg:
707 if p.startupOptions.has(withoutKeyEnhancements) {
708 break
709 }
710
711 if isWindows() {
712 // We use the Windows Console API which supports keyboard
713 // enhancements.
714 // Send an empty message to tell the user we support
715 // keyboard enhancements on Windows.
716 go p.Send(KeyboardEnhancementsMsg{})
717 break
718 }
719
720 var ke KeyboardEnhancements
721 for _, e := range msg {
722 e(&ke)
723 }
724
725 p.requestedEnhancements.kittyFlags |= ke.kittyFlags
726 if ke.modifyOtherKeys > p.requestedEnhancements.modifyOtherKeys {
727 p.requestedEnhancements.modifyOtherKeys = ke.modifyOtherKeys
728 }
729
730 p.requestKeyboardEnhancements()
731
732 case disableKeyboardEnhancementsMsg:
733 if p.startupOptions.has(withoutKeyEnhancements) {
734 break
735 }
736
737 if isWindows() {
738 // We use the Windows Console API which supports keyboard
739 // enhancements.
740 break
741 }
742
743 if p.activeEnhancements.modifyOtherKeys > 0 {
744 p.execute(ansi.ResetModifyOtherKeys)
745 p.activeEnhancements.modifyOtherKeys = 0
746 p.requestedEnhancements.modifyOtherKeys = 0
747 }
748 if p.activeEnhancements.kittyFlags > 0 {
749 p.execute(ansi.DisableKittyKeyboard)
750 p.activeEnhancements.kittyFlags = 0
751 p.requestedEnhancements.kittyFlags = 0
752 }
753
754 case execMsg:
755 // NB: this blocks.
756 p.exec(msg.cmd, msg.fn)
757
758 case terminalVersion:
759 p.execute(ansi.RequestNameVersion)
760
761 case requestCapabilityMsg:
762 p.execute(ansi.RequestTermcap(string(msg)))
763
764 case BatchMsg:
765 for _, cmd := range msg {
766 select {
767 case <-p.ctx.Done():
768 return model, nil
769 case cmds <- cmd:
770 }
771 }
772 continue
773
774 case sequenceMsg:
775 go func() {
776 // Execute commands one at a time, in order.
777 for _, cmd := range msg {
778 if cmd == nil {
779 continue
780 }
781
782 switch msg := cmd().(type) {
783 case BatchMsg:
784 g, _ := errgroup.WithContext(p.ctx)
785 for _, cmd := range msg {
786 cmd := cmd
787 g.Go(func() error {
788 p.Send(cmd())
789 return nil
790 })
791 }
792
793 _ = g.Wait() // wait for all commands from batch msg to finish
794 continue
795 case sequenceMsg:
796 for _, cmd := range msg {
797 p.Send(cmd())
798 }
799 default:
800 p.Send(msg)
801 }
802 }
803 }()
804
805 case setWindowTitleMsg:
806 p.renderer.setWindowTitle(p.lastWindowTitle)
807 p.lastWindowTitle = string(msg)
808
809 case WindowSizeMsg:
810 p.renderer.resize(msg.Width, msg.Height)
811
812 case windowSizeMsg:
813 go p.checkResize()
814
815 case requestCursorPosMsg:
816 p.execute(ansi.RequestCursorPositionReport)
817
818 case RawMsg:
819 p.execute(fmt.Sprint(msg.Msg))
820
821 case printLineMessage:
822 p.renderer.insertAbove(msg.messageBody)
823
824 case repaintMsg:
825 p.renderer.repaint()
826
827 case clearScreenMsg:
828 p.renderer.clearScreen()
829
830 case ColorProfileMsg:
831 p.renderer.setColorProfile(msg.Profile)
832 }
833
834 var cmd Cmd
835 model, cmd = model.Update(msg) // run update
836
837 select {
838 case <-p.ctx.Done():
839 return model, nil
840 case cmds <- cmd: // process command (if any)
841 }
842
843 p.render(model) // render view
844 }
845 }
846}
847
848// hasView returns true if the model has a view.
849func hasView(model Model) (ok bool) {
850 switch model.(type) {
851 case ViewModel, CursorModel, ViewableModel:
852 ok = true
853 }
854 return
855}
856
857// render renders the given view to the renderer.
858func (p *Program) render(model Model) {
859 var view View
860 switch model := model.(type) {
861 case ViewModel, CursorModel:
862 var frame string
863 switch model := model.(type) {
864 case ViewModel:
865 frame = model.View()
866 case CursorModel:
867 frame, view.Cursor = model.View()
868 }
869 view.Layer = uv.NewStyledString(frame)
870 view.BackgroundColor = p.lastBgColor
871 view.ForegroundColor = p.lastFgColor
872 view.WindowTitle = p.lastWindowTitle
873 if view.Cursor != nil && p.lastCursorColor != nil {
874 view.Cursor.Color = p.lastCursorColor
875 }
876 case ViewableModel:
877 view = model.View()
878 }
879 if p.renderer != nil {
880 p.renderer.render(view) // send view to renderer
881 }
882}
883
884// Run initializes the program and runs its event loops, blocking until it gets
885// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
886// Returns the final model.
887func (p *Program) Run() (returnModel Model, returnErr error) {
888 p.handlers = channelHandlers{}
889 cmds := make(chan Cmd)
890 p.errs = make(chan error, 1)
891
892 p.finished = make(chan struct{})
893 defer func() {
894 close(p.finished)
895 }()
896
897 defer p.cancel()
898
899 switch p.inputType {
900 case defaultInput:
901 p.input = os.Stdin
902
903 // The user has not set a custom input, so we need to check whether or
904 // not standard input is a terminal. If it's not, we open a new TTY for
905 // input. This will allow things to "just work" in cases where data was
906 // piped in or redirected to the application.
907 //
908 // To disable input entirely pass nil to the [WithInput] program option.
909 f, isFile := p.input.(term.File)
910 if !isFile {
911 break
912 }
913 if term.IsTerminal(f.Fd()) {
914 break
915 }
916
917 f, err := openInputTTY()
918 if err != nil {
919 return p.initialModel, err
920 }
921 defer f.Close() //nolint:errcheck
922 p.input = f
923
924 case ttyInput:
925 // Open a new TTY, by request
926 f, err := openInputTTY()
927 if err != nil {
928 return p.initialModel, err
929 }
930 defer f.Close() //nolint:errcheck
931 p.input = f
932
933 case customInput:
934 // (There is nothing extra to do.)
935 }
936
937 // Handle signals.
938 if !p.startupOptions.has(withoutSignalHandler) {
939 p.handlers.add(p.handleSignals())
940 }
941
942 // Recover from panics.
943 if !p.startupOptions.has(withoutCatchPanics) {
944 defer func() {
945 if r := recover(); r != nil {
946 returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic)
947 p.recoverFromPanic(r)
948 }
949 }()
950 }
951
952 // Check if output is a TTY before entering raw mode, hiding the cursor and
953 // so on.
954 if err := p.initTerminal(); err != nil {
955 return p.initialModel, err
956 }
957
958 // Get the initial window size.
959 resizeMsg := WindowSizeMsg{Width: p.width, Height: p.height}
960 if p.ttyOutput != nil {
961 // Set the initial size of the terminal.
962 w, h, err := term.GetSize(p.ttyOutput.Fd())
963 if err != nil {
964 return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
965 }
966
967 resizeMsg.Width, resizeMsg.Height = w, h
968 }
969
970 if p.renderer == nil { //nolint:nestif
971 if hasView(p.initialModel) {
972 stdr, ok := os.LookupEnv("TEA_STANDARD_RENDERER")
973 if has, _ := strconv.ParseBool(stdr); ok && has {
974 p.renderer = newRenderer(p.output)
975 } else {
976 // If no renderer is set use the cursed one.
977 p.renderer = newCursedRenderer(
978 p.output,
979 p.environ,
980 resizeMsg.Width,
981 resizeMsg.Height,
982 p.useHardTabs,
983 p.useBackspace,
984 p.ttyInput == nil,
985 p.logger,
986 )
987 }
988 } else {
989 // If the model has no view we don't need a renderer.
990 p.renderer = &nilRenderer{}
991 }
992 }
993
994 // Get the color profile and send it to the program.
995 if !p.startupOptions.has(withColorProfile) {
996 p.profile = colorprofile.Detect(p.output, p.environ)
997 }
998
999 // Set the color profile on the renderer and send it to the program.
1000 p.renderer.setColorProfile(p.profile)
1001 go p.Send(ColorProfileMsg{p.profile})
1002
1003 // Send the initial size to the program.
1004 go p.Send(resizeMsg)
1005 p.renderer.resize(resizeMsg.Width, resizeMsg.Height)
1006
1007 // Send the environment variables used by the program.
1008 go p.Send(EnvMsg(p.environ))
1009
1010 // Init the input reader and initial model.
1011 model := p.initialModel
1012 if p.input != nil {
1013 if err := p.initInputReader(false); err != nil {
1014 return model, err
1015 }
1016 }
1017
1018 // Hide the cursor before starting the renderer. This is handled by the
1019 // renderer so we don't need to write the sequence here.
1020 p.modes.Reset(ansi.TextCursorEnableMode)
1021 p.renderer.hideCursor()
1022
1023 // Honor program startup options.
1024 if p.startupTitle != "" {
1025 p.execute(ansi.SetWindowTitle(p.startupTitle))
1026 }
1027 if p.startupOptions&withAltScreen != 0 {
1028 // Enter alternate screen mode. This is handled by the renderer so we
1029 // don't need to write the sequence here.
1030 p.modes.Set(ansi.AltScreenSaveCursorMode)
1031 p.renderer.enterAltScreen()
1032 }
1033 if p.startupOptions&withoutBracketedPaste == 0 {
1034 p.execute(ansi.SetBracketedPasteMode)
1035 p.modes.Set(ansi.BracketedPasteMode)
1036 }
1037 if p.startupOptions&withGraphemeClustering != 0 {
1038 p.execute(ansi.SetGraphemeClusteringMode)
1039 p.execute(ansi.RequestGraphemeClusteringMode)
1040 // We store the state of grapheme clustering after we query it and get
1041 // a response in the eventLoop.
1042 }
1043
1044 // Enable mouse mode.
1045 cellMotion := p.startupOptions&withMouseCellMotion != 0
1046 allMotion := p.startupOptions&withMouseAllMotion != 0
1047 if cellMotion || allMotion {
1048 p.enableMouse(allMotion)
1049 }
1050
1051 if p.startupOptions&withReportFocus != 0 {
1052 p.execute(ansi.SetFocusEventMode)
1053 p.modes.Set(ansi.FocusEventMode)
1054 }
1055
1056 if !p.startupOptions.has(withoutKeyEnhancements) {
1057 if !isWindows() {
1058 // Enable unambiguous keys using whichever protocol the terminal prefer.
1059 p.requestedEnhancements.kittyFlags |= ansi.KittyDisambiguateEscapeCodes
1060 if p.requestedEnhancements.modifyOtherKeys == 0 {
1061 p.requestedEnhancements.modifyOtherKeys = 1 // mode 1
1062 }
1063 // We use the Windows Console API which supports keyboard
1064 // enhancements.
1065 p.requestKeyboardEnhancements()
1066 } else {
1067 // Send an empty message to tell the user we support
1068 // keyboard enhancements on Windows.
1069 go p.Send(KeyboardEnhancementsMsg{})
1070 }
1071 }
1072
1073 // Start the renderer.
1074 p.startRenderer()
1075
1076 // Initialize the program.
1077 initCmd := model.Init()
1078 if initCmd != nil {
1079 ch := make(chan struct{})
1080 p.handlers.add(ch)
1081
1082 go func() {
1083 defer close(ch)
1084
1085 select {
1086 case cmds <- initCmd:
1087 case <-p.ctx.Done():
1088 }
1089 }()
1090 }
1091
1092 // Render the initial view.
1093 p.render(model)
1094
1095 // Handle resize events.
1096 p.handlers.add(p.handleResize())
1097
1098 // Process commands.
1099 p.handlers.add(p.handleCommands(cmds))
1100
1101 // Run event loop, handle updates and draw.
1102 var err error
1103 model, err = p.eventLoop(model, cmds)
1104
1105 if err == nil && len(p.errs) > 0 {
1106 err = <-p.errs // Drain a leftover error in case eventLoop crashed.
1107 }
1108
1109 killed := p.externalCtx.Err() != nil || p.ctx.Err() != nil || err != nil
1110 if killed {
1111 if err == nil && p.externalCtx.Err() != nil {
1112 // Return also as context error the cancellation of an external context.
1113 // This is the context the user knows about and should be able to act on.
1114 err = fmt.Errorf("%w: %w", ErrProgramKilled, p.externalCtx.Err())
1115 } else if err == nil && p.ctx.Err() != nil {
1116 // Return only that the program was killed (not the internal mechanism).
1117 // The user does not know or need to care about the internal program context.
1118 err = ErrProgramKilled
1119 } else {
1120 // Return that the program was killed and also the error that caused it.
1121 err = fmt.Errorf("%w: %w", ErrProgramKilled, err)
1122 }
1123 } else {
1124 // Graceful shutdown of the program (not killed):
1125 // Ensure we rendered the final state of the model.
1126 p.render(model)
1127 }
1128
1129 // Restore terminal state.
1130 p.shutdown(killed)
1131
1132 return model, err
1133}
1134
1135// Send sends a message to the main update function, effectively allowing
1136// messages to be injected from outside the program for interoperability
1137// purposes.
1138//
1139// If the program hasn't started yet this will be a blocking operation.
1140// If the program has already been terminated this will be a no-op, so it's safe
1141// to send messages after the program has exited.
1142func (p *Program) Send(msg Msg) {
1143 select {
1144 case <-p.ctx.Done():
1145 case p.msgs <- msg:
1146 }
1147}
1148
1149// Quit is a convenience function for quitting Bubble Tea programs. Use it
1150// when you need to shut down a Bubble Tea program from the outside.
1151//
1152// If you wish to quit from within a Bubble Tea program use the Quit command.
1153//
1154// If the program is not running this will be a no-op, so it's safe to call
1155// if the program is unstarted or has already exited.
1156func (p *Program) Quit() {
1157 p.Send(Quit())
1158}
1159
1160// Kill stops the program immediately and restores the former terminal state.
1161// The final render that you would normally see when quitting will be skipped.
1162// [program.Run] returns a [ErrProgramKilled] error.
1163func (p *Program) Kill() {
1164 p.shutdown(true)
1165}
1166
1167// Wait waits/blocks until the underlying Program finished shutting down.
1168func (p *Program) Wait() {
1169 <-p.finished
1170}
1171
1172// execute writes the given sequence to the program output.
1173func (p *Program) execute(seq string) {
1174 _, _ = p.outputBuf.WriteString(seq)
1175}
1176
1177// flush flushes the output buffer to the program output.
1178func (p *Program) flush() error {
1179 if p.outputBuf.Len() == 0 {
1180 return nil
1181 }
1182 if p.logger != nil {
1183 p.logger.Printf("output: %q", p.outputBuf.String())
1184 }
1185 _, err := p.output.Write(p.outputBuf.Bytes())
1186 p.outputBuf.Reset()
1187 if err != nil {
1188 return fmt.Errorf("error writing to output: %w", err)
1189 }
1190 return nil
1191}
1192
1193// shutdown performs operations to free up resources and restore the terminal
1194// to its original state.
1195func (p *Program) shutdown(kill bool) {
1196 p.shutdownOnce.Do(func() {
1197 p.cancel()
1198
1199 // Wait for all handlers to finish.
1200 p.handlers.shutdown()
1201
1202 // Check if the cancel reader has been setup before waiting and closing.
1203 if p.inputReader != nil {
1204 // Wait for input loop to finish.
1205 if p.inputReader.Cancel() {
1206 if !kill {
1207 p.waitForReadLoop()
1208 }
1209 }
1210 _ = p.inputReader.Close()
1211 }
1212
1213 if p.renderer != nil {
1214 p.stopRenderer(kill)
1215 }
1216
1217 _ = p.restoreTerminalState()
1218 })
1219}
1220
1221// recoverFromPanic recovers from a panic, prints the stack trace, and restores
1222// the terminal to a usable state.
1223func (p *Program) recoverFromPanic(r any) {
1224 select {
1225 case p.errs <- ErrProgramPanic:
1226 default:
1227 }
1228 p.cancel() // Just in case a previous shutdown has failed.
1229 p.shutdown(true)
1230 // We use "\r\n" to ensure the output is formatted even when restoring the
1231 // terminal does not work or when raw mode is still active.
1232 rec := strings.ReplaceAll(fmt.Sprintf("%s", r), "\n", "\r\n")
1233 fmt.Fprintf(os.Stderr, "Caught panic:\r\n\r\n%s\r\n\r\nRestoring terminal...\r\n\r\n", rec)
1234 stack := strings.ReplaceAll(fmt.Sprintf("%s\n", debug.Stack()), "\n", "\r\n")
1235 fmt.Fprint(os.Stderr, stack)
1236 if v, err := strconv.ParseBool(os.Getenv("TEA_DEBUG")); err == nil && v {
1237 f, err := os.Create(fmt.Sprintf("bubbletea-panic-%d.log", time.Now().Unix()))
1238 if err == nil {
1239 defer f.Close() //nolint:errcheck
1240 fmt.Fprintln(f, rec) //nolint:errcheck
1241 fmt.Fprintln(f) //nolint:errcheck
1242 fmt.Fprintln(f, stack) //nolint:errcheck
1243 }
1244 }
1245}
1246
1247// ReleaseTerminal restores the original terminal state and cancels the input
1248// reader. You can return control to the Program with RestoreTerminal.
1249func (p *Program) ReleaseTerminal() error {
1250 return p.releaseTerminal(false)
1251}
1252
1253func (p *Program) releaseTerminal(reset bool) error {
1254 atomic.StoreUint32(&p.ignoreSignals, 1)
1255 if p.inputReader != nil {
1256 p.inputReader.Cancel()
1257 }
1258
1259 p.waitForReadLoop()
1260
1261 if p.renderer != nil {
1262 p.stopRenderer(false)
1263 if reset {
1264 p.renderer.reset()
1265 }
1266 }
1267
1268 return p.restoreTerminalState()
1269}
1270
1271// RestoreTerminal reinitializes the Program's input reader, restores the
1272// terminal to the former state when the program was running, and repaints.
1273// Use it to reinitialize a Program after running ReleaseTerminal.
1274func (p *Program) RestoreTerminal() error {
1275 atomic.StoreUint32(&p.ignoreSignals, 0)
1276
1277 if err := p.initTerminal(); err != nil {
1278 return err
1279 }
1280 if err := p.initInputReader(false); err != nil {
1281 return err
1282 }
1283 if p.modes.IsReset(ansi.AltScreenSaveCursorMode) {
1284 // entering alt screen already causes a repaint.
1285 go p.Send(repaintMsg{})
1286 }
1287
1288 p.startRenderer()
1289 if p.modes.IsSet(ansi.BracketedPasteMode) {
1290 p.execute(ansi.SetBracketedPasteMode)
1291 }
1292 if p.activeEnhancements.modifyOtherKeys != 0 {
1293 p.execute(ansi.KeyModifierOptions(4, p.activeEnhancements.modifyOtherKeys)) //nolint:mnd
1294 }
1295 if p.activeEnhancements.kittyFlags != 0 {
1296 p.execute(ansi.PushKittyKeyboard(p.activeEnhancements.kittyFlags))
1297 }
1298 if p.modes.IsSet(ansi.FocusEventMode) {
1299 p.execute(ansi.SetFocusEventMode)
1300 }
1301 if p.modes.IsSet(ansi.ButtonEventMouseMode) || p.modes.IsSet(ansi.AnyEventMouseMode) {
1302 if p.startupOptions&withMouseCellMotion != 0 {
1303 p.execute(ansi.SetButtonEventMouseMode)
1304 p.execute(ansi.SetSgrExtMouseMode)
1305 } else if p.startupOptions&withMouseAllMotion != 0 {
1306 p.execute(ansi.SetAnyEventMouseMode)
1307 p.execute(ansi.SetSgrExtMouseMode)
1308 }
1309 }
1310 if p.modes.IsSet(ansi.GraphemeClusteringMode) {
1311 p.execute(ansi.SetGraphemeClusteringMode)
1312 }
1313
1314 // Restore terminal colors.
1315 if p.setBg != nil {
1316 c, ok := colorful.MakeColor(p.setBg)
1317 if ok {
1318 p.execute(ansi.SetBackgroundColor(c.Hex()))
1319 }
1320 }
1321 if p.setFg != nil {
1322 c, ok := colorful.MakeColor(p.setFg)
1323 if ok {
1324 p.execute(ansi.SetForegroundColor(c.Hex()))
1325 }
1326 }
1327 if p.setCc != nil {
1328 c, ok := colorful.MakeColor(p.setCc)
1329 if ok {
1330 p.execute(ansi.SetCursorColor(c.Hex()))
1331 }
1332 }
1333
1334 // If the output is a terminal, it may have been resized while another
1335 // process was at the foreground, in which case we may not have received
1336 // SIGWINCH. Detect any size change now and propagate the new size as
1337 // needed.
1338 go p.checkResize()
1339
1340 // Flush queued commands.
1341 return p.flush()
1342}
1343
1344// Println prints above the Program. This output is unmanaged by the program
1345// and will persist across renders by the Program.
1346//
1347// If the altscreen is active no output will be printed.
1348func (p *Program) Println(args ...any) {
1349 p.msgs <- printLineMessage{
1350 messageBody: fmt.Sprint(args...),
1351 }
1352}
1353
1354// Printf prints above the Program. It takes a format template followed by
1355// values similar to fmt.Printf. This output is unmanaged by the program and
1356// will persist across renders by the Program.
1357//
1358// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
1359// its own line.
1360//
1361// If the altscreen is active no output will be printed.
1362func (p *Program) Printf(template string, args ...any) {
1363 p.msgs <- printLineMessage{
1364 messageBody: fmt.Sprintf(template, args...),
1365 }
1366}
1367
1368// startRenderer starts the renderer.
1369func (p *Program) startRenderer() {
1370 framerate := time.Second / time.Duration(p.fps)
1371 if p.ticker == nil {
1372 p.ticker = time.NewTicker(framerate)
1373 } else {
1374 // If the ticker already exists, it has been stopped and we need to
1375 // reset it.
1376 p.ticker.Reset(framerate)
1377 }
1378
1379 // Since the renderer can be restarted after a stop, we need to reset
1380 // the done channel and its corresponding sync.Once.
1381 p.once = sync.Once{}
1382
1383 // Start the renderer.
1384 go func() {
1385 for {
1386 select {
1387 case <-p.rendererDone:
1388 p.ticker.Stop()
1389 return
1390
1391 case <-p.ticker.C:
1392 _ = p.flush()
1393 _ = p.renderer.flush(p)
1394 }
1395 }
1396 }()
1397}
1398
1399// stopRenderer stops the renderer.
1400// If kill is true, the renderer will be stopped immediately without flushing
1401// the last frame.
1402func (p *Program) stopRenderer(kill bool) {
1403 // Stop the renderer before acquiring the mutex to avoid a deadlock.
1404 p.once.Do(func() {
1405 p.rendererDone <- struct{}{}
1406 })
1407
1408 if !kill {
1409 // flush locks the mutex
1410 _ = p.renderer.flush(p)
1411 }
1412
1413 _ = p.renderer.close()
1414}
1415
1416// requestKeyboardEnhancements tries to enable keyboard enhancements and read
1417// the active keyboard enhancements from the terminal.
1418func (p *Program) requestKeyboardEnhancements() {
1419 // XXX: We write to the renderer directly so that we synchronize with the
1420 // alt-screen state of the renderer. This is because the main screen and
1421 // alternate screen have their own Kitty keyboard state stack.
1422 if p.requestedEnhancements.modifyOtherKeys > 0 {
1423 _, _ = p.renderer.writeString(ansi.KeyModifierOptions(4, p.requestedEnhancements.modifyOtherKeys)) //nolint:mnd
1424 _, _ = p.renderer.writeString(ansi.QueryModifyOtherKeys)
1425 }
1426 if p.requestedEnhancements.kittyFlags > 0 {
1427 _, _ = p.renderer.writeString(ansi.PushKittyKeyboard(p.requestedEnhancements.kittyFlags))
1428 _, _ = p.renderer.writeString(ansi.RequestKittyKeyboard)
1429 }
1430}
1431
1432// enableMouse enables mouse events on the terminal. When all is true, it will
1433// enable [ansi.AnyEventMouseMode], otherwise, it will use
1434// [ansi.ButtonEventMouseMode].
1435// Note this has no effect on Windows since we use the Windows Console API.
1436func (p *Program) enableMouse(all bool) {
1437 if isWindows() {
1438 // XXX: This is used to enable mouse mode on Windows. We need
1439 // to reinitialize the cancel reader to get the mouse events to
1440 // work.
1441 if !p.mouseMode {
1442 p.mouseMode = true
1443 if p.inputReader != nil {
1444 // Only reinitialize if the input reader has been initialized.
1445 _ = p.initInputReader(true)
1446 }
1447 }
1448 }
1449
1450 if all {
1451 p.execute(ansi.SetAnyEventMouseMode + ansi.SetSgrExtMouseMode)
1452 p.modes.Set(ansi.AnyEventMouseMode, ansi.SgrExtMouseMode)
1453 } else {
1454 p.execute(ansi.SetButtonEventMouseMode + ansi.SetSgrExtMouseMode)
1455 p.modes.Set(ansi.ButtonEventMouseMode, ansi.SgrExtMouseMode)
1456 }
1457}
1458
1459// disableMouse disables mouse events on the terminal.
1460// Note this has no effect on Windows since we use the Windows Console API.
1461func (p *Program) disableMouse() {
1462 if isWindows() {
1463 // XXX: On Windows, mouse mode is enabled on the input reader
1464 // level. We need to instruct the input reader to stop reading
1465 // mouse events.
1466 if p.mouseMode {
1467 p.mouseMode = false
1468 if p.inputReader != nil {
1469 // Only reinitialize if the input reader has been initialized.
1470 _ = p.initInputReader(true)
1471 }
1472 }
1473 }
1474
1475 var modes []ansi.Mode
1476 if p.modes.IsSet(ansi.AnyEventMouseMode) {
1477 modes = append(modes, ansi.AnyEventMouseMode)
1478 }
1479 if p.modes.IsSet(ansi.ButtonEventMouseMode) {
1480 modes = append(modes, ansi.ButtonEventMouseMode)
1481 }
1482 if len(modes) > 0 {
1483 modes = append(modes, ansi.SgrExtMouseMode)
1484 for _, m := range modes {
1485 // We could combine all of these modes into one single sequence,
1486 // but we're being cautious here for terminals that might not support
1487 // that format i.e. `CSI ? 10003 ; 1006 l`.
1488 p.execute(ansi.ResetMode(m))
1489 p.modes.Reset(m)
1490 }
1491 }
1492}