Skip to content

Repository files navigation

multibubble

A Bubble Tea component for rendering multiple tea.Model views side-by-side in a single TUI.

multibubble demo

Tab / Shift+Tab to change focus · Run the demo


Features

Feature Description
Multiple models Wrap each tea.Model in MultiBubbleModel; layout order matches the slice order.
Message routing Optionally restrict which message types each model receives via registered types or RegisterAllMessageTypes.
Equal-width columns One row, equal width per model, configurable margin (2% of width, min 1).
Keyboard focus Tab / Shift+Tab change focus; optional per-view styling for focused vs unfocused.
Optional title Title above the views with configurable lipgloss style.
Layout behavior Long lines truncated with "…"; shorter lines padded; each styled line ends with SGR reset so styles do not bleed.

Requirements


Installation

go get github.com/a0ngo/multibubble

Import the package (the component lives under the module path):

import "github.com/a0ngo/multibubble"

models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModel(myModel1),
    multibubble.NewMultiBubbleModel(myModel2),
}
m := multibubble.NewMultiBubble(models)

Usage

Create a multi-bubble with NewMultiBubble and a slice of MultiBubbleModel. Use NewMultiBubbleModel(tea.Model) so each model receives all messages, or NewMultiBubbleModelWithTypes(model, &types) to restrict messages by type. Optionally set a title and size, then run with Bubble Tea. Handle tea.WindowSizeMsg by calling SetSize. Use Tab / Shift+Tab to change focus; call FocusedModelIndex() to read the current focus.

import (
    tea "github.com/charmbracelet/bubbletea"
    "github.com/a0ngo/multibubble"
)

models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModel(model1),
    multibubble.NewMultiBubbleModel(model2),
}
m := multibubble.NewMultiBubble(models)
m.SetTitle("My App")
m.SetSize(80, 24)
// In your Update, on tea.WindowSizeMsg: m.SetSize(msg.Width, msg.Height)

p := tea.NewProgram(m, tea.WithAltScreen())
p.Run()

Runnable demo: examples/simple. From the repo root:

go run ./examples/simple

Selective message routing

By default, NewMultiBubbleModel(model) delivers every message to the model (it uses RegisterAllMessageTypes). Use NewMultiBubbleModelWithTypes(model, &types) so a model only receives messages whose type is in types. The focused model always receives every message regardless of registration (so focus and key handling work as expected). That keeps input handling local (e.g. only the focused input gets key events) and avoids unnecessary updates (e.g. a spinner need not see resize messages).

Example: model receives all messages

// Every tea.Msg (KeyMsg, WindowSizeMsg, etc.) is delivered.
models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModel(mySpinner),
    multibubble.NewMultiBubbleModel(myStatusPanel),
}

Example: model receives only key events

import "reflect"

keyTypes := []reflect.Type{
    reflect.TypeFor[tea.KeyMsg](),
}
models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModelWithTypes(myTextInput, &keyTypes),
}
// myTextInput.Update is only called for tea.KeyMsg.

Example: model receives keys and a custom focus message

type focusMsg int

msgTypes := []reflect.Type{
    reflect.TypeFor[tea.KeyMsg](),
    reflect.TypeFor[focusMsg](),
}
models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModelWithTypes(myInput, &msgTypes),
}
// Send focusMsg from the root when focus changes so the input can call Focus()/Blur().

Example: explicitly “receive all” by reference

// Same as NewMultiBubbleModel: all message types are delivered.
models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModelWithTypes(myModel, multibubble.RegisterAllMessageTypes),
}

Mixing models

// Spinner and status get everything; input only gets KeyMsg and focusMsg.
keyAndFocusTypes := []reflect.Type{
    reflect.TypeFor[tea.KeyMsg](),
    reflect.TypeFor[focusMsg](),
}
models := []multibubble.MultiBubbleModel{
    multibubble.NewMultiBubbleModel(spinnerModel),
    multibubble.NewMultiBubbleModel(statusModel),
    multibubble.NewMultiBubbleModelWithTypes(inputModel, &keyAndFocusTypes),
}
m := multibubble.NewMultiBubble(models)

API overview

MultiBubble

Symbol Description
NewMultiBubble(models []MultiBubbleModel) *MultiBubble Constructor; layout order matches the slice.
SetModels(models []MultiBubbleModel) Set or replace the child models.
FocusedModelIndex() int Returns the 0-based index of the currently focused model.
SetTitle(title string) Title above the views; empty means no title line.
SetTitleStyle(style lipgloss.Style) Lipgloss style for the title.
SetSize(width, height int) Required: call when your program receives tea.WindowSizeMsg.
SetStyleForView(f StyleForViewFunc) Optional. Style for unfocused views. func(modelIndex int, view string, width, height int) lipgloss.Style.
SetStyleForFocusedView(f StyleForFocusedViewFunc) Optional. Style for the focused view. func(modelIndex int, view string) lipgloss.Style.

MultiBubble implements tea.Model (Init, Update, View).

MultiBubbleModel and message routing

Symbol Description
MultiBubbleModel Wraps a tea.Model and optional registered message types.
NewMultiBubbleModel(model tea.Model) MultiBubbleModel Model receives all message types.
NewMultiBubbleModelWithTypes(model tea.Model, types *[]reflect.Type) MultiBubbleModel Model receives only the given message types. Pass RegisterAllMessageTypes for all; pass nil for none.
RegisterAllMessageTypes Sentinel *[]reflect.Type; when used as the types argument, the model receives all messages.
IsRegisteredMsgType(msg tea.Msg) bool Reports whether the message type is registered (all if RegisterAllMessageTypes; none if nil; otherwise per-slice).

Layout

Views are laid out in a single row. Each model gets equal width; a margin (2% of total width, min 1) is placed between columns. If a title is set, it is rendered on the first line. Per-column: lines longer than the column width are truncated with "…"; shorter lines are space-padded. Each styled line is closed with an SGR reset so styles do not bleed into padding or adjacent columns.


Running tests

go test ./...

Example

See examples/simple for a runnable program: spinner (bubbles), focus-aware text panel, and interactive text input; Tab/Shift+Tab to change focus, Ctrl+C to quit, and optional focused-view styling.


License

Apache 2.0. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages