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