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