main ab6686c

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	// styleCache remembers the most recent raw Style passed to internStyle and
52	// the live slot it resolved to. It is a pure accelerator: it is cleared
53	// whenever that slot is freed, and a fresh page starts with it invalid.
54	// Clone copies it along with the style slots it refers to, so a clone stays
55	// reflect.DeepEqual to its source.
56	styleCache   Style
57	styleCacheID uint32
58	styleCacheOK bool
59	payloads     []payloadSlot
60	payloadIndex map[CellPayload]uint32
61	freePayloads []uint32
62	payloadBytes uint64
63}
64
65// Frame is a fixed-size semantic grid backed by one compact page. Cells carry
66// page-local style IDs; callers never observe those IDs or the physical row
67// layout. Logical rows are mapped through compact row descriptors so full-width
68// scrolling rotates descriptors instead of copying cells.
69type Frame struct {
70	Width  int
71	Height int
72	page   *cellPage
73}
74
75func NewFrame(width, height int) Frame {
76	cellCount, ok := frameCellCount(width, height)
77	if !ok {
78		panic("frame cell count exceeds page limit")
79	}
80	page := &cellPage{
81		cells:      make([]storedCell, cellCount),
82		rows:       make([]uint32, height),
83		styles:     []styleSlot{{style: DefaultStyle(), refs: uint32(cellCount), used: true}},
84		styleIndex: map[Style]uint32{DefaultStyle(): 0},
85		styleCount: 1,
86	}
87	for i := range page.cells {
88		page.cells[i].rune = ' '
89	}
90	for y := range page.rows {
91		page.rows[y] = uint32(y * width)
92	}
93	return Frame{Width: width, Height: height, page: page}
94}
95
96// Clone returns an independent compact page preserving physical row rotation
97// and page-local style IDs.
98func (f Frame) Clone() Frame {
99	if f.page == nil {
100		return Frame{Width: f.Width, Height: f.Height}
101	}
102	return Frame{
103		Width:  f.Width,
104		Height: f.Height,
105		page: &cellPage{
106			cells:        append([]storedCell(nil), f.page.cells...),
107			rows:         append([]uint32(nil), f.page.rows...),
108			styles:       append([]styleSlot(nil), f.page.styles...),
109			styleIndex:   maps.Clone(f.page.styleIndex),
110			freeStyles:   append([]uint32(nil), f.page.freeStyles...),
111			styleCount:   f.page.styleCount,
112			styleCache:   f.page.styleCache,
113			styleCacheID: f.page.styleCacheID,
114			styleCacheOK: f.page.styleCacheOK,
115			payloads:     append([]payloadSlot(nil), f.page.payloads...),
116			payloadIndex: maps.Clone(f.page.payloadIndex),
117			freePayloads: append([]uint32(nil), f.page.freePayloads...),
118			payloadBytes: f.page.payloadBytes,
119		},
120	}
121}
122
123// Replace replaces f with an independent structural copy of src's compact page.
124// An invalid or empty source leaves the destination unchanged.
125func (f *Frame) Replace(src Frame) {
126	if f == nil {
127		return
128	}
129	if err := src.validateStorage(); err != nil || src.Width <= 0 || src.Height <= 0 {
130		return
131	}
132	*f = src.Clone()
133}
134
135func frameCellCount(width, height int) (int, bool) {
136	if width < 0 || height < 0 {
137		return 0, false
138	}
139	limit := min(uint64(math.MaxUint32), uint64(math.MaxInt))
140	w, h := uint64(width), uint64(height)
141	if w > limit || h > limit || w != 0 && w*h > limit {
142		return 0, false
143	}
144	return int(w * h), true
145}
146
147func (f Frame) validateStorage() error {
148	if f.page == nil {
149		return fmt.Errorf("missing frame page")
150	}
151	cellCount, ok := frameCellCount(f.Width, f.Height)
152	if !ok {
153		return fmt.Errorf("frame cell count exceeds page limit")
154	}
155	if len(f.page.cells) != cellCount {
156		return fmt.Errorf("invalid cell count: got %d want %d", len(f.page.cells), cellCount)
157	}
158	if len(f.page.rows) != f.Height {
159		return fmt.Errorf("row descriptor count: got %d want %d", len(f.page.rows), f.Height)
160	}
161	return nil
162}
163
164func (f Frame) Validate() error {
165	if f.Width <= 0 || f.Height <= 0 {
166		return fmt.Errorf("invalid frame size %dx%d", f.Width, f.Height)
167	}
168	return f.validateStorage()
169}
170
171// CheckInvariants validates row ownership, style references, dictionary keys,
172// and exact reference counts. It is intended for tests and assertions.
173func (f Frame) CheckInvariants() error {
174	if err := f.validateStorage(); err != nil {
175		return err
176	}
177	seenRows := make([]bool, f.Height)
178	refs := make([]uint32, len(f.page.styles))
179	for y, offset := range f.page.rows {
180		if f.Width == 0 {
181			if offset != 0 {
182				return fmt.Errorf("row %d: nonzero offset %d for zero width", y, offset)
183			}
184			continue
185		}
186		if offset%uint32(f.Width) != 0 {
187			return fmt.Errorf("row %d: offset %d is not a multiple of width %d", y, offset, f.Width)
188		}
189		physical := int(offset) / f.Width
190		if physical >= f.Height || seenRows[physical] {
191			return fmt.Errorf("row %d: invalid or duplicate physical row %d", y, physical)
192		}
193		seenRows[physical] = true
194	}
195	for i, cell := range f.page.cells {
196		if int(cell.styleID) >= len(f.page.styles) || !f.page.styles[cell.styleID].used {
197			return fmt.Errorf("cell %d: unresolved style ID %d", i, cell.styleID)
198		}
199		refs[cell.styleID]++
200	}
201	free := make([]bool, len(f.page.styles))
202	for position, id := range f.page.freeStyles {
203		if int(id) >= len(f.page.styles) {
204			return fmt.Errorf("free style entry %d has out-of-range ID %d", position, id)
205		}
206		if f.page.styles[id].used {
207			return fmt.Errorf("free style entry %d identifies used style %d", position, id)
208		}
209		if free[id] {
210			return fmt.Errorf("free style ID %d appears more than once", id)
211		}
212		free[id] = true
213	}
214	var used uint32
215	for id, slot := range f.page.styles {
216		if !slot.used {
217			if slot.refs != 0 {
218				return fmt.Errorf("free style %d has %d references", id, slot.refs)
219			}
220			if !free[id] {
221				return fmt.Errorf("unused style %d is missing from free list", id)
222			}
223			continue
224		}
225		used++
226		if slot.refs != refs[id] {
227			return fmt.Errorf("style %d references = %d want %d", id, slot.refs, refs[id])
228		}
229		if slot.style != slot.style.Canonical() {
230			return fmt.Errorf("style %d is not canonical", id)
231		}
232		if indexed, ok := f.page.styleIndex[slot.style]; !ok || indexed != uint32(id) {
233			return fmt.Errorf("style %d missing from index", id)
234		}
235	}
236	if len(f.page.styles) == 0 || !f.page.styles[0].used || f.page.styles[0].style != DefaultStyle() {
237		return fmt.Errorf("style ID zero is not the default style")
238	}
239	if used != f.page.styleCount || len(f.page.styleIndex) != int(used) {
240		return fmt.Errorf("style count = %d/%d, index entries %d", used, f.page.styleCount, len(f.page.styleIndex))
241	}
242	return f.checkPayloadInvariants()
243}
244
245// LogicalBytes returns deterministic uncompressed page bytes independent of Go
246// pointer size, map capacity, and allocator overhead.
247func (f Frame) LogicalBytes() uint64 {
248	if f.page == nil {
249		return 0
250	}
251	return uint64(len(f.page.cells))*StoredCellLogicalBytes + uint64(len(f.page.rows))*RowDescriptorLogicalBytes + uint64(f.page.styleCount)*StyleRecordLogicalBytes + f.page.payloadBytes
252}
253
254// StyleCount returns the number of live page-local styles, including default ID zero.
255func (f Frame) StyleCount() int {
256	if f.page == nil {
257		return 0
258	}
259	return int(f.page.styleCount)
260}
261
262func (f Frame) Columns() int { return f.Width }
263func (f Frame) Rows() int    { return f.Height }
264func (f Frame) Cell(x, y int) Cell {
265	stored := f.page.cells[f.offset(x, y)]
266	return Cell{Rune: rune(stored.rune), Style: f.page.styles[stored.styleID].style, Payload: f.payload(stored.payloadID), Continuation: stored.flags&continuationFlag != 0}
267}
268func (f Frame) At(x, y int) Cell { return f.Cell(x, y) }
269
270// AppendRowText appends one rune per non-continuation cell of row y to runes,
271// and that cell's column to columns, and returns both extended slices. A zero
272// rune is reported as a space. It reads only runes and flags, so text scans
273// avoid materializing styled cells.
274func (f Frame) AppendRowText(runes []rune, columns []int, y int) ([]rune, []int) {
275	if f.Width == 0 {
276		return runes, columns
277	}
278	start := f.offset(0, y)
279	for x, stored := range f.page.cells[start : start+f.Width] {
280		if stored.flags&continuationFlag != 0 {
281			continue
282		}
283		r := rune(stored.rune)
284		if r == 0 {
285			r = ' '
286		}
287		runes = append(runes, r)
288		columns = append(columns, x)
289	}
290	return runes, columns
291}
292
293// IsContinuation reports whether (x,y) is the right half of a wide pair
294// without materializing the cell.
295func (f Frame) IsContinuation(x, y int) bool {
296	return f.page.cells[f.offset(x, y)].flags&continuationFlag != 0
297}
298
299func (f Frame) Set(x, y int, cell Cell) {
300	index := f.offset(x, y)
301	payloadID := f.internPayload(cell.Payload)
302	f.releasePayload(f.page.cells[index].payloadID)
303	oldID := f.page.cells[index].styleID
304	styleID := oldID
305	// Repainting a cell usually keeps its style. Stored styles are canonical,
306	// so a raw match proves the canonical form matches too; this skips
307	// canonicalization and the dictionary for the common case.
308	if f.page.styles[oldID].style != cell.Style {
309		styleID = f.internStyle(cell.Style)
310		f.releaseStyle(oldID)
311	}
312	flags := uint8(0)
313	if cell.Continuation {
314		flags = continuationFlag
315	}
316	f.page.cells[index] = storedCell{rune: int32(cell.Rune), styleID: styleID, payloadID: payloadID, flags: flags}
317}
318
319func (f Frame) Row(y int) []Cell {
320	if y < 0 || y >= f.Height {
321		panic("frame row out of range")
322	}
323	row := make([]Cell, f.Width)
324	for x := range row {
325		row[x] = f.Cell(x, y)
326	}
327	return row
328}
329
330func (f Frame) WriteRow(y, x int, cells []Cell) int {
331	count := min(len(cells), f.Width-x)
332	for i := range count {
333		f.Set(x+i, y, cells[i])
334	}
335	return count
336}
337
338func (f Frame) CopyRow(y, dst, src, count int) {
339	if count <= 0 || dst == src {
340		return
341	}
342	if dst < src {
343		for i := range count {
344			f.Set(dst+i, y, f.Cell(src+i, y))
345		}
346		return
347	}
348	for i := count - 1; i >= 0; i-- {
349		f.Set(dst+i, y, f.Cell(src+i, y))
350	}
351}
352
353func (f Frame) FillRow(y, start, end int, cell Cell) {
354	for x := max(0, start); x < min(f.Width, end); x++ {
355		f.Set(x, y, cell)
356	}
357}
358
359func (f Frame) ScrollUp(top, bottom, n int) {
360	for ; n > 0; n-- {
361		recycled := f.page.rows[top]
362		copy(f.page.rows[top:bottom], f.page.rows[top+1:bottom+1])
363		f.page.rows[bottom] = recycled
364		f.blankPhysicalRow(recycled)
365	}
366}
367
368func (f Frame) ScrollDown(top, bottom, n int) {
369	for ; n > 0; n-- {
370		recycled := f.page.rows[bottom]
371		copy(f.page.rows[top+1:bottom+1], f.page.rows[top:bottom])
372		f.page.rows[top] = recycled
373		f.blankPhysicalRow(recycled)
374	}
375}
376
377func (f Frame) offset(x, y int) int {
378	if x < 0 || x >= f.Width {
379		panic("frame column out of range")
380	}
381	if y < 0 || y >= f.Height {
382		panic("frame row out of range")
383	}
384	return int(f.page.rows[y]) + x
385}
386
387func (f Frame) blankPhysicalRow(offset uint32) {
388	row := f.page.cells[offset : int(offset)+f.Width]
389	for i := range row {
390		cell := &row[i]
391		if cell.payloadID != 0 {
392			f.releasePayload(cell.payloadID)
393		}
394		if oldID := cell.styleID; oldID != 0 {
395			f.page.styles[0].refs++
396			f.releaseStyle(oldID)
397		}
398	}
399	if len(row) == 0 {
400		return
401	}
402	row[0] = storedCell{rune: ' '}
403	for filled := 1; filled < len(row); filled *= 2 {
404		copy(row[filled:], row[:filled])
405	}
406}
407
408// internStyle returns the page-local ID for style and acquires one reference.
409func (f Frame) internStyle(raw Style) uint32 {
410	p := f.page
411	if p.styleCacheOK && raw == p.styleCache {
412		p.styles[p.styleCacheID].refs++
413		return p.styleCacheID
414	}
415	style := raw.Canonical()
416	id, ok := p.styleIndex[style]
417	if ok {
418		p.styles[id].refs++
419	} else {
420		if n := len(p.freeStyles); n != 0 {
421			id = p.freeStyles[n-1]
422			p.freeStyles = p.freeStyles[:n-1]
423			p.styles[id] = styleSlot{style: style, refs: 1, used: true}
424		} else {
425			id = uint32(len(p.styles))
426			if len(p.styles) == cap(p.styles) {
427				p.styles = f.growStyles()
428			}
429			p.styles = append(p.styles, styleSlot{style: style, refs: 1, used: true})
430		}
431		p.styleIndex[style] = id
432		p.styleCount++
433	}
434	p.styleCache, p.styleCacheID, p.styleCacheOK = raw, id, true
435	return id
436}
437
438// growStyles doubles style capacity instead of Go's gentler large-slice growth,
439// roughly halving total bytes allocated by high-cardinality frames. Capacity
440// never exceeds the one-style-per-cell bound, so small frames stay small.
441func (f Frame) growStyles() []styleSlot {
442	old := f.page.styles
443	limit := len(f.page.cells) + 1
444	newCap := min(max(2*cap(old), 4), max(limit, len(old)+1))
445	grown := make([]styleSlot, len(old), newCap)
446	copy(grown, old)
447	return grown
448}
449
450func (f Frame) releaseStyle(id uint32) {
451	slot := &f.page.styles[id]
452	slot.refs--
453	if id == 0 || slot.refs != 0 {
454		return
455	}
456	if f.page.styleCacheID == id {
457		f.page.styleCacheOK = false
458	}
459	delete(f.page.styleIndex, slot.style)
460	*slot = styleSlot{}
461	f.page.freeStyles = append(f.page.freeStyles, id)
462	f.page.styleCount--
463}

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