@@ -13,6 +13,7 @@ import (
1313 "github.com/atotto/clipboard"
1414 tea "github.com/charmbracelet/bubbletea"
1515 "github.com/marcus/sidecar/internal/community"
16+ "github.com/marcus/sidecar/internal/tty"
1617 "github.com/marcus/sidecar/internal/config"
1718 "github.com/marcus/sidecar/internal/mouse"
1819 "github.com/marcus/sidecar/internal/palette"
@@ -23,23 +24,6 @@ import (
2324 "github.com/marcus/sidecar/internal/version"
2425)
2526
26- // isMouseEscapeSequence returns true if the key message appears to be
27- // an unparsed mouse escape sequence (SGR format: [<...M or [<...m)
28- func isMouseEscapeSequence (msg tea.KeyMsg ) bool {
29- s := msg .String ()
30- // SGR mouse sequences contain [< and end with M or m
31- if strings .Contains (s , "[<" ) && (strings .HasSuffix (s , "M" ) || strings .HasSuffix (s , "m" )) {
32- return true
33- }
34- // Check for semicolon-separated coordinate patterns typical of mouse sequences
35- if strings .Contains (s , ";" ) && strings .ContainsAny (s , "0123456789" ) {
36- if strings .HasSuffix (s , "M" ) || strings .HasSuffix (s , "m" ) {
37- return true
38- }
39- }
40- return false
41- }
42-
4327// Update handles all messages and returns the updated model and commands.
4428func (m Model ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
4529 var cmds []tea.Cmd
@@ -80,6 +64,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8064 return m , tea .Batch (cmds ... )
8165
8266 case tea.MouseMsg :
67+ m .recentMouseEvent = true
8368 // Route mouse events to active modal (priority order)
8469 switch m .activeModal () {
8570 case ModalPalette :
@@ -514,6 +499,25 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
514499 }
515500 }
516501
502+ // Universal mouse fragment filter (Linux SGR sequence splitting).
503+ // On Linux, terminals split mouse escape sequences across reads, causing
504+ // Bubble Tea to deliver fragments like "[<67;78;9M" as KeyMsg.
505+ // Fast path checks runes directly to avoid string allocation on every keystroke.
506+ if msg .Type == tea .KeyRunes && len (msg .Runes ) > 0 {
507+ // Fast path: single "[" after a mouse event is a CSI fragment
508+ if len (msg .Runes ) == 1 && msg .Runes [0 ] == '[' && m .recentMouseEvent {
509+ m .recentMouseEvent = false
510+ return m , nil
511+ }
512+ // Multi-char fragments: only allocate string when first rune suggests mouse data
513+ if r := msg .Runes [0 ]; len (msg .Runes ) > 1 && (r == '[' || r == '<' || r == ';' || (r >= '0' && r <= '9' ) || r == 'M' || r == 'm' ) {
514+ if tty .LooksLikeMouseFragment (string (msg .Runes )) {
515+ return m , nil
516+ }
517+ }
518+ }
519+ m .recentMouseEvent = false
520+
517521 if m .showQuitConfirm {
518522 action , cmd := m .quitModal .HandleKey (msg )
519523 switch action {
@@ -700,11 +704,6 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
700704 return m , nil
701705 }
702706
703- // Filter out unparsed mouse escape sequences
704- if isMouseEscapeSequence (msg ) {
705- return m , nil
706- }
707-
708707 // Forward other keys to text input for filtering
709708 var cmd tea.Cmd
710709 m .worktreeSwitcherInput , cmd = m .worktreeSwitcherInput .Update (msg )
@@ -817,11 +816,6 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
817816 return m , nil
818817 }
819818
820- // Filter out unparsed mouse escape sequences
821- if isMouseEscapeSequence (msg ) {
822- return m , nil
823- }
824-
825819 // Forward other keys to text input for filtering
826820 var cmd tea.Cmd
827821 m .projectSwitcherInput , cmd = m .projectSwitcherInput .Update (msg )
@@ -947,11 +941,6 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
947941 return m , nil
948942 }
949943
950- // Filter out unparsed mouse escape sequences
951- if isMouseEscapeSequence (msg ) {
952- return m , nil
953- }
954-
955944 // Forward other keys to text input for filtering
956945 var cmd tea.Cmd
957946 m .themeSwitcherInput , cmd = m .themeSwitcherInput .Update (msg )
@@ -1033,10 +1022,6 @@ func (m *Model) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
10331022 return m , nil
10341023 }
10351024
1036- if isMouseEscapeSequence (msg ) {
1037- return m , nil
1038- }
1039-
10401025 // Forward key to text input, then clear modal cache so it rebuilds
10411026 var cmd tea.Cmd
10421027 m .issueInputInput , cmd = m .issueInputInput .Update (msg )
@@ -1850,11 +1835,6 @@ func (m *Model) handleProjectAddThemePickerKeys(msg tea.KeyMsg) (tea.Model, tea.
18501835 return m , nil
18511836 }
18521837
1853- // Filter out unparsed mouse escape sequences
1854- if isMouseEscapeSequence (msg ) {
1855- return m , nil
1856- }
1857-
18581838 // Forward to filter input
18591839 var cmd tea.Cmd
18601840 m .projectAddThemeInput , cmd = m .projectAddThemeInput .Update (msg )
0 commit comments