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