Skip to content

Commit 04404fd

Browse files
committed
feat: allow chainable msg filters; add MouseThrottleFilter
Signed-off-by: Liam Stanley <[email protected]>
1 parent 55b3503 commit 04404fd

File tree

3 files changed

+60
-43
lines changed

3 files changed

+60
-43
lines changed

options.go

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"io"
66
"sync/atomic"
7+
"time"
78

89
"github.com/charmbracelet/colorprofile"
910
)
@@ -101,17 +102,23 @@ func WithoutRenderer() ProgramOption {
101102
}
102103
}
103104

104-
// WithFilter supplies an event filter that will be invoked before Bubble Tea
105-
// processes a tea.Msg. The event filter can return any tea.Msg which will then
106-
// get handled by Bubble Tea instead of the original event. If the event filter
107-
// returns nil, the event will be ignored and Bubble Tea will not process it.
105+
// MsgFilter is a function that can be used to filter messages before they are
106+
// processed by Bubble Tea. If the provided function returns nil, the message will
107+
// be ignored and Bubble Tea will not process it.
108+
type MsgFilter func(Model, Msg) Msg
109+
110+
// WithFilters supplies one or more message filters that will be invoked before
111+
// Bubble Tea processes a [Msg]. The message filter can return any [Msg] which
112+
// will then get handled by Bubble Tea instead of the original message. If the
113+
// filter returns nil for a specific message, the message will be ignored and
114+
// Bubble Tea will not process it, and not continue to the next filter.
108115
//
109116
// As an example, this could be used to prevent a program from shutting down if
110-
// there are unsaved changes.
117+
// there are unsaved changes, or used to throttle/drop high-frequency messages.
111118
//
112-
// Example:
119+
// Example -- preventing a program from shutting down if there are unsaved changes:
113120
//
114-
// func filter(m tea.Model, msg tea.Msg) tea.Msg {
121+
// func preventUnsavedFilter(m tea.Model, msg tea.Msg) tea.Msg {
115122
// if _, ok := msg.(tea.QuitMsg); !ok {
116123
// return msg
117124
// }
@@ -124,15 +131,54 @@ func WithoutRenderer() ProgramOption {
124131
// return msg
125132
// }
126133
//
127-
// p := tea.NewProgram(Model{}, tea.WithFilter(filter));
134+
// p := tea.NewProgram(Model{}, tea.WithFilters(preventUnsavedFilter));
128135
//
129136
// if _,err := p.Run(); err != nil {
130137
// fmt.Println("Error running program:", err)
131138
// os.Exit(1)
132139
// }
133-
func WithFilter(filter func(Model, Msg) Msg) ProgramOption {
140+
func WithFilters(filters ...MsgFilter) ProgramOption {
134141
return func(p *Program) {
135-
p.filter = filter
142+
if len(filters) == 0 {
143+
p.filter = nil
144+
return
145+
}
146+
p.filter = func(m Model, msg Msg) Msg {
147+
for _, filter := range filters {
148+
msg = filter(m, msg)
149+
if msg == nil {
150+
return nil
151+
}
152+
}
153+
return msg
154+
}
155+
}
156+
}
157+
158+
// MouseThrottleFilter is a message filter that throttles [MouseWheelMsg] and
159+
// [MouseMotionMsg] messages. This is particularly useful when enabling
160+
// [MouseModeCellMotion] or [MouseModeAllMotion] mouse modes, which can often
161+
// send excessive messages when the user is moving the mouse very fast, causing
162+
// high-resource usage and sluggish re-rendering.
163+
//
164+
// If the provided throttle duration is 0, the default value of 15ms will be used.
165+
func MouseThrottleFilter(throttle time.Duration) MsgFilter {
166+
if throttle <= 0 {
167+
throttle = 15 * time.Millisecond
168+
}
169+
170+
var lastMouseMsg, now time.Time
171+
172+
return func(_ Model, msg Msg) Msg {
173+
switch msg.(type) {
174+
case MouseWheelMsg, MouseMotionMsg:
175+
now = time.Now()
176+
if now.Sub(lastMouseMsg) < throttle {
177+
return nil
178+
}
179+
lastMouseMsg = now
180+
}
181+
return msg
136182
}
137183
}
138184

options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestOptions(t *testing.T) {
3232
})
3333

3434
t.Run("filter", func(t *testing.T) {
35-
p := NewProgram(nil, WithFilter(func(_ Model, msg Msg) Msg { return msg }))
35+
p := NewProgram(nil, WithFilters(func(_ Model, msg Msg) Msg { return msg }))
3636
if p.filter == nil {
3737
t.Errorf("expected filter to be set")
3838
}

tea.go

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -440,38 +440,9 @@ type Program struct {
440440
// cleanup on exit.
441441
disableCatchPanics bool
442442

443-
// filter supplies an event filter that will be invoked before Bubble Tea
444-
// processes a tea.Msg. The event filter can return any tea.Msg which will
445-
// then get handled by Bubble Tea instead of the original event. If the
446-
// event filter returns nil, the event will be ignored and Bubble Tea will
447-
// not process it.
448-
//
449-
// As an example, this could be used to prevent a program from shutting
450-
// down if there are unsaved changes.
451-
//
452-
// Example:
453-
//
454-
// func filter(m tea.Model, msg tea.Msg) tea.Msg {
455-
// if _, ok := msg.(tea.QuitMsg); !ok {
456-
// return msg
457-
// }
458-
//
459-
// model := m.(myModel)
460-
// if model.hasChanges {
461-
// return nil
462-
// }
463-
//
464-
// return msg
465-
// }
466-
//
467-
// p := tea.NewProgram(Model{});
468-
// p.filter = filter
469-
//
470-
// if _,err := p.Run(context.Background()); err != nil {
471-
// fmt.Println("Error running program:", err)
472-
// os.Exit(1)
473-
// }
474-
filter func(Model, Msg) Msg
443+
// filter provides a way of filtering messages before they are processed by
444+
// Bubble Tea. See [WithFilters] for more information.
445+
filter MsgFilter
475446

476447
// fps sets a custom maximum fps at which the renderer should run. If less
477448
// than 1, the default value of 60 will be used. If over 120, the fps will

0 commit comments

Comments
 (0)