Direct mutation
Handle(state, event) writes to your struct in place.
No dispatch, no reducer, no round trip through a message type.
Go-native terminal UI · running live in this browser
Mutate one state struct. Handle events directly. Render one frame.
No Msg/Cmd/Update loop, no
message plumbing — just the program you'd write anyway.
flatte · compiling WebAssembly
the same Go core that renders to your terminal
A background ticker plus a keypress — the exact
cmd/bubble-modal and cmd/flat-modal
samples in this repo, side by side.
// a message type, just to say "time passed"
type tickMsg struct{}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case tickMsg:
m.ticks++
return m, tickCmd() // reschedule via Cmd
case tea.KeyMsg:
return m.updateKey(msg) // (Model, Cmd)
}
return m, nil // copy back the model
}
// no message type — just a named update
func applyTick(s *State, _ time.Time) {
s.ticks++ // mutate in place
}
func Init(s *State, fx flatte.Effects[State]) {
flatte.Every(fx, "tick", ival, applyTick)
}
func Handle(s *State, ev flatte.Event, fx …) {
if key.Key == flatte.KeyEnter {
s.modalOpen = true // no return, no Cmd
}
}
Msg types, no (Model, Cmd) returns
Handle(state, event) writes to your struct in place.
No dispatch, no reducer, no round trip through a message type.
View(state) returns a Frame. Same input,
same pixels — every screen is a golden snapshot you can test.
Async work is a goroutine that sends one named
StateUpdate back. No mutexes, no goroutine-per-widget.