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					// 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}