charmbracelet / bubbletea

A powerful little TUI framework 🏗
MIT License
27.24k stars 788 forks source link

Proposal: Model v2, program context #1010

Closed aymanbagabas closed 1 month ago

aymanbagabas commented 4 months ago

During the execution of the program, models need to access the underlying terminal features and capabilities. We can achieved this with an API change that introduces Bubble Tea Contexts. The nice thing about this Context type is that it can be used to control executions of goroutines because it embeds context.Context. Using tea.WithContext(ctx), users can access the provided context before running the program in their Bubble Tea model.

Bubble Tea Model Context

This fixes all sorts of issues with input and color, particularly in Wish. Basically, apps built on Bubble Tea v2 will “just work” if put behind a Wish server.

This also means that Bubble Tea now has first-class Lip Gloss support. Now, with the new advanced input handler, Bubble Tea can read and parse any type of sequence events as the terminal input buffer receive them. This means that important events that can change the look and behavor of our program can be detected in Bubble Tea. When the program starts, it will query the terminal for Kitty Keyboard support and the current terminal's background color. The background color is important because it affects Lip Gloss styles.

You can now use the new tea.Context to read different terminal capabilities. Bubble Tea now does all the heavy lifting of detecting terminal colors and background and can create ctx.NewStyle() that are specific to the current running program (whether local or in a remote session). This will also pave the road for future improvements to be added on the context.

The new interface will look like this:

// 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(ctx Context) (Model, 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(ctx Context, msg Msg) (Model, Cmd)

    // View renders the program's UI, which is just a string. The view is
    // rendered after every Update.
    View(ctx Context) string
}

// Context represents a Bubble Tea program's context. It is passed to the
// program's Init, Update, and View functions to provide information about the
// program's state and to allow them to interact with the terminal.
type Context interface {
    context.Context

    // BackgroundColor returns the current background color of the terminal.
    // It returns nil if the terminal's doesn't support querying the background
    // color.
    BackgroundColor() color.Color

    // HasLightBackground returns true if the terminal's background color is
    // light. This is useful for determining whether to use light or dark colors
    // in the program's UI.
    HasLightBackground() bool

    // SupportsEnhancedKeyboard reports whether the terminal supports enhanced
    // keyboard keys. On Windows, this means it supports virtual keys like and
    // the Windows Console API. On Unix, this means it supports the Kitty
    // Keyboard Protocol.
    SupportsEnhancedKeyboard() bool

    // NewStyle returns a new Lip Gloss style that is suitable for the program's
    // environment.
    NewStyle() lipgloss.Style

    // ColorProfile returns the terminal's color profile.
    ColorProfile() lipgloss.Profile

    // what else?
}

How to upgrade

//// Before

// Uses globals, bad 👎
var myStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#4CFFCC"))

type myModel struct{}

// Init does nothing!
func (m myModel) Init() tea.Cmd {
    return nil
}

// Update
func (m myModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg.(type) {
    case tea.KeyMsg:
      return m, tea.Printf("You pressed %s", msg)
    }
    return m, nil
}

// View
func (m myModel) View() string {
    return myStyle.Render("Hello there!")
}
//// After

// Model now takes a tea.Context argument
type myModel struct{
  style lipgloss.Style
}

// Init now also returns both the model and a cmd
func (m myModel) Init(ctx tea.Context) (tea.Model, tea.Cmd) {
    m.style = ctx.NewStyle().Foreground(lipgloss.Color("#4CFFCC"))
    return m, nil
}

// Update now takes a ctx and a msg
func (m myModel) Update(ctx tea.Context, msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg.(type) {
    case tea.KeyMsg:
      return m, tea.Printf("You pressed %s", msg)
    }
    return m, nil
}

// View now takes a ctx
func (m myModel) View(ctx tea.Context) string {
    return m.style.Render("Hello there!")
}

You can find a WIP version of this proposal here

carreter commented 3 months ago

This would be incredibly useful for passing global context to submodels.

meowgorithm commented 1 month ago

After some testing and feedback, we've determined we can proceed towards v2 without the context changes proposed here. This will reduce the barrier to upgrading and keep Bubble Tea easier to work with overall.

Thanks to everyone who helped us test this proposal!

carreter commented 1 month ago

@meowgorithm Since this change isn't happening, how do you recommend passing state between submodules in a bubble tea program?

pme-openai commented 1 month ago

@carreter you use

p := tea.NewProgram(model{}, tea.WithContext(ctx))

https://github.com/charmbracelet/bubbletea/blob/105d88a7282a810529c37f08fa7c03392860a80e/options.go#L20 though this is only for cancelling the program, not plumbing it into other parts