1package vt
2
3import renderer "github.com/bnema/vev-vt/core"
4
5// RecoveryTranscriptSnapshot is an owned, immutable capture of the viewport
6// rows that should be replayed after retained terminal history during recovery.
7type RecoveryTranscriptSnapshot struct {
8 segments []recoveryTranscriptSegment
9 nextRowID RowID
10}
11
12type recoveryTranscriptSegment struct {
13 rows [][]renderer.Cell
14 bounds []LineBound
15 rowIDs []RowID
16}
17
18// RecoveryTranscriptSnapshot captures the active primary viewport, or the
19// saved primary viewport followed by the active alternate viewport.
20func (s *Screen) RecoveryTranscriptSnapshot() RecoveryTranscriptSnapshot {
21 if s == nil {
22 return RecoveryTranscriptSnapshot{}
23 }
24
25 buffers := []*buffer{s.buffer}
26 if s.alternate != nil {
27 buffers = []*buffer{s.alternate.buffer, s.buffer}
28 }
29
30 nextRowID := s.nextRowID
31 if nextRowID < ^RowID(0) {
32 nextRowID++
33 }
34 snapshot := RecoveryTranscriptSnapshot{segments: make([]recoveryTranscriptSegment, 0, len(buffers)), nextRowID: nextRowID}
35 for _, b := range buffers {
36 segment := captureRecoveryTranscriptSegment(b)
37 if len(segment.rows) > 0 {
38 snapshot.segments = append(snapshot.segments, segment)
39 }
40 }
41 return snapshot
42}
43
44func captureRecoveryTranscriptSegment(b *buffer) recoveryTranscriptSegment {
45 if b == nil {
46 return recoveryTranscriptSegment{}
47 }
48
49 rowCount := b.frame.Height
50 for rowCount > 0 && recoveryTranscriptRowUntouched(b.frame.Row(rowCount-1), b.bound(rowCount-1)) {
51 rowCount--
52 }
53 if rowCount == 0 {
54 return recoveryTranscriptSegment{}
55 }
56
57 segment := recoveryTranscriptSegment{
58 rows: make([][]renderer.Cell, rowCount),
59 bounds: append([]LineBound(nil), b.boundaries[:rowCount]...),
60 rowIDs: append([]RowID(nil), b.rowIDs[:rowCount]...),
61 }
62 cells := make([]renderer.Cell, rowCount*b.frame.Width)
63 for y := range rowCount {
64 start := y * b.frame.Width
65 end := start + b.frame.Width
66 segment.rows[y] = cells[start:end:end]
67 copy(segment.rows[y], b.frame.Row(y))
68 }
69 segment.bounds[rowCount-1].Soft = false
70 return segment
71}
72
73func recoveryTranscriptRowUntouched(row []renderer.Cell, bound LineBound) bool {
74 if bound.End != 0 || bound.Soft {
75 return false
76 }
77 blank := renderer.BlankCell()
78 for _, cell := range row {
79 if !cell.Equal(blank) {
80 return false
81 }
82 }
83 return true
84}
85
86// Marshal encodes the captured rows as canonical terminal history without
87// consulting or mutating live Screen history.
88func (snapshot RecoveryTranscriptSnapshot) Marshal() ([]byte, error) {
89 rows := 0
90 cells := 0
91 for _, segment := range snapshot.segments {
92 rows += len(segment.rows)
93 for _, row := range segment.rows {
94 cells += len(row)
95 }
96 }
97
98 view := HistoryView{rows: rows, cells: cells, nextRowID: snapshot.nextRowID}
99 for _, segment := range snapshot.segments {
100 for start := 0; start < len(segment.rows); {
101 space := maxHistoryChunkRows
102 if len(view.chunks) > 0 {
103 last := view.chunks[len(view.chunks)-1]
104 if len(last.rows) < maxHistoryChunkRows {
105 space = maxHistoryChunkRows - len(last.rows)
106 end := min(start+space, len(segment.rows))
107 last.rows = append(last.rows, segment.rows[start:end]...)
108 last.bounds = append(last.bounds, segment.bounds[start:end]...)
109 last.rowIDs = append(last.rowIDs, segment.rowIDs[start:end]...)
110 start = end
111 continue
112 }
113 }
114
115 end := min(start+space, len(segment.rows))
116 view.chunks = append(view.chunks, &HistoryChunk{
117 rows: append([][]renderer.Cell(nil), segment.rows[start:end]...),
118 bounds: append([]LineBound(nil), segment.bounds[start:end]...),
119 rowIDs: append([]RowID(nil), segment.rowIDs[start:end]...),
120 })
121 start = end
122 }
123 }
124 return MarshalHistory(view)
125}