v0.5.1 8e4df5c

screen_scroll.go RawHistoryFolder

1package vt
2
3import (
4	"errors"
5	"fmt"
6	"math"
7
8	renderer "github.com/bnema/vev-vt/core"
9	"github.com/bnema/vev-vt/graphics"
10)
11
12// index moves to the next physical row, scrolling the region when the cursor
13// already sits on its last row. It reports whether the row the cursor was on
14// survived that scroll by moving up to s.Row-1, which only a deferred wrap
15// cares about: a wrap must relink to the row it flowed out of, and
16// buffer.scrollUp severs that link on the way. Callers that merely advance,
17// such as LF and ESC D, ignore the result.
18func (s *Screen) index() (movedUp bool) {
19	if s.frame.Height == 0 {
20		return false
21	}
22	if s.Row == s.scrollBottom && s.Row >= s.scrollTop {
23		return s.scrollUpRegion(s.scrollTop, s.scrollBottom, 1)
24	}
25	if s.Row+1 < s.frame.Height {
26		s.Row++
27		return false
28	}
29	s.Row = s.frame.Height - 1
30	return false
31}
32
33func (s *Screen) nextLine() {
34	s.buffer.hard(s.Row)
35	s.Col = 0
36	s.index()
37}
38
39func (s *Screen) reverseIndex() {
40	if s.frame.Height == 0 {
41		return
42	}
43	if s.Row == s.scrollTop && s.Row <= s.scrollBottom {
44		s.scrollDownRegion(s.scrollTop, s.scrollBottom, 1)
45		return
46	}
47	if s.Row > 0 {
48		s.Row--
49	}
50}
51
52// scrollUpBy scrolls the current scroll region up by n lines (CSI S / SU).
53// The cursor position is preserved.
54func (s *Screen) scrollUpBy(n int) {
55	if n <= 0 {
56		return
57	}
58	s.scrollUpRegion(s.scrollTop, s.scrollBottom, n)
59}
60
61func (s *Screen) scrollDownBy(n int) {
62	if n <= 0 {
63		return
64	}
65	s.scrollDownRegion(s.scrollTop, s.scrollBottom, n)
66}
67
68// scrollUpRegion scrolls [top,bottom] up by n. It reports whether any row
69// survived by moving up: a region exactly n rows tall is blanked in place, so
70// the receiving range [top,bottom-n] is empty and nothing survives. Reporting
71// what the scroll observed keeps callers from re-deriving a predicate that this
72// function may have declined to act on at all.
73func (s *Screen) scrollUpRegion(top, bottom, n int) (shifted bool) {
74	if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
75		return false
76	}
77	top, bottom, ok := s.normalizedRegion(top, bottom)
78	if !ok {
79		return false
80	}
81	w := s.frame.Width
82	height := bottom - top + 1
83	graphicsRows := n
84	if n > height {
85		n = height
86	}
87	s.scrollGraphicsRegion(top, bottom, -graphicsRows)
88	// VT scroll regions always span the full frame width, so we rotate the
89	// frame's line offsets (recycling and blanking the evicted rows in place)
90	// instead of copying cells. See renderer.Frame.ScrollUp.
91	s.emitLineEvicted(top, n)
92	s.frame.ScrollUp(top, bottom, n)
93	s.buffer.scrollUp(top, bottom, n)
94	s.fillMissingRowIDs(s.buffer)
95	s.record(renderer.Damage{Kind: renderer.DamageScrollUp, X: 0, Y: top, Width: w, Height: height, Count: n})
96	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: bottom - n + 1, Width: w, Height: n, Count: 1})
97	return bottom-n >= top
98}
99
100func (s *Screen) emitLineEvicted(top, n int) {
101	// Only rows leaving the top edge of the primary screen belong to global
102	// scrollback. Interior DECSTBM scroll regions are local mutations.
103	if s.alternate != nil || top != 0 {
104		return
105	}
106	// Read boundaries and IDs before the caller rotates the frame: a soft link
107	// belongs to the row it follows, and rotation reassigns row indices.
108	for y := top; y < top+n; y++ {
109		s.recordEvicted(s.frame.Row(y), s.buffer.bound(y), s.buffer.rowIDs[y])
110	}
111}
112
113func (s *Screen) recordEvicted(row []renderer.Cell, bound LineBound, id RowID) {
114	if s.history != nil {
115		err := s.history.AppendWithID(row, bound, id)
116		if err != nil && !errors.Is(err, ErrHistoryRowTooLarge) {
117			panic(err)
118		}
119		// A nil error records the row; ErrHistoryRowTooLarge explicitly leaves it
120		// unrecorded. Both cases continue to the eviction observer below.
121	}
122	if s.OnLineEvicted != nil {
123		s.OnLineEvicted(append([]renderer.Cell(nil), row...))
124	}
125}
126
127func (s *Screen) scrollDownRegion(top, bottom, n int) {
128	if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
129		return
130	}
131	top, bottom, ok := s.normalizedRegion(top, bottom)
132	if !ok {
133		return
134	}
135	w := s.frame.Width
136	height := bottom - top + 1
137	graphicsRows := n
138	if n > height {
139		n = height
140	}
141	s.scrollGraphicsRegion(top, bottom, graphicsRows)
142	// Full-width region: rotate line offsets instead of copying cells.
143	s.frame.ScrollDown(top, bottom, n)
144	s.buffer.scrollDown(top, bottom, n)
145	s.fillMissingRowIDs(s.buffer)
146	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: top, Width: w, Height: height, Count: 1})
147}
148
149func (s *Screen) scrollGraphicsRegion(top, bottom, rows int) {
150	if rows == 0 || s.graphics == nil || s.graphics.scene == nil {
151		return
152	}
153	_, cellHeight, ok := s.kittyCellPixels()
154	if !ok || cellHeight <= 0 {
155		return
156	}
157	delta := int64(rows) * int64(cellHeight)
158	fullScreen := top == 0 && bottom == s.frame.Height-1
159	snapshot := s.graphics.scene.Snapshot()
160	placements := snapshot.Placements()
161	operations := make([]graphics.Operation, 0, len(placements))
162	for _, placement := range placements {
163		destination := placement.Destination()
164		if !fullScreen {
165			end, valid := destination.Bottom()
166			if !valid || destination.Y < 0 || end <= destination.Y {
167				continue
168			}
169			startRow := int(destination.Y / int64(cellHeight))
170			endRow := int((end - 1) / int64(cellHeight))
171			if startRow < top || endRow > bottom {
172				continue
173			}
174		}
175		if delta > 0 && destination.Y > math.MaxInt64-delta || delta < 0 && destination.Y < math.MinInt64-delta {
176			continue
177		}
178		destination.Y += delta
179		if _, valid := destination.Bottom(); !valid {
180			continue
181		}
182		spec := placement.Spec()
183		spec.Destination = destination
184		operations = append(operations, graphics.Operation{Kind: graphics.OperationUpdatePlacement, PlacementID: placement.ID(), Placement: spec})
185	}
186	if len(operations) != 0 {
187		if err := s.graphics.scene.Apply(operations...); err != nil {
188			panic(fmt.Errorf("scroll graphics placements: %w", err))
189		}
190	}
191}
192
193func (s *Screen) normalizedRegion(top, bottom int) (int, int, bool) {
194	if s.frame.Height == 0 || top > bottom {
195		return 0, 0, false
196	}
197	top = clamp(top, 0, s.frame.Height-1)
198	bottom = clamp(bottom, 0, s.frame.Height-1)
199	return top, bottom, top <= bottom
200}

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 2

Tags 13

↑↓ moveenter openesc close