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 cfg := d.currentRuntimeConfig()
63 tb.mu.Lock()
64 geometry, ok := hitTestCopyMouseGeometryLocked(tb, cfg.floating, ev.Col, ev.Row)
65 tb.mu.Unlock()
66 if !ok || geometry.pane != routed {
67 ac.overlays.copyMu.Lock()
68 ac.overlays.invalidateCopyPointerLocked(true)
69 ac.overlays.copyMu.Unlock()
70 return true
71 }
72 geometry.pane.mu.Lock()
73 mouseMode, _ := geometry.pane.screen.MouseMode()
74 altScreen := geometry.pane.screen.AltScreenActive()
75 snapshot := scopy.NewSnapshotFromScreen(geometry.pane.history, geometry.pane.screen)
76 geometry.pane.mu.Unlock()
77 if mouseMode != 0 || altScreen {
78 return false // child forwarding retains its existing raw-byte path.
79 }
80 document := scopy.NewDocument(snapshot, cfg.copy.WordSeparators)
81 mapped, ok := mapCopyMouse(ev, geometry, max(document.Len()-document.Height(), 0), document, false)
82 if !ok {
83 ac.overlays.copyMu.Lock()
84 ac.overlays.invalidateCopyPointerLocked(true)
85 ac.overlays.copyMu.Unlock()
86 return true
87 }
88 rt := ac.overlays
89 rt.copyMu.Lock()
90 if rt.copyMode != nil || rt.copyCandidate != nil {
91 rt.copyMu.Unlock()
92 return true
93 }
94 rt.beginCopyPointerLocked(copyPointerState{pane: geometry.pane, document: document, geometry: geometry, press: mapped.pos})
95 pointer := rt.copyPointer
96 now := d.clock.Now()
97 doubleClick := d.isCopyDoubleClickLocked(rt, geometry.pane, mapped.pos, now)
98 if doubleClick {
99 rt.copyClick = copyClickCandidate{}
100 } else {
101 rt.copyClick = copyClickCandidate{valid: true, pane: geometry.pane, pos: mapped.pos, at: now}
102 }
103 rt.copyMu.Unlock()
104 if !doubleClick {
105 return true
106 }
107
108 selectedWord := false
109 if d.publishCopyMode(sess, ac, tb, geometry.pane, document, func(mode *scopy.Mode) {
110 mode.SetPosition(mapped.pos)
111 selectedWord = mode.SelectWordAt(mapped.pos)
112 }, func(runtime *overlayRuntime, mode *scopy.Mode) {
113 if runtime.copyPointerEpoch != pointer.epoch || runtime.copyPane != pointer.pane || mode.Document() != pointer.document {
114 return
115 }
116 pointer.wordDrag = selectedWord
117 runtime.copyPointer = pointer
118 }) {
119 d.invalidateRender(sess, ac, true, "input.go")
120 }
121 return true
122}
123
124// handleFreshCopyPointer maps a pre-copy drag against its press-owned document
125// and geometry. It is intentionally independent of the currently hovered pane.
126func (d *Daemon) handleFreshCopyPointer(sess *session, ac *attachedClient, ev mouse.Event) bool {
127 rt := ac.overlays
128 rt.copyMu.Lock()
129 pointer := rt.copyPointer
130 epoch := rt.copyPointerEpoch
131 active := rt.copyMode != nil || rt.copyCandidate != nil
132 rt.copyMu.Unlock()
133 if !pointer.valid {
134 if active && ev.Type == mouse.Release {
135 rt.copyMu.Lock()
136 if rt.copyPointerEpoch == epoch {
137 rt.invalidateCopyPointerLocked(false)
138 }
139 rt.copyMu.Unlock()
140 return true
141 }
142 return false
143 }
144 if active {
145 if ev.Type == mouse.Release {
146 rt.copyMu.Lock()
147 if rt.copyPointerEpoch == epoch {
148 rt.invalidateCopyPointerLocked(false)
149 }
150 rt.copyMu.Unlock()
151 return true
152 }
153 return false
154 }
155 pointer.pane.mu.Lock()
156 mouseMode, _ := pointer.pane.screen.MouseMode()
157 altScreen := pointer.pane.screen.AltScreenActive()
158 pointer.pane.mu.Unlock()
159 if mouseMode != 0 || altScreen {
160 rt.copyMu.Lock()
161 if rt.copyPointerEpoch == epoch {
162 rt.invalidateCopyPointerLocked(true)
163 }
164 rt.copyMu.Unlock()
165 return false // preserve child forwarding when it enabled mouse reporting.
166 }
167 mapped, ok := mapCopyMouse(ev, pointer.geometry, max(pointer.document.Len()-pointer.document.Height(), 0), pointer.document, ev.Type == mouse.Motion)
168 if ev.Type == mouse.Motion {
169 rt.copyMu.Lock()
170 if rt.copyPointerEpoch == epoch && rt.copyClick.valid && rt.copyClick.pane == pointer.pane {
171 rt.copyClick.dragged = true
172 }
173 rt.copyMu.Unlock()
174 }
175 if ev.Type == mouse.Release {
176 rt.copyMu.Lock()
177 if rt.copyPointerEpoch == epoch {
178 rt.invalidateCopyPointerLocked(false)
179 }
180 rt.copyMu.Unlock()
181 return true
182 }
183 if !ok {
184 return true
185 }
186 if !d.publishCopyMode(sess, ac, sess.tabForAttachment(ac), pointer.pane, pointer.document, func(mode *scopy.Mode) {
187 mode.StartCharacterSelection(pointer.press)
188 mode.ExtendCharacterSelection(mapped.pos)
189 }, func(runtime *overlayRuntime, mode *scopy.Mode) {
190 if runtime.copyPointerEpoch != epoch || runtime.copyPane != pointer.pane || mode.Document() != pointer.document {
191 return
192 }
193 pointer.dragging = true
194 runtime.copyPointer = pointer
195 }) {
196 return true
197 }
198 d.invalidateRender(sess, ac, true, "input.go")
199 return true
200}
201
202type copyMouseInputSnapshot struct {
203 mode *scopy.Mode
204 pane *pane
205 document *scopy.Document
206 viewportTop int
207 pointer copyPointerState
208 epoch uint64
209}
210
211// snapshotCopyMouseInput captures every mutable copy-mode value needed for a
212// mapping while copyMu is held. Mapping must never inspect Mode afterwards.
213func snapshotCopyMouseInput(rt *overlayRuntime) (copyMouseInputSnapshot, bool) {
214 if rt == nil {
215 return copyMouseInputSnapshot{}, false
216 }
217 rt.copyMu.Lock()
218 defer rt.copyMu.Unlock()
219 if rt.copyMode == nil || rt.copyPane == nil || rt.copyDocument == nil {
220 return copyMouseInputSnapshot{}, false
221 }
222 return copyMouseInputSnapshot{
223 mode: rt.copyMode,
224 pane: rt.copyPane,
225 document: rt.copyDocument,
226 viewportTop: rt.copyMode.ViewportTop,
227 pointer: rt.copyPointer,
228 epoch: rt.copyPointerEpoch,
229 }, true
230}
231
232// invalidateSnapshotCopyPointer clears only the pointer captured with the
233// input snapshot. Mapping runs outside copyMu, so a newer press can otherwise
234// be erased by an older rejected event.
235func invalidateSnapshotCopyPointer(rt *overlayRuntime, snapshot copyMouseInputSnapshot) {
236 rt.copyMu.Lock()
237 defer rt.copyMu.Unlock()
238 if rt.copyPointerEpoch != snapshot.epoch || rt.copyMode != snapshot.mode ||
239 rt.copyPane != snapshot.pane || rt.copyDocument != snapshot.document {
240 return
241 }
242 rt.invalidateCopyPointerLocked(true)
243}
244
245// handleActiveCopyMouse keeps every drag tied to the pane/document captured at
246// press time. Geometry is resolved after copyMu is released to preserve the
247// daemon's copyMu -> tab/pane lock prohibition.
248func (d *Daemon) handleActiveCopyMouse(sess *session, ac *attachedClient, tb *tab, ev mouse.Event) {
249 rt := ac.overlays
250 snapshot, active := snapshotCopyMouseInput(rt)
251 if !active {
252 return
253 }
254 if d.beforeCopyMouseMap != nil {
255 d.beforeCopyMouseMap()
256 }
257 cfg := d.currentFloatingConfig()
258 // Once pressed, use the committed rectangle captured by that press. A
259 // later floating/layout change must not turn an in-flight drag into a hit
260 // on a different pane.
261 if ev.Type != mouse.Press && snapshot.pointer.valid && snapshot.pointer.pane == snapshot.pane && snapshot.pointer.document == snapshot.document {
262 mapped, ok := mapCopyMouse(ev, snapshot.pointer.geometry, snapshot.viewportTop, snapshot.document, ev.Type == mouse.Motion || ev.Type == mouse.Release)
263 if ev.Type == mouse.Release && !ok {
264 invalidateSnapshotCopyPointer(rt, snapshot)
265 return
266 }
267 if ok {
268 d.copyMouse(sess, ac, ev, mapped, snapshot, snapshot.pointer.geometry)
269 }
270 return
271 }
272 tb.mu.Lock()
273 geometry, ok := copyMouseGeometryForPaneLocked(tb, cfg, snapshot.pane)
274 otherPane := false
275 layoutChanged := false
276 if ev.Type == mouse.Press {
277 // A new press is allowed to change focus, but never the document owned
278 // by the active copy mode. Do all tab work first; exitCopyMode takes
279 // copyMu only after tb.mu is released.
280 if hit, hitOK := hitTestCopyMouseGeometryLocked(tb, cfg, ev.Col, ev.Row); hitOK && hit.pane != snapshot.pane {
281 otherPane = true
282 if tb.tree != nil {
283 layoutChanged = focusPlacementLocked(tb, hit.pane.id)
284 }
285 }
286 }
287 tb.mu.Unlock()
288 if layoutChanged {
289 sess.geometry.applyTabLayout(d, sess, tb)
290 }
291 if otherPane {
292 d.exitCopyMode(ac)
293 return
294 }
295 if !ok {
296 if ev.Type == mouse.Release || ev.Type == mouse.Press {
297 invalidateSnapshotCopyPointer(rt, snapshot)
298 }
299 return
300 }
301 clamp := ev.Type == mouse.Motion || ev.Type == mouse.Release
302 mapped, mappedOK := mapCopyMouse(ev, geometry, snapshot.viewportTop, snapshot.document, clamp)
303 if !mappedOK {
304 if ev.Type == mouse.Release || ev.Type == mouse.Press {
305 invalidateSnapshotCopyPointer(rt, snapshot)
306 }
307 return
308 }
309 d.copyMouse(sess, ac, ev, mapped, snapshot, geometry)
310}