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 // Main and alternate screen have their own Kitty keyboard
628 // stack. We need to request keyboard enhancements again
629 // when entering/exiting the alternate screen.
630 p.requestKeyboardEnhancements()
631 case ansi.TextCursorEnableMode:
632 p.renderer.showCursor()
633 case ansi.GraphemeClusteringMode:
634 // We store the state of grapheme clustering after we enable it
635 // and get a response in the eventLoop.
636 p.execute(ansi.SetGraphemeClusteringMode + ansi.RequestGraphemeClusteringMode)
637 default:
638 p.execute(ansi.SetMode(msg.Mode))
639 }
640
641 case disableModeMsg:
642 mode := p.modes.Get(msg.Mode)
643 if mode.IsReset() {
644 break
645 }
646
647 p.modes.Reset(msg.Mode)
648
649 switch msg.Mode {
650 case ansi.AltScreenSaveCursorMode:
651 p.renderer.exitAltScreen()
652 // Main and alternate screen have their own Kitty keyboard
653 // stack. We need to request keyboard enhancements again
654 // when entering/exiting the alternate screen.
655 p.requestKeyboardEnhancements()
656 case ansi.TextCursorEnableMode:
657 p.renderer.hideCursor()
658 default:
659 p.execute(ansi.ResetMode(msg.Mode))
660 }
661
662 case enableMouseCellMotionMsg:
663 p.enableMouse(false)
664
665 case enableMouseAllMotionMsg:
666 p.enableMouse(true)
667
668 case disableMouseMotionMsg:
669 p.disableMouse()
670
671 case readClipboardMsg:
672 p.execute(ansi.RequestSystemClipboard)
673
674 case setClipboardMsg:
675 p.execute(ansi.SetSystemClipboard(string(msg)))
676
677 case readPrimaryClipboardMsg:
678 p.execute(ansi.RequestPrimaryClipboard)
679
680 case setPrimaryClipboardMsg:
681 p.execute(ansi.SetPrimaryClipboard(string(msg)))
682
683 case setBackgroundColorMsg:
684 // The renderer handles flushing the color to the terminal.
685 p.lastBgColor = msg.Color
686
687 case setForegroundColorMsg:
688 // The renderer handles flushing the color to the terminal.
689 p.lastFgColor = msg.Color
690
691 case setCursorColorMsg:
692 // The renderer handles flushing the color to the terminal.
693 p.lastCursorColor = msg.Color
694
695 case backgroundColorMsg:
696 p.execute(ansi.RequestBackgroundColor)
697
698 case foregroundColorMsg:
699 p.execute(ansi.RequestForegroundColor)
700
701 case cursorColorMsg:
702 p.execute(ansi.RequestCursorColor)
703
704 case KeyboardEnhancementsMsg:
705 p.activeEnhancements.kittyFlags = msg.kittyFlags
706 p.activeEnhancements.modifyOtherKeys = msg.modifyOtherKeys
707
708 case enableKeyboardEnhancementsMsg:
709 if p.startupOptions.has(withoutKeyEnhancements) {
710 break
711 }
712
713 if isWindows() {
714 // We use the Windows Console API which supports keyboard
715 // enhancements.
716 // Send an empty message to tell the user we support
717 // keyboard enhancements on Windows.
718 go p.Send(KeyboardEnhancementsMsg{})
719 break
720 }
721
722 var ke KeyboardEnhancements
723 for _, e := range msg {
724 e(&ke)
725 }
726
727 p.requestedEnhancements.kittyFlags |= ke.kittyFlags
728 if ke.modifyOtherKeys > p.requestedEnhancements.modifyOtherKeys {
729 p.requestedEnhancements.modifyOtherKeys = ke.modifyOtherKeys
730 }
731
732 p.requestKeyboardEnhancements()
733
734 case disableKeyboardEnhancementsMsg:
735 if p.startupOptions.has(withoutKeyEnhancements) {
736 break
737 }
738
739 if isWindows() {
740 // We use the Windows Console API which supports keyboard
741 // enhancements.
742 break
743 }
744
745 if p.activeEnhancements.modifyOtherKeys > 0 {
746 p.execute(ansi.ResetModifyOtherKeys)
747 p.activeEnhancements.modifyOtherKeys = 0
748 p.requestedEnhancements.modifyOtherKeys = 0
749 }
750 if p.activeEnhancements.kittyFlags > 0 {
751 p.execute(ansi.DisableKittyKeyboard)
752 p.activeEnhancements.kittyFlags = 0
753 p.requestedEnhancements.kittyFlags = 0
754 }
755
756 case execMsg:
757 // NB: this blocks.
758 p.exec(msg.cmd, msg.fn)
759
760 case terminalVersion:
761 p.execute(ansi.RequestNameVersion)
762
763 case requestCapabilityMsg:
764 p.execute(ansi.RequestTermcap(string(msg)))
765
766 case BatchMsg:
767 for _, cmd := range msg {
768 select {
769 case <-p.ctx.Done():
770 return model, nil
771 case cmds <- cmd:
772 }
773 }
774 continue
775
776 case sequenceMsg:
777 go func() {
778 // Execute commands one at a time, in order.
779 for _, cmd := range msg {
780 if cmd == nil {
781 continue
782 }
783
784 switch msg := cmd().(type) {
785 case BatchMsg:
786 g, _ := errgroup.WithContext(p.ctx)
787 for _, cmd := range msg {
788 cmd := cmd
789 g.Go(func() error {
790 p.Send(cmd())
791 return nil
792 })
793 }
794
795 _ = g.Wait() // wait for all commands from batch msg to finish
796 continue
797 case sequenceMsg:
798 for _, cmd := range msg {
799 p.Send(cmd())
800 }
801 default:
802 p.Send(msg)
803 }
804 }
805 }()
806
807 case setWindowTitleMsg:
808 p.renderer.setWindowTitle(p.lastWindowTitle)
809 p.lastWindowTitle = string(msg)
810
811 case WindowSizeMsg:
812 p.renderer.resize(msg.Width, msg.Height)
813
814 case windowSizeMsg:
815 go p.checkResize()
816
817 case requestCursorPosMsg:
818 p.execute(ansi.RequestCursorPositionReport)
819
820 case RawMsg:
821 p.execute(fmt.Sprint(msg.Msg))
822
823 case printLineMessage:
824 p.renderer.insertAbove(msg.messageBody)
825
826 case repaintMsg:
827 p.renderer.repaint()
828
829 case clearScreenMsg:
830 p.renderer.clearScreen()
831
832 case ColorProfileMsg:
833 p.renderer.setColorProfile(msg.Profile)
834 }
835
836 var cmd Cmd
837 model, cmd = model.Update(msg) // run update
838
839 select {
840 case <-p.ctx.Done():
841 return model, nil
842 case cmds <- cmd: // process command (if any)
843 }
844
845 p.render(model) // render view
846 }
847 }
848}
849
850// hasView returns true if the model has a view.
851func hasView(model Model) (ok bool) {
852 switch model.(type) {
853 case ViewModel, CursorModel, ViewableModel:
854 ok = true
855 }
856 return
857}
858
859// render renders the given view to the renderer.
860func (p *Program) render(model Model) {
861 var view View
862 switch model := model.(type) {
863 case ViewModel, CursorModel:
864 var frame string
865 switch model := model.(type) {
866 case ViewModel:
867 frame = model.View()
868 case CursorModel:
869 frame, view.Cursor = model.View()
870 }
871 view.Layer = uv.NewStyledString(frame)
872 view.BackgroundColor = p.lastBgColor
873 view.ForegroundColor = p.lastFgColor
874 view.WindowTitle = p.lastWindowTitle
875 if view.Cursor != nil && p.lastCursorColor != nil {
876 view.Cursor.Color = p.lastCursorColor
877 }
878 case ViewableModel:
879 view = model.View()
880 }
881 if p.renderer != nil {
882 p.renderer.render(view) // send view to renderer
883 }
884}
885
886// Run initializes the program and runs its event loops, blocking until it gets
887// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
888// Returns the final model.
889func (p *Program) Run() (returnModel Model, returnErr error) {
890 p.handlers = channelHandlers{}
891 cmds := make(chan Cmd)
892 p.errs = make(chan error, 1)
893
894 p.finished = make(chan struct{})
895 defer func() {
896 close(p.finished)
897 }()
898
899 defer p.cancel()
900
901 switch p.inputType {
902 case defaultInput:
903 p.input = os.Stdin
904
905 // The user has not set a custom input, so we need to check whether or
906 // not standard input is a terminal. If it's not, we open a new TTY for
907 // input. This will allow things to "just work" in cases where data was
908 // piped in or redirected to the application.
909 //
910 // To disable input entirely pass nil to the [WithInput] program option.
911 f, isFile := p.input.(term.File)
912 if !isFile {
913 break
914 }
915 if term.IsTerminal(f.Fd()) {
916 break
917 }
918
919 f, err := openInputTTY()
920 if err != nil {
921 return p.initialModel, err
922 }
923 defer f.Close() //nolint:errcheck
924 p.input = f
925
926 case ttyInput:
927 // Open a new TTY, by request
928 f, err := openInputTTY()
929 if err != nil {
930 return p.initialModel, err
931 }
932 defer f.Close() //nolint:errcheck
933 p.input = f
934
935 case customInput:
936 // (There is nothing extra to do.)
937 }
938
939 // Handle signals.
940 if !p.startupOptions.has(withoutSignalHandler) {
941 p.handlers.add(p.handleSignals())
942 }
943
944 // Recover from panics.
945 if !p.startupOptions.has(withoutCatchPanics) {
946 defer func() {
947 if r := recover(); r != nil {
948 returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic)
949 p.recoverFromPanic(r)
950 }
951 }()
952 }
953
954 // Check if output is a TTY before entering raw mode, hiding the cursor and
955 // so on.
956 if err := p.initTerminal(); err != nil {
957 return p.initialModel, err
958 }
959
960 // Get the initial window size.
961 resizeMsg := WindowSizeMsg{Width: p.width, Height: p.height}
962 if p.ttyOutput != nil {
963 // Set the initial size of the terminal.
964 w, h, err := term.GetSize(p.ttyOutput.Fd())
965 if err != nil {
966 return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
967 }
968
969 resizeMsg.Width, resizeMsg.Height = w, h
970 }
971
972 if p.renderer == nil { //nolint:nestif
973 if hasView(p.initialModel) {
974 stdr, ok := os.LookupEnv("TEA_STANDARD_RENDERER")
975 if has, _ := strconv.ParseBool(stdr); ok && has {
976 p.renderer = newRenderer(p.output)
977 } else {
978 // If no renderer is set use the cursed one.
979 p.renderer = newCursedRenderer(
980 p.output,
981 p.environ,
982 resizeMsg.Width,
983 resizeMsg.Height,
984 p.useHardTabs,
985 p.useBackspace,
986 p.ttyInput == nil,
987 p.logger,
988 )
989 }
990 } else {
991 // If the model has no view we don't need a renderer.
992 p.renderer = &nilRenderer{}
993 }
994 }
995
996 // Get the color profile and send it to the program.
997 if !p.startupOptions.has(withColorProfile) {
998 p.profile = colorprofile.Detect(p.output, p.environ)
999 }
1000
1001 // Set the color profile on the renderer and send it to the program.
1002 p.renderer.setColorProfile(p.profile)
1003 go p.Send(ColorProfileMsg{p.profile})
1004
1005 // Send the initial size to the program.
1006 go p.Send(resizeMsg)
1007 p.renderer.resize(resizeMsg.Width, resizeMsg.Height)
1008
1009 // Send the environment variables used by the program.
1010 go p.Send(EnvMsg(p.environ))
1011
1012 // Init the input reader and initial model.
1013 model := p.initialModel
1014 if p.input != nil {
1015 if err := p.initInputReader(false); err != nil {
1016 return model, err
1017 }
1018 }
1019
1020 // Hide the cursor before starting the renderer. This is handled by the
1021 // renderer so we don't need to write the sequence here.
1022 p.modes.Reset(ansi.TextCursorEnableMode)
1023 p.renderer.hideCursor()
1024
1025 // Honor program startup options.
1026 if p.startupTitle != "" {
1027 p.execute(ansi.SetWindowTitle(p.startupTitle))
1028 }
1029 if p.startupOptions&withAltScreen != 0 {
1030 // Enter alternate screen mode. This is handled by the renderer so we
1031 // don't need to write the sequence here.
1032 p.modes.Set(ansi.AltScreenSaveCursorMode)
1033 p.renderer.enterAltScreen()
1034 }
1035 if p.startupOptions&withoutBracketedPaste == 0 {
1036 p.execute(ansi.SetBracketedPasteMode)
1037 p.modes.Set(ansi.BracketedPasteMode)
1038 }
1039 if p.startupOptions&withGraphemeClustering != 0 {
1040 p.execute(ansi.SetGraphemeClusteringMode)
1041 p.execute(ansi.RequestGraphemeClusteringMode)
1042 // We store the state of grapheme clustering after we query it and get
1043 // a response in the eventLoop.
1044 }
1045
1046 // Enable mouse mode.
1047 cellMotion := p.startupOptions&withMouseCellMotion != 0
1048 allMotion := p.startupOptions&withMouseAllMotion != 0
1049 if cellMotion || allMotion {
1050 p.enableMouse(allMotion)
1051 }
1052
1053 if p.startupOptions&withReportFocus != 0 {
1054 p.execute(ansi.SetFocusEventMode)
1055 p.modes.Set(ansi.FocusEventMode)
1056 }
1057
1058 if !p.startupOptions.has(withoutKeyEnhancements) {
1059 if !isWindows() {
1060 // Enable unambiguous keys using whichever protocol the terminal prefer.
1061 p.requestedEnhancements.kittyFlags |= ansi.KittyDisambiguateEscapeCodes
1062 if p.requestedEnhancements.modifyOtherKeys == 0 {
1063 p.requestedEnhancements.modifyOtherKeys = 1 // mode 1
1064 }
1065 // We use the Windows Console API which supports keyboard
1066 // enhancements.
1067 p.requestKeyboardEnhancements()
1068 } else {
1069 // Send an empty message to tell the user we support
1070 // keyboard enhancements on Windows.
1071 go p.Send(KeyboardEnhancementsMsg{})
1072 }
1073 }
1074
1075 // Start the renderer.
1076 p.startRenderer()
1077
1078 // Initialize the program.
1079 initCmd := model.Init()
1080 if initCmd != nil {
1081 ch := make(chan struct{})
1082 p.handlers.add(ch)
1083
1084 go func() {
1085 defer close(ch)
1086
1087 select {
1088 case cmds <- initCmd:
1089 case <-p.ctx.Done():
1090 }
1091 }()
1092 }
1093
1094 // Render the initial view.
1095 p.render(model)
1096
1097 // Handle resize events.
1098 p.handlers.add(p.handleResize())
1099
1100 // Process commands.
1101 p.handlers.add(p.handleCommands(cmds))
1102
1103 // Run event loop, handle updates and draw.
1104 var err error
1105 model, err = p.eventLoop(model, cmds)
1106
1107 if err == nil && len(p.errs) > 0 {
1108 err = <-p.errs // Drain a leftover error in case eventLoop crashed.
1109 }
1110
1111 killed := p.externalCtx.Err() != nil || p.ctx.Err() != nil || err != nil
1112 if killed {
1113 if err == nil && p.externalCtx.Err() != nil {
1114 // Return also as context error the cancellation of an external context.
1115 // This is the context the user knows about and should be able to act on.
1116 err = fmt.Errorf("%w: %w", ErrProgramKilled, p.externalCtx.Err())
1117 } else if err == nil && p.ctx.Err() != nil {
1118 // Return only that the program was killed (not the internal mechanism).
1119 // The user does not know or need to care about the internal program context.
1120 err = ErrProgramKilled
1121 } else {
1122 // Return that the program was killed and also the error that caused it.
1123 err = fmt.Errorf("%w: %w", ErrProgramKilled, err)
1124 }
1125 } else {
1126 // Graceful shutdown of the program (not killed):
1127 // Ensure we rendered the final state of the model.
1128 p.render(model)
1129 }
1130
1131 // Restore terminal state.
1132 p.shutdown(killed)
1133
1134 return model, err
1135}
1136
1137// Send sends a message to the main update function, effectively allowing
1138// messages to be injected from outside the program for interoperability
1139// purposes.
1140//
1141// If the program hasn't started yet this will be a blocking operation.
1142// If the program has already been terminated this will be a no-op, so it's safe
1143// to send messages after the program has exited.
1144func (p *Program) Send(msg Msg) {
1145 select {
1146 case <-p.ctx.Done():
1147 case p.msgs <- msg:
1148 }
1149}
1150
1151// Quit is a convenience function for quitting Bubble Tea programs. Use it
1152// when you need to shut down a Bubble Tea program from the outside.
1153//
1154// If you wish to quit from within a Bubble Tea program use the Quit command.
1155//
1156// If the program is not running this will be a no-op, so it's safe to call
1157// if the program is unstarted or has already exited.
1158func (p *Program) Quit() {
1159 p.Send(Quit())
1160}
1161
1162// Kill stops the program immediately and restores the former terminal state.
1163// The final render that you would normally see when quitting will be skipped.
1164// [program.Run] returns a [ErrProgramKilled] error.
1165func (p *Program) Kill() {
1166 p.shutdown(true)
1167}
1168
1169// Wait waits/blocks until the underlying Program finished shutting down.
1170func (p *Program) Wait() {
1171 <-p.finished
1172}
1173
1174// execute writes the given sequence to the program output.
1175func (p *Program) execute(seq string) {
1176 _, _ = p.outputBuf.WriteString(seq)
1177}
1178
1179// flush flushes the output buffer to the program output.
1180func (p *Program) flush() error {
1181 if p.outputBuf.Len() == 0 {
1182 return nil
1183 }
1184 if p.logger != nil {
1185 p.logger.Printf("output: %q", p.outputBuf.String())
1186 }
1187 _, err := p.output.Write(p.outputBuf.Bytes())
1188 p.outputBuf.Reset()
1189 if err != nil {
1190 return fmt.Errorf("error writing to output: %w", err)
1191 }
1192 return nil
1193}
1194
1195// shutdown performs operations to free up resources and restore the terminal
1196// to its original state.
1197func (p *Program) shutdown(kill bool) {
1198 p.shutdownOnce.Do(func() {
1199 p.cancel()
1200
1201 // Wait for all handlers to finish.
1202 p.handlers.shutdown()
1203
1204 // Check if the cancel reader has been setup before waiting and closing.
1205 if p.inputReader != nil {
1206 // Wait for input loop to finish.
1207 if p.inputReader.Cancel() {
1208 if !kill {
1209 p.waitForReadLoop()
1210 }
1211 }
1212 _ = p.inputReader.Close()
1213 }
1214
1215 if p.renderer != nil {
1216 p.stopRenderer(kill)
1217 }
1218
1219 _ = p.restoreTerminalState()
1220 })
1221}
1222
1223// recoverFromPanic recovers from a panic, prints the stack trace, and restores
1224// the terminal to a usable state.
1225func (p *Program) recoverFromPanic(r any) {
1226 select {
1227 case p.errs <- ErrProgramPanic:
1228 default:
1229 }
1230 p.cancel() // Just in case a previous shutdown has failed.
1231 p.shutdown(true)
1232 // We use "\r\n" to ensure the output is formatted even when restoring the
1233 // terminal does not work or when raw mode is still active.
1234 rec := strings.ReplaceAll(fmt.Sprintf("%s", r), "\n", "\r\n")
1235 fmt.Fprintf(os.Stderr, "Caught panic:\r\n\r\n%s\r\n\r\nRestoring terminal...\r\n\r\n", rec)
1236 stack := strings.ReplaceAll(fmt.Sprintf("%s\n", debug.Stack()), "\n", "\r\n")
1237 fmt.Fprint(os.Stderr, stack)
1238 if v, err := strconv.ParseBool(os.Getenv("TEA_DEBUG")); err == nil && v {
1239 f, err := os.Create(fmt.Sprintf("bubbletea-panic-%d.log", time.Now().Unix()))
1240 if err == nil {
1241 defer f.Close() //nolint:errcheck
1242 fmt.Fprintln(f, rec) //nolint:errcheck
1243 fmt.Fprintln(f) //nolint:errcheck
1244 fmt.Fprintln(f, stack) //nolint:errcheck
1245 }
1246 }
1247}
1248
1249// ReleaseTerminal restores the original terminal state and cancels the input
1250// reader. You can return control to the Program with RestoreTerminal.
1251func (p *Program) ReleaseTerminal() error {
1252 return p.releaseTerminal(false)
1253}
1254
1255func (p *Program) releaseTerminal(reset bool) error {
1256 atomic.StoreUint32(&p.ignoreSignals, 1)
1257 if p.inputReader != nil {
1258 p.inputReader.Cancel()
1259 }
1260
1261 p.waitForReadLoop()
1262
1263 if p.renderer != nil {
1264 p.stopRenderer(false)
1265 if reset {
1266 p.renderer.reset()
1267 }
1268 }
1269
1270 return p.restoreTerminalState()
1271}
1272
1273// RestoreTerminal reinitializes the Program's input reader, restores the
1274// terminal to the former state when the program was running, and repaints.
1275// Use it to reinitialize a Program after running ReleaseTerminal.
1276func (p *Program) RestoreTerminal() error {
1277 atomic.StoreUint32(&p.ignoreSignals, 0)
1278
1279 if err := p.initTerminal(); err != nil {
1280 return err
1281 }
1282 if err := p.initInputReader(false); err != nil {
1283 return err
1284 }
1285 if p.modes.IsReset(ansi.AltScreenSaveCursorMode) {
1286 // entering alt screen already causes a repaint.
1287 go p.Send(repaintMsg{})
1288 }
1289
1290 p.startRenderer()
1291 if p.modes.IsSet(ansi.BracketedPasteMode) {
1292 p.execute(ansi.SetBracketedPasteMode)
1293 }
1294 if p.activeEnhancements.modifyOtherKeys != 0 {
1295 p.execute(ansi.KeyModifierOptions(4, p.activeEnhancements.modifyOtherKeys)) //nolint:mnd
1296 }
1297 if p.activeEnhancements.kittyFlags != 0 {
1298 p.execute(ansi.PushKittyKeyboard(p.activeEnhancements.kittyFlags))
1299 }
1300 if p.modes.IsSet(ansi.FocusEventMode) {
1301 p.execute(ansi.SetFocusEventMode)
1302 }
1303 if p.modes.IsSet(ansi.ButtonEventMouseMode) || p.modes.IsSet(ansi.AnyEventMouseMode) {
1304 if p.startupOptions&withMouseCellMotion != 0 {
1305 p.execute(ansi.SetButtonEventMouseMode)
1306 p.execute(ansi.SetSgrExtMouseMode)
1307 } else if p.startupOptions&withMouseAllMotion != 0 {
1308 p.execute(ansi.SetAnyEventMouseMode)
1309 p.execute(ansi.SetSgrExtMouseMode)
1310 }
1311 }
1312 if p.modes.IsSet(ansi.GraphemeClusteringMode) {
1313 p.execute(ansi.SetGraphemeClusteringMode)
1314 }
1315
1316 // Restore terminal colors.
1317 if p.setBg != nil {
1318 c, ok := colorful.MakeColor(p.setBg)
1319 if ok {
1320 p.execute(ansi.SetBackgroundColor(c.Hex()))
1321 }
1322 }
1323 if p.setFg != nil {
1324 c, ok := colorful.MakeColor(p.setFg)
1325 if ok {
1326 p.execute(ansi.SetForegroundColor(c.Hex()))
1327 }
1328 }
1329 if p.setCc != nil {
1330 c, ok := colorful.MakeColor(p.setCc)
1331 if ok {
1332 p.execute(ansi.SetCursorColor(c.Hex()))
1333 }
1334 }
1335
1336 // If the output is a terminal, it may have been resized while another
1337 // process was at the foreground, in which case we may not have received
1338 // SIGWINCH. Detect any size change now and propagate the new size as
1339 // needed.
1340 go p.checkResize()
1341
1342 // Flush queued commands.
1343 return p.flush()
1344}
1345
1346// Println prints above the Program. This output is unmanaged by the program
1347// and will persist across renders by the Program.
1348//
1349// If the altscreen is active no output will be printed.
1350func (p *Program) Println(args ...any) {
1351 p.msgs <- printLineMessage{
1352 messageBody: fmt.Sprint(args...),
1353 }
1354}
1355
1356// Printf prints above the Program. It takes a format template followed by
1357// values similar to fmt.Printf. This output is unmanaged by the program and
1358// will persist across renders by the Program.
1359//
1360// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
1361// its own line.
1362//
1363// If the altscreen is active no output will be printed.
1364func (p *Program) Printf(template string, args ...any) {
1365 p.msgs <- printLineMessage{
1366 messageBody: fmt.Sprintf(template, args...),
1367 }
1368}
1369
1370// startRenderer starts the renderer.
1371func (p *Program) startRenderer() {
1372 framerate := time.Second / time.Duration(p.fps)
1373 if p.ticker == nil {
1374 p.ticker = time.NewTicker(framerate)
1375 } else {
1376 // If the ticker already exists, it has been stopped and we need to
1377 // reset it.
1378 p.ticker.Reset(framerate)
1379 }
1380
1381 // Since the renderer can be restarted after a stop, we need to reset
1382 // the done channel and its corresponding sync.Once.
1383 p.once = sync.Once{}
1384
1385 // Start the renderer.
1386 go func() {
1387 for {
1388 select {
1389 case <-p.rendererDone:
1390 p.ticker.Stop()
1391 return
1392
1393 case <-p.ticker.C:
1394 _ = p.flush()
1395 _ = p.renderer.flush(p)
1396 }
1397 }
1398 }()
1399}
1400
1401// stopRenderer stops the renderer.
1402// If kill is true, the renderer will be stopped immediately without flushing
1403// the last frame.
1404func (p *Program) stopRenderer(kill bool) {
1405 // Stop the renderer before acquiring the mutex to avoid a deadlock.
1406 p.once.Do(func() {
1407 p.rendererDone <- struct{}{}
1408 })
1409
1410 if !kill {
1411 // flush locks the mutex
1412 _ = p.renderer.flush(p)
1413 }
1414
1415 _ = p.renderer.close()
1416}
1417
1418// requestKeyboardEnhancements tries to enable keyboard enhancements and read
1419// the active keyboard enhancements from the terminal.
1420func (p *Program) requestKeyboardEnhancements() {
1421 // XXX: We write to the renderer directly so that we synchronize with the
1422 // alt-screen state of the renderer. This is because the main screen and
1423 // alternate screen have their own Kitty keyboard state stack.
1424 if p.requestedEnhancements.modifyOtherKeys > 0 {
1425 _, _ = p.renderer.writeString(ansi.KeyModifierOptions(4, p.requestedEnhancements.modifyOtherKeys)) //nolint:mnd
1426 _, _ = p.renderer.writeString(ansi.QueryModifyOtherKeys)
1427 }
1428 if p.requestedEnhancements.kittyFlags > 0 {
1429 _, _ = p.renderer.writeString(ansi.PushKittyKeyboard(p.requestedEnhancements.kittyFlags))
1430 _, _ = p.renderer.writeString(ansi.RequestKittyKeyboard)
1431 }
1432}
1433
1434// enableMouse enables mouse events on the terminal. When all is true, it will
1435// enable [ansi.AnyEventMouseMode], otherwise, it will use
1436// [ansi.ButtonEventMouseMode].
1437// Note this has no effect on Windows since we use the Windows Console API.
1438func (p *Program) enableMouse(all bool) {
1439 if isWindows() {
1440 // XXX: This is used to enable mouse mode on Windows. We need
1441 // to reinitialize the cancel reader to get the mouse events to
1442 // work.
1443 if !p.mouseMode {
1444 p.mouseMode = true
1445 if p.inputReader != nil {
1446 // Only reinitialize if the input reader has been initialized.
1447 _ = p.initInputReader(true)
1448 }
1449 }
1450 }
1451
1452 if all {
1453 p.execute(ansi.SetAnyEventMouseMode + ansi.SetSgrExtMouseMode)
1454 p.modes.Set(ansi.AnyEventMouseMode, ansi.SgrExtMouseMode)
1455 } else {
1456 p.execute(ansi.SetButtonEventMouseMode + ansi.SetSgrExtMouseMode)
1457 p.modes.Set(ansi.ButtonEventMouseMode, ansi.SgrExtMouseMode)
1458 }
1459}
1460
1461// disableMouse disables mouse events on the terminal.
1462// Note this has no effect on Windows since we use the Windows Console API.
1463func (p *Program) disableMouse() {
1464 if isWindows() {
1465 // XXX: On Windows, mouse mode is enabled on the input reader
1466 // level. We need to instruct the input reader to stop reading
1467 // mouse events.
1468 if p.mouseMode {
1469 p.mouseMode = false
1470 if p.inputReader != nil {
1471 // Only reinitialize if the input reader has been initialized.
1472 _ = p.initInputReader(true)
1473 }
1474 }
1475 }
1476
1477 var modes []ansi.Mode
1478 if p.modes.IsSet(ansi.AnyEventMouseMode) {
1479 modes = append(modes, ansi.AnyEventMouseMode)
1480 }
1481 if p.modes.IsSet(ansi.ButtonEventMouseMode) {
1482 modes = append(modes, ansi.ButtonEventMouseMode)
1483 }
1484 if len(modes) > 0 {
1485 modes = append(modes, ansi.SgrExtMouseMode)
1486 for _, m := range modes {
1487 // We could combine all of these modes into one single sequence,
1488 // but we're being cautious here for terminals that might not support
1489 // that format i.e. `CSI ? 10003 ; 1006 l`.
1490 p.execute(ansi.ResetMode(m))
1491 p.modes.Reset(m)
1492 }
1493 }
1494}