1package vt
2
3import renderer "github.com/bnema/vev-vt/core"
4
5// RecoveryTranscriptSnapshot owns compact viewport pages to replay after
6// retained history. No semantic row slices are retained by the snapshot.
7type RecoveryTranscriptSnapshot struct {
8 segments []recoveryTranscriptSegment
9 nextRowID RowID
10}
11
12type recoveryTranscriptSegment struct {
13 frame renderer.Frame
14 bounds []LineBound
15 rowIDs []RowID
16}
17
18// RecoveryTranscriptSnapshot captures the primary viewport, followed by the
19// alternate viewport when active. Untouched trailing rows are omitted and each
20// viewport ends with a hard line, independently of physical row rotation.
21func (s *Screen) RecoveryTranscriptSnapshot() RecoveryTranscriptSnapshot {
22 if s == nil {
23 return RecoveryTranscriptSnapshot{}
24 }
25 buffers := []*buffer{s.buffer}
26 if s.alternate != nil {
27 buffers = []*buffer{s.alternate.buffer, s.buffer}
28 }
29 nextID := s.nextRowID
30 if nextID < ^RowID(0) {
31 nextID++
32 }
33 snapshot := RecoveryTranscriptSnapshot{nextRowID: nextID, segments: make([]recoveryTranscriptSegment, 0, len(buffers))}
34 for _, b := range buffers {
35 if b == nil {
36 continue
37 }
38 rows := b.frame.Height
39 for rows > 0 && recoveryTranscriptFrameRowUntouched(b.frame, rows-1, b.bound(rows-1)) {
40 rows--
41 }
42 if rows == 0 {
43 continue
44 }
45 frame := renderer.NewFrame(b.frame.Width, rows)
46 for y := range rows {
47 for x := range frame.Width {
48 frame.Set(x, y, b.frame.Cell(x, y))
49 }
50 }
51 bounds := append([]LineBound(nil), b.boundaries[:rows]...)
52 bounds[rows-1].Soft = false
53 snapshot.segments = append(snapshot.segments, recoveryTranscriptSegment{
54 frame: frame, bounds: bounds, rowIDs: append([]RowID(nil), b.rowIDs[:rows]...),
55 })
56 }
57 return snapshot
58}
59
60func recoveryTranscriptFrameRowUntouched(frame renderer.Frame, y int, bound LineBound) bool {
61 if bound.End != 0 || bound.Soft {
62 return false
63 }
64 blank := renderer.BlankCell()
65 for x := range frame.Width {
66 if !frame.Cell(x, y).Equal(blank) {
67 return false
68 }
69 }
70 return true
71}
72
73// Marshal encodes the compact capture without consulting or mutating live state.
74func (snapshot RecoveryTranscriptSnapshot) Marshal() ([]byte, error) {
75 view := HistoryView{nextRowID: snapshot.nextRowID}
76 for _, segment := range snapshot.segments {
77 for start := 0; start < segment.frame.Height; {
78 count := min(segment.frame.Height-start, maxHistoryChunkRows, maxHistorySlabRows(segment.frame.Width))
79 frame := renderer.NewFrame(segment.frame.Width, count)
80 lastStyleRow := make(map[renderer.Style]int)
81 for y := range count {
82 for x := range frame.Width {
83 cell := segment.frame.Cell(x, start+y)
84 frame.Set(x, y, cell)
85 style := cell.Style.Canonical()
86 if style != renderer.DefaultStyle() {
87 lastStyleRow[style] = y
88 }
89 }
90 }
91 drops := make([]uint64, count)
92 for _, y := range lastStyleRow {
93 drops[y]++
94 }
95 chunk := &HistoryChunk{
96 page: newSealedPage(frame), count: count, width: frame.Width,
97 bounds: segment.bounds[start : start+count], rowIDs: segment.rowIDs[start : start+count],
98 styleDrops: drops, styleCount: uint64(len(lastStyleRow)) + 1,
99 }
100 chunk.recordPayloads()
101 view.chunks = append(view.chunks, chunk)
102 view.rows += count
103 view.cells += count * frame.Width
104 view.logicalBytes += historyChunkLogicalBytes(chunk)
105 start += count
106 }
107 }
108 return MarshalHistory(view)
109}