| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695 |
- // Package tea provides a framework for building rich terminal user interfaces
- // based on the paradigms of The Elm Architecture. It's well-suited for simple
- // and complex terminal applications, either inline, full-window, or a mix of
- // both. It's been battle-tested in several large projects and is
- // production-ready.
- //
- // A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials
- //
- // Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples
- package tea
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "os"
- "os/signal"
- "runtime/debug"
- "sync"
- "syscall"
- "github.com/containerd/console"
- isatty "github.com/mattn/go-isatty"
- "github.com/muesli/cancelreader"
- "github.com/muesli/termenv"
- "golang.org/x/sync/errgroup"
- )
- // ErrProgramKilled is returned by [Program.Run] when the program got killed.
- var ErrProgramKilled = errors.New("program was killed")
- // Msg contain data from the result of a IO operation. Msgs trigger the update
- // function and, henceforth, the UI.
- type Msg interface{}
- // Model contains the program's state as well as its core functions.
- type Model interface {
- // Init is the first function that will be called. It returns an optional
- // initial command. To not perform an initial command return nil.
- Init() Cmd
- // Update is called when a message is received. Use it to inspect messages
- // and, in response, update the model and/or send a command.
- Update(Msg) (Model, Cmd)
- // View renders the program's UI, which is just a string. The view is
- // rendered after every Update.
- View() string
- }
- // Cmd is an IO operation that returns a message when it's complete. If it's
- // nil it's considered a no-op. Use it for things like HTTP requests, timers,
- // saving and loading from disk, and so on.
- //
- // Note that there's almost never a reason to use a command to send a message
- // to another part of your program. That can almost always be done in the
- // update function.
- type Cmd func() Msg
- type handlers []chan struct{}
- type inputType int
- const (
- defaultInput inputType = iota
- ttyInput
- customInput
- )
- // String implements the stringer interface for [inputType]. It is inteded to
- // be used in testing.
- func (i inputType) String() string {
- return [...]string{
- "default input",
- "tty input",
- "custom input",
- }[i]
- }
- // Options to customize the program during its initialization. These are
- // generally set with ProgramOptions.
- //
- // The options here are treated as bits.
- type startupOptions byte
- func (s startupOptions) has(option startupOptions) bool {
- return s&option != 0
- }
- const (
- withAltScreen startupOptions = 1 << iota
- withMouseCellMotion
- withMouseAllMotion
- withANSICompressor
- withoutSignalHandler
- // Catching panics is incredibly useful for restoring the terminal to a
- // usable state after a panic occurs. When this is set, Bubble Tea will
- // recover from panics, print the stack trace, and disable raw mode. This
- // feature is on by default.
- withoutCatchPanics
- )
- // Program is a terminal user interface.
- type Program struct {
- initialModel Model
- // Configuration options that will set as the program is initializing,
- // treated as bits. These options can be set via various ProgramOptions.
- startupOptions startupOptions
- inputType inputType
- ctx context.Context
- cancel context.CancelFunc
- msgs chan Msg
- errs chan error
- finished chan struct{}
- // where to send output, this will usually be os.Stdout.
- output *termenv.Output
- restoreOutput func() error
- renderer renderer
- // where to read inputs from, this will usually be os.Stdin.
- input io.Reader
- cancelReader cancelreader.CancelReader
- readLoopDone chan struct{}
- console console.Console
- // was the altscreen active before releasing the terminal?
- altScreenWasActive bool
- ignoreSignals bool
- // Stores the original reference to stdin for cases where input is not a
- // TTY on windows and we've automatically opened CONIN$ to receive input.
- // When the program exits this will be restored.
- //
- // Lint ignore note: the linter will find false positive on unix systems
- // as this value only comes into play on Windows, hence the ignore comment
- // below.
- windowsStdin *os.File //nolint:golint,structcheck,unused
- filter func(Model, Msg) Msg
- }
- // Quit is a special command that tells the Bubble Tea program to exit.
- func Quit() Msg {
- return QuitMsg{}
- }
- // QuitMsg signals that the program should quit. You can send a QuitMsg with
- // Quit.
- type QuitMsg struct{}
- // NewProgram creates a new Program.
- func NewProgram(model Model, opts ...ProgramOption) *Program {
- p := &Program{
- initialModel: model,
- msgs: make(chan Msg),
- }
- // Apply all options to the program.
- for _, opt := range opts {
- opt(p)
- }
- // A context can be provided with a ProgramOption, but if none was provided
- // we'll use the default background context.
- if p.ctx == nil {
- p.ctx = context.Background()
- }
- // Initialize context and teardown channel.
- p.ctx, p.cancel = context.WithCancel(p.ctx)
- // if no output was set, set it to stdout
- if p.output == nil {
- p.output = termenv.DefaultOutput()
- // cache detected color values
- termenv.WithColorCache(true)(p.output)
- }
- p.restoreOutput, _ = termenv.EnableVirtualTerminalProcessing(p.output)
- return p
- }
- func (p *Program) handleSignals() chan struct{} {
- ch := make(chan struct{})
- // Listen for SIGINT and SIGTERM.
- //
- // In most cases ^C will not send an interrupt because the terminal will be
- // in raw mode and ^C will be captured as a keystroke and sent along to
- // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
- // caught here.
- //
- // SIGTERM is sent by unix utilities (like kill) to terminate a process.
- go func() {
- sig := make(chan os.Signal, 1)
- signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
- defer func() {
- signal.Stop(sig)
- close(ch)
- }()
- for {
- select {
- case <-p.ctx.Done():
- return
- case <-sig:
- if !p.ignoreSignals {
- p.msgs <- QuitMsg{}
- return
- }
- }
- }
- }()
- return ch
- }
- // handleResize handles terminal resize events.
- func (p *Program) handleResize() chan struct{} {
- ch := make(chan struct{})
- if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) {
- // Get the initial terminal size and send it to the program.
- go p.checkResize()
- // Listen for window resizes.
- go p.listenForResize(ch)
- } else {
- close(ch)
- }
- return ch
- }
- // handleCommands runs commands in a goroutine and sends the result to the
- // program's message channel.
- func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
- ch := make(chan struct{})
- go func() {
- defer close(ch)
- for {
- select {
- case <-p.ctx.Done():
- return
- case cmd := <-cmds:
- if cmd == nil {
- continue
- }
- // Don't wait on these goroutines, otherwise the shutdown
- // latency would get too large as a Cmd can run for some time
- // (e.g. tick commands that sleep for half a second). It's not
- // possible to cancel them so we'll have to leak the goroutine
- // until Cmd returns.
- go func() {
- msg := cmd() // this can be long.
- p.Send(msg)
- }()
- }
- }
- }()
- return ch
- }
- // eventLoop is the central message loop. It receives and handles the default
- // Bubble Tea messages, update the model and triggers redraws.
- func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
- for {
- select {
- case <-p.ctx.Done():
- return model, nil
- case err := <-p.errs:
- return model, err
- case msg := <-p.msgs:
- // Filter messages.
- if p.filter != nil {
- msg = p.filter(model, msg)
- }
- if msg == nil {
- continue
- }
- // Handle special internal messages.
- switch msg := msg.(type) {
- case QuitMsg:
- return model, nil
- case clearScreenMsg:
- p.renderer.clearScreen()
- case enterAltScreenMsg:
- p.renderer.enterAltScreen()
- case exitAltScreenMsg:
- p.renderer.exitAltScreen()
- case enableMouseCellMotionMsg:
- p.renderer.enableMouseCellMotion()
- case enableMouseAllMotionMsg:
- p.renderer.enableMouseAllMotion()
- case disableMouseMsg:
- p.renderer.disableMouseCellMotion()
- p.renderer.disableMouseAllMotion()
- case showCursorMsg:
- p.renderer.showCursor()
- case hideCursorMsg:
- p.renderer.hideCursor()
- case execMsg:
- // NB: this blocks.
- p.exec(msg.cmd, msg.fn)
- case BatchMsg:
- for _, cmd := range msg {
- cmds <- cmd
- }
- continue
- case sequenceMsg:
- go func() {
- // Execute commands one at a time, in order.
- for _, cmd := range msg {
- if cmd == nil {
- continue
- }
- msg := cmd()
- if batchMsg, ok := msg.(BatchMsg); ok {
- g, _ := errgroup.WithContext(p.ctx)
- for _, cmd := range batchMsg {
- cmd := cmd
- g.Go(func() error {
- p.Send(cmd())
- return nil
- })
- }
- //nolint:errcheck
- g.Wait() // wait for all commands from batch msg to finish
- continue
- }
- p.Send(msg)
- }
- }()
- }
- // Process internal messages for the renderer.
- if r, ok := p.renderer.(*standardRenderer); ok {
- r.handleMessages(msg)
- }
- var cmd Cmd
- model, cmd = model.Update(msg) // run update
- cmds <- cmd // process command (if any)
- p.renderer.write(model.View()) // send view to renderer
- }
- }
- }
- // Run initializes the program and runs its event loops, blocking until it gets
- // terminated by either [Program.Quit], [Program.Kill], or its signal handler.
- // Returns the final model.
- func (p *Program) Run() (Model, error) {
- handlers := handlers{}
- cmds := make(chan Cmd)
- p.errs = make(chan error)
- p.finished = make(chan struct{}, 1)
- defer p.cancel()
- switch p.inputType {
- case defaultInput:
- p.input = os.Stdin
- // The user has not set a custom input, so we need to check whether or
- // not standard input is a terminal. If it's not, we open a new TTY for
- // input. This will allow things to "just work" in cases where data was
- // piped in or redirected to the application.
- //
- // To disable input entirely pass nil to the [WithInput] program option.
- f, isFile := p.input.(*os.File)
- if !isFile {
- break
- }
- if isatty.IsTerminal(f.Fd()) {
- break
- }
- f, err := openInputTTY()
- if err != nil {
- return p.initialModel, err
- }
- defer f.Close() //nolint:errcheck
- p.input = f
- case ttyInput:
- // Open a new TTY, by request
- f, err := openInputTTY()
- if err != nil {
- return p.initialModel, err
- }
- defer f.Close() //nolint:errcheck
- p.input = f
- case customInput:
- // (There is nothing extra to do.)
- }
- // Handle signals.
- if !p.startupOptions.has(withoutSignalHandler) {
- handlers.add(p.handleSignals())
- }
- // Recover from panics.
- if !p.startupOptions.has(withoutCatchPanics) {
- defer func() {
- if r := recover(); r != nil {
- p.shutdown(true)
- fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
- debug.PrintStack()
- return
- }
- }()
- }
- // If no renderer is set use the standard one.
- if p.renderer == nil {
- p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor))
- }
- // Check if output is a TTY before entering raw mode, hiding the cursor and
- // so on.
- if err := p.initTerminal(); err != nil {
- return p.initialModel, err
- }
- // Honor program startup options.
- if p.startupOptions&withAltScreen != 0 {
- p.renderer.enterAltScreen()
- }
- if p.startupOptions&withMouseCellMotion != 0 {
- p.renderer.enableMouseCellMotion()
- } else if p.startupOptions&withMouseAllMotion != 0 {
- p.renderer.enableMouseAllMotion()
- }
- // Initialize the program.
- model := p.initialModel
- if initCmd := model.Init(); initCmd != nil {
- ch := make(chan struct{})
- handlers.add(ch)
- go func() {
- defer close(ch)
- select {
- case cmds <- initCmd:
- case <-p.ctx.Done():
- }
- }()
- }
- // Start the renderer.
- p.renderer.start()
- // Render the initial view.
- p.renderer.write(model.View())
- // Subscribe to user input.
- if p.input != nil {
- if err := p.initCancelReader(); err != nil {
- return model, err
- }
- }
- // Handle resize events.
- handlers.add(p.handleResize())
- // Process commands.
- handlers.add(p.handleCommands(cmds))
- // Run event loop, handle updates and draw.
- model, err := p.eventLoop(model, cmds)
- killed := p.ctx.Err() != nil
- if killed {
- err = ErrProgramKilled
- } else {
- // Ensure we rendered the final state of the model.
- p.renderer.write(model.View())
- }
- // Tear down.
- p.cancel()
- // Check if the cancel reader has been setup before waiting and closing.
- if p.cancelReader != nil {
- // Wait for input loop to finish.
- if p.cancelReader.Cancel() {
- p.waitForReadLoop()
- }
- _ = p.cancelReader.Close()
- }
- // Wait for all handlers to finish.
- handlers.shutdown()
- // Restore terminal state.
- p.shutdown(killed)
- return model, err
- }
- // StartReturningModel initializes the program and runs its event loops,
- // blocking until it gets terminated by either [Program.Quit], [Program.Kill],
- // or its signal handler. Returns the final model.
- //
- // Deprecated: please use [Program.Run] instead.
- func (p *Program) StartReturningModel() (Model, error) {
- return p.Run()
- }
- // Start initializes the program and runs its event loops, blocking until it
- // gets terminated by either [Program.Quit], [Program.Kill], or its signal
- // handler.
- //
- // Deprecated: please use [Program.Run] instead.
- func (p *Program) Start() error {
- _, err := p.Run()
- return err
- }
- // Send sends a message to the main update function, effectively allowing
- // messages to be injected from outside the program for interoperability
- // purposes.
- //
- // If the program hasn't started yet this will be a blocking operation.
- // If the program has already been terminated this will be a no-op, so it's safe
- // to send messages after the program has exited.
- func (p *Program) Send(msg Msg) {
- select {
- case <-p.ctx.Done():
- case p.msgs <- msg:
- }
- }
- // Quit is a convenience function for quitting Bubble Tea programs. Use it
- // when you need to shut down a Bubble Tea program from the outside.
- //
- // If you wish to quit from within a Bubble Tea program use the Quit command.
- //
- // If the program is not running this will be a no-op, so it's safe to call
- // if the program is unstarted or has already exited.
- func (p *Program) Quit() {
- p.Send(Quit())
- }
- // Kill stops the program immediately and restores the former terminal state.
- // The final render that you would normally see when quitting will be skipped.
- // [program.Run] returns a [ErrProgramKilled] error.
- func (p *Program) Kill() {
- p.cancel()
- }
- // Wait waits/blocks until the underlying Program finished shutting down.
- func (p *Program) Wait() {
- <-p.finished
- }
- // shutdown performs operations to free up resources and restore the terminal
- // to its original state.
- func (p *Program) shutdown(kill bool) {
- if p.renderer != nil {
- if kill {
- p.renderer.kill()
- } else {
- p.renderer.stop()
- }
- }
- _ = p.restoreTerminalState()
- if p.restoreOutput != nil {
- _ = p.restoreOutput()
- }
- p.finished <- struct{}{}
- }
- // ReleaseTerminal restores the original terminal state and cancels the input
- // reader. You can return control to the Program with RestoreTerminal.
- func (p *Program) ReleaseTerminal() error {
- p.ignoreSignals = true
- p.cancelReader.Cancel()
- p.waitForReadLoop()
- if p.renderer != nil {
- p.renderer.stop()
- }
- p.altScreenWasActive = p.renderer.altScreen()
- return p.restoreTerminalState()
- }
- // RestoreTerminal reinitializes the Program's input reader, restores the
- // terminal to the former state when the program was running, and repaints.
- // Use it to reinitialize a Program after running ReleaseTerminal.
- func (p *Program) RestoreTerminal() error {
- p.ignoreSignals = false
- if err := p.initTerminal(); err != nil {
- return err
- }
- if err := p.initCancelReader(); err != nil {
- return err
- }
- if p.altScreenWasActive {
- p.renderer.enterAltScreen()
- } else {
- // entering alt screen already causes a repaint.
- go p.Send(repaintMsg{})
- }
- if p.renderer != nil {
- p.renderer.start()
- }
- // If the output is a terminal, it may have been resized while another
- // process was at the foreground, in which case we may not have received
- // SIGWINCH. Detect any size change now and propagate the new size as
- // needed.
- go p.checkResize()
- return nil
- }
- // Println prints above the Program. This output is unmanaged by the program
- // and will persist across renders by the Program.
- //
- // If the altscreen is active no output will be printed.
- func (p *Program) Println(args ...interface{}) {
- p.msgs <- printLineMessage{
- messageBody: fmt.Sprint(args...),
- }
- }
- // Printf prints above the Program. It takes a format template followed by
- // values similar to fmt.Printf. This output is unmanaged by the program and
- // will persist across renders by the Program.
- //
- // Unlike fmt.Printf (but similar to log.Printf) the message will be print on
- // its own line.
- //
- // If the altscreen is active no output will be printed.
- func (p *Program) Printf(template string, args ...interface{}) {
- p.msgs <- printLineMessage{
- messageBody: fmt.Sprintf(template, args...),
- }
- }
- // Adds a handler to the list of handlers. We wait for all handlers to terminate
- // gracefully on shutdown.
- func (h *handlers) add(ch chan struct{}) {
- *h = append(*h, ch)
- }
- // Shutdown waits for all handlers to terminate.
- func (h handlers) shutdown() {
- var wg sync.WaitGroup
- for _, ch := range h {
- wg.Add(1)
- go func(ch chan struct{}) {
- <-ch
- wg.Done()
- }(ch)
- }
- wg.Wait()
- }
|