v0.8.1 1f54f61

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

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