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