Skip to content

Commit

Permalink
feat: add generic event filter
Browse files Browse the repository at this point in the history
WithFilter lets you supply an event filter that will be invoked
before Bubble Tea processes a tea.Msg. The event filter can return
any tea.Msg which will then get handled by Bubble Tea instead of
the original event. If the event filter returns nil, the event
will be ignored and Bubble Tea will not process it.

As an example, this could be used to prevent a program from
shutting down if there are unsaved changes.

Based on the fantastic work by @aschey and supersedes #521.

Resolves #472.
  • Loading branch information
muesli committed Oct 13, 2022
1 parent d90b8d5 commit 9545e7c
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 49 deletions.
15 changes: 10 additions & 5 deletions examples/prevent-quit/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package main

// A program demonstrating how to use the WithOnQuit option to intercept quit events
// A program demonstrating how to use the WithFilter option to intercept events.

import (
"fmt"
Expand All @@ -20,19 +20,24 @@ var (
)

func main() {
p := tea.NewProgram(initialModel(), tea.WithOnQuit(onQuit))
p := tea.NewProgram(initialModel(), tea.WithFilter(filter))

if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}

func onQuit(teaModel tea.Model) tea.QuitBehavior {
func filter(teaModel tea.Model, msg tea.Msg) tea.Msg {
if _, ok := msg.(tea.QuitMsg); !ok {
return msg
}

m := teaModel.(model)
if m.hasChanges {
return tea.PreventShutdown
return nil
}
return tea.Shutdown

return msg
}

type model struct {
Expand Down
36 changes: 21 additions & 15 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,31 +142,37 @@ func WithANSICompressor() ProgramOption {
}
}

// WithOnQuit supplies an event handler that will be invoked whenever Bubble
// Tea receives a QuitMsg. The event handler can return tea.Shutdown to
// instruct Bubble Tea to handle the QuitMsg normally and shut the program
// down, or it can return tea.PreventShutdown to prevent the program from
// shutting down and instead handle the QuitMsg like a normal message and
// pass it along to the model's Update method.
// WithFilter supplies an event filter that will be invoked before Bubble Tea
// processes a tea.Msg. The event filter can return any tea.Msg which will then
// get handled by Bubble Tea instead of the original event. If the event filter
// returns nil, the event will be ignored and Bubble Tea will not process it.
//
// As an example, this could be used to prevent a program from shutting down if
// there are unsaved changes.
//
// Example:
//
// func onQuit(m tea.Model) tea.QuitBehavior {
// model := m.(myModel)
// if model.hasChanges {
// return tea.PreventShutdown
// }
// return tea.Shutdown
// func filter(m tea.Model, msg tea.Msg) tea.Msg {
// if _, ok := msg.(tea.QuitMsg); !ok {
// return msg
// }
//
// model := m.(myModel)
// if model.hasChanges {
// return nil
// }
//
// return msg
// }
//
// p := tea.NewProgram(Model{}, tea.WithOnQuit(onQuit));
// p := tea.NewProgram(Model{}, tea.WithFilter(filter));
//
// if _,err := p.Run(); err != nil {
// fmt.Println("Error running program:", err)
// os.Exit(1)
// }
func WithOnQuit(onQuit func(Model) QuitBehavior) ProgramOption {
func WithFilter(filter func(Model, Msg) Msg) ProgramOption {
return func(p *Program) {
p.onQuit = onQuit
p.filter = filter
}
}
8 changes: 4 additions & 4 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ func TestOptions(t *testing.T) {
}
})

t.Run("on quit", func(t *testing.T) {
p := NewProgram(nil, WithOnQuit(func(Model) QuitBehavior { return Shutdown }))
if p.onQuit == nil {
t.Errorf("expected onQuit to be set")
t.Run("filter", func(t *testing.T) {
p := NewProgram(nil, WithFilter(func(_ Model, msg Msg) Msg { return msg }))
if p.filter == nil {
t.Errorf("expected filter to be set")
}
})

Expand Down
26 changes: 9 additions & 17 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,10 @@ type Program struct {
// below.
windowsStdin *os.File //nolint:golint,structcheck,unused

onQuit func(Model) QuitBehavior
filter func(Model, Msg) Msg
}

// QuitBehavior defines how Bubble Tea handles QuitMsgs.
type QuitBehavior int

const (
// Shutdown instructs Bubble Tea to shut down the program normally when a
// QuitMsg is received.
Shutdown QuitBehavior = iota
// PreventShutdown instructs Bubble Tea to ignore the QuitMsg that it
// received and instead pass the message to the model's Update function.
PreventShutdown
)

// Quit is a special command that tells the Bubble Tea program to exit.
// This behavior can be controlled using the WithOnQuit option.
func Quit() Msg {
return QuitMsg{}
}
Expand Down Expand Up @@ -286,12 +273,17 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
return model, err

case msg := <-p.msgs:
// Filter messages.
if p.filter != nil {
msg = p.filter(model, msg)
}
if msg == nil {
continue
}

// Handle special internal messages.
switch msg := msg.(type) {
case QuitMsg:
if p.onQuit != nil && p.onQuit(model) == PreventShutdown {
break
}
return model, nil

case clearScreenMsg:
Expand Down
19 changes: 11 additions & 8 deletions tea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ func TestTeaQuit(t *testing.T) {
}
}

func TestTeaWithOnQuit(t *testing.T) {
testTeaWithOnQuit(t, 0)
testTeaWithOnQuit(t, 1)
testTeaWithOnQuit(t, 2)
func TestTeaWithFilter(t *testing.T) {
testTeaWithFilter(t, 0)
testTeaWithFilter(t, 1)
testTeaWithFilter(t, 2)
}

func testTeaWithOnQuit(t *testing.T, preventCount uint32) {
func testTeaWithFilter(t *testing.T, preventCount uint32) {
var buf bytes.Buffer
var in bytes.Buffer

Expand All @@ -91,12 +91,15 @@ func testTeaWithOnQuit(t *testing.T, preventCount uint32) {
p := NewProgram(m,
WithInput(&in),
WithOutput(&buf),
WithOnQuit(func(Model) QuitBehavior {
WithFilter(func(_ Model, msg Msg) Msg {
if _, ok := msg.(QuitMsg); !ok {
return msg
}
if shutdowns < preventCount {
atomic.AddUint32(&shutdowns, 1)
return PreventShutdown
return nil
}
return Shutdown
return msg
}))

go func() {
Expand Down

0 comments on commit 9545e7c

Please sign in to comment.