copy_mouse_input.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close