v0.1.0 7deb448

frame.go RawHistoryFolder

1package renderer
2
3import "fmt"
4
5// Frame is a fixed-size grid of cells. Logical rows are decoupled from their
6// physical storage through lineOffset: logical row y lives at
7// Cells[lineOffset[y] : lineOffset[y]+Width]. This indirection lets full-width
8// scrolls rotate offsets instead of copying cell memory (NeoVim's grid
9// technique; cf. nvim grid.c line_offset[]). All cell access must go through
10// the accessors (At/Set/Row) so callers never assume a physical layout.
11type Frame struct {
12	Width  int
13	Height int
14	Cells  []Cell
15	// lineOffset maps a logical row index to its physical base index into
16	// Cells. It is always a permutation of the canonical offsets y*Width.
17	lineOffset []int
18}
19
20func NewFrame(width, height int) Frame {
21	cells := make([]Cell, width*height)
22	for i := range cells {
23		cells[i] = BlankCell()
24	}
25	lineOffset := make([]int, height)
26	for y := range lineOffset {
27		lineOffset[y] = y * width
28	}
29	return Frame{Width: width, Height: height, Cells: cells, lineOffset: lineOffset}
30}
31
32// Clone returns an independent copy preserving logical row contents. The clone
33// uses canonical row storage so later scrolls or writes to the source frame do
34// not affect it.
35func (f Frame) Clone() Frame {
36	clone := NewFrame(f.Width, f.Height)
37	for y := range f.Height {
38		copy(clone.Row(y), f.Row(y))
39	}
40	return clone
41}
42
43func (f Frame) Validate() error {
44	if f.Width <= 0 || f.Height <= 0 {
45		return fmt.Errorf("invalid frame size %dx%d", f.Width, f.Height)
46	}
47	if len(f.Cells) != f.Width*f.Height {
48		return fmt.Errorf("invalid cell count: got %d want %d", len(f.Cells), f.Width*f.Height)
49	}
50	if len(f.lineOffset) != f.Height {
51		return fmt.Errorf("invalid lineOffset length: got %d want %d", len(f.lineOffset), f.Height)
52	}
53	return nil
54}
55
56// CheckInvariants verifies that lineOffset is a permutation of the canonical
57// row offsets: every entry is a non-negative multiple of Width within bounds,
58// and every logical row maps to a distinct physical row. It is used by tests
59// and assertions; the hot path uses the lighter Validate.
60func (f Frame) CheckInvariants() error {
61	if f.Width <= 0 {
62		return fmt.Errorf("invalid width %d", f.Width)
63	}
64	if len(f.lineOffset) != f.Height {
65		return fmt.Errorf("lineOffset length %d != height %d", len(f.lineOffset), f.Height)
66	}
67	seen := make([]bool, f.Height)
68	for y, off := range f.lineOffset {
69		if off < 0 || off%f.Width != 0 {
70			return fmt.Errorf("row %d: offset %d is not a non-negative multiple of width %d", y, off, f.Width)
71		}
72		phys := off / f.Width
73		if phys >= f.Height {
74			return fmt.Errorf("row %d: offset %d maps to physical row %d out of range [0,%d)", y, off, phys, f.Height)
75		}
76		if seen[phys] {
77			return fmt.Errorf("row %d: physical row %d already mapped by another logical row", y, phys)
78		}
79		seen[phys] = true
80	}
81	return nil
82}
83
84func (f Frame) At(x, y int) Cell {
85	return f.Cells[f.lineOffset[y]+x]
86}
87
88func (f Frame) Set(x, y int, cell Cell) {
89	f.Cells[f.lineOffset[y]+x] = cell
90}
91
92// Row returns the backing slice for logical row y. Mutating the returned slice
93// mutates the frame in place. The slice is valid until the next scroll or
94// resize that reassigns offsets.
95func (f Frame) Row(y int) []Cell {
96	base := f.lineOffset[y]
97	return f.Cells[base : base+f.Width]
98}
99
100// ScrollUp scrolls the logical rows in [top,bottom] up by n lines by rotating
101// their physical offsets: no per-cell copying. Rows scrolled off the top are
102// recycled to the bottom and blanked in place. It assumes the full frame width
103// and 0 <= top <= bottom < Height and 0 < n <= bottom-top+1 (the caller
104// clamps). The receiver is a value, but lineOffset and Cells alias the caller's
105// backing arrays, so in-place mutations are visible to the caller.
106func (f Frame) ScrollUp(top, bottom, n int) {
107	for ; n > 0; n-- {
108		recycled := f.lineOffset[top]
109		for y := top; y < bottom; y++ {
110			f.lineOffset[y] = f.lineOffset[y+1]
111		}
112		f.lineOffset[bottom] = recycled
113		blankRow(f.Cells[recycled : recycled+f.Width])
114	}
115}
116
117// ScrollDown scrolls the logical rows in [top,bottom] down by n lines by
118// rotating their physical offsets. Rows scrolled off the bottom are recycled to
119// the top and blanked in place. See ScrollUp for preconditions.
120func (f Frame) ScrollDown(top, bottom, n int) {
121	for ; n > 0; n-- {
122		recycled := f.lineOffset[bottom]
123		for y := bottom; y > top; y-- {
124			f.lineOffset[y] = f.lineOffset[y-1]
125		}
126		f.lineOffset[top] = recycled
127		blankRow(f.Cells[recycled : recycled+f.Width])
128	}
129}
130
131func blankRow(row []Cell) {
132	for i := range row {
133		row[i] = BlankCell()
134	}
135}

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