Bubble Tea

repository·main·Indexed 13 days ago

https://github.com/charmbracelet/bubbletea

A Go framework for building functional, stateful terminal applications using The Elm Architecture. It provides a high-performance, declarative way to manage TUI state, input, and rendering through a Model-Update-View pattern.

Tokens
16.4K
Snippets
71
Records
105
Agent score
97%

What's inside Bubble Tea

  1. How Declarative Views work in Bubble Tea v2

    main

    In v2, Bubble Tea moves from imperative commands (e.g., sending a command to enter the alt screen) to declarative View fields.

    Instead of configuring the program at startup or sending commands during Update, you define the desired terminal state by setting fields on a tea.View struct within your View() method. Bubble Tea then reconciles this state with the terminal.

    Common fields in tea.View include:

    • AltScreen: Enter/exit the alternate screen buffer.
    • MouseMode: Set to tea.MouseModeNone, tea.MouseModeCellMotion, or tea.MouseModeAllMotion.
    • Cursor: Control position, shape, and color.
    • WindowTitle: Set the terminal window title.
    • ReportFocus: Enable focus/blur event reporting.
    // v2: declarative — everything lives in View()
    func (m model) View() tea.View {
        v := tea.NewView("Hello!")
        v.AltScreen = true
        v.MouseMode = tea.MouseModeCellMotion
        return v
    }
  2. How Commands and Messages work in Bubble Tea

    main

    In Bubble Tea, tea.Cmd is the mechanism for performing I/O (network requests, disk access, timers, etc.) without blocking the main UI loop.

    Key Concepts:

    • tea.Cmd: A function with the signature func() tea.Msg. Commands run asynchronously in a separate goroutine managed by the Bubble Tea runtime.
    • tea.Msg: Any type (including empty structs or custom types) returned by a tea.Cmd. Once a command finishes, the runtime sends the returned Msg to your Update method.
    • The Workflow:
      1. The Init method returns a tea.Cmd.
      2. The runtime executes the command in a goroutine.
      3. The command returns a tea.Msg.
      4. The Update method receives the Msg and updates the model.

    To keep your program responsive, always move I/O operations into commands rather than performing them directly inside Update or View.

    // A command is a function that returns a Msg
    func checkServer() tea.Msg {
        // ... perform I/O ...
        return statusMsg(200)
    }
    
    // The Update method handles the resulting Msg
    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg := msg.(type) {
        case statusMsg:
            m.status = int(msg)
            return m, tea.Quit
        }
        return m, nil
    }
  3. Core Architecture: The Model-Update-View Pattern

    main

    Bubble Tea follows a functional design paradigm inspired by The Elm Architecture. A Bubble Tea application is comprised of a model that describes the application state and three essential methods implemented on that model:

    1. Init(): A function that returns an initial tea.Cmd for the application to run (e.g., for initial I/O).
    2. Update(msg tea.Msg): A function that handles incoming events (tea.Msg) and returns an updated model and an optional tea.Cmd.
    3. View(): A function that renders the UI based on the current state of the model, returning a tea.View.

    Bubble Tea manages the rendering loop and event dispatching, so you only need to focus on how state changes in response to messages and how that state is represented visually.

    type model struct {
        // application state
    }
    
    func (m model) Init() tea.Cmd {
        return nil
    }
    
    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        // handle messages and return updated model
        return m, nil
    }
    
    func (m model) View() tea.View {
        // return UI content
        return tea.NewView("...")
    }
  4. How to pass arguments to a Command

    main

    Because tea.Cmd is defined as a function that takes no arguments (func() tea.Msg), you cannot pass parameters directly to the command itself. Instead, you must use a wrapper function that accepts your arguments and returns a tea.Cmd (a closure).

    Pattern:

    1. Define a function that takes your required arguments.
    2. This function returns a tea.Cmd.
    3. Inside that returned function, perform the logic and return the tea.Msg.

    This ensures the actual execution (the closure) remains argument-less, satisfying the tea.Cmd type signature.

    // Wrapper function to allow passing an argument
    func checkSomeUrl(url string) tea.Cmd {
        return func() tea.Msg {
            // The actual I/O happens here in the closure
            c := &http.Client{Timeout: 10 * time.Second}
            res, err := c.Get(url)
            if err != nil {
                return errMsg{err}
            }
            return statusMsg(res.StatusCode)
        }
    }
  5. Implement download progress in Bubble Tea

    main

    To show download progress in a Bubble Tea application, you can combine io.TeeReader with Program.Send().

    1. Use io.TeeReader to wrap your download reader. This allows you to intercept the data being read and calculate how many bytes have been processed.
    2. Use Program.Send(msg) to send progress updates (as custom Msg types) from your background download routine to the Bubble Tea Update loop. This allows the Model to update its state and the View to render a progress bar (such as from the bubbles/progress component).
  6. Compose multiple bubbles into a single application

    main
    You can compose multiple independent bubbles (e.g., a spinner and a timer) into a single application. This involves managing the state of each sub-bubble within your main model's Update method and rendering them in your View method. You can also implement logic to switch between different composed views.
  7. How Bubble Tea works: The Elm Architecture

    main

    Bubble Tea is a Go framework for building terminal applications based on The Elm Architecture. An application is composed of a model (the state) and three core methods that define its lifecycle:

    1. Init: A function that returns an initial tea.Cmd for the application to run (e.g., performing initial I/O).
    2. Update: A function that handles incoming tea.Msg events (like keypresses or timer ticks) and returns an updated model and an optional tea.Cmd.
    3. View: A function that renders the UI by looking at the current state of the model and returning a tea.View.

    This declarative approach means you don't manage redrawing logic; you simply describe what the UI should look like based on the current state, and the Bubble Tea runtime handles the rest.

    type model struct {
        // application state
    }
    
    func (m model) Init() tea.Cmd {
        return nil
    }
    
    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        // handle events and return updated model
        return m, nil
    }
    
    func (m model) View() tea.View {
        // return the UI representation
        return tea.NewView("hello world")
    }
  8. Run a Bubble Tea program

    main

    To start your application, use tea.NewProgram with your initial model, then call .Run() on the resulting program instance. The Run() method returns an error if the program fails to execute.

    func main() {
        p := tea.NewProgram(initialModel())
        if _, err := p.Run(); err != nil {
            fmt.Printf("Alas, there's been an error: %v", err)
            os.Exit(1)
        }
    }
  9. Update Import Paths for Bubble Tea and Lip Gloss

    main

    The module paths for Bubble Tea and Lip Gloss have changed to a vanity domain in v2.

    // Before
    import tea "github.com/charmbracelet/bubbletea"
    import "github.com/charmbracelet/lipgloss"
    
    // After
    import tea "charm.land/bubbletea/v2"
    import "charm.land/lipgloss/v2"
  10. Log to a file for debugging

    main

    Since the terminal UI occupies stdout, you cannot use standard logging to the console. Use tea.LogToFile to redirect debug logs to a file. This is useful when you want to inspect state or errors without breaking the TUI layout.

    if len(os.Getenv("DEBUG")) > 0 {
    	if _, err := tea.LogToFile("debug.log", "debug"); err != nil {
    		fmt.Println("fatal:", err)
    		os.Exit(1)
    	}
    	defer f.Close()
    }
  11. Implement the Model interface

    main

    To build a Bubble Tea application, you must implement the Model interface. This interface defines the core lifecycle of your application based on The Elm Architecture:

    1. Init() Cmd: The first function called. It returns an optional initial command (e.g., to fetch data or start a timer). Return nil if no initial action is needed.
    2. Update(Msg) (Model, Cmd): The central logic hub. It is called whenever a new Msg (event) is received. You inspect the message, update your model's state, and optionally return a Cmd to perform an IO operation.
    3. View() View: The rendering function. It is called after every Update. It returns a View object representing what should be displayed on the screen.
    type Model interface {
    	Init() Cmd
    	Update(Msg) (Model, Cmd)
    	View() View
    }
  12. Create periodic ticks with Every or Tick

    main

    Both Every and Tick produce a single command that returns a message after a specified duration. To create a continuous loop (a recurring timer), you must return the command again from your Update method after receiving the message.

    Every

    Every synchronizes with the system clock. If you set a duration of one minute, the tick will occur at the next clock boundary (e.g., at the start of the next minute), which may be less than the specified duration.

    Tick

    Tick is independent of the system clock. The timer begins precisely when the command is invoked and runs for the full specified duration.

    Implementation Pattern for Recurring Ticks

    To implement a recurring timer, follow this pattern:

    type TickMsg time.Time
    
    func doTick() tea.Cmd {
        return tea.Tick(time.Second, func(t time.Time) tea.Msg {
            return TickMsg(t)
        })
    }
    
    func (m model) Init() (tea.Model, tea.Cmd) {
        return m, doTick()
    }
    
    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg.(type) {
        case TickMsg:
            // Return the command again to loop
            return m, doTick()
        }
        return m, nil
    }
    type TickMsg time.Time
    
    // Send a message every second.
    func doTick() tea.Cmd {
        return tea.Tick(time.Second, func(t time.Time) tea.Msg {
            return TickMsg(t)
        })
    }