tea.go

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