on main help sign in
wip: implement selection
ebd68acKujtim Hoxha
internal/tui/components/chat/chat.go+101−6
@@ -2,8 +2,10 @@ package chat
2
2
3import (
3import (
4 "context"
4 "context"
5+ "fmt"
5 "time"
6 "time"
6
7
8+ "github.com/atotto/clipboard"
7 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/key"
8 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/app"
11 "github.com/charmbracelet/crush/internal/app"
@@ -42,6 +44,8 @@ type MessageListCmp interface {
42
44
43 SetSession(session.Session) tea.Cmd
45 SetSession(session.Session) tea.Cmd
44 GoToBottom() tea.Cmd
46 GoToBottom() tea.Cmd
47+ GetSelectedText() string
48+ CopySelectedText() tea.Cmd
45}
49}
46
50
47// messageListCmp implements MessageListCmp, providing a virtualized list
51// messageListCmp implements MessageListCmp, providing a virtualized list
@@ -56,6 +60,12 @@ type messageListCmp struct {
56
60
57 lastUserMessageTime int64
61 lastUserMessageTime int64
58 defaultListKeyMap list.KeyMap
62 defaultListKeyMap list.KeyMap
63+
64+ // Click tracking for double/triple click detection
65+ lastClickTime time.Time
66+ lastClickX int
67+ lastClickY int
68+ clickCount int
59}
69}
60
70
61// New creates a new message list component with custom keybindings
71// New creates a new message list component with custom keybindings
@@ -87,23 +97,42 @@ func (m *messageListCmp) Init() tea.Cmd {
87func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
97func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
88 switch msg := msg.(type) {
98 switch msg := msg.(type) {
89 case tea.MouseClickMsg:
99 case tea.MouseClickMsg:
100+ x := msg.X - 1 // Adjust for padding
101+ y := msg.Y - 1 // Adjust for padding
102+ if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
103+ return m, nil // Ignore clicks outside the component
104+ }
90 if msg.Button == tea.MouseLeft {
105 if msg.Button == tea.MouseLeft {
91− m.listCmp.StartSelection(msg.X, msg.Y-1)
106+ return m, m.handleMouseClick(x, y)
92 }
107 }
93 return m, nil
108 return m, nil
94 case tea.MouseMotionMsg:
109 case tea.MouseMotionMsg:
95− if msg.Button == tea.MouseLeft {
110+ x := msg.X - 1 // Adjust for padding
96− m.listCmp.EndSelection(msg.X, msg.Y-1)
111+ y := msg.Y - 1 // Adjust for padding
97− if msg.Y <= 1 {
112+ if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
113+ if y < 0 {
98 return m, m.listCmp.MoveUp(1)
114 return m, m.listCmp.MoveUp(1)
115+ }
99− } else if msg.Y >= m.height-1 {
116+ if y >= m.height-1 {
100 return m, m.listCmp.MoveDown(1)
117 return m, m.listCmp.MoveDown(1)
101 }
118 }
119+ return m, nil // Ignore clicks outside the component
120+ }
121+ if msg.Button == tea.MouseLeft {
122+ m.listCmp.EndSelection(x, y)
102 }
123 }
103 return m, nil
124 return m, nil
104 case tea.MouseReleaseMsg:
125 case tea.MouseReleaseMsg:
126+ x := msg.X - 1 // Adjust for padding
127+ y := msg.Y - 1 // Adjust for padding
105 if msg.Button == tea.MouseLeft {
128 if msg.Button == tea.MouseLeft {
129+ if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 {
130+ m.listCmp.SelectionStop()
131+ return m, m.CopySelectedText()
132+ }
106− m.listCmp.EndSelection(msg.X, msg.Y-1)
133+ m.listCmp.EndSelection(x, y)
134+ m.listCmp.SelectionStop()
135+ return m, m.CopySelectedText()
107 }
136 }
108 return m, nil
137 return m, nil
109 case pubsub.Event[permission.PermissionNotification]:
138 case pubsub.Event[permission.PermissionNotification]:
@@ -586,3 +615,69 @@ func (m *messageListCmp) Bindings() []key.Binding {
586func (m *messageListCmp) GoToBottom() tea.Cmd {
615func (m *messageListCmp) GoToBottom() tea.Cmd {
587 return m.listCmp.GoToBottom()
616 return m.listCmp.GoToBottom()
588}
617}
618+
619+// handleMouseClick handles mouse click events and detects double/triple clicks.
620+func (m *messageListCmp) handleMouseClick(x, y int) tea.Cmd {
621+ const (
622+ doubleClickThreshold = 500 * time.Millisecond
623+ clickTolerance = 2 // pixels
624+ )
625+
626+ now := time.Now()
627+
628+ // Check if this is a potential multi-click
629+ if now.Sub(m.lastClickTime) <= doubleClickThreshold &&
630+ abs(x-m.lastClickX) <= clickTolerance &&
631+ abs(y-m.lastClickY) <= clickTolerance {
632+ m.clickCount++
633+ } else {
634+ m.clickCount = 1
635+ }
636+
637+ m.lastClickTime = now
638+ m.lastClickX = x
639+ m.lastClickY = y
640+
641+ switch m.clickCount {
642+ case 1:
643+ // Single click - start selection
644+ m.listCmp.StartSelection(x, y)
645+ case 2:
646+ // Double click - select word
647+ m.listCmp.SelectWord(x, y)
648+ case 3:
649+ // Triple click - select paragraph
650+ m.listCmp.SelectParagraph(x, y)
651+ m.clickCount = 0 // Reset after triple click
652+ }
653+
654+ return nil
655+}
656+
657+// GetSelectedText returns the currently selected text from the list component.
658+func (m *messageListCmp) GetSelectedText() string {
659+ return m.listCmp.GetSelectedText(3) // 3 padding for the left border/padding
660+}
661+
662+// CopySelectedText copies the currently selected text to the clipboard.
663+func (m *messageListCmp) CopySelectedText() tea.Cmd {
664+ return nil
665+ selectedText := m.GetSelectedText()
666+ if selectedText == "" {
667+ return util.ReportInfo("No text selected")
668+ }
669+
670+ err := clipboard.WriteAll(selectedText)
671+ if err != nil {
672+ return util.ReportError(fmt.Errorf("failed to copy selected text to clipboard: %w", err))
673+ }
674+ return util.ReportInfo("Selected text copied to clipboard")
675+}
676+
677+// abs returns the absolute value of an integer.
678+func abs(x int) int {
679+ if x < 0 {
680+ return -x
681+ }
682+ return x
683+}
internal/tui/exp/list/list.go+267−6
@@ -1,7 +1,6 @@
1package list
1package list
2
2
3import (
3import (
4− "log/slog"
5 "slices"
4 "slices"
6 "strings"
5 "strings"
7 "sync"
6 "sync"
@@ -15,6 +14,8 @@ import (
15 "github.com/charmbracelet/crush/internal/tui/util"
14 "github.com/charmbracelet/crush/internal/tui/util"
16 "github.com/charmbracelet/lipgloss/v2"
15 "github.com/charmbracelet/lipgloss/v2"
17 uv "github.com/charmbracelet/ultraviolet"
16 uv "github.com/charmbracelet/ultraviolet"
17+ "github.com/charmbracelet/x/ansi"
18+ "github.com/rivo/uniseg"
18)
19)
19
20
20type Item interface {
21type Item interface {
@@ -50,6 +51,11 @@ type List[T Item] interface {
50 AppendItem(T) tea.Cmd
51 AppendItem(T) tea.Cmd
51 StartSelection(col, line int)
52 StartSelection(col, line int)
52 EndSelection(col, line int)
53 EndSelection(col, line int)
54+ SelectionStop()
55+ SelectionClear()
56+ SelectWord(col, line int)
57+ SelectParagraph(col, line int)
58+ GetSelectedText(paddingLeft int) string
53}
59}
54
60
55type direction int
61type direction int
@@ -103,6 +109,8 @@ type list[T Item] struct {
103 selectionStartLine int
109 selectionStartLine int
104 selectionEndCol int
110 selectionEndCol int
105 selectionEndLine int
111 selectionEndLine int
112+
113+ selectionActive bool
106}
114}
107
115
108type ListOption func(*confOptions)
116type ListOption func(*confOptions)
@@ -298,7 +306,8 @@ func (l *list[T]) View() string {
298 Height(l.height).
306 Height(l.height).
299 Width(l.width).
307 Width(l.width).
300 Render(strings.Join(lines, "\n"))
308 Render(strings.Join(lines, "\n"))
301− if l.selectionStartCol < 0 {
309+
310+ if !l.hasSelection() {
302 return view
311 return view
303 }
312 }
304 area := uv.Rect(0, 0, l.width, l.height)
313 area := uv.Rect(0, 0, l.width, l.height)
@@ -311,8 +320,8 @@ func (l *list[T]) View() string {
311 }
320 }
312 selArea = selArea.Canon()
321 selArea = selArea.Canon()
313
322
314− specialChars := make(map[string]bool, len(styles.AllIcons))
323+ specialChars := make(map[string]bool, len(styles.SelectionIgnoreIcons))
315− for _, icon := range styles.AllIcons {
324+ for _, icon := range styles.SelectionIgnoreIcons {
316 specialChars[icon] = true
325 specialChars[icon] = true
317 }
326 }
318
327
@@ -951,21 +960,67 @@ func (l *list[T]) decrementOffset(n int) {
951
960
952// MoveDown implements List.
961// MoveDown implements List.
953func (l *list[T]) MoveDown(n int) tea.Cmd {
962func (l *list[T]) MoveDown(n int) tea.Cmd {
963+ oldOffset := l.offset
954 if l.direction == DirectionForward {
964 if l.direction == DirectionForward {
955 l.incrementOffset(n)
965 l.incrementOffset(n)
956 } else {
966 } else {
957 l.decrementOffset(n)
967 l.decrementOffset(n)
958 }
968 }
969+
970+ if oldOffset == l.offset {
971+ // no change in offset, so no need to change selection
972+ return nil
973+ }
974+ // if we are not actively selecting move the whole selection down
975+ if l.hasSelection() && !l.selectionActive {
976+ if l.selectionStartLine < l.selectionEndLine {
977+ l.selectionStartLine -= n
978+ l.selectionEndLine -= n
979+ } else {
980+ l.selectionStartLine -= n
981+ l.selectionEndLine -= n
982+ }
983+ }
984+ if l.selectionActive {
985+ if l.selectionStartLine < l.selectionEndLine {
986+ l.selectionStartLine -= n
987+ } else {
988+ l.selectionEndLine -= n
989+ }
990+ }
959 return l.changeSelectionWhenScrolling()
991 return l.changeSelectionWhenScrolling()
960}
992}
961
993
962// MoveUp implements List.
994// MoveUp implements List.
963func (l *list[T]) MoveUp(n int) tea.Cmd {
995func (l *list[T]) MoveUp(n int) tea.Cmd {
996+ oldOffset := l.offset
964 if l.direction == DirectionForward {
997 if l.direction == DirectionForward {
965 l.decrementOffset(n)
998 l.decrementOffset(n)
966 } else {
999 } else {
967 l.incrementOffset(n)
1000 l.incrementOffset(n)
968 }
1001 }
1002+
1003+ if oldOffset == l.offset {
1004+ // no change in offset, so no need to change selection
1005+ return nil
1006+ }
1007+
1008+ if l.hasSelection() && !l.selectionActive {
1009+ if l.selectionStartLine > l.selectionEndLine {
1010+ l.selectionStartLine += n
1011+ l.selectionEndLine += n
1012+ } else {
1013+ l.selectionStartLine += n
1014+ l.selectionEndLine += n
1015+ }
1016+ }
1017+ if l.selectionActive {
1018+ if l.selectionStartLine > l.selectionEndLine {
1019+ l.selectionStartLine += n
1020+ } else {
1021+ l.selectionEndLine += n
1022+ }
1023+ }
969 return l.changeSelectionWhenScrolling()
1024 return l.changeSelectionWhenScrolling()
970}
1025}
971
1026
@@ -1164,18 +1219,224 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
1164 return tea.Sequence(cmds...)
1219 return tea.Sequence(cmds...)
1165}
1220}
1166
1221
1222+func (l *list[T]) hasSelection() bool {
1223+ return l.selectionEndCol != l.selectionStartCol || l.selectionEndLine != l.selectionStartLine
1224+}
1225+
1167// StartSelection implements List.
1226// StartSelection implements List.
1168func (l *list[T]) StartSelection(col, line int) {
1227func (l *list[T]) StartSelection(col, line int) {
1169 l.selectionStartCol = col
1228 l.selectionStartCol = col
1170 l.selectionStartLine = line
1229 l.selectionStartLine = line
1171 l.selectionEndCol = col
1230 l.selectionEndCol = col
1172 l.selectionEndLine = line
1231 l.selectionEndLine = line
1173− slog.Info("Position", "col", col, "line", line)
1232+ l.selectionActive = true
1174}
1233}
1175
1234
1176// EndSelection implements List.
1235// EndSelection implements List.
1177func (l *list[T]) EndSelection(col, line int) {
1236func (l *list[T]) EndSelection(col, line int) {
1237+ if !l.selectionActive {
1238+ return
1239+ }
1178 l.selectionEndCol = col
1240 l.selectionEndCol = col
1179 l.selectionEndLine = line
1241 l.selectionEndLine = line
1180− slog.Info("Position", "col", col, "line", line)
1242+}
1243+
1244+func (l *list[T]) SelectionStop() {
1245+ l.selectionActive = false
1246+}
1247+
1248+func (l *list[T]) SelectionClear() {
1249+ l.selectionStartCol = -1
1250+ l.selectionStartLine = -1
1251+ l.selectionEndCol = -1
1252+ l.selectionEndLine = -1
1253+ l.selectionActive = false
1254+}
1255+
1256+func (l *list[T]) findWordBoundaries(col, line int) (startCol, endCol int) {
1257+ lines := strings.Split(l.rendered, "\n")
1258+ for i, l := range lines {
1259+ lines[i] = ansi.Strip(l)
1260+ }
1261+
1262+ if l.direction == DirectionBackward {
1263+ line = ((len(lines) - 1) - l.height) + line + 1
1264+ }
1265+
1266+ if l.offset > 0 {
1267+ if l.direction == DirectionBackward {
1268+ line -= l.offset
1269+ } else {
1270+ line += l.offset
1271+ }
1272+ }
1273+
1274+ currentLine := lines[line]
1275+ gr := uniseg.NewGraphemes(currentLine)
1276+ startCol = -1
1277+ upTo := col
1278+ for gr.Next() {
1279+ if gr.IsWordBoundary() && upTo > 0 {
1280+ startCol = col - upTo + 1
1281+ } else if gr.IsWordBoundary() && upTo < 0 {
1282+ endCol = col - upTo + 1
1283+ break
1284+ }
1285+ if upTo == 0 && gr.Str() == " " {
1286+ return 0, 0
1287+ }
1288+ upTo -= 1
1289+ }
1290+ if startCol == -1 {
1291+ return 0, 0
1292+ }
1293+ return
1294+}
1295+
1296+func (l *list[T]) findParagraphBoundaries(line int) (startLine, endLine int, found bool) {
1297+ lines := strings.Split(l.rendered, "\n")
1298+ for i, l := range lines {
1299+ lines[i] = ansi.Strip(l)
1300+ for _, icon := range styles.SelectionIgnoreIcons {
1301+ lines[i] = strings.ReplaceAll(lines[i], icon, " ")
1302+ }
1303+ }
1304+ if l.direction == DirectionBackward {
1305+ line = (len(lines) - 1) - l.height + line + 1
1306+ }
1307+
1308+ if strings.TrimSpace(lines[line]) == "" {
1309+ return 0, 0, false
1310+ }
1311+
1312+ if l.offset > 0 {
1313+ if l.direction == DirectionBackward {
1314+ line -= l.offset
1315+ } else {
1316+ line += l.offset
1317+ }
1318+ }
1319+
1320+ // Ensure line is within bounds
1321+ if line < 0 || line >= len(lines) {
1322+ return 0, 0, false
1323+ }
1324+
1325+ // Find start of paragraph (search backwards for empty line or start of text)
1326+ startLine = line
1327+ for startLine > 0 && strings.TrimSpace(lines[startLine-1]) != "" {
1328+ startLine--
1329+ }
1330+
1331+ // Find end of paragraph (search forwards for empty line or end of text)
1332+ endLine = line
1333+ for endLine < len(lines)-1 && strings.TrimSpace(lines[endLine+1]) != "" {
1334+ endLine++
1335+ }
1336+
1337+ // revert the line numbers if we are in backward direction
1338+ if l.direction == DirectionBackward {
1339+ startLine = startLine - (len(lines) - 1) + l.height - 1
1340+ endLine = endLine - (len(lines) - 1) + l.height - 1
1341+ }
1342+ if l.offset > 0 {
1343+ if l.direction == DirectionBackward {
1344+ startLine += l.offset
1345+ endLine += l.offset
1346+ } else {
1347+ startLine -= l.offset
1348+ endLine -= l.offset
1349+ }
1350+ }
1351+ return startLine, endLine, true
1352+}
1353+
1354+// SelectWord selects the word at the given position.
1355+func (l *list[T]) SelectWord(col, line int) {
1356+ startCol, endCol := l.findWordBoundaries(col, line)
1357+ l.selectionStartCol = startCol
1358+ l.selectionStartLine = line
1359+ l.selectionEndCol = endCol
1360+ l.selectionEndLine = line
1361+ l.selectionActive = false // Not actively selecting, just selected
1362+}
1363+
1364+// SelectParagraph selects the paragraph at the given position.
1365+func (l *list[T]) SelectParagraph(col, line int) {
1366+ startLine, endLine, found := l.findParagraphBoundaries(line)
1367+ if !found {
1368+ return
1369+ }
1370+ l.selectionStartCol = 0
1371+ l.selectionStartLine = startLine
1372+ l.selectionEndCol = l.width - 1
1373+ l.selectionEndLine = endLine
1374+ l.selectionActive = false // Not actively selecting, just selected
1375+}
1376+
1377+// GetSelectedText returns the currently selected text.
1378+func (l *list[T]) GetSelectedText(paddingLeft int) string {
1379+ return ""
1380+ // if !l.hasSelection() {
1381+ // return ""
1382+ // }
1383+ //
1384+ // startLine := l.selectionStartLine
1385+ // endLine := l.selectionEndLine
1386+ // startCol := l.selectionStartCol
1387+ // endCol := l.selectionEndCol
1388+ //
1389+ // if l.direction == DirectionBackward {
1390+ // startLine = (lipgloss.Height(l.rendered) - 1) - startLine
1391+ // endLine = (lipgloss.Height(l.rendered) - 1) - endLine
1392+ // }
1393+ //
1394+ // if l.offset > 0 {
1395+ // if l.direction == DirectionBackward {
1396+ // startLine += l.offset
1397+ // endLine += l.offset
1398+ // } else {
1399+ // startLine -= l.offset
1400+ // endLine -= l.offset
1401+ // }
1402+ // }
1403+ //
1404+ // lines := strings.Split(l.rendered, "\n")
1405+ //
1406+ // if startLine < 0 || endLine < 0 || startLine >= len(lines) || endLine >= len(lines) {
1407+ // return ""
1408+ // }
1409+ //
1410+ // var result strings.Builder
1411+ // for i := range lines {
1412+ // lines[i] = ansi.Strip(lines[i])
1413+ // for _, icon := range styles.SelectionIgnoreIcons {
1414+ // lines[i] = strings.ReplaceAll(lines[i], icon, " ")
1415+ // }
1416+ //
1417+ // if i == startLine {
1418+ // if startCol < 0 || startCol >= len(lines[i]) {
1419+ // startCol = 0
1420+ // }
1421+ // if startCol < paddingLeft {
1422+ // startCol = paddingLeft
1423+ // }
1424+ // if i != endLine {
1425+ // endCol = len(lines[i])
1426+ // }
1427+ // result.WriteString(strings.TrimRightFunc(lines[i][startCol:endCol], unicode.IsSpace))
1428+ // } else if i > startLine && i < endLine {
1429+ // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:], unicode.IsSpace))
1430+ // } else if i == endLine {
1431+ // if endCol < 0 || endCol >= len(lines[i]) {
1432+ // endCol = len(lines[i])
1433+ // }
1434+ // if endCol < paddingLeft {
1435+ // endCol = paddingLeft
1436+ // }
1437+ // result.WriteString(strings.TrimRightFunc(lines[i][paddingLeft:endCol], unicode.IsSpace))
1438+ // }
1439+ // }
1440+ //
1441+ // return result.String()
1181}
1442}
internal/tui/page/chat/chat.go+9−17
@@ -172,30 +172,22 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
172 }
172 }
173 return p, nil
173 return p, nil
174 case tea.MouseClickMsg:
174 case tea.MouseClickMsg:
175− if msg.Button == tea.MouseLeft {
176− if p.isMouseOverChat(msg.X, msg.Y) {
177− u, cmd := p.chat.Update(msg)
175+ u, cmd := p.chat.Update(msg)
178− p.chat = u.(chat.MessageListCmp)
176+ p.chat = u.(chat.MessageListCmp)
179− return p, cmd
177+ return p, cmd
180− }
181− }
182 return p, nil
178 return p, nil
183 case tea.MouseMotionMsg:
179 case tea.MouseMotionMsg:
184 if msg.Button == tea.MouseLeft {
180 if msg.Button == tea.MouseLeft {
185− if p.isMouseOverChat(msg.X, msg.Y) {
186− u, cmd := p.chat.Update(msg)
181+ u, cmd := p.chat.Update(msg)
187− p.chat = u.(chat.MessageListCmp)
182+ p.chat = u.(chat.MessageListCmp)
188− return p, cmd
183+ return p, cmd
189− }
190 }
184 }
191 return p, nil
185 return p, nil
192 case tea.MouseReleaseMsg:
186 case tea.MouseReleaseMsg:
193 if msg.Button == tea.MouseLeft {
187 if msg.Button == tea.MouseLeft {
194− if p.isMouseOverChat(msg.X, msg.Y) {
195− u, cmd := p.chat.Update(msg)
188+ u, cmd := p.chat.Update(msg)
196− p.chat = u.(chat.MessageListCmp)
189+ p.chat = u.(chat.MessageListCmp)
197− return p, cmd
190+ return p, cmd
198− }
199 }
191 }
200 return p, nil
192 return p, nil
201 case tea.WindowSizeMsg:
193 case tea.WindowSizeMsg:
internal/tui/styles/icons.go+18−16
@@ -16,24 +16,26 @@ const (
16 ToolSuccess string = "✓"
16 ToolSuccess string = "✓"
17 ToolError string = "×"
17 ToolError string = "×"
18
18
19− BorderThin string = "│"
19+ BorderThin string = "│"
20+ BorderThick string = "▌"
20)
21)
21
22
22−var AllIcons = []string{
23+var SelectionIgnoreIcons = []string{
23− CheckIcon,
24+ // CheckIcon,
24− ErrorIcon,
25+ // ErrorIcon,
25− WarningIcon,
26+ // WarningIcon,
26− InfoIcon,
27+ // InfoIcon,
27− HintIcon,
28+ // HintIcon,
28− SpinnerIcon,
29+ // SpinnerIcon,
29− LoadingIcon,
30+ // LoadingIcon,
30− DocumentIcon,
31+ // DocumentIcon,
31− ModelIcon,
32+ // ModelIcon,
32−
33+ //
33− // Tool call icons
34+ // // Tool call icons
34− ToolPending,
35+ // ToolPending,
35− ToolSuccess,
36+ // ToolSuccess,
36− ToolError,
37+ // ToolError,
37
38
38 BorderThin,
39 BorderThin,
40+ BorderThick,
39}
41}
internal/tui/tui.go+1−1
@@ -41,7 +41,7 @@ func MouseEventFilter(m tea.Model, msg tea.Msg) tea.Msg {
41 case tea.MouseWheelMsg, tea.MouseMotionMsg:
41 case tea.MouseWheelMsg, tea.MouseMotionMsg:
42 now := time.Now()
42 now := time.Now()
43 // trackpad is sending too many requests
43 // trackpad is sending too many requests
44− if now.Sub(lastMouseEvent) < 20*time.Millisecond {
44+ if now.Sub(lastMouseEvent) < 15*time.Millisecond {
45 return nil
45 return nil
46 }
46 }
47 lastMouseEvent = now
47 lastMouseEvent = now