返回 DeepSeek-Reasonix
mouse_reenable.go
根目录 / internal / cli / mouse_reenable.go
1 package cli
2
3 import (
4 "time"
5
6 tea "charm.land/bubbletea/v2"
7 "github.com/charmbracelet/x/ansi"
8 )
9
10 // enableMouseTracking is the cell-motion + SGR enable sequence matching what
11 // View() requests via tea.MouseModeCellMotion. Windows Terminal / ConPTY can
12 // drop mouse tracking after long turns, resize storms, or focus loss (#7583);
13 // re-sending this sequence restores wheel → MouseWheelMsg instead of bare
14 // Up/Down history cycling.
15 const enableMouseTracking = ansi.SetModeMouseButtonEvent + ansi.SetModeMouseExtSgr
16
17 // mouseReenableMinInterval bounds how often we re-emit enableMouseTracking.
18 // Requests inside the window set a pending flag and arm a trailing-edge timer
19 // so the *last* resize/focus in a storm still re-enables after the quiet period.
20 const mouseReenableMinInterval = 500 * time.Millisecond
21
22 // mouseReenableMsg is delivered after the rate-limit quiet period so a pending
23 // trailing re-enable can fire without sleeping in tests (inject the msg).
24 // Only one timer is armed at a time (mouseReenableTimerArmed); later requests
25 // just set mouseReenablePending, so no generation/seq is required.
26 type mouseReenableMsg struct{}
27
28 // maybeReenableMouse returns a tea.Raw cmd that re-enables mouse tracking when
29 // capture is on and the rate limit allows. Inside the rate-limit window it
30 // records a pending trailing request and arms a one-shot timer (if needed)
31 // instead of dropping the request permanently.
32 //
33 // No-op for Termux native scrollback (mouse is intentionally off so the soft
34 // keyboard can focus) and when the user has toggled capture off via /mouse.
35 func (m *chatTUI) maybeReenableMouse() tea.Cmd {
36 if m.nativeScrollback || m.mouseCaptureOff {
37 m.mouseReenablePending = false
38 return nil
39 }
40 now := m.mouseNow()
41 if !m.lastMouseReenable.IsZero() && now.Sub(m.lastMouseReenable) < mouseReenableMinInterval {
42 m.mouseReenablePending = true
43 return m.armMouseReenableTimer(now)
44 }
45 return m.emitMouseReenable(now)
46 }
47
48 // armMouseReenableTimer schedules a trailing-edge fire for the remaining
49 // rate-limit window. Only one timer is armed at a time; later requests just
50 // set mouseReenablePending.
51 func (m *chatTUI) armMouseReenableTimer(now time.Time) tea.Cmd {
52 if m.mouseReenableTimerArmed {
53 return nil
54 }
55 wait := mouseReenableMinInterval - now.Sub(m.lastMouseReenable)
56 if wait < 0 {
57 wait = 0
58 }
59 m.mouseReenableTimerArmed = true
60 return tea.Tick(wait, func(time.Time) tea.Msg {
61 return mouseReenableMsg{}
62 })
63 }
64
65 // emitMouseReenable sends the enable sequence and clears trailing state.
66 func (m *chatTUI) emitMouseReenable(now time.Time) tea.Cmd {
67 m.lastMouseReenable = now
68 m.mouseReenablePending = false
69 m.mouseReenableTimerArmed = false
70 return tea.Raw(enableMouseTracking)
71 }
72
73 // handleMouseReenableMsg processes a trailing-edge timer. If a request was
74 // still pending after the quiet period, emit enable now.
75 func (m *chatTUI) handleMouseReenableMsg(_ mouseReenableMsg) tea.Cmd {
76 m.mouseReenableTimerArmed = false
77 if !m.mouseReenablePending {
78 return nil
79 }
80 // Pending request survived the quiet period — fire now (and allow another
81 // trailing cycle if more requests arrive inside the next window).
82 return m.maybeReenableMouse()
83 }
84
85 // mouseNow is the clock for rate limiting. Tests replace mouseNowFn.
86 func (m *chatTUI) mouseNow() time.Time {
87 if mouseNowFn != nil {
88 return mouseNowFn()
89 }
90 return time.Now()
91 }
92
93 // mouseNowFn, when non-nil, overrides time.Now for mouse re-enable tests.
94 var mouseNowFn func() time.Time
95
95 lines GO