Bubble Bath is a component framework for Charm's excellent BubbleTea framework that's intended to make writing components (Bubbles) easier, particularly for fullscreen TUIs.
Write a component that satisfies InteractiveComponent
interface:
import bubble_bath "github.com/mieubrisse/bubble-bath"
type MyApp interface {
bubble_bath.InteractiveComponent
}
import bubble_bath "github.com/mieubrisse/bubble-bath"
// Implementation of MyApp
type implementation interface {
bubble_bath.InteractiveComponent
}
func New() MyApp {
}
Then use it in your main.go
:
- For each component you create, create a public interface and a private implementation
- Keep each component in its own package (directory)
RunBubbleBathProgram
, a wrapper overtea.NewProgram().Run()
with sane defaults (e.g. handles resizes and quit events out of the box)- If you'd prefer not to use
RunBubbleBathProgram
, aNewBubbleBathModel
function to create atea.Model
for use withtea.NewProgram
- A
Component
interface with standardizedView
,Resize
,GetHeight
, andGetWidth
functions - An
InteractiveComponent
interface with:- A by-reference
Update(msg tea.Msg)
function, so component updating is by-reference. This sacrifices pure Redux-like state machine transitioning, but I don't need/use that right now and should make everything faster (because less by-value copying). If I need the Redux-like state machine transitioning I'll figure out a way to do it. - Standardized
SetFocus
andIsFocused
functions
- A by-reference
- Several out-of-the-box components conforming to
Component
that can be used to build other components:- Flexbox, which allows mixed fixed-size and flexing items
- Text block
- Text input
- Text area
- Text area with Vim bindings
- Filterable list (which can handle nested inputs)
- Filterable checklist
- Several helper methods (e.g.
GetMinInt
,GetMaxInt
, etc.)
During the course of building a decently complex TUI using BubbleTea, I found the vanilla BubbleTea framework useful, but difficult to work with for several reasons:
- When I started with BubbleTea, it seemed like all my custom components should implement the
tea.Model
interface. However, I found it suitable only for the top-level model that gets slotted intotea.NewProgram
, because I hit problems when I tried following the same pattern for subcomponents:- The
tea.Model.Update
command returnstea.Model
by-value. However, this means that you need a force-cast when callingUpdate
on a subcomponent implementing thetea.Model.Update
signature, because the subcomponent will only returntea.Model
(not itself). For example:It seems that the Charm team hit the same thing, because the Bubbles in the example repository don't conform totype Parent struct{ child Child } func (parent Parent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd parent.child, cmd = parent.child.Update(msg).(Child) // <--- This cast is necessary because we're conforming to tea.Model return parent, cmd }
tea.Model
either. - The by-value
Update
is also problematic when trying to create a generic component. For example, I was writingFilterableList[T].Update
, withT
being the element component that the list would contain. No matter how I tried, I couldn't get implementations of theFilterableList[T]
interface to conform to theUpdate(msg tea.Msg) T
function on the interface (though a better Go programmer than I may be able to). - I never needed
Init()
, and my default instinct - to use it to initialize a new component's state - was wrong.
- The
- The concept of "focusable component" is very useful and showed up in nearly all the example Bubbles, but it's not encoded in the BubbleTea framework in any way (all the example Bubbles recreate
Focus
,Blur
, andFocused
by hand). - A resize of my terminal window should have each parent resizing their children (because the parent knows what size the children should be), but there was no out-of-the-box way for components to do this.
- I needed
GetMinInt
,GetMaxInt
, andClamp
everywhere, but BubbleTea doesn't provide this. Instead, the example Bubbles each reimplement these as private methods where needed.
These are problems this system doesn't yet solve but I'd like it to:
- Sizing is strictly top-down: the parent component receives a message, and it tells children what size they should be. There's no way for children components to suggest sizes back up the tree, like the web has with intrinsic vs extrinsic sizes. This would be particularly useful with the flexbox component.
- Due to everything in BubbleTea being strings, the layout of a component (width, height, padding, margin) and its styling (colors, bold, etc.) are deeply coupled. It seems like these should be decoupled - maybe by building in a DOM-like abstraction with the terminal equivalent of CSS.
Aside: as I built this, I (a backend programmer) started to deeply grok the web.