v0.2.0 80c61e3

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

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