Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488// Package tea provides a framework for building rich terminal user interfaces// based on the paradigms of The Elm Architecture. It's well-suited for simple// and complex terminal applications, either inline, full-window, or a mix of// both. It's been battle-tested in several large projects and is// production-ready.//// A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/main/tutorials//// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/main/examplespackage tea
import ( "bytes" "context" "errors" "fmt" "image/color" "io" "log" "os" "os/signal" "runtime" "runtime/debug" "strconv" "strings" "sync" "sync/atomic" "syscall" "time"
"github.com/charmbracelet/colorprofile" uv "github.com/charmbracelet/ultraviolet" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/term" "github.com/muesli/cancelreader")
// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.var ErrProgramPanic = errors.New("program experienced a panic")
// ErrProgramKilled is returned by [Program.Run] when the program gets killed.var ErrProgramKilled = errors.New("program was killed")
// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT// signal, or when it receives a [InterruptMsg].var ErrInterrupted = errors.New("program was interrupted")
// Msg contain data from the result of a IO operation. Msgs trigger the update// function and, henceforth, the UI.type Msg = uv.Event
// Model contains the program's state as well as its core functions.type Model interface { // Init is the first function that will be called. It returns an optional // initial command. To not perform an initial command return nil. Init() Cmd
// Update is called when a message is received. Use it to inspect messages // and, in response, update the model and/or send a command. Update(Msg) (Model, Cmd)
// View renders the program's UI, which can be a string or a [Layer]. The // view is rendered after every Update. View() View}
// NewView is a helper function to create a new [View] with the given styled// string. A styled string represents text with styles and hyperlinks encoded// as ANSI escape codes.//// Example://// ```go// v := tea.NewView("Hello, World!")// ```func NewView(s string) View { var view View view.SetContent(s) return view}
// View represents a terminal view that can be composed of multiple layers.// It can also contain a cursor that will be rendered on top of the layers.type View struct { // Content is the screen content of the view. It holds styled strings that // will be rendered to the terminal when the view is rendered. // // A styled string represents text with styles and hyperlinks encoded as // ANSI escape codes. // // Example: // // ```go // v := tea.NewView("Hello, World!") // ``` Content string
// OnMouse is an optional mouse message handler that can be used to // intercept mouse messages that depends on view content from last render. // It can be useful for implementing view-specific behavior without // breaking the unidirectional data flow of Bubble Tea. // // Example: // // ```go // content := "Hello, World!" // v := tea.NewView(content) // v.OnMouse = func(msg tea.MouseMsg) tea.Cmd { // return func() tea.Msg { // m := msg.Mouse() // // Check if the mouse is within the bounds of "World!" // start := strings.Index(content, "World!") // end := start + len("World!") // if m.Y == 0 && m.X >= start && m.X < end { // // Mouse is over "World!" // return MyCustomMsg{ // MouseMsg: msg, // } // } // } // } // return nil // } // return v // ``` OnMouse func(msg MouseMsg) Cmd
// Cursor represents the cursor position, style, and visibility on the // screen. When not nil, the cursor will be shown at the specified // position. Cursor *Cursor
// BackgroundColor when not nil, sets the terminal background color. Use // nil to reset to the terminal's default background color. BackgroundColor color.Color
// ForegroundColor when not nil, sets the terminal foreground color. Use // nil to reset to the terminal's default foreground color. ForegroundColor color.Color
// WindowTitle sets the terminal window title. Support depends on the // terminal. WindowTitle string
// ProgressBar when not nil, shows a progress bar in the terminal's // progress bar section. Support depends on the terminal. ProgressBar *ProgressBar
// AltScreen puts the program in the alternate screen buffer // (i.e. the program goes into full window mode). Note that the altscreen will // be automatically exited when the program quits. // // Example: // // func (m model) View() tea.View { // v := tea.NewView("Hello, World!") // v.AltScreen = true // return v // } // AltScreen bool
// ReportFocus enables reporting when the terminal gains and loses focus. // When this is enabled [FocusMsg] and [BlurMsg] messages will be sent to // your Update method. // // Note that while most terminals and multiplexers support focus reporting, // some do not. Also note that tmux needs to be configured to report focus // events. ReportFocus bool
// DisableBracketedPasteMode disables bracketed paste mode for this view. DisableBracketedPasteMode bool
// MouseMode sets the mouse mode for this view. It can be one of // [MouseModeNone], [MouseModeCellMotion], or [MouseModeAllMotion]. MouseMode MouseMode
// KeyboardEnhancements describes what keyboard enhancement features Bubble // Tea should request from the terminal. // // Bubble Tea supports requesting the following keyboard enhancement features: // - ReportEventTypes: requests the terminal to report key repeat and // release events. // // If the terminal supports any of these features, your program will // receive a [KeyboardEnhancementsMsg] that indicates which features are // available. KeyboardEnhancements KeyboardEnhancements}
// KeyboardEnhancements describes the requested keyboard enhancement features.// If the terminal supports any of them, it will respond with a// [KeyboardEnhancementsMsg] that indicates which features are supported.
// KeyboardEnhancements defines different keyboard enhancement features that// can be requested from the terminal.
// KeyboardEnhancements defines different keyboard enhancement features that// can be requested from the terminal.//// By default, Bubble Tea requests basic key disambiguation features from the// terminal. If the terminal supports keyboard enhancements, or any of its// additional features, it will respond with a [KeyboardEnhancementsMsg] that// indicates which features are supported.//// Example://// ```go// func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {// switch msg := msg.(type) {// case tea.KeyboardEnhancementsMsg:// // We have basic key disambiguation support.// // We can handle "shift+enter", "ctrl+i", etc.// m.keyboardEnhancements = msg// if msg.ReportEventTypes {// // Even better! We can now handle key repeat and release events.// }// case tea.KeyPressMsg:// switch msg.String() {// case "shift+enter":// // Handle shift+enter// // This would not be possible without keyboard enhancements.// case "ctrl+j":// // Handle ctrl+j// }// case tea.KeyReleaseMsg:// // Whoa! A key was released!// }//// return m, nil// }//// func (m model) View() tea.View {// v := tea.NewView("Press some keys!")// // Request reporting key repeat and release events.// v.KeyboardEnhancements.ReportEventTypes = true// return v// }// ```type KeyboardEnhancements struct { // ReportEventTypes requests the terminal to report key repeat and release // events. // If supported, your program will receive [KeyReleaseMsg]s and // [KeyPressMsg] with the [Key.IsRepeat] field set indicating that this is // a it's part of a key repeat sequence. ReportEventTypes bool
// ReportAlternateKeys requests the terminal to report alternate key values // in addition to the main ones. // Note that only key events represented as escape codes will affected by // this enhancement. ReportAlternateKeys bool
// ReportAllKeysAsEscapeCodes requests the terminal to report all key // events, including plain text keys, as escape codes. // When this is enabled, text won't be sent as plain text but instead as // escape codes that encode the key value and modifiers. ReportAllKeysAsEscapeCodes bool
// ReportAssociatedText requests the terminal to report the text associated // with key events. // Note that this is an enhancement to // [KeyboardEnhancements.ReportAllKeysAsEscapeCodes] and only has an effect // if that is enabled. ReportAssociatedText bool}
// SetContent is a helper method to set the content of a [View] with a styled// string. A styled string represents text with styles and hyperlinks encoded// as ANSI escape codes.//// Example://// ```go// var v tea.View// v.SetContent("Hello, World!")// ```func (v *View) SetContent(s string) { v.Content = s}
// MouseMode represents the mouse mode of a view.type MouseMode int
const ( // MouseModeNone disables mouse events. MouseModeNone MouseMode = iota
// MouseModeCellMotion enables mouse click, release, and wheel events. // Mouse movement events are also captured if a mouse button is pressed // (i.e., drag events). Cell motion mode is better supported than all // motion mode. // // This will try to enable the mouse in extended mode (SGR), if that is not // supported by the terminal it will fall back to normal mode (X10). MouseModeCellMotion
// MouseModeAllMotion enables all mouse events, including click, release, // wheel, and movement events. You will receive mouse movement events even // when no buttons are pressed. // // This will try to enable the mouse in extended mode (SGR), if that is not // supported by the terminal it will fall back to normal mode (X10). MouseModeAllMotion)
// ProgressBarState represents the state of the progress bar.type ProgressBarState int
// Progress bar states.const ( ProgressBarNone ProgressBarState = iota ProgressBarDefault ProgressBarError ProgressBarIndeterminate ProgressBarWarning)
// String returns a human-readable name for the given [ProgressBarState].// Values outside the known range return "Unknown".func (s ProgressBarState) String() string { switch s { case ProgressBarNone: return "None" case ProgressBarDefault: return "Default" case ProgressBarError: return "Error" case ProgressBarIndeterminate: return "Indeterminate" case ProgressBarWarning: return "Warning" default: return "Unknown" }}
// ProgressBar represents the terminal progress bar.//// Support depends on the terminal.//// See https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequencestype ProgressBar struct { // State is the current state of the progress bar. It can be one of // [ProgressBarNone], [ProgressBarDefault], [ProgressBarError], // [ProgressBarIndeterminate], and [ProgressBarWarning]. State ProgressBarState // Value is the current value of the progress bar. It should be between // 0 and 100. Value int}
// NewProgressBar returns a new progress bar with the given state and value.// The value is ignored if the state is [ProgressBarNone] or// [ProgressBarIndeterminate].func NewProgressBar(state ProgressBarState, value int) *ProgressBar { return &ProgressBar{ State: state, Value: min(max(value, 0), 100), }}
// Cursor represents a cursor on the terminal screen.type Cursor struct { // Position is a [Position] that determines the cursor's position on the // screen relative to the top left corner of the frame. Position
// Color is a [color.Color] that determines the cursor's color. Color color.Color
// Shape is a [CursorShape] that determines the cursor's shape. Shape CursorShape
// Blink is a boolean that determines whether the cursor should blink. Blink bool}
// NewCursor returns a new cursor with the default settings and the given// position.func NewCursor(x, y int) *Cursor { return &Cursor{ Position: Position{X: x, Y: y}, Color: nil, Shape: CursorBlock, Blink: true, }}
// Cmd is an IO operation that returns a message when it's complete. If it's// nil it's considered a no-op. Use it for things like HTTP requests, timers,// saving and loading from disk, and so on.//// Note that there's almost never a reason to use a command to send a message// to another part of your program. That can almost always be done in the// update function.type Cmd func() Msg
// channelHandlers manages the series of channels returned by various processes.// It allows us to wait for those processes to terminate before exiting the// program.type channelHandlers struct { handlers []chan struct{} mu sync.RWMutex}
// Adds a channel to the list of handlers. We wait for all handlers to terminate// gracefully on shutdown.func (h *channelHandlers) add(ch chan struct{}) { h.mu.Lock() h.handlers = append(h.handlers, ch) h.mu.Unlock()}
// shutdown waits for all handlers to terminate.func (h *channelHandlers) shutdown() { var wg sync.WaitGroup
h.mu.RLock() defer h.mu.RUnlock()
for _, ch := range h.handlers { wg.Add(1) go func(ch chan struct{}) { <-ch wg.Done() }(ch) } wg.Wait()}
// Program is a terminal user interface.type Program struct { // disableInput disables all input. This is useful for programs that // don't need input, like a progress bar or a spinner. disableInput bool
// disableSignalHandler disables the signal handler that Bubble Tea sets up // for Programs. This is useful if you want to handle signals yourself. disableSignalHandler bool
// disableCatchPanics disables the panic catching that Bubble Tea does by // default. If panic catching is disabled the terminal will be in a fairly // unusable state after a panic because Bubble Tea will not perform its usual // cleanup on exit. disableCatchPanics bool
// filter supplies an event filter that will be invoked before Bubble Tea // processes a tea.Msg. The event filter can return any tea.Msg which will // then get handled by Bubble Tea instead of the original event. If the // event filter returns nil, the event will be ignored and Bubble Tea will // not process it. // // As an example, this could be used to prevent a program from shutting // down if there are unsaved changes. // // Example: // // func filter(m tea.Model, msg tea.Msg) tea.Msg { // if _, ok := msg.(tea.QuitMsg); !ok { // return msg // } // // model := m.(myModel) // if model.hasChanges { // return nil // } // // return msg // } // // p := tea.NewProgram(Model{}); // p.filter = filter // // if _,err := p.Run(context.Background()); err != nil { // fmt.Println("Error running program:", err) // os.Exit(1) // } filter func(Model, Msg) Msg
// fps sets a custom maximum fps at which the renderer should run. If less // than 1, the default value of 60 will be used. If over 120, the fps will // be capped at 120. fps int
// initialModel is the initial model for the program and is the only // required field when creating a new program. initialModel Model
// disableRenderer prevents the program from rendering to the terminal. // This can be useful for running daemon-like programs that don't require a // UI but still want to take advantage of Bubble Tea's architecture. disableRenderer bool
// handlers is a list of channels that need to be waited on before the // program can exit. handlers channelHandlers
// ctx is the programs's internal context for signalling internal teardown. // It is built and derived from the externalCtx in NewProgram(). ctx context.Context cancel context.CancelFunc
// externalCtx is a context that was passed in via WithContext, otherwise defaulting // to ctx.Background() (in case it was not), the internal context is derived from it. externalCtx context.Context
msgs chan Msg errs chan error finished chan struct{} shutdownOnce sync.Once
profile *colorprofile.Profile // the terminal color profile
// where to send output, this will usually be os.Stdout. output io.Writer outputBuf bytes.Buffer // buffer used to queue commands to be sent to the output
// ttyOutput is null if output is not a TTY. ttyOutput term.File previousOutputState *term.State renderer renderer
// the environment variables for the program, defaults to os.Environ(). environ uv.Environ // the program's logger for debugging. logger uv.Logger
// where to read inputs from, this will usually be os.Stdin. input io.Reader // ttyInput is null if input is not a TTY. ttyInput term.File previousTtyInputState *term.State cancelReader cancelreader.CancelReader inputScanner *uv.TerminalReader readLoopDone chan struct{}
// modes keeps track of terminal modes that have been enabled or disabled. ignoreSignals uint32
// frames wakes the frame writer when there is something to paint. It // replaces upstream's always-on ticker, so an idle program never wakes. frames chan struct{}
// once is used to stop the renderer. once sync.Once
// rendererDone is used to stop the renderer. rendererDone chan struct{}
// Initial window size. Mainly used for testing. width, height int
// whether to use hard tabs to optimize cursor movements useHardTabs bool // whether to use backspace to optimize cursor movements useBackspace bool
mu sync.Mutex}
// Quit is a special command that tells the Bubble Tea program to exit.func Quit() Msg { return QuitMsg{}}
// QuitMsg signals that the program should quit. You can send a [QuitMsg] with// [Quit].type QuitMsg struct{}
// Suspend is a special command that tells the Bubble Tea program to suspend.func Suspend() Msg { return SuspendMsg{}}
// SuspendMsg signals the program should suspend.// This usually happens when ctrl+z is pressed on common programs, but since// bubbletea puts the terminal in raw mode, we need to handle it in a// per-program basis.//// You can send this message with [Suspend()].type SuspendMsg struct{}
// ResumeMsg can be listen to do something once a program is resumed back// from a suspend state.type ResumeMsg struct{}
// InterruptMsg signals the program should suspend.// This usually happens when ctrl+c is pressed on common programs, but since// bubbletea puts the terminal in raw mode, we need to handle it in a// per-program basis.//// You can send this message with [Interrupt()].type InterruptMsg struct{}
// Interrupt is a special command that tells the Bubble Tea program to// interrupt.func Interrupt() Msg { return InterruptMsg{}}
// NewProgram creates a new [Program].func NewProgram(model Model, opts ...ProgramOption) *Program { p := &Program{ initialModel: model, msgs: make(chan Msg), errs: make(chan error, 1), rendererDone: make(chan struct{}), frames: make(chan struct{}, 1), }
// Apply all options to the program. for _, opt := range opts { opt(p) }
// A context can be provided with a ProgramOption, but if none was provided // we'll use the default background context. if p.externalCtx == nil { p.externalCtx = context.Background() } // Initialize context and teardown channel. p.ctx, p.cancel = context.WithCancel(p.externalCtx)
// if no output was set, set it to stdout if p.output == nil { p.output = os.Stdout }
// if no environment was set, set it to os.Environ() if p.environ == nil { p.environ = os.Environ() }
if p.fps < 1 { p.fps = defaultFPS } else if p.fps > maxFPS { p.fps = maxFPS }
tracePath, traceOk := os.LookupEnv("TEA_TRACE") if traceOk && len(tracePath) > 0 { // We have a trace filepath. if f, err := os.OpenFile(tracePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600); err == nil { p.logger = log.New(f, "bubbletea: ", log.LstdFlags|log.Lshortfile) } }
return p}
func (p *Program) handleSignals() chan struct{} { ch := make(chan struct{})
// Listen for SIGINT and SIGTERM. // // In most cases ^C will not send an interrupt because the terminal will be // in raw mode and ^C will be captured as a keystroke and sent along to // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be // caught here. // // SIGTERM is sent by unix utilities (like kill) to terminate a process. go func() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) defer func() { signal.Stop(sig) close(ch) }()
for { select { case <-p.ctx.Done(): return
case s := <-sig: if atomic.LoadUint32(&p.ignoreSignals) == 0 { switch s { case syscall.SIGINT: p.msgs <- InterruptMsg{} default: p.msgs <- QuitMsg{} } return } } } }()
return ch}
// handleResize handles terminal resize events.func (p *Program) handleResize() chan struct{} { ch := make(chan struct{})
if p.ttyOutput != nil { // Listen for window resizes. go p.listenForResize(ch) } else { close(ch) }
return ch}
// handleCommands runs commands in a goroutine and sends the result to the// program's message channel.func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { ch := make(chan struct{})
go func() { defer close(ch)
for { select { case <-p.ctx.Done(): return
case cmd := <-cmds: if cmd == nil { continue }
// Don't wait on these goroutines, otherwise the shutdown // latency would get too large as a Cmd can run for some time // (e.g. tick commands that sleep for half a second). It's not // possible to cancel them so we'll have to leak the goroutine // until Cmd returns. go func() { // Recover from panics. if !p.disableCatchPanics { defer func() { if r := recover(); r != nil { p.recoverFromPanic(r) } }() }
msg := cmd() // this can be long. p.Send(msg) }() } } }()
return ch}
// eventLoop is the central message loop. It receives and handles the default// Bubble Tea messages, update the model and triggers redraws.func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { for { select { case <-p.ctx.Done(): return model, nil
case err := <-p.errs: return model, err
case msg := <-p.msgs: msg = p.translateInputEvent(msg)
// A command that takes the terminal runs here (NB: this blocks), // and its result is handled as this message, so input read once // the terminal is back cannot overtake it. if exec, ok := msg.(execMsg); ok { if result := p.exec(exec.cmd, exec.fn); result != nil { msg = result } }
// Filter messages. if p.filter != nil { msg = p.filter(model, msg) } if msg == nil { continue }
// Handle special internal messages. switch msg := msg.(type) { case QuitMsg: return model, nil
case InterruptMsg: return model, ErrInterrupted
case SuspendMsg: if suspendSupported { p.suspend() }
case CapabilityMsg: switch msg.Content { case "RGB", "Tc": if *p.profile != colorprofile.TrueColor { tc := colorprofile.TrueColor p.profile = &tc go p.Send(ColorProfileMsg{*p.profile}) } }
case ModeReportMsg: switch msg.Mode { case ansi.ModeSynchronizedOutput: if msg.Value == ansi.ModeReset { // The terminal supports synchronized output and it's // currently disabled, so we can enable it on the renderer. p.renderer.setSyncdUpdates(true) } case ansi.ModeUnicodeCore: if msg.Value == ansi.ModeReset || msg.Value == ansi.ModeSet || msg.Value == ansi.ModePermanentlySet { p.renderer.setWidthMethod(ansi.GraphemeWidth) } }
case MouseMsg: switch msg.(type) { case MouseClickMsg, MouseReleaseMsg, MouseWheelMsg, MouseMotionMsg: // Only send mouse messages to the renderer if they are an // actual mouse event. if cmd := p.renderer.onMouse(msg); cmd != nil { go p.Send(cmd()) } }
case readClipboardMsg: p.executeQuery(ansi.RequestSystemClipboard)
case setClipboardMsg: p.execute(ansi.SetSystemClipboard(string(msg)))
case readPrimaryClipboardMsg: p.executeQuery(ansi.RequestPrimaryClipboard)
case setPrimaryClipboardMsg: p.execute(ansi.SetPrimaryClipboard(string(msg)))
case backgroundColorMsg: p.executeQuery(ansi.RequestBackgroundColor)
case foregroundColorMsg: p.executeQuery(ansi.RequestForegroundColor)
case cursorColorMsg: p.executeQuery(ansi.RequestCursorColor)
case terminalVersion: p.executeQuery(ansi.RequestNameVersion)
case requestCapabilityMsg: p.executeQuery(ansi.RequestTermcap(string(msg)))
case BatchMsg: go p.execBatchMsg(msg) continue
case sequenceMsg: go p.execSequenceMsg(msg) continue
case WindowSizeMsg: p.renderer.resize(msg.Width, msg.Height)
case windowSizeMsg: go p.checkResize()
case requestCursorPosMsg: p.executeQuery(ansi.RequestCursorPositionReport)
case RawMsg: p.execute(fmt.Sprint(msg.Msg))
case printLineMessage: p.renderer.insertAbove(msg.messageBody) //nolint:errcheck,gosec
case clearScreenMsg: p.renderer.clearScreen()
case ColorProfileMsg: p.renderer.setColorProfile(msg.Profile) }
var cmd Cmd model, cmd = model.Update(msg) // run update
select { case <-p.ctx.Done(): return model, nil case cmds <- cmd: // process command (if any) }
p.render(model) // render view } }}
// render renders the given view to the renderer.func (p *Program) render(model Model) { if p.renderer != nil { p.renderer.render(model.View()) // send view to renderer } p.requestFrame()}
// requestFrame asks the frame writer to paint. A pending request already// covers this one.func (p *Program) requestFrame() { select { case p.frames <- struct{}{}: default: }}
func (p *Program) execSequenceMsg(msg sequenceMsg) { if !p.disableCatchPanics { defer func() { if r := recover(); r != nil { p.recoverFromGoPanic(r) } }() }
// Execute commands one at a time, in order. for _, cmd := range msg { if cmd == nil { continue } msg := cmd() switch msg := msg.(type) { case BatchMsg: p.execBatchMsg(msg) case sequenceMsg: p.execSequenceMsg(msg) default: p.Send(msg) } }}
func (p *Program) execBatchMsg(msg BatchMsg) { if !p.disableCatchPanics { defer func() { if r := recover(); r != nil { p.recoverFromGoPanic(r) } }() }
// Execute commands one at a time. var wg sync.WaitGroup for _, cmd := range msg { if cmd == nil { continue } wg.Add(1) go func() { defer wg.Done()
if !p.disableCatchPanics { defer func() { if r := recover(); r != nil { p.recoverFromGoPanic(r) } }() }
msg := cmd() switch msg := msg.(type) { case BatchMsg: p.execBatchMsg(msg) case sequenceMsg: p.execSequenceMsg(msg) default: p.Send(msg) } }() }
wg.Wait() // wait for all commands from batch msg to finish}
// shouldQuerySynchronizedOutput determines whether the terminal should be// queried for various capabilities.//// This function checks for terminals that are known to support mode 2026,// while excluding SSH sessions which may be unreliable, unless it's a// known-good terminal like Windows Terminal.//// The function returns true for:// - Terminals without TERM_PROGRAM set and not in SSH sessions// - Windows Terminal (WT_SESSION is set)// - Terminals with TERM_PROGRAM set (except Apple Terminal) and not in SSH sessions// - Specific terminal types: ghostty, wezterm, alacritty, kitty, riofunc shouldQuerySynchronizedOutput(environ uv.Environ) bool { termType := environ.Getenv("TERM") termProg, okTermProg := environ.LookupEnv("TERM_PROGRAM") _, okSSHTTY := environ.LookupEnv("SSH_TTY") _, okWTSession := environ.LookupEnv("WT_SESSION")
return (!okTermProg && !okSSHTTY) || okWTSession || (okTermProg && !strings.Contains(termProg, "Apple") && !okSSHTTY) || strings.Contains(termType, "ghostty") || strings.Contains(termType, "wezterm") || strings.Contains(termType, "alacritty") || strings.Contains(termType, "kitty") || strings.Contains(termType, "rio")}
// Run initializes the program and runs its event loops, blocking until it gets// terminated by either [Program.Quit], [Program.Kill], or its signal handler.// Returns the final model.func (p *Program) Run() (returnModel Model, returnErr error) { if p.initialModel == nil { return nil, errors.New("bubbletea: InitialModel cannot be nil") }
// Initialize context and teardown channel. p.handlers = channelHandlers{} cmds := make(chan Cmd)
p.finished = make(chan struct{}) defer func() { close(p.finished) }()
defer p.cancel()
if p.disableInput { p.input = nil } else if p.input == nil { p.input = os.Stdin if !term.IsTerminal(os.Stdin.Fd()) { ttyIn, _, err := OpenTTY() if err != nil { return p.initialModel, fmt.Errorf("bubbletea: error opening TTY: %w", err) } p.input = ttyIn } }
// Handle signals. if !p.disableSignalHandler { p.handlers.add(p.handleSignals()) }
// Recover from panics. if !p.disableCatchPanics { defer func() { if r := recover(); r != nil { returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic) p.recoverFromPanic(r) } }() }
// Check if output is a TTY before entering raw mode, hiding the cursor and // so on. if err := p.initTerminal(); err != nil { return p.initialModel, err }
// Get the initial window size. width, height := p.width, p.height if p.ttyOutput != nil { // Set the initial size of the terminal. w, h, err := term.GetSize(p.ttyOutput.Fd()) if err != nil { return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err) }
width, height = w, h }
p.width, p.height = width, height resizeMsg := WindowSizeMsg{Width: p.width, Height: p.height}
if p.renderer == nil { if p.disableRenderer { p.renderer = &nilRenderer{} } else { // If no renderer is set use the cursed one. r := newCursedRenderer( p.output, p.environ, p.width, p.height, ) r.setLogger(p.logger) r.setNoInput(p.disableInput) // XXX: This breaks many things especially when we want the output // to be compatible with terminals that are not necessary a TTY. // This was originally done to work around a Wish emulated-pty // issue where when a PTY session is detected, and we don't // allocate a real PTY, the terminal settings (Termios and WinCon) // don't change and the we end up working in cooked mode instead of // raw mode. See issue #1572. mapNl := runtime.GOOS != "windows" && p.ttyInput == nil r.setOptimizations(p.useHardTabs, p.useBackspace, mapNl) p.renderer = r } }
// Get the color profile and send it to the program. if p.profile == nil { cp := colorprofile.Detect(p.output, p.environ) p.profile = &cp }
// Set the color profile on the renderer and send it to the program. p.renderer.setColorProfile(*p.profile) go p.Send(ColorProfileMsg{*p.profile})
// Send the initial size to the program. go p.Send(resizeMsg) p.renderer.resize(resizeMsg.Width, resizeMsg.Height)
// Send the environment variables used by the program. go p.Send(EnvMsg(p.environ))
// Init the input reader and initial model. model := p.initialModel if p.input != nil { if err := p.initInputReader(false); err != nil { return model, err } }
// Start the renderer. p.startRenderer()
if !p.disableRenderer && shouldQuerySynchronizedOutput(p.environ) { // Query for synchronized updates support (mode 2026) and unicode core // (mode 2027). If the terminal supports it, the renderer will enable // it once we get the response. p.executeQuery(ansi.RequestModeSynchronizedOutput + ansi.RequestModeUnicodeCore) }
// Initialize the program. initCmd := model.Init() if initCmd != nil { ch := make(chan struct{}) p.handlers.add(ch)
go func() { defer close(ch)
select { case cmds <- initCmd: case <-p.ctx.Done(): } }() }
// Render the initial view. p.render(model)
// Handle resize events. p.handlers.add(p.handleResize())
// Process commands. p.handlers.add(p.handleCommands(cmds))
// Run event loop, handle updates and draw. var err error model, err = p.eventLoop(model, cmds)
if err == nil && len(p.errs) > 0 { err = <-p.errs // Drain a leftover error in case eventLoop crashed. }
killed := p.externalCtx.Err() != nil || p.ctx.Err() != nil || err != nil if killed { if err == nil && p.externalCtx.Err() != nil { // Return also as context error the cancellation of an external context. // This is the context the user knows about and should be able to act on. err = fmt.Errorf("%w: %w", ErrProgramKilled, p.externalCtx.Err()) } else if err == nil && p.ctx.Err() != nil { // Return only that the program was killed (not the internal mechanism). // The user does not know or need to care about the internal program context. err = ErrProgramKilled } else { // Return that the program was killed and also the error that caused it. err = fmt.Errorf("%w: %w", ErrProgramKilled, err) } } else { // Graceful shutdown of the program (not killed): // Ensure we rendered the final state of the model. p.render(model) }
// Restore terminal state. p.shutdown(killed)
return model, err}
// Send sends a message to the main update function, effectively allowing// messages to be injected from outside the program for interoperability// purposes.//// If the program hasn't started yet this will be a blocking operation.// If the program has already been terminated this will be a no-op, so it's safe// to send messages after the program has exited.func (p *Program) Send(msg Msg) { select { case <-p.ctx.Done(): case p.msgs <- msg: }}
// Quit is a convenience function for quitting Bubble Tea programs. Use it// when you need to shut down a Bubble Tea program from the outside.//// If you wish to quit from within a Bubble Tea program use the Quit command.//// If the program is not running this will be a no-op, so it's safe to call// if the program is unstarted or has already exited.func (p *Program) Quit() { p.Send(Quit())}
// Kill stops the program immediately and restores the former terminal state.// The final render that you would normally see when quitting will be skipped.// [program.Run] returns a [ErrProgramKilled] error.func (p *Program) Kill() { p.shutdown(true)}
// Wait waits/blocks until the underlying Program finished shutting down.func (p *Program) Wait() { <-p.finished}
// execute writes the given sequence to the program output.func (p *Program) execute(seq string) { p.mu.Lock() _, _ = p.outputBuf.WriteString(seq) p.mu.Unlock() p.requestFrame()}
// executeQuery writes a terminal query to the program output. Queries expect a// reply on p.input, so when input is disabled the query is skipped: the// program cannot read the response, and the terminal's reply would leak into// the shell once the program exits (#1590).//// This covers every query that expects a reply, not just the startup// capability probe: background, foreground, and cursor color requests, cursor// position reports, clipboard reads, and terminal version and termcap// queries.func (p *Program) executeQuery(seq string) { if p.disableInput { return } p.execute(seq)}
// flush flushes the output buffer to the program output.func (p *Program) flush() error { p.mu.Lock() defer p.mu.Unlock()
if p.outputBuf.Len() == 0 { return nil } if p.logger != nil { p.logger.Printf("output: %q", p.outputBuf.String()) } _, err := p.output.Write(p.outputBuf.Bytes()) p.outputBuf.Reset() if err != nil { return fmt.Errorf("error writing to output: %w", err) } return nil}
// shutdown performs operations to free up resources and restore the terminal// to its original state.func (p *Program) shutdown(kill bool) { p.shutdownOnce.Do(func() { p.cancel()
// Wait for all handlers to finish. p.handlers.shutdown()
// Check if the cancel reader has been setup before waiting and closing. if p.cancelReader != nil { // Wait for input loop to finish. if p.cancelReader.Cancel() { if !kill { p.waitForReadLoop() } } _ = p.cancelReader.Close() }
if p.renderer != nil { p.stopRenderer(kill) }
_ = p.restoreTerminalState() })}
// recoverFromPanic recovers from a panic, prints the stack trace, and restores// the terminal to a usable state.func (p *Program) recoverFromPanic(r interface{}) { select { case p.errs <- ErrProgramPanic: default: } p.shutdown(true) // Ok to call here, p.Run() cannot do it anymore. // We use "\r\n" to ensure the output is formatted even when restoring the // terminal does not work or when raw mode is still active. rec := strings.ReplaceAll(fmt.Sprintf("%s", r), "\n", "\r\n") fmt.Fprintf(os.Stderr, "Caught panic:\r\n\r\n%s\r\n\r\nRestoring terminal...\r\n\r\n", rec) stack := strings.ReplaceAll(fmt.Sprintf("%s\n", debug.Stack()), "\n", "\r\n") fmt.Fprint(os.Stderr, stack) if v, err := strconv.ParseBool(os.Getenv("TEA_DEBUG")); err == nil && v { f, err := os.Create(fmt.Sprintf("bubbletea-panic-%d.log", time.Now().Unix())) if err == nil { defer f.Close() //nolint:errcheck fmt.Fprintln(f, rec) //nolint:errcheck fmt.Fprintln(f) //nolint:errcheck fmt.Fprintln(f, stack) //nolint:errcheck } }}
// recoverFromGoPanic recovers from a goroutine panic, prints a stack trace and// signals for the program to be killed and terminal restored to a usable state.func (p *Program) recoverFromGoPanic(r interface{}) { select { case p.errs <- ErrProgramPanic: default: } p.cancel() // We use "\r\n" to ensure the output is formatted even when restoring the // terminal does not work or when raw mode is still active. rec := strings.ReplaceAll(fmt.Sprintf("%s", r), "\n", "\r\n") fmt.Fprintf(os.Stderr, "Caught panic:\r\n\r\n%s\r\n\r\nRestoring terminal...\r\n\r\n", rec) stack := strings.ReplaceAll(fmt.Sprintf("%s\n", debug.Stack()), "\n", "\r\n") fmt.Fprint(os.Stderr, stack) if v, err := strconv.ParseBool(os.Getenv("TEA_DEBUG")); err == nil && v { f, err := os.Create(fmt.Sprintf("bubbletea-panic-%d.log", time.Now().Unix())) if err == nil { defer f.Close() //nolint:errcheck fmt.Fprintln(f, rec) //nolint:errcheck fmt.Fprintln(f) //nolint:errcheck fmt.Fprintln(f, stack) //nolint:errcheck } }}
// ReleaseTerminal restores the original terminal state and cancels the input// reader. You can return control to the Program with RestoreTerminal.func (p *Program) ReleaseTerminal() error { return p.releaseTerminal(false)}
func (p *Program) releaseTerminal(reset bool) error { atomic.StoreUint32(&p.ignoreSignals, 1) if p.cancelReader != nil { p.cancelReader.Cancel() }
p.waitForReadLoop()
if p.renderer != nil { p.stopRenderer(false) if reset { p.renderer.reset() } }
return p.restoreTerminalState()}
// RestoreTerminal reinitializes the Program's input reader, restores the// terminal to the former state when the program was running, and repaints.// Use it to reinitialize a Program after running ReleaseTerminal.func (p *Program) RestoreTerminal() error { atomic.StoreUint32(&p.ignoreSignals, 0)
if err := p.initTerminal(); err != nil { return err } if p.input != nil { if err := p.initInputReader(false); err != nil { return err } }
p.startRenderer()
// If the output is a terminal, it may have been resized while another // process was at the foreground, in which case we may not have received // SIGWINCH. Detect any size change now and propagate the new size as // needed. go p.checkResize()
// Flush queued commands. return p.flush()}
// Println prints above the Program. This output is unmanaged by the program// and will persist across renders by the Program.//// If the altscreen is active no output will be printed.func (p *Program) Println(args ...any) { p.msgs <- printLineMessage{ messageBody: fmt.Sprint(args...), }}
// Printf prints above the Program. It takes a format template followed by// values similar to fmt.Printf. This output is unmanaged by the program and// will persist across renders by the Program.//// Unlike fmt.Printf (but similar to log.Printf) the message will be print on// its own line.//// If the altscreen is active no output will be printed.func (p *Program) Printf(template string, args ...any) { p.msgs <- printLineMessage{ messageBody: fmt.Sprintf(template, args...), }}
// startRenderer starts the renderer. It paints when asked to, at most once// per frame, instead of on a fixed-rate ticker.func (p *Program) startRenderer() { framerate := time.Second / time.Duration(p.fps)
// Since the renderer can be restarted after a stop, we need to reset // the done channel and its corresponding sync.Once. p.once = sync.Once{}
// Start the renderer. p.renderer.start() p.requestFrame() // paint whatever was rendered while stopped go func() { pace := time.NewTimer(framerate) pace.Stop() var last time.Time for { select { case <-p.rendererDone: return case <-p.frames: } if wait := framerate - time.Since(last); wait > 0 { pace.Reset(wait) select { case <-p.rendererDone: return case <-pace.C: } } _ = p.flush() _ = p.renderer.flush(false) last = time.Now() } }()}
// stopRenderer stops the renderer.// If kill is true, the renderer will be stopped immediately without flushing// the last frame.func (p *Program) stopRenderer(kill bool) { // Stop the renderer before acquiring the mutex to avoid a deadlock. p.once.Do(func() { p.rendererDone <- struct{}{} })
if !kill { // flush locks the mutex _ = p.renderer.flush(true) }
_ = p.renderer.close()}