v0.5.1 8e4df5c

frame.go RawHistoryFolder

1package core
2
3import (
4	"fmt"
5	"maps"
6	"math"
7)
8
9// CellSource exposes semantic terminal cells without exposing mutable storage.
10// Implementations may use any physical layout; callers must use Cell for reads.
11type CellSource interface {
12	Columns() int
13	Rows() int
14	Cell(x, y int) Cell
15}
16
17const (
18	// StoredCellLogicalBytes is the deterministic uncompressed size of one
19	// compact cell, independent of the Go allocator and host architecture.
20	StoredCellLogicalBytes uint64 = 16
21	// RowDescriptorLogicalBytes is the deterministic size of one compact row
22	// descriptor.
23	RowDescriptorLogicalBytes uint64 = 4
24	// StyleRecordLogicalBytes is the deterministic size of one canonical
25	// page-local style record.
26	StyleRecordLogicalBytes uint64 = 32
27
28	continuationFlag = uint8(1)
29)
30
31type storedCell struct {
32	rune      int32
33	styleID   uint32
34	payloadID uint32
35	flags     uint8
36}
37
38type styleSlot struct {
39	style Style
40	refs  uint32
41	used  bool
42}
43
44type cellPage struct {
45	cells        []storedCell
46	rows         []uint32
47	styles       []styleSlot
48	styleIndex   map[Style]uint32
49	freeStyles   []uint32
50	styleCount   uint32
51	payloads     []payloadSlot
52	payloadIndex map[CellPayload]uint32
53	freePayloads []uint32
54	payloadBytes uint64
55}
56
57// Frame is a fixed-size semantic grid backed by one compact page. Cells carry
58// page-local style IDs; callers never observe those IDs or the physical row
59// layout. Logical rows are mapped through compact row descriptors so full-width
60// scrolling rotates descriptors instead of copying cells.
61type Frame struct {
62	Width  int
63	Height int
64	page   *cellPage
65}
66
67func NewFrame(width, height int) Frame {
68	cellCount, ok := frameCellCount(width, height)
69	if !ok {
70		panic("frame cell count exceeds page limit")
71	}
72	page := &cellPage{
73		cells:      make([]storedCell, cellCount),
74		rows:       make([]uint32, height),
75		styles:     []styleSlot{{style: DefaultStyle(), refs: uint32(cellCount), used: true}},
76		styleIndex: map[Style]uint32{DefaultStyle(): 0},
77		styleCount: 1,
78	}
79	for i := range page.cells {
80		page.cells[i].rune = ' '
81	}
82	for y := range page.rows {
83		page.rows[y] = uint32(y * width)
84	}
85	return Frame{Width: width, Height: height, page: page}
86}
87
88// Clone returns an independent compact page preserving physical row rotation
89// and page-local style IDs.
90func (f Frame) Clone() Frame {
91	if f.page == nil {
92		return Frame{Width: f.Width, Height: f.Height}
93	}
94	return Frame{
95		Width:  f.Width,
96		Height: f.Height,
97		page: &cellPage{
98			cells:        append([]storedCell(nil), f.page.cells...),
99			rows:         append([]uint32(nil), f.page.rows...),
100			styles:       append([]styleSlot(nil), f.page.styles...),
101			styleIndex:   maps.Clone(f.page.styleIndex),
102			freeStyles:   append([]uint32(nil), f.page.freeStyles...),
103			styleCount:   f.page.styleCount,
104			payloads:     append([]payloadSlot(nil), f.page.payloads...),
105			payloadIndex: maps.Clone(f.page.payloadIndex),
106			freePayloads: append([]uint32(nil), f.page.freePayloads...),
107			payloadBytes: f.page.payloadBytes,
108		},
109	}
110}
111
112// Replace replaces f with an independent structural copy of src's compact page.
113// An invalid or empty source leaves the destination unchanged.
114func (f *Frame) Replace(src Frame) {
115	if f == nil {
116		return
117	}
118	if err := src.validateStorage(); err != nil || src.Width <= 0 || src.Height <= 0 {
119		return
120	}
121	*f = src.Clone()
122}
123
124func frameCellCount(width, height int) (int, bool) {
125	if width < 0 || height < 0 {
126		return 0, false
127	}
128	limit := min(uint64(math.MaxUint32), uint64(math.MaxInt))
129	w, h := uint64(width), uint64(height)
130	if w > limit || h > limit || w != 0 && w*h > limit {
131		return 0, false
132	}
133	return int(w * h), true
134}
135
136func (f Frame) validateStorage() error {
137	if f.page == nil {
138		return fmt.Errorf("missing frame page")
139	}
140	cellCount, ok := frameCellCount(f.Width, f.Height)
141	if !ok {
142		return fmt.Errorf("frame cell count exceeds page limit")
143	}
144	if len(f.page.cells) != cellCount {
145		return fmt.Errorf("invalid cell count: got %d want %d", len(f.page.cells), cellCount)
146	}
147	if len(f.page.rows) != f.Height {
148		return fmt.Errorf("row descriptor count: got %d want %d", len(f.page.rows), f.Height)
149	}
150	return nil
151}
152
153func (f Frame) Validate() error {
154	if f.Width <= 0 || f.Height <= 0 {
155		return fmt.Errorf("invalid frame size %dx%d", f.Width, f.Height)
156	}
157	return f.validateStorage()
158}
159
160// CheckInvariants validates row ownership, style references, dictionary keys,
161// and exact reference counts. It is intended for tests and assertions.
162func (f Frame) CheckInvariants() error {
163	if err := f.validateStorage(); err != nil {
164		return err
165	}
166	seenRows := make([]bool, f.Height)
167	refs := make([]uint32, len(f.page.styles))
168	for y, offset := range f.page.rows {
169		if f.Width == 0 {
170			if offset != 0 {
171				return fmt.Errorf("row %d: nonzero offset %d for zero width", y, offset)
172			}
173			continue
174		}
175		if offset%uint32(f.Width) != 0 {
176			return fmt.Errorf("row %d: offset %d is not a multiple of width %d", y, offset, f.Width)
177		}
178		physical := int(offset) / f.Width
179		if physical >= f.Height || seenRows[physical] {
180			return fmt.Errorf("row %d: invalid or duplicate physical row %d", y, physical)
181		}
182		seenRows[physical] = true
183	}
184	for i, cell := range f.page.cells {
185		if int(cell.styleID) >= len(f.page.styles) || !f.page.styles[cell.styleID].used {
186			return fmt.Errorf("cell %d: unresolved style ID %d", i, cell.styleID)
187		}
188		refs[cell.styleID]++
189	}
190	free := make([]bool, len(f.page.styles))
191	for position, id := range f.page.freeStyles {
192		if int(id) >= len(f.page.styles) {
193			return fmt.Errorf("free style entry %d has out-of-range ID %d", position, id)
194		}
195		if f.page.styles[id].used {
196			return fmt.Errorf("free style entry %d identifies used style %d", position, id)
197		}
198		if free[id] {
199			return fmt.Errorf("free style ID %d appears more than once", id)
200		}
201		free[id] = true
202	}
203	var used uint32
204	for id, slot := range f.page.styles {
205		if !slot.used {
206			if slot.refs != 0 {
207				return fmt.Errorf("free style %d has %d references", id, slot.refs)
208			}
209			if !free[id] {
210				return fmt.Errorf("unused style %d is missing from free list", id)
211			}
212			continue
213		}
214		used++
215		if slot.refs != refs[id] {
216			return fmt.Errorf("style %d references = %d want %d", id, slot.refs, refs[id])
217		}
218		if slot.style != slot.style.Canonical() {
219			return fmt.Errorf("style %d is not canonical", id)
220		}
221		if indexed, ok := f.page.styleIndex[slot.style]; !ok || indexed != uint32(id) {
222			return fmt.Errorf("style %d missing from index", id)
223		}
224	}
225	if len(f.page.styles) == 0 || !f.page.styles[0].used || f.page.styles[0].style != DefaultStyle() {
226		return fmt.Errorf("style ID zero is not the default style")
227	}
228	if used != f.page.styleCount || len(f.page.styleIndex) != int(used) {
229		return fmt.Errorf("style count = %d/%d, index entries %d", used, f.page.styleCount, len(f.page.styleIndex))
230	}
231	return f.checkPayloadInvariants()
232}
233
234// LogicalBytes returns deterministic uncompressed page bytes independent of Go
235// pointer size, map capacity, and allocator overhead.
236func (f Frame) LogicalBytes() uint64 {
237	if f.page == nil {
238		return 0
239	}
240	return uint64(len(f.page.cells))*StoredCellLogicalBytes + uint64(len(f.page.rows))*RowDescriptorLogicalBytes + uint64(f.page.styleCount)*StyleRecordLogicalBytes + f.page.payloadBytes
241}
242
243// StyleCount returns the number of live page-local styles, including default ID zero.
244func (f Frame) StyleCount() int {
245	if f.page == nil {
246		return 0
247	}
248	return int(f.page.styleCount)
249}
250
251func (f Frame) Columns() int { return f.Width }
252func (f Frame) Rows() int    { return f.Height }
253func (f Frame) Cell(x, y int) Cell {
254	stored := f.page.cells[f.offset(x, y)]
255	return Cell{Rune: rune(stored.rune), Style: f.page.styles[stored.styleID].style, Payload: f.payload(stored.payloadID), Continuation: stored.flags&continuationFlag != 0}
256}
257func (f Frame) At(x, y int) Cell { return f.Cell(x, y) }
258
259func (f Frame) Set(x, y int, cell Cell) {
260	index := f.offset(x, y)
261	payloadID := f.internPayload(cell.Payload)
262	f.releasePayload(f.page.cells[index].payloadID)
263	oldID := f.page.cells[index].styleID
264	styleID := oldID
265	// Repainting a cell usually keeps its style. Compare the canonical value
266	// before hashing it and changing references in the page-local dictionary.
267	if f.page.styles[oldID].style != cell.Style.Canonical() {
268		styleID = f.internStyle(cell.Style)
269		f.releaseStyle(oldID)
270	}
271	flags := uint8(0)
272	if cell.Continuation {
273		flags = continuationFlag
274	}
275	f.page.cells[index] = storedCell{rune: int32(cell.Rune), styleID: styleID, payloadID: payloadID, flags: flags}
276}
277
278func (f Frame) Row(y int) []Cell {
279	if y < 0 || y >= f.Height {
280		panic("frame row out of range")
281	}
282	row := make([]Cell, f.Width)
283	for x := range row {
284		row[x] = f.Cell(x, y)
285	}
286	return row
287}
288
289func (f Frame) WriteRow(y, x int, cells []Cell) int {
290	count := min(len(cells), f.Width-x)
291	for i := range count {
292		f.Set(x+i, y, cells[i])
293	}
294	return count
295}
296
297func (f Frame) CopyRow(y, dst, src, count int) {
298	if count <= 0 || dst == src {
299		return
300	}
301	if dst < src {
302		for i := range count {
303			f.Set(dst+i, y, f.Cell(src+i, y))
304		}
305		return
306	}
307	for i := count - 1; i >= 0; i-- {
308		f.Set(dst+i, y, f.Cell(src+i, y))
309	}
310}
311
312func (f Frame) FillRow(y, start, end int, cell Cell) {
313	for x := max(0, start); x < min(f.Width, end); x++ {
314		f.Set(x, y, cell)
315	}
316}
317
318func (f Frame) ScrollUp(top, bottom, n int) {
319	for ; n > 0; n-- {
320		recycled := f.page.rows[top]
321		copy(f.page.rows[top:bottom], f.page.rows[top+1:bottom+1])
322		f.page.rows[bottom] = recycled
323		f.blankPhysicalRow(recycled)
324	}
325}
326
327func (f Frame) ScrollDown(top, bottom, n int) {
328	for ; n > 0; n-- {
329		recycled := f.page.rows[bottom]
330		copy(f.page.rows[top+1:bottom+1], f.page.rows[top:bottom])
331		f.page.rows[top] = recycled
332		f.blankPhysicalRow(recycled)
333	}
334}
335
336func (f Frame) offset(x, y int) int {
337	if x < 0 || x >= f.Width {
338		panic("frame column out of range")
339	}
340	if y < 0 || y >= f.Height {
341		panic("frame row out of range")
342	}
343	return int(f.page.rows[y]) + x
344}
345
346func (f Frame) blankPhysicalRow(offset uint32) {
347	for x := range f.Width {
348		index := int(offset) + x
349		f.releasePayload(f.page.cells[index].payloadID)
350		oldID := f.page.cells[index].styleID
351		if oldID != 0 {
352			f.page.styles[0].refs++
353			f.releaseStyle(oldID)
354		}
355		f.page.cells[index] = storedCell{rune: ' '}
356	}
357}
358
359func (f Frame) internStyle(style Style) uint32 {
360	style = style.Canonical()
361	if id, ok := f.page.styleIndex[style]; ok {
362		f.page.styles[id].refs++
363		return id
364	}
365	var id uint32
366	if n := len(f.page.freeStyles); n != 0 {
367		id = f.page.freeStyles[n-1]
368		f.page.freeStyles = f.page.freeStyles[:n-1]
369		f.page.styles[id] = styleSlot{style: style, refs: 1, used: true}
370	} else {
371		id = uint32(len(f.page.styles))
372		f.page.styles = append(f.page.styles, styleSlot{style: style, refs: 1, used: true})
373	}
374	f.page.styleIndex[style] = id
375	f.page.styleCount++
376	return id
377}
378
379func (f Frame) releaseStyle(id uint32) {
380	slot := &f.page.styles[id]
381	slot.refs--
382	if id == 0 || slot.refs != 0 {
383		return
384	}
385	delete(f.page.styleIndex, slot.style)
386	*slot = styleSlot{}
387	f.page.freeStyles = append(f.page.freeStyles, id)
388	f.page.styleCount--
389}

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