on main help sign in
Improve TUI performance with Bubbletea v2 event filtering
82d7747Raphael Amorim
cmd/root.go+2
@@ -120,6 +120,8 @@ to assist developers in writing, debugging, and understanding code directly from
120 tea.WithAltScreen(),
120 tea.WithAltScreen(),
121 tea.WithKeyReleases(),
121 tea.WithKeyReleases(),
122 tea.WithUniformKeyLayout(),
122 tea.WithUniformKeyLayout(),
123+ tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
124+ tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
123 )
125 )
124
126
125 // Setup the subscriptions, this will send services events to the TUI
127 // Setup the subscriptions, this will send services events to the TUI
internal/tui/components/chat/editor/editor.go+44−6
@@ -7,6 +7,7 @@ import (
7 "runtime"
7 "runtime"
8 "slices"
8 "slices"
9 "strings"
9 "strings"
10+ "time"
10 "unicode"
11 "unicode"
11
12
12 "github.com/charmbracelet/bubbles/v2/key"
13 "github.com/charmbracelet/bubbles/v2/key"
@@ -30,6 +31,8 @@ type FileCompletionItem struct {
30 Path string // The file path
31 Path string // The file path
31}
32}
32
33
34+type CompletionDebounceMsg struct{}
35+
33type editorCmp struct {
36type editorCmp struct {
34 width int
37 width int
35 height int
38 height int
@@ -46,6 +49,9 @@ type editorCmp struct {
46 currentQuery string
49 currentQuery string
47 completionsStartIndex int
50 completionsStartIndex int
48 isCompletionsOpen bool
51 isCompletionsOpen bool
52+
53+ // Debouncing for completions
54+ debounceTimer *time.Timer
49}
55}
50
56
51var DeleteKeyMaps = DeleteAttachmentKeyMaps{
57var DeleteKeyMaps = DeleteAttachmentKeyMaps{
@@ -141,6 +147,22 @@ func (m *editorCmp) send() tea.Cmd {
141 )
147 )
142}
148}
143
149
150+// debouncedCompletionFilter creates a debounced command for filtering completions
151+func (m *editorCmp) debouncedCompletionFilter() tea.Cmd {
152+ // Cancel existing timer if any
153+ if m.debounceTimer != nil {
154+ m.debounceTimer.Stop()
155+ }
156+
157+ // Create new timer for debouncing
158+ m.debounceTimer = time.NewTimer(150 * time.Millisecond)
159+
160+ return func() tea.Msg {
161+ <-m.debounceTimer.C
162+ return CompletionDebounceMsg{}
163+ }
164+}
165+
144func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
166func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145 var cmd tea.Cmd
167 var cmd tea.Cmd
146 var cmds []tea.Cmd
168 var cmds []tea.Cmd
@@ -160,6 +182,18 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
160 m.isCompletionsOpen = false
182 m.isCompletionsOpen = false
161 m.currentQuery = ""
183 m.currentQuery = ""
162 m.completionsStartIndex = 0
184 m.completionsStartIndex = 0
185+ // Cancel any pending debounce timer
186+ if m.debounceTimer != nil {
187+ m.debounceTimer.Stop()
188+ m.debounceTimer = nil
189+ }
190+ case CompletionDebounceMsg:
191+ // Handle debounced completion filtering
192+ if m.isCompletionsOpen {
193+ return m, util.CmdHandler(completions.FilterCompletionsMsg{
194+ Query: m.currentQuery,
195+ })
196+ }
163 case completions.SelectCompletionMsg:
197 case completions.SelectCompletionMsg:
164 if !m.isCompletionsOpen {
198 if !m.isCompletionsOpen {
165 return m, nil
199 return m, nil
@@ -196,9 +230,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
196 case msg.String() == "backspace" && m.isCompletionsOpen:
230 case msg.String() == "backspace" && m.isCompletionsOpen:
197 if len(m.currentQuery) > 0 {
231 if len(m.currentQuery) > 0 {
198 m.currentQuery = m.currentQuery[:len(m.currentQuery)-1]
232 m.currentQuery = m.currentQuery[:len(m.currentQuery)-1]
199− cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{
233+ // Use debounced filtering instead of immediate filtering
200− Query: m.currentQuery,
234+ cmds = append(cmds, m.debouncedCompletionFilter())
201− }))
202 } else {
235 } else {
203 m.isCompletionsOpen = false
236 m.isCompletionsOpen = false
204 m.currentQuery = ""
237 m.currentQuery = ""
@@ -208,9 +241,8 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
208 default:
241 default:
209 if m.isCompletionsOpen {
242 if m.isCompletionsOpen {
210 m.currentQuery += msg.String()
243 m.currentQuery += msg.String()
211− cmds = append(cmds, util.CmdHandler(completions.FilterCompletionsMsg{
244+ // Use debounced filtering instead of immediate filtering
212− Query: m.currentQuery,
245+ cmds = append(cmds, m.debouncedCompletionFilter())
213− }))
214 }
246 }
215 }
247 }
216 if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
248 if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
@@ -258,6 +290,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
258 }
290 }
259 }
291 }
260 }
292 }
293+
261 m.textarea, cmd = m.textarea.Update(msg)
294 m.textarea, cmd = m.textarea.Update(msg)
262 cmds = append(cmds, cmd)
295 cmds = append(cmds, cmd)
263 return m, tea.Batch(cmds...)
296 return m, tea.Batch(cmds...)
@@ -355,6 +388,11 @@ func (m *editorCmp) startCompletions() tea.Msg {
355// Blur implements Container.
388// Blur implements Container.
356func (c *editorCmp) Blur() tea.Cmd {
389func (c *editorCmp) Blur() tea.Cmd {
357 c.textarea.Blur()
390 c.textarea.Blur()
391+ // Clean up debounce timer when losing focus
392+ if c.debounceTimer != nil {
393+ c.debounceTimer.Stop()
394+ c.debounceTimer = nil
395+ }
358 return nil
396 return nil
359}
397}
360
398
internal/tui/page/chat/chat.go+6
@@ -39,6 +39,7 @@ type (
39type ChatPage interface {
39type ChatPage interface {
40 util.Model
40 util.Model
41 layout.Help
41 layout.Help
42+ IsChatFocused() bool
42}
43}
43
44
44type chatPage struct {
45type chatPage struct {
@@ -415,3 +416,8 @@ func NewChatPage(app *app.App) ChatPage {
415 header: header.New(app.LSPClients),
416 header: header.New(app.LSPClients),
416 }
417 }
417}
418}
419+
420+// IsChatFocused returns whether the chat messages are focused (true) or editor is focused (false)
421+func (p *chatPage) IsChatFocused() bool {
422+ return p.chatFocused
423+}
internal/tui/tui.go+23−3
@@ -31,6 +31,29 @@ import (
31 "github.com/charmbracelet/lipgloss/v2"
31 "github.com/charmbracelet/lipgloss/v2"
32)
32)
33
33
34+// MouseEventFilter filters mouse events based on the current focus state
35+// This is used with tea.WithFilter to prevent mouse scroll events from
36+// interfering with typing performance in the editor
37+func MouseEventFilter(m tea.Model, msg tea.Msg) tea.Msg {
38+ // Only filter mouse events
39+ switch msg.(type) {
40+ case tea.MouseWheelMsg, tea.MouseMotionMsg:
41+ // Check if we have an appModel and if editor is focused
42+ if appModel, ok := m.(*appModel); ok {
43+ if appModel.currentPage == chat.ChatPageID {
44+ if chatPage, ok := appModel.pages[appModel.currentPage].(chat.ChatPage); ok {
45+ // If editor is focused (not chatFocused), filter out mouse wheel/motion events
46+ if !chatPage.IsChatFocused() {
47+ return nil // Filter out the event
48+ }
49+ }
50+ }
51+ }
52+ }
53+ // Allow all other events to pass through
54+ return msg
55+}
56+
34// appModel represents the main application model that manages pages, dialogs, and UI state.
57// appModel represents the main application model that manages pages, dialogs, and UI state.
35type appModel struct {
58type appModel struct {
36 wWidth, wHeight int // Window dimensions
59 wWidth, wHeight int // Window dimensions
@@ -82,9 +105,6 @@ func (a appModel) Init() tea.Cmd {
82 return nil
105 return nil
83 })
106 })
84
107
85− // Enable mouse support.
86− cmds = append(cmds, tea.EnableMouseAllMotion)
87−
88 return tea.Batch(cmds...)
108 return tea.Batch(cmds...)
89}
109}
90
110