1package vt
2
3import (
4 "errors"
5 "math"
6
7 "github.com/bnema/vev/pkg/renderer"
8)
9
10// ErrHistoryRowTooWide is returned when a row cannot fit within the configured
11// cell budget. The history is not modified when this error is returned.
12var ErrHistoryRowTooWide = errors.New("history row exceeds cell capacity")
13
14// HistoryConfig controls the bounded terminal history retained by a Screen.
15type HistoryConfig struct {
16 MaxRows int
17 MaxCells int
18 ChunkRows int
19}
20
21// History stores terminal rows in immutable chunks. It is intended to be
22// mutated by the owner of a Screen; views are safe to retain after later
23// appends.
24type History struct {
25 maxRows int
26 maxCells int
27 chunkRows int
28 chunks []*HistoryChunk
29 tail [][]renderer.Cell
30 tailBounds []LineBound
31 rows int
32 cells int
33}
34
35// HistoryChunk is an immutable group of history rows. Its identity is stable
36// and can be used by consumers to avoid copying unchanged sealed chunks.
37// bounds is parallel to rows and always has the same length.
38type HistoryChunk struct {
39 rows [][]renderer.Cell
40 bounds []LineBound
41}
42
43// HistoryView is an immutable snapshot of history. Row returns a copy so the
44// storage behind a sealed chunk remains owned by VT.
45type HistoryView struct {
46 chunks []*HistoryChunk
47 rows int
48 cells int
49}
50
51// HistorySnapshotView captures sealed history chunks and the mutable tail
52// independently. Sealed chunks are shared by identity; Tail is owned by the
53// view and can be serialized without rotating the live tail into a chunk.
54type HistorySnapshotView struct {
55 chunks []*HistoryChunk
56 tail [][]renderer.Cell
57 tailBounds []LineBound
58 rows int
59 cells int
60}
61
62func NewHistory(config HistoryConfig) *History {
63 if config.MaxRows <= 0 {
64 return &History{}
65 }
66 chunkRows := config.ChunkRows
67 if chunkRows <= 0 {
68 chunkRows = 256
69 }
70 chunkRows = min(chunkRows, 256)
71 chunkRows = min(chunkRows, config.MaxRows)
72 maxCells := config.MaxCells
73 if maxCells <= 0 {
74 maxCells = defaultHistoryMaxCells(config.MaxRows)
75 }
76 return &History{maxRows: config.MaxRows, maxCells: maxCells, chunkRows: chunkRows}
77}
78
79func defaultHistoryMaxCells(maxRows int) int {
80 if maxRows > math.MaxInt/160 {
81 return math.MaxInt
82 }
83 return maxRows * 160
84}
85
86// Append records a copy of row along with its logical extent. Once a chunk is
87// full it is sealed forever. Rows wider than the total cell capacity are
88// rejected without mutation.
89func (h *History) Append(row []renderer.Cell, bound LineBound) error {
90 if h == nil || h.maxRows == 0 {
91 return nil
92 }
93 if len(row) > h.maxCells {
94 return ErrHistoryRowTooWide
95 }
96 // Make room before adding so Cells cannot overflow even when the default
97 // capacity is MaxInt.
98 h.evictFor(len(row))
99 h.tail = append(h.tail, append([]renderer.Cell(nil), row...))
100 h.tailBounds = append(h.tailBounds, clampBound(bound, len(row)))
101 h.rows++
102 h.cells += len(row)
103 if len(h.tail) == h.chunkRows {
104 h.sealTail()
105 }
106 return nil
107}
108
109// clampBound keeps End inside the row it describes so a persisted bound can be
110// validated without consulting its row again.
111func clampBound(b LineBound, cells int) LineBound {
112 b.End = min(max(b.End, 0), cells)
113 return b
114}
115
116// growBounds returns bounds sized to rows, padding with zero values. It keeps
117// the parallel-slice invariant when a chunk arrives with fewer bounds than rows.
118func growBounds(bounds []LineBound, rows int) []LineBound {
119 if len(bounds) == rows {
120 return bounds
121 }
122 out := make([]LineBound, rows)
123 copy(out, bounds)
124 return out
125}
126
127func (h *History) sealTail() {
128 if len(h.tail) == 0 {
129 return
130 }
131 h.chunks = append(h.chunks, &HistoryChunk{rows: h.tail, bounds: growBounds(h.tailBounds, len(h.tail))})
132 h.tail = nil
133 h.tailBounds = nil
134}
135
136// normalizeTail seals complete chunks so the mutable tail remains shorter than
137// chunkRows. Restored snapshots may have been written with a different chunk
138// size than the current history configuration.
139func (h *History) normalizeTail() {
140 for h.chunkRows > 0 && len(h.tail) >= h.chunkRows {
141 h.chunks = append(h.chunks, &HistoryChunk{
142 rows: h.tail[:h.chunkRows],
143 bounds: growBounds(h.tailBounds, len(h.tail))[:h.chunkRows],
144 })
145 h.tail = h.tail[h.chunkRows:]
146 h.tailBounds = growBounds(h.tailBounds, len(h.tail)+h.chunkRows)[h.chunkRows:]
147 }
148}
149
150func (h *History) evict() {
151 h.evictUntil(0, 0)
152}
153
154// evictFor discards oldest rows until another row using cellCount cells can fit.
155func (h *History) evictFor(cellCount int) {
156 h.evictUntil(1, cellCount)
157}
158
159// evictUntil makes room for rowCount rows and cellCount cells without
160// overflowing either accounting total.
161func (h *History) evictUntil(rowCount, cellCount int) {
162 for h.rows > h.maxRows-rowCount || h.cells > h.maxCells-cellCount {
163 if len(h.chunks) > 0 {
164 chunk := h.chunks[0]
165 row := chunk.rows[0]
166 h.rows--
167 h.cells -= len(row)
168 if len(chunk.rows) == 1 {
169 copy(h.chunks, h.chunks[1:])
170 h.chunks[len(h.chunks)-1] = nil
171 h.chunks = h.chunks[:len(h.chunks)-1]
172 } else {
173 // Preserve cell storage while replacing only the chunk wrapper: a
174 // retained view may still refer to the original immutable chunk.
175 h.chunks[0] = &HistoryChunk{rows: chunk.rows[1:], bounds: growBounds(chunk.bounds, len(chunk.rows))[1:]}
176 }
177 continue
178 }
179 if len(h.tail) == 0 {
180 return
181 }
182 row := h.tail[0]
183 h.rows--
184 h.cells -= len(row)
185 h.tail[0] = nil
186 h.tail = h.tail[1:]
187 h.tailBounds = growBounds(h.tailBounds, len(h.tail)+1)[1:]
188 }
189}
190
191// SealAndView rotates the mutable tail into an immutable chunk, then captures
192// the sealed chunks by identity. Callers must synchronize access to History.
193func (h *History) SealAndView() HistoryView {
194 if h != nil {
195 h.sealTail()
196 }
197 return h.View()
198}
199
200// View captures the current history. Sealed chunks are shared by identity; a
201// partially-filled tail is copied into a new immutable chunk for this view.
202func (h *History) View() HistoryView {
203 if h == nil || h.rows == 0 {
204 return HistoryView{}
205 }
206 chunks := append([]*HistoryChunk(nil), h.chunks...)
207 if len(h.tail) > 0 {
208 chunks = append(chunks, &HistoryChunk{
209 rows: cloneHistoryRows(h.tail),
210 bounds: append([]LineBound(nil), growBounds(h.tailBounds, len(h.tail))...),
211 })
212 }
213 return HistoryView{chunks: chunks, rows: h.rows, cells: h.cells}
214}
215
216// SnapshotView captures history for persistence without sealing the mutable
217// tail. Sealed chunks are shared by identity and the tail is deeply copied.
218func (h *History) SnapshotView() HistorySnapshotView {
219 if h == nil || h.rows == 0 {
220 return HistorySnapshotView{}
221 }
222 return HistorySnapshotView{
223 chunks: append([]*HistoryChunk(nil), h.chunks...),
224 tail: cloneHistoryRows(h.tail),
225 tailBounds: append([]LineBound(nil), growBounds(h.tailBounds, len(h.tail))...),
226 rows: h.rows,
227 cells: h.cells,
228 }
229}
230
231func cloneHistoryRows(rows [][]renderer.Cell) [][]renderer.Cell {
232 if len(rows) == 0 {
233 return nil
234 }
235 copyRows := make([][]renderer.Cell, len(rows))
236 for i, row := range rows {
237 copyRows[i] = append([]renderer.Cell(nil), row...)
238 }
239 return copyRows
240}
241
242// Len returns the currently retained history row count.
243func (h *History) Len() int {
244 if h == nil {
245 return 0
246 }
247 return h.rows
248}
249
250// Cells returns the currently retained history cell count.
251func (h *History) Cells() int {
252 if h == nil {
253 return 0
254 }
255 return h.cells
256}
257
258// Cap returns the configured bounded row capacity.
259func (h *History) Cap() int {
260 if h == nil {
261 return 0
262 }
263 return h.maxRows
264}
265
266// CellCap returns the configured bounded cell capacity.
267func (h *History) CellCap() int {
268 if h == nil {
269 return 0
270 }
271 return h.maxCells
272}
273
274func (v HistoryView) Len() int { return v.rows }
275func (v HistoryView) Cells() int { return v.cells }
276func (v HistoryView) ChunkCount() int { return len(v.chunks) }
277
278// Chunk returns the immutable chunk at i, or nil when i is out of range.
279func (v HistoryView) Chunk(i int) *HistoryChunk {
280 if i < 0 || i >= len(v.chunks) {
281 return nil
282 }
283 return v.chunks[i]
284}
285
286// Range calls yield for each row in oldest-first order. The row is borrowed
287// immutable storage: yield must not mutate or retain it after returning.
288func (v HistoryView) Range(yield func([]renderer.Cell) bool) {
289 for _, chunk := range v.chunks {
290 for _, row := range chunk.rows {
291 if !yield(row) {
292 return
293 }
294 }
295 }
296}
297
298// Row returns a copy of the row at i, or nil when i is out of range.
299func (v HistoryView) Row(i int) []renderer.Cell {
300 return append([]renderer.Cell(nil), v.BorrowedRow(i)...)
301}
302
303// BorrowedRow returns immutable storage for the row at i, or nil when i is out
304// of range. The caller must not mutate or retain the result after it no longer
305// retains v. Consumers that need ownership should use Row instead.
306func (v HistoryView) BorrowedRow(i int) []renderer.Cell {
307 if i < 0 {
308 return nil
309 }
310 for _, chunk := range v.chunks {
311 if i < len(chunk.rows) {
312 return chunk.rows[i]
313 }
314 i -= len(chunk.rows)
315 }
316 return nil
317}
318
319// Bound returns the logical extent of the row at i, or the zero value when i is
320// out of range. A zero value describes a hard row whose End is not meaningful.
321func (v HistoryView) Bound(i int) LineBound {
322 if i < 0 {
323 return LineBound{}
324 }
325 for _, chunk := range v.chunks {
326 if i < len(chunk.rows) {
327 if i < len(chunk.bounds) {
328 return chunk.bounds[i]
329 }
330 return LineBound{}
331 }
332 i -= len(chunk.rows)
333 }
334 return LineBound{}
335}
336
337func (v HistorySnapshotView) Len() int { return v.rows }
338func (v HistorySnapshotView) Cells() int { return v.cells }
339func (v HistorySnapshotView) ChunkCount() int { return len(v.chunks) }
340
341// Chunk returns a sealed immutable chunk at i, or nil when i is out of range.
342func (v HistorySnapshotView) Chunk(i int) *HistoryChunk {
343 if i < 0 || i >= len(v.chunks) {
344 return nil
345 }
346 return v.chunks[i]
347}
348
349// Tail returns an immutable view of the copied mutable tail.
350func (v HistorySnapshotView) Tail() HistoryView {
351 if len(v.tail) == 0 {
352 return HistoryView{}
353 }
354 return HistoryView{
355 chunks: []*HistoryChunk{{rows: v.tail, bounds: growBounds(v.tailBounds, len(v.tail))}},
356 rows: len(v.tail),
357 cells: historyRowsCells(v.tail),
358 }
359}
360
361func historyRowsCells(rows [][]renderer.Cell) int {
362 cells := 0
363 for _, row := range rows {
364 cells += len(row)
365 }
366 return cells
367}