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