copy_scroll_delta.go RawHistoryFolder

1package daemon
2
3import (
4	renderer "github.com/bnema/vev-vt"
5	"github.com/bnema/vev/internal/domain"
6	scopy "github.com/bnema/vev/internal/usecase/copy"
7)
8
9// Only successfully emitted composition caches carry this metadata forward.
10// It describes the terminal shadow, not the unadorned live-pane cache.
11type copyViewportState struct {
12	document *scopy.Document
13	target   domain.Rect
14	top      int
15	cursor   scopy.Pos
16	frame    renderer.Frame
17}
18
19func captureCopyViewport(state capturedRenderState, content domain.Rect) copyViewportState {
20	mode := state.overlays.copyMode
21	if mode == nil || mode.Document() == nil || mode.Selection().Enabled || mode.SearchQuery != "" {
22		return copyViewportState{}
23	}
24	return copyViewportState{document: mode.Document(), target: capturedCopyTarget(state, content), top: mode.ViewportTop, cursor: mode.Cursor()}
25}
26
27// A scroll hint is safe only for a whole-width, unchanged document viewport.
28// The ANSI renderer independently verifies retained cells against its committed
29// shadow and falls back to a snapshot if search or other adornments changed.
30func copyViewportDamage(previous, next copyViewportState, frame renderer.Frame) []renderer.Damage {
31	target := next.target
32	if next.document == nil || previous.document != next.document || previous.target != target ||
33		target.X != 0 || target.Width != frame.Width || target.Y < 0 || target.Height <= 0 ||
34		target.Y >= frame.Height || target.Height >= frame.Height-target.Y ||
35		target.Height != next.document.Height() || target.Width != next.document.Width() {
36		return nil
37	}
38	delta := next.top - previous.top
39	if delta == 0 || delta <= -target.Height || delta >= target.Height {
40		return nil
41	}
42	kind, count, exposedY := renderer.DamageScrollUp, delta, target.Y+target.Height-delta
43	if delta < 0 {
44		kind, count, exposedY = renderer.DamageScrollDown, -delta, target.Y
45	}
46	damage := []renderer.Damage{
47		{Kind: kind, Y: target.Y, Width: target.Width, Height: target.Height, Count: count},
48		{Kind: renderer.DamageText, Y: exposedY, Width: frame.Width, Height: count},
49	}
50	// Refresh the old cursor after shifting it, and paint the new cursor. The
51	// highlighted cursor need not occupy the same screen row after navigation.
52	for _, cursor := range []scopy.Pos{previous.cursor, next.cursor} {
53		y := cursor.Row - next.top
54		if y >= 0 && y < target.Height {
55			damage = append(damage, renderer.Damage{Kind: renderer.DamageText, Y: target.Y + y, Width: frame.Width, Height: 1})
56		}
57	}
58	// Preserve all chrome and other panes above/below the scroll region, even
59	// when live PTY output or status updates arrive together with the wheel.
60	if target.Y > 0 {
61		damage = append(damage, renderer.Damage{Kind: renderer.DamageText, Width: frame.Width, Height: target.Y})
62	}
63	bottom := target.Y + target.Height
64	damage = append(damage, renderer.Damage{Kind: renderer.DamageText, Y: bottom, Width: frame.Width, Height: frame.Height - bottom})
65	return damage
66}
67
68// Reuse retained compact rows instead of decoding and interning the whole
69// immutable viewport. The result is built in spare, the alternate viewport
70// buffer, so both committed frames remain untouched until send succeeds.
71func composeScrolledCopyViewport(state capturedRenderState, base, spare renderer.Frame, previous, next copyViewportState, damage []renderer.Damage) renderer.Frame {
72	frame := spare
73	frame.CopyFrom(previous.frame)
74	target := next.target
75	scroll := damage[0]
76	if scroll.Kind == renderer.DamageScrollDown {
77		frame.ScrollDown(target.Y, target.Y+target.Height-1, scroll.Count)
78	} else {
79		frame.ScrollUp(target.Y, target.Y+target.Height-1, scroll.Count)
80	}
81	for y := range frame.Height {
82		if y < target.Y || y >= target.Y+target.Height {
83			for x := range frame.Width {
84				frame.Set(x, y, base.Cell(x, y))
85			}
86		}
87	}
88	paint := func(y int, row []renderer.Cell) {
89		if y == next.document.Height() {
90			frame.WriteRow(frame.Height-1, 0, row)
91		} else {
92			frame.WriteRow(target.Y+y, 0, row)
93		}
94	}
95	for _, d := range damage[1:] {
96		start, end := max(d.Y-target.Y, 0), min(d.Y+d.Height-target.Y, target.Height)
97		if start < end {
98			state.overlays.copyMode.RenderRowsRange(start, end, paint, state.styles.CopyStatus, state.styles.Selection)
99		}
100	}
101	return frame
102}

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