1// Package daemon holds vev's server-side session multiplexer use case.
2package daemon
3
4import (
5 scopy "github.com/bnema/vev/internal/usecase/copy"
6 "github.com/bnema/vev/internal/usecase/mouse"
7)
8
9// handleCopyMouse owns copy-mode mouse routing. A release is lifecycle input
10// even while visual search owns all other copy-mode mouse input.
11func (d *Daemon) handleCopyMouse(sess *session, ac *attachedClient, tb *tab, ev mouse.Event) bool {
12 rt := ac.overlays
13 if !rt.copyActive() {
14 return false
15 }
16 if rt.copySearchActive() {
17 // Search owns ordinary mouse input, but a release completes an
18 // in-flight drag. Route it through the active mapper so its endpoint is
19 // clamped against the press-owned geometry before clearing the pointer.
20 if ev.Button == mouse.Left && ev.Type == mouse.Release {
21 d.handleActiveCopyMouse(sess, ac, tb, ev)
22 }
23 return true
24 }
25 if ev.Button == mouse.Left && ev.Type == mouse.Release {
26 d.handleActiveCopyMouse(sess, ac, tb, ev)
27 return true
28 }
29 switch ev.Button {
30 case mouse.WheelUp:
31 d.smoothCopyWheel(sess, ac, -3)
32 case mouse.WheelDown:
33 d.smoothCopyWheel(sess, ac, 3)
34 case mouse.Left:
35 rt.copyMu.Lock()
36 rt.stopCopyScrollLocked()
37 rt.copyMu.Unlock()
38 d.handleActiveCopyMouse(sess, ac, tb, ev)
39 }
40 return true
41}
42
43// invalidateRejectedLeftPointer clears a stale drag before returning from an
44// event that cannot be routed to terminal content. It never runs while tab or
45// pane locks are held, preserving the copyMu -> tab/pane lock prohibition.
46func invalidateRejectedLeftPointer(rt *overlayRuntime, ev mouse.Event) {
47 if rt == nil || ev.Button != mouse.Left || (ev.Type != mouse.Press && ev.Type != mouse.Release) {
48 return
49 }
50 rt.copyMu.Lock()
51 rt.invalidateCopyPointerLocked(true)
52 rt.copyMu.Unlock()
53}
54
55// handleFreshCopyPress is the only normal-screen press path that creates a
56// selection candidate. Its geometry comes from the same frame-absolute hit
57// test used by active copy mode, never from pane-local synthetic rectangles.
58func (d *Daemon) handleFreshCopyPress(sess *session, ac *attachedClient, tb *tab, routed *pane, ev mouse.Event) bool {
59 if routed == nil {
60 return false
61 }
62 tb.mu.Lock()
63 geometry, ok := hitTestCopyMouseGeometryLocked(tb, d.currentFloatingConfig(), ev.Col, ev.Row)
64 tb.mu.Unlock()
65 if !ok || geometry.pane != routed {
66 ac.overlays.copyMu.Lock()
67 ac.overlays.invalidateCopyPointerLocked(true)
68 ac.overlays.copyMu.Unlock()
69 return true
70 }
71 geometry.pane.mu.Lock()
72 mouseMode, _ := geometry.pane.screen.MouseMode()
73 altScreen := geometry.pane.screen.AltScreenActive()
74 snapshot := scopy.NewSnapshotFromScreen(geometry.pane.history, geometry.pane.screen)
75 geometry.pane.mu.Unlock()
76 if mouseMode != 0 || altScreen {
77 return false // child forwarding retains its existing raw-byte path.
78 }
79 document := scopy.NewDocument(snapshot, d.currentCopyConfig().WordSeparators)
80 mapped, ok := mapCopyMouse(ev, geometry, max(document.Len()-document.Height(), 0), document, false)
81 if !ok {
82 ac.overlays.copyMu.Lock()
83 ac.overlays.invalidateCopyPointerLocked(true)
84 ac.overlays.copyMu.Unlock()
85 return true
86 }
87 rt := ac.overlays
88 rt.copyMu.Lock()
89 if rt.copyMode != nil || rt.copyCandidate != nil {
90 rt.copyMu.Unlock()
91 return true
92 }
93 rt.beginCopyPointerLocked(copyPointerState{pane: geometry.pane, document: document, geometry: geometry, press: mapped.pos})
94 pointer := rt.copyPointer
95 now := d.clock.Now()
96 doubleClick := d.isCopyDoubleClickLocked(rt, geometry.pane, mapped.pos, now)
97 if doubleClick {
98 rt.copyClick = copyClickCandidate{}
99 } else {
100 rt.copyClick = copyClickCandidate{valid: true, pane: geometry.pane, pos: mapped.pos, at: now}
101 }
102 rt.copyMu.Unlock()
103 if !doubleClick {
104 return true
105 }
106
107 selectedWord := false
108 if d.publishCopyMode(sess, ac, tb, geometry.pane, document, func(mode *scopy.Mode) {
109 mode.SetPosition(mapped.pos)
110 selectedWord = mode.SelectWordAt(mapped.pos)
111 }, func(runtime *overlayRuntime, mode *scopy.Mode) {
112 if runtime.copyPointerEpoch != pointer.epoch || runtime.copyPane != pointer.pane || mode.Document() != pointer.document {
113 return
114 }
115 pointer.wordDrag = selectedWord
116 runtime.copyPointer = pointer
117 }) {
118 d.invalidateRender(sess, ac, true, "input.go")
119 }
120 return true
121}
122
123// handleFreshCopyPointer maps a pre-copy drag against its press-owned document
124// and geometry. It is intentionally independent of the currently hovered pane.
125func (d *Daemon) handleFreshCopyPointer(sess *session, ac *attachedClient, ev mouse.Event) bool {
126 rt := ac.overlays
127 rt.copyMu.Lock()
128 pointer := rt.copyPointer
129 epoch := rt.copyPointerEpoch
130 active := rt.copyMode != nil || rt.copyCandidate != nil
131 rt.copyMu.Unlock()
132 if !pointer.valid {
133 if active && ev.Type == mouse.Release {
134 rt.copyMu.Lock()
135 if rt.copyPointerEpoch == epoch {
136 rt.invalidateCopyPointerLocked(false)
137 }
138 rt.copyMu.Unlock()
139 return true
140 }
141 return false
142 }
143 if active {
144 if ev.Type == mouse.Release {
145 rt.copyMu.Lock()
146 if rt.copyPointerEpoch == epoch {
147 rt.invalidateCopyPointerLocked(false)
148 }
149 rt.copyMu.Unlock()
150 return true
151 }
152 return false
153 }
154 pointer.pane.mu.Lock()
155 mouseMode, _ := pointer.pane.screen.MouseMode()
156 altScreen := pointer.pane.screen.AltScreenActive()
157 pointer.pane.mu.Unlock()
158 if mouseMode != 0 || altScreen {
159 rt.copyMu.Lock()
160 if rt.copyPointerEpoch == epoch {
161 rt.invalidateCopyPointerLocked(true)
162 }
163 rt.copyMu.Unlock()
164 return false // preserve child forwarding when it enabled mouse reporting.
165 }
166 mapped, ok := mapCopyMouse(ev, pointer.geometry, max(pointer.document.Len()-pointer.document.Height(), 0), pointer.document, ev.Type == mouse.Motion)
167 if ev.Type == mouse.Motion {
168 rt.copyMu.Lock()
169 if rt.copyPointerEpoch == epoch && rt.copyClick.valid && rt.copyClick.pane == pointer.pane {
170 rt.copyClick.dragged = true
171 }
172 rt.copyMu.Unlock()
173 }
174 if ev.Type == mouse.Release {
175 rt.copyMu.Lock()
176 if rt.copyPointerEpoch == epoch {
177 rt.invalidateCopyPointerLocked(false)
178 }
179 rt.copyMu.Unlock()
180 return true
181 }
182 if !ok {
183 return true
184 }
185 if !d.publishCopyMode(sess, ac, sess.tabForAttachment(ac), pointer.pane, pointer.document, func(mode *scopy.Mode) {
186 mode.StartCharacterSelection(pointer.press)
187 mode.ExtendCharacterSelection(mapped.pos)
188 }, func(runtime *overlayRuntime, mode *scopy.Mode) {
189 if runtime.copyPointerEpoch != epoch || runtime.copyPane != pointer.pane || mode.Document() != pointer.document {
190 return
191 }
192 pointer.dragging = true
193 runtime.copyPointer = pointer
194 }) {
195 return true
196 }
197 d.invalidateRender(sess, ac, true, "input.go")
198 return true
199}
200
201type copyMouseInputSnapshot struct {
202 mode *scopy.Mode
203 pane *pane
204 document *scopy.Document
205 viewportTop int
206 pointer copyPointerState
207 epoch uint64
208}
209
210// snapshotCopyMouseInput captures every mutable copy-mode value needed for a
211// mapping while copyMu is held. Mapping must never inspect Mode afterwards.
212func snapshotCopyMouseInput(rt *overlayRuntime) (copyMouseInputSnapshot, bool) {
213 if rt == nil {
214 return copyMouseInputSnapshot{}, false
215 }
216 rt.copyMu.Lock()
217 defer rt.copyMu.Unlock()
218 if rt.copyMode == nil || rt.copyPane == nil || rt.copyDocument == nil {
219 return copyMouseInputSnapshot{}, false
220 }
221 return copyMouseInputSnapshot{
222 mode: rt.copyMode,
223 pane: rt.copyPane,
224 document: rt.copyDocument,
225 viewportTop: rt.copyMode.ViewportTop,
226 pointer: rt.copyPointer,
227 epoch: rt.copyPointerEpoch,
228 }, true
229}
230
231// invalidateSnapshotCopyPointer clears only the pointer captured with the
232// input snapshot. Mapping runs outside copyMu, so a newer press can otherwise
233// be erased by an older rejected event.
234func invalidateSnapshotCopyPointer(rt *overlayRuntime, snapshot copyMouseInputSnapshot) {
235 rt.copyMu.Lock()
236 defer rt.copyMu.Unlock()
237 if rt.copyPointerEpoch != snapshot.epoch || rt.copyMode != snapshot.mode ||
238 rt.copyPane != snapshot.pane || rt.copyDocument != snapshot.document {
239 return
240 }
241 rt.invalidateCopyPointerLocked(true)
242}
243
244// handleActiveCopyMouse keeps every drag tied to the pane/document captured at
245// press time. Geometry is resolved after copyMu is released to preserve the
246// daemon's copyMu -> tab/pane lock prohibition.
247func (d *Daemon) handleActiveCopyMouse(sess *session, ac *attachedClient, tb *tab, ev mouse.Event) {
248 rt := ac.overlays
249 snapshot, active := snapshotCopyMouseInput(rt)
250 if !active {
251 return
252 }
253 if d.beforeCopyMouseMap != nil {
254 d.beforeCopyMouseMap()
255 }
256 cfg := d.currentFloatingConfig()
257 // Once pressed, use the committed rectangle captured by that press. A
258 // later floating/layout change must not turn an in-flight drag into a hit
259 // on a different pane.
260 if ev.Type != mouse.Press && snapshot.pointer.valid && snapshot.pointer.pane == snapshot.pane && snapshot.pointer.document == snapshot.document {
261 mapped, ok := mapCopyMouse(ev, snapshot.pointer.geometry, snapshot.viewportTop, snapshot.document, ev.Type == mouse.Motion || ev.Type == mouse.Release)
262 if ev.Type == mouse.Release && !ok {
263 invalidateSnapshotCopyPointer(rt, snapshot)
264 return
265 }
266 if ok {
267 d.copyMouse(sess, ac, ev, mapped, snapshot, snapshot.pointer.geometry)
268 }
269 return
270 }
271 tb.mu.Lock()
272 geometry, ok := copyMouseGeometryForPaneLocked(tb, cfg, snapshot.pane)
273 otherPane := false
274 layoutChanged := false
275 if ev.Type == mouse.Press {
276 // A new press is allowed to change focus, but never the document owned
277 // by the active copy mode. Do all tab work first; exitCopyMode takes
278 // copyMu only after tb.mu is released.
279 if hit, hitOK := hitTestCopyMouseGeometryLocked(tb, cfg, ev.Col, ev.Row); hitOK && hit.pane != snapshot.pane {
280 otherPane = true
281 if tb.tree != nil {
282 layoutChanged = focusPlacementLocked(tb, hit.pane.id)
283 }
284 }
285 }
286 tb.mu.Unlock()
287 if layoutChanged {
288 sess.geometry.applyTabLayout(d, sess, tb)
289 }
290 if otherPane {
291 d.exitCopyMode(ac)
292 return
293 }
294 if !ok {
295 if ev.Type == mouse.Release || ev.Type == mouse.Press {
296 invalidateSnapshotCopyPointer(rt, snapshot)
297 }
298 return
299 }
300 clamp := ev.Type == mouse.Motion || ev.Type == mouse.Release
301 mapped, mappedOK := mapCopyMouse(ev, geometry, snapshot.viewportTop, snapshot.document, clamp)
302 if !mappedOK {
303 if ev.Type == mouse.Release || ev.Type == mouse.Press {
304 invalidateSnapshotCopyPointer(rt, snapshot)
305 }
306 return
307 }
308 d.copyMouse(sess, ac, ev, mapped, snapshot, geometry)
309}