| 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 |