detached fcb4c06

refactor(vt): remove visible snapshot codec

commit
fcb4c06dfae468035285d4067ffe6f95f3f52d40
author
brice · · 2026-07-27 12:56 UTC
parent
2dca0c2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +127 -368
M pkg/vt/history_acceptance_test.go +14 -16
@@ -6,7 +6,7 @@ import (
66 "github.com/bnema/vev/pkg/renderer"
77 )
88
9-func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *testing.T) {
9+func TestSnapshotCodecsPreserveEveryTerminalCellAcrossHistoryAndRecoveryTranscript(t *testing.T) {
1010 indexed := renderer.Style{
1111 Bold: true,
1212 Italic: true,
@@ -79,26 +79,24 @@ func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *te
7979 },
8080 },
8181 {
82- name: "visible frame retains blanks wide heads continuations and styles",
82+ name: "recovery transcript retains blanks wide heads continuations and styles",
8383 run: func(t *testing.T) {
84- want := renderer.NewFrame(4, 2)
85- copy(want.Row(0), makeRow('D', rgb))
86- copy(want.Row(1), makeRow('E', indexed))
87- encoded, err := MarshalVisible(want)
88- if err != nil {
89- t.Fatalf("MarshalVisible: %v", err)
84+ screen := NewScreen(4, 2)
85+ want := [][]renderer.Cell{makeRow('D', rgb), makeRow('E', indexed)}
86+ for y, row := range want {
87+ copy(screen.Frame.Row(y), row)
88+ screen.buffer.boundaries[y] = LineBound{End: len(row)}
9089 }
91- got, err := UnmarshalVisible(encoded)
90+
91+ encoded, err := screen.RecoveryTranscriptSnapshot().Marshal()
9292 if err != nil {
93- t.Fatalf("UnmarshalVisible: %v", err)
93+ t.Fatalf("marshal recovery transcript: %v", err)
9494 }
95- for y := range want.Height {
96- for x, cell := range want.Row(y) {
97- if !got.At(x, y).Equal(cell) {
98- t.Fatalf("cell (%d,%d) = %#v, want %#v", x, y, got.At(x, y), cell)
99- }
100- }
95+ got, err := UnmarshalHistory(encoded)
96+ if err != nil {
97+ t.Fatalf("unmarshal recovery transcript: %v", err)
10198 }
99+ assertHistoryRowsEqual(t, got, want)
102100 },
103101 },
104102 }
@@ -6,7 +6,7 @@ import (
6 "github.com/bnema/vev/pkg/renderer"6 "github.com/bnema/vev/pkg/renderer"
7 )7 )
8 8
9-func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *testing.T) {9+func TestSnapshotCodecsPreserveEveryTerminalCellAcrossHistoryAndRecoveryTranscript(t *testing.T) {
10 indexed := renderer.Style{10 indexed := renderer.Style{
11 Bold: true,11 Bold: true,
12 Italic: true,12 Italic: true,
@@ -79,26 +79,24 @@ func TestSnapshotCodecsPreserveEveryTerminalCellAcrossSealedTailAndVisible(t *te
79 },79 },
80 },80 },
81 {81 {
82- name: "visible frame retains blanks wide heads continuations and styles",82+ name: "recovery transcript retains blanks wide heads continuations and styles",
83 run: func(t *testing.T) {83 run: func(t *testing.T) {
84- want := renderer.NewFrame(4, 2)84+ screen := NewScreen(4, 2)
85- copy(want.Row(0), makeRow('D', rgb))85+ want := [][]renderer.Cell{makeRow('D', rgb), makeRow('E', indexed)}
86- copy(want.Row(1), makeRow('E', indexed))86+ for y, row := range want {
87- encoded, err := MarshalVisible(want)87+ copy(screen.Frame.Row(y), row)
88- if err != nil {88+ screen.buffer.boundaries[y] = LineBound{End: len(row)}
89- t.Fatalf("MarshalVisible: %v", err)
90 }89 }
91- got, err := UnmarshalVisible(encoded)90+
91+ encoded, err := screen.RecoveryTranscriptSnapshot().Marshal()
92 if err != nil {92 if err != nil {
93- t.Fatalf("UnmarshalVisible: %v", err)93+ t.Fatalf("marshal recovery transcript: %v", err)
94 }94 }
95- for y := range want.Height {95+ got, err := UnmarshalHistory(encoded)
96- for x, cell := range want.Row(y) {96+ if err != nil {
97- if !got.At(x, y).Equal(cell) {97+ t.Fatalf("unmarshal recovery transcript: %v", err)
98- t.Fatalf("cell (%d,%d) = %#v, want %#v", x, y, got.At(x, y), cell)
99- }
100- }
101 }98 }
99+ assertHistoryRowsEqual(t, got, want)
102 },100 },
103 },101 },
104 }102 }
M pkg/vt/history_codec.go +6 -6
@@ -14,6 +14,7 @@ const (
1414 historyMagic = "VTH1"
1515 historyVersion = 2
1616 historyCellBytes = 41
17+ historyBoundBytes = 5
1718 maxHistoryChunkRows = 256
1819
1920 // The daemon retains 10,000 history rows. Support a 20% row margin and
@@ -24,9 +25,8 @@ const (
2425 maxHistoryRowCells = 160
2526 maxHistoryCells = maxHistoryRows * maxHistoryRowCells
2627
27- maxHistoryDecodeStyles = maxHistoryCells
28- maxHistoryDecodedBytes = maxHistoryCells * historyCellBytes
29- maxVisibleBoundaryBytes = maxHistoryRows * visibleBoundaryBytes
28+ maxHistoryDecodeStyles = maxHistoryCells
29+ maxHistoryDecodedBytes = maxHistoryCells * historyCellBytes
3030 )
3131
3232 var errInvalidHistory = errors.New("invalid history payload")
@@ -206,7 +206,7 @@ func parseHistory(data []byte, populate bool) (HistoryView, historyDecodeStats,
206206 for range chunkCount {
207207 rowCount, ok := p.uint32()
208208 if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows ||
209- uint64(rowCount) > uint64(len(p.data))/(4+visibleBoundaryBytes) ||
209+ uint64(rowCount) > uint64(len(p.data))/(4+historyBoundBytes) ||
210210 !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryRows) {
211211 return HistoryView{}, historyDecodeStats{}, false
212212 }
@@ -329,12 +329,12 @@ func (p *historyParser) cell() (renderer.Cell, bool) {
329329 }
330330
331331 func (p *historyParser) bound() (LineBound, bool) {
332- if len(p.data) < visibleBoundaryBytes {
332+ if len(p.data) < historyBoundBytes {
333333 return LineBound{}, false
334334 }
335335 end := binary.BigEndian.Uint32(p.data[:4])
336336 flag := p.data[4]
337- p.data = p.data[visibleBoundaryBytes:]
337+ p.data = p.data[historyBoundBytes:]
338338 if flag > 1 || uint64(end) > math.MaxInt32 {
339339 return LineBound{}, false
340340 }
@@ -14,6 +14,7 @@ const (
14 historyMagic = "VTH1"14 historyMagic = "VTH1"
15 historyVersion = 215 historyVersion = 2
16 historyCellBytes = 4116 historyCellBytes = 41
17+ historyBoundBytes = 5
17 maxHistoryChunkRows = 25618 maxHistoryChunkRows = 256
18 19
19 // The daemon retains 10,000 history rows. Support a 20% row margin and20 // The daemon retains 10,000 history rows. Support a 20% row margin and
@@ -24,9 +25,8 @@ const (
24 maxHistoryRowCells = 16025 maxHistoryRowCells = 160
25 maxHistoryCells = maxHistoryRows * maxHistoryRowCells26 maxHistoryCells = maxHistoryRows * maxHistoryRowCells
26 27
27- maxHistoryDecodeStyles = maxHistoryCells28+ maxHistoryDecodeStyles = maxHistoryCells
28- maxHistoryDecodedBytes = maxHistoryCells * historyCellBytes29+ maxHistoryDecodedBytes = maxHistoryCells * historyCellBytes
29- maxVisibleBoundaryBytes = maxHistoryRows * visibleBoundaryBytes
30 )30 )
31 31
32 var errInvalidHistory = errors.New("invalid history payload")32 var errInvalidHistory = errors.New("invalid history payload")
@@ -206,7 +206,7 @@ func parseHistory(data []byte, populate bool) (HistoryView, historyDecodeStats,
206 for range chunkCount {206 for range chunkCount {
207 rowCount, ok := p.uint32()207 rowCount, ok := p.uint32()
208 if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows ||208 if !ok || rowCount == 0 || rowCount > maxHistoryChunkRows ||
209- uint64(rowCount) > uint64(len(p.data))/(4+visibleBoundaryBytes) ||209+ uint64(rowCount) > uint64(len(p.data))/(4+historyBoundBytes) ||
210 !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryRows) {210 !addHistoryDecodeBudget(&stats.rows, uint64(rowCount), maxHistoryRows) {
211 return HistoryView{}, historyDecodeStats{}, false211 return HistoryView{}, historyDecodeStats{}, false
212 }212 }
@@ -329,12 +329,12 @@ func (p *historyParser) cell() (renderer.Cell, bool) {
329 }329 }
330 330
331 func (p *historyParser) bound() (LineBound, bool) {331 func (p *historyParser) bound() (LineBound, bool) {
332- if len(p.data) < visibleBoundaryBytes {332+ if len(p.data) < historyBoundBytes {
333 return LineBound{}, false333 return LineBound{}, false
334 }334 }
335 end := binary.BigEndian.Uint32(p.data[:4])335 end := binary.BigEndian.Uint32(p.data[:4])
336 flag := p.data[4]336 flag := p.data[4]
337- p.data = p.data[visibleBoundaryBytes:]337+ p.data = p.data[historyBoundBytes:]
338 if flag > 1 || uint64(end) > math.MaxInt32 {338 if flag > 1 || uint64(end) > math.MaxInt32 {
339 return LineBound{}, false339 return LineBound{}, false
340 }340 }
A pkg/vt/history_codec_bound_test.go +49 -0
@@ -0,0 +1,49 @@
1+package vt
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "github.com/bnema/vev/pkg/renderer"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestHistoryCodecRejectsMalformedBounds(t *testing.T) {
12+ blob, err := MarshalHistory(HistoryView{
13+ chunks: []*HistoryChunk{{rows: [][]renderer.Cell{historyRow("abcd")}, bounds: []LineBound{{End: 4, Soft: true}}}},
14+ rows: 1,
15+ cells: 4,
16+ })
17+ require.NoError(t, err)
18+ boundOffset := len(blob) - historyBoundBytes
19+
20+ tests := []struct {
21+ name string
22+ mutate func([]byte)
23+ }{
24+ {
25+ name: "end exceeds row width",
26+ mutate: func(data []byte) {
27+ binary.BigEndian.PutUint32(data[boundOffset:boundOffset+4], 5)
28+ },
29+ },
30+ {
31+ name: "soft flag is not canonical",
32+ mutate: func(data []byte) {
33+ data[boundOffset+4] = 2
34+ },
35+ },
36+ }
37+
38+ for _, tt := range tests {
39+ t.Run(tt.name, func(t *testing.T) {
40+ data := append([]byte(nil), blob...)
41+ tt.mutate(data)
42+
43+ _, preflightErr := PreflightHistoryBlob(data)
44+ require.Error(t, preflightErr)
45+ _, unmarshalErr := UnmarshalHistory(data)
46+ require.Error(t, unmarshalErr)
47+ })
48+ }
49+}
@@ -0,0 +1,49 @@
1+package vt
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "github.com/bnema/vev/pkg/renderer"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestHistoryCodecRejectsMalformedBounds(t *testing.T) {
12+ blob, err := MarshalHistory(HistoryView{
13+ chunks: []*HistoryChunk{{rows: [][]renderer.Cell{historyRow("abcd")}, bounds: []LineBound{{End: 4, Soft: true}}}},
14+ rows: 1,
15+ cells: 4,
16+ })
17+ require.NoError(t, err)
18+ boundOffset := len(blob) - historyBoundBytes
19+
20+ tests := []struct {
21+ name string
22+ mutate func([]byte)
23+ }{
24+ {
25+ name: "end exceeds row width",
26+ mutate: func(data []byte) {
27+ binary.BigEndian.PutUint32(data[boundOffset:boundOffset+4], 5)
28+ },
29+ },
30+ {
31+ name: "soft flag is not canonical",
32+ mutate: func(data []byte) {
33+ data[boundOffset+4] = 2
34+ },
35+ },
36+ }
37+
38+ for _, tt := range tests {
39+ t.Run(tt.name, func(t *testing.T) {
40+ data := append([]byte(nil), blob...)
41+ tt.mutate(data)
42+
43+ _, preflightErr := PreflightHistoryBlob(data)
44+ require.Error(t, preflightErr)
45+ _, unmarshalErr := UnmarshalHistory(data)
46+ require.Error(t, unmarshalErr)
47+ })
48+ }
49+}
M pkg/vt/history_codec_test.go +1 -1
@@ -82,7 +82,7 @@ func TestHistoryCodecRejectsMalformedAndV1Payloads(t *testing.T) {
8282
8383 // This is a genuine v1-layout payload: version 1 and no trailing bound
8484 // bytes. It is not a v2 payload with only its version byte changed.
85- v1 := append([]byte(nil), v2[:len(v2)-visibleBoundaryBytes]...)
85+ v1 := append([]byte(nil), v2[:len(v2)-historyBoundBytes]...)
8686 v1[4] = 1
8787 if _, err := UnmarshalHistory(v1); err == nil {
8888 t.Fatal("accepted a genuine v1 history payload")
@@ -82,7 +82,7 @@ func TestHistoryCodecRejectsMalformedAndV1Payloads(t *testing.T) {
82 82
83 // This is a genuine v1-layout payload: version 1 and no trailing bound83 // This is a genuine v1-layout payload: version 1 and no trailing bound
84 // bytes. It is not a v2 payload with only its version byte changed.84 // bytes. It is not a v2 payload with only its version byte changed.
85- v1 := append([]byte(nil), v2[:len(v2)-visibleBoundaryBytes]...)85+ v1 := append([]byte(nil), v2[:len(v2)-historyBoundBytes]...)
86 v1[4] = 186 v1[4] = 1
87 if _, err := UnmarshalHistory(v1); err == nil {87 if _, err := UnmarshalHistory(v1); err == nil {
88 t.Fatal("accepted a genuine v1 history payload")88 t.Fatal("accepted a genuine v1 history payload")
R pkg/vt/primary_visible_snapshot_test.go → pkg/vt/recovery_transcript_ownership_test.go +5 -5 renamed, 58% similar
@@ -6,12 +6,12 @@ import (
66 "github.com/stretchr/testify/require"
77 )
88
9-func TestPrimaryVisibleSnapshotOwnsSavedPrimaryAcrossLiveMutations(t *testing.T) {
9+func TestRecoveryTranscriptSnapshotOwnsSavedPrimaryAcrossLiveMutations(t *testing.T) {
1010 s := NewScreen(8, 2)
1111 s.Write([]byte("primary"))
1212 s.Write([]byte("\x1b[?1049h"))
1313
14- captured := s.PrimaryVisibleSnapshot()
14+ captured := s.RecoveryTranscriptSnapshot()
1515 mutated := make(chan struct{})
1616 go func() {
1717 s.Write([]byte("alternate"))
@@ -23,7 +23,7 @@ func TestPrimaryVisibleSnapshotOwnsSavedPrimaryAcrossLiveMutations(t *testing.T)
2323 blob, err := captured.Marshal()
2424 <-mutated
2525 require.NoError(t, err)
26- restored := NewScreen(1, 1)
27- require.NoError(t, restored.RestorePrimaryVisible(blob))
28- require.Equal(t, "primary ", rowString(restored.Frame.Row(0)))
26+ view, err := UnmarshalHistory(blob)
27+ require.NoError(t, err)
28+ require.Equal(t, []string{"primary "}, historyViewTexts(view))
2929 }
@@ -6,12 +6,12 @@ import (
6 "github.com/stretchr/testify/require"6 "github.com/stretchr/testify/require"
7 )7 )
8 8
9-func TestPrimaryVisibleSnapshotOwnsSavedPrimaryAcrossLiveMutations(t *testing.T) {9+func TestRecoveryTranscriptSnapshotOwnsSavedPrimaryAcrossLiveMutations(t *testing.T) {
10 s := NewScreen(8, 2)10 s := NewScreen(8, 2)
11 s.Write([]byte("primary"))11 s.Write([]byte("primary"))
12 s.Write([]byte("\x1b[?1049h"))12 s.Write([]byte("\x1b[?1049h"))
13 13
14- captured := s.PrimaryVisibleSnapshot()14+ captured := s.RecoveryTranscriptSnapshot()
15 mutated := make(chan struct{})15 mutated := make(chan struct{})
16 go func() {16 go func() {
17 s.Write([]byte("alternate"))17 s.Write([]byte("alternate"))
@@ -23,7 +23,7 @@ func TestPrimaryVisibleSnapshotOwnsSavedPrimaryAcrossLiveMutations(t *testing.T)
23 blob, err := captured.Marshal()23 blob, err := captured.Marshal()
24 <-mutated24 <-mutated
25 require.NoError(t, err)25 require.NoError(t, err)
26- restored := NewScreen(1, 1)26+ view, err := UnmarshalHistory(blob)
27- require.NoError(t, restored.RestorePrimaryVisible(blob))27+ require.NoError(t, err)
28- require.Equal(t, "primary ", rowString(restored.Frame.Row(0)))28+ require.Equal(t, []string{"primary "}, historyViewTexts(view))
29 }29 }
M pkg/vt/recovery_transcript_test.go +51 -0
@@ -25,6 +25,17 @@ func TestRecoveryTranscriptSnapshotOwnsPrimaryCellsAndBounds(t *testing.T) {
2525 require.Equal(t, LineBound{End: 4}, view.Bound(0))
2626 }
2727
28+func TestRecoveryTranscriptSnapshotRetainsReflowBounds(t *testing.T) {
29+ s := NewScreen(5, 3)
30+ s.Write([]byte("abcdefgh"))
31+
32+ view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
33+
34+ require.Equal(t, []string{"abcde", "fgh "}, historyViewTexts(view))
35+ require.Equal(t, LineBound{End: 5, Soft: true}, view.Bound(0))
36+ require.Equal(t, LineBound{End: 3}, view.Bound(1))
37+}
38+
2839 func TestRecoveryTranscriptSnapshotAllocationsDoNotScaleWithRetainedRows(t *testing.T) {
2940 const (
3041 shortHeight = 8
@@ -288,6 +299,46 @@ func TestNewScreenWithRecoveryTranscriptRejectsOversizedTranscriptRow(t *testing
288299 require.Nil(t, screen)
289300 }
290301
302+func BenchmarkRecoveryTranscriptSnapshot(b *testing.B) {
303+ for _, tt := range []struct {
304+ name string
305+ alternate bool
306+ wantRows int
307+ }{
308+ {name: "primary", wantRows: 40},
309+ {name: "primary-and-active-alternate", alternate: true, wantRows: 80},
310+ } {
311+ b.Run(tt.name, func(b *testing.B) {
312+ screen := NewScreen(120, 40)
313+ fillRecoveryTranscriptBenchmarkFrame(screen)
314+ if tt.alternate {
315+ screen.Write([]byte("\x1b[?1049h"))
316+ fillRecoveryTranscriptBenchmarkFrame(screen)
317+ }
318+
319+ b.ReportAllocs()
320+ b.ReportMetric(float64(tt.wantRows), "rows/snapshot")
321+ b.ResetTimer()
322+ for b.Loop() {
323+ recoveryTranscriptSnapshotSink = screen.RecoveryTranscriptSnapshot()
324+ }
325+ b.StopTimer()
326+ if got := decodeRecoveryTranscript(b, recoveryTranscriptSnapshotSink).Len(); got != tt.wantRows {
327+ b.Fatalf("captured rows = %d, want %d", got, tt.wantRows)
328+ }
329+ })
330+ }
331+}
332+
333+func fillRecoveryTranscriptBenchmarkFrame(screen *Screen) {
334+ for y := range screen.Frame.Height {
335+ for x := range screen.Frame.Width {
336+ screen.Frame.Set(x, y, renderer.Cell{Rune: rune('a' + (x+y)%26), Style: renderer.DefaultStyle()})
337+ }
338+ screen.buffer.boundaries[y] = LineBound{End: screen.Frame.Width}
339+ }
340+}
341+
291342 func BenchmarkNewScreenWithRecoveryTranscriptManyChunks(b *testing.B) {
292343 const rows = maxHistoryRows
293344 source := NewScreen(1, rows)
@@ -25,6 +25,17 @@ func TestRecoveryTranscriptSnapshotOwnsPrimaryCellsAndBounds(t *testing.T) {
25 require.Equal(t, LineBound{End: 4}, view.Bound(0))25 require.Equal(t, LineBound{End: 4}, view.Bound(0))
26 }26 }
27 27
28+func TestRecoveryTranscriptSnapshotRetainsReflowBounds(t *testing.T) {
29+ s := NewScreen(5, 3)
30+ s.Write([]byte("abcdefgh"))
31+
32+ view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
33+
34+ require.Equal(t, []string{"abcde", "fgh "}, historyViewTexts(view))
35+ require.Equal(t, LineBound{End: 5, Soft: true}, view.Bound(0))
36+ require.Equal(t, LineBound{End: 3}, view.Bound(1))
37+}
38+
28 func TestRecoveryTranscriptSnapshotAllocationsDoNotScaleWithRetainedRows(t *testing.T) {39 func TestRecoveryTranscriptSnapshotAllocationsDoNotScaleWithRetainedRows(t *testing.T) {
29 const (40 const (
30 shortHeight = 841 shortHeight = 8
@@ -288,6 +299,46 @@ func TestNewScreenWithRecoveryTranscriptRejectsOversizedTranscriptRow(t *testing
288 require.Nil(t, screen)299 require.Nil(t, screen)
289 }300 }
290 301
302+func BenchmarkRecoveryTranscriptSnapshot(b *testing.B) {
303+ for _, tt := range []struct {
304+ name string
305+ alternate bool
306+ wantRows int
307+ }{
308+ {name: "primary", wantRows: 40},
309+ {name: "primary-and-active-alternate", alternate: true, wantRows: 80},
310+ } {
311+ b.Run(tt.name, func(b *testing.B) {
312+ screen := NewScreen(120, 40)
313+ fillRecoveryTranscriptBenchmarkFrame(screen)
314+ if tt.alternate {
315+ screen.Write([]byte("\x1b[?1049h"))
316+ fillRecoveryTranscriptBenchmarkFrame(screen)
317+ }
318+
319+ b.ReportAllocs()
320+ b.ReportMetric(float64(tt.wantRows), "rows/snapshot")
321+ b.ResetTimer()
322+ for b.Loop() {
323+ recoveryTranscriptSnapshotSink = screen.RecoveryTranscriptSnapshot()
324+ }
325+ b.StopTimer()
326+ if got := decodeRecoveryTranscript(b, recoveryTranscriptSnapshotSink).Len(); got != tt.wantRows {
327+ b.Fatalf("captured rows = %d, want %d", got, tt.wantRows)
328+ }
329+ })
330+ }
331+}
332+
333+func fillRecoveryTranscriptBenchmarkFrame(screen *Screen) {
334+ for y := range screen.Frame.Height {
335+ for x := range screen.Frame.Width {
336+ screen.Frame.Set(x, y, renderer.Cell{Rune: rune('a' + (x+y)%26), Style: renderer.DefaultStyle()})
337+ }
338+ screen.buffer.boundaries[y] = LineBound{End: screen.Frame.Width}
339+ }
340+}
341+
291 func BenchmarkNewScreenWithRecoveryTranscriptManyChunks(b *testing.B) {342 func BenchmarkNewScreenWithRecoveryTranscriptManyChunks(b *testing.B) {
292 const rows = maxHistoryRows343 const rows = maxHistoryRows
293 source := NewScreen(1, rows)344 source := NewScreen(1, rows)
M pkg/vt/resize_reflow_regression_test.go +1 -29
@@ -1,11 +1,6 @@
11 package vt
22
3-import (
4- "encoding/binary"
5- "testing"
6-
7- "github.com/bnema/vev/pkg/renderer"
8-)
3+import "testing"
94
105 func TestCollapsedResizeUpdatesSavedPrimaryAndClearsEscape(t *testing.T) {
116 s := NewScreen(5, 3)
@@ -57,26 +52,3 @@ func TestInsertModeRetainsShiftedTailInReflowExtent(t *testing.T) {
5752 assertCell(t, s, x, 0, r)
5853 }
5954 }
60-
61-func TestVisibleBoundaryMetadataBudget(t *testing.T) {
62- const height = maxHistoryRows + 1
63- if _, err := MarshalVisible(renderer.NewFrame(0, height)); err == nil {
64- t.Fatal("marshal accepted boundary metadata beyond its allocation budget")
65- }
66- s := NewScreen(0, height)
67- if _, err := s.MarshalPrimaryVisible(); err == nil {
68- t.Fatal("marshal accepted boundary metadata beyond its allocation budget")
69- }
70-
71- data := make([]byte, 13+height*visibleBoundaryBytes)
72- copy(data[:4], visibleMagic)
73- data[4] = historyVersion
74- binary.BigEndian.PutUint32(data[9:13], height)
75- if _, err := PreflightVisibleBlob(data); err == nil {
76- t.Fatal("preflight accepted boundary metadata beyond its allocation budget")
77- }
78- if _, err := UnmarshalVisible(data); err == nil {
79- t.Fatal("unmarshal accepted boundary metadata beyond its allocation budget")
80- }
81-
82-}
@@ -1,11 +1,6 @@
1 package vt1 package vt
2 2
3-import (3+import "testing"
4- "encoding/binary"
5- "testing"
6-
7- "github.com/bnema/vev/pkg/renderer"
8-)
9 4
10 func TestCollapsedResizeUpdatesSavedPrimaryAndClearsEscape(t *testing.T) {5 func TestCollapsedResizeUpdatesSavedPrimaryAndClearsEscape(t *testing.T) {
11 s := NewScreen(5, 3)6 s := NewScreen(5, 3)
@@ -57,26 +52,3 @@ func TestInsertModeRetainsShiftedTailInReflowExtent(t *testing.T) {
57 assertCell(t, s, x, 0, r)52 assertCell(t, s, x, 0, r)
58 }53 }
59 }54 }
60-
61-func TestVisibleBoundaryMetadataBudget(t *testing.T) {
62- const height = maxHistoryRows + 1
63- if _, err := MarshalVisible(renderer.NewFrame(0, height)); err == nil {
64- t.Fatal("marshal accepted boundary metadata beyond its allocation budget")
65- }
66- s := NewScreen(0, height)
67- if _, err := s.MarshalPrimaryVisible(); err == nil {
68- t.Fatal("marshal accepted boundary metadata beyond its allocation budget")
69- }
70-
71- data := make([]byte, 13+height*visibleBoundaryBytes)
72- copy(data[:4], visibleMagic)
73- data[4] = historyVersion
74- binary.BigEndian.PutUint32(data[9:13], height)
75- if _, err := PreflightVisibleBlob(data); err == nil {
76- t.Fatal("preflight accepted boundary metadata beyond its allocation budget")
77- }
78- if _, err := UnmarshalVisible(data); err == nil {
79- t.Fatal("unmarshal accepted boundary metadata beyond its allocation budget")
80- }
81-
82-}
M pkg/vt/snapshot.go +0 -222
@@ -1,16 +1,8 @@
11 package vt
22
33 import (
4- "encoding/binary"
54 "fmt"
65 "math"
7-
8- "github.com/bnema/vev/pkg/renderer"
9-)
10-
11-const (
12- visibleMagic = "VTV2"
13- visibleBoundaryBytes = 5
146 )
157
168 // DecodeStats describes resources declared by one canonical VT blob.
@@ -142,217 +134,3 @@ func NewScreenWithRecoveryTranscript(width, height int, config HistoryConfig, se
142134 screen.history = history
143135 return screen, nil
144136 }
145-
146-// MarshalVisible encodes the exact visible primary frame, including every
147-// blank, row width, and terminal cell attribute.
148-func MarshalVisible(frame renderer.Frame) ([]byte, error) {
149- if _, _, _, ok := visibleEncodingBudget(frame.Width, frame.Height); !ok {
150- return nil, fmt.Errorf("marshal visible: %w", errInvalidHistory)
151- }
152- boundaries := make([]LineBound, frame.Height)
153- for y := range boundaries {
154- for x := frame.Width - 1; x >= 0; x-- {
155- if !frame.At(x, y).Equal(renderer.BlankCell()) {
156- boundaries[y].End = x + 1
157- break
158- }
159- }
160- }
161- return marshalVisible(frame, boundaries)
162-}
163-
164-// PrimaryVisibleSnapshot is an owned copy of the primary buffer's visible
165-// state. Capture it while the screen owner is synchronized, then Marshal it
166-// after releasing that lock.
167-type PrimaryVisibleSnapshot struct {
168- frame renderer.Frame
169- boundaries []LineBound
170-}
171-
172-// PrimaryVisibleSnapshot copies the saved primary frame and its logical-line
173-// boundaries. When the alternate screen is active, the saved primary buffer is
174-// copied rather than the currently displayed alternate buffer.
175-func (s *Screen) PrimaryVisibleSnapshot() PrimaryVisibleSnapshot {
176- b := s.buffer
177- if s.alternate != nil {
178- b = s.alternate.buffer
179- }
180- if b == nil {
181- return PrimaryVisibleSnapshot{frame: s.PrimaryVisibleFrame()}
182- }
183- return PrimaryVisibleSnapshot{
184- frame: b.frame.Clone(),
185- boundaries: append([]LineBound(nil), b.boundaries...),
186- }
187-}
188-
189-// Marshal encodes an owned primary visible snapshot without accessing live VT
190-// state.
191-func (v PrimaryVisibleSnapshot) Marshal() ([]byte, error) {
192- if len(v.boundaries) == 0 && v.frame.Height > 0 {
193- return MarshalVisible(v.frame)
194- }
195- return marshalVisible(v.frame, v.boundaries)
196-}
197-
198-// MarshalPrimaryVisible preserves the primary buffer's logical-line boundaries
199-// alongside its cells so a restored viewport can subsequently reflow.
200-//
201-// Deprecated: callers holding a pane lock should use PrimaryVisibleSnapshot
202-// and marshal the result after unlocking.
203-func (s *Screen) MarshalPrimaryVisible() ([]byte, error) {
204- return s.PrimaryVisibleSnapshot().Marshal()
205-}
206-
207-func marshalVisible(frame renderer.Frame, boundaries []LineBound) ([]byte, error) {
208- _, bytes, boundaryBytes, ok := visibleEncodingBudget(frame.Width, frame.Height)
209- if !ok || len(boundaries) != frame.Height {
210- return nil, fmt.Errorf("marshal visible: %w", errInvalidHistory)
211- }
212- out := make([]byte, 0, int(13+bytes+boundaryBytes))
213- out = append(out, visibleMagic...)
214- out = append(out, historyVersion)
215- out = binary.BigEndian.AppendUint32(out, uint32(frame.Width))
216- out = binary.BigEndian.AppendUint32(out, uint32(frame.Height))
217- for y := range frame.Height {
218- for _, cell := range frame.Row(y) {
219- if !validHistoryCell(cell) {
220- return nil, fmt.Errorf("marshal visible: %w", errInvalidHistory)
221- }
222- out = appendHistoryCell(out, cell)
223- }
224- }
225- for _, boundary := range boundaries {
226- out = binary.BigEndian.AppendUint32(out, uint32(boundary.End))
227- if boundary.Soft {
228- out = append(out, 1)
229- } else {
230- out = append(out, 0)
231- }
232- }
233- return out, nil
234-}
235-
236-// PreflightVisibleBlob validates dimensions and cell declarations without
237-// allocating a frame.
238-func PreflightVisibleBlob(data []byte) (DecodeStats, error) {
239- if len(data) < 13 || string(data[:4]) != visibleMagic || data[4] != historyVersion {
240- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
241- }
242- width, height := uint64(binary.BigEndian.Uint32(data[5:9])), uint64(binary.BigEndian.Uint32(data[9:13]))
243- cells, bytes, boundaryBytes, ok := visibleEncodingBudget64(width, height)
244- if !ok || bytes > math.MaxUint64-boundaryBytes || uint64(len(data)-13) != bytes+boundaryBytes {
245- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
246- }
247- p := historyParser{data: data[13:]}
248- for range cells {
249- cell, ok := p.cell()
250- if !ok || !validHistoryCell(cell) {
251- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
252- }
253- }
254- for range height {
255- if uint64(binary.BigEndian.Uint32(p.data[:4])) > width || p.data[4] > 1 {
256- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
257- }
258- p.data = p.data[5:]
259- }
260- return DecodeStats{Rows: height, Cells: cells, Styles: cells, Bytes: bytes}, nil
261-}
262-
263-func visibleEncodingBudget(width, height int) (cells, cellBytes, boundaryBytes uint64, ok bool) {
264- if width < 0 || height < 0 {
265- return 0, 0, 0, false
266- }
267- return visibleEncodingBudget64(uint64(width), uint64(height))
268-}
269-
270-func visibleEncodingBudget64(width, height uint64) (cells, cellBytes, boundaryBytes uint64, ok bool) {
271- if width > math.MaxUint32 || height > math.MaxUint32 || (width != 0 && height > math.MaxUint64/width) || height > math.MaxUint64/visibleBoundaryBytes {
272- return 0, 0, 0, false
273- }
274- cells = width * height
275- cellBytes, ok = historyCellByteCount(cells)
276- if !ok || cells > maxHistoryCells || cellBytes > maxHistoryDecodedBytes {
277- return 0, 0, 0, false
278- }
279- boundaryBytes = height * visibleBoundaryBytes
280- if boundaryBytes > maxVisibleBoundaryBytes {
281- return 0, 0, 0, false
282- }
283- return cells, cellBytes, boundaryBytes, true
284-}
285-
286-// UnmarshalVisible decodes a preflighted exact visible frame.
287-func UnmarshalVisible(data []byte) (renderer.Frame, error) {
288- frame, _, err := unmarshalVisible(data)
289- return frame, err
290-}
291-
292-func unmarshalVisible(data []byte) (renderer.Frame, []LineBound, error) {
293- if _, err := PreflightVisibleBlob(data); err != nil {
294- return renderer.Frame{}, nil, err
295- }
296- width, height := int(binary.BigEndian.Uint32(data[5:9])), int(binary.BigEndian.Uint32(data[9:13]))
297- frame := renderer.NewFrame(width, height)
298- cellBytes, _ := historyCellByteCount(uint64(width) * uint64(height))
299- p := historyParser{data: data[13 : 13+cellBytes]}
300- for y := range height {
301- for x := range width {
302- cell, _ := p.cell()
303- frame.Set(x, y, cell)
304- }
305- }
306- boundaries := make([]LineBound, height)
307- offset := 13 + int(cellBytes)
308- for y := range boundaries {
309- boundaries[y] = LineBound{End: clamp(int(binary.BigEndian.Uint32(data[offset:offset+4])), 0, width), Soft: data[offset+4] == 1}
310- offset += 5
311- }
312- return frame, boundaries, nil
313-}
314-
315-// PrimaryVisibleFrame returns a deep copy of the exact visible primary frame.
316-func (s *Screen) PrimaryVisibleFrame() renderer.Frame {
317- if s.alternate != nil {
318- return s.alternate.frame.Clone()
319- }
320- return s.Frame.Clone()
321-}
322-
323-// PrimaryVisibleRows returns a deep copy of the visible primary-screen rows.
324-// When the alternate screen is active, it snapshots the saved primary screen
325-// rather than the currently displayed alternate frame.
326-func (s *Screen) PrimaryVisibleRows() [][]renderer.Cell {
327- frame := s.Frame
328- if s.alternate != nil {
329- frame = s.alternate.frame
330- }
331- rows := make([][]renderer.Cell, frame.Height)
332- for y := range frame.Height {
333- rows[y] = append([]renderer.Cell(nil), frame.Row(y)...)
334- }
335- return rows
336-}
337-
338-// RestorePrimaryVisible replaces the primary frame from an exact visible blob.
339-func (s *Screen) RestorePrimaryVisible(blob []byte) error {
340- frame, boundaries, err := unmarshalVisible(blob)
341- if err != nil {
342- return err
343- }
344- b := bufferFromFrame(frame)
345- b.boundaries = boundaries
346- // A persisted primary frame is authoritative: restoring it directly keeps
347- // exact row widths and height even when a collapsed layout chose a smaller
348- // initial PTY size. A subsequent resize reconciles it with live geometry.
349- if s.alternate != nil {
350- s.alternate.frame = frame
351- s.alternate.buffer = b
352- } else {
353- s.buffer = b
354- s.Frame = b.frame
355- }
356- s.fullRedraw()
357- return nil
358-}
@@ -1,16 +1,8 @@
1 package vt1 package vt
2 2
3 import (3 import (
4- "encoding/binary"
5 "fmt"4 "fmt"
6 "math"5 "math"
7-
8- "github.com/bnema/vev/pkg/renderer"
9-)
10-
11-const (
12- visibleMagic = "VTV2"
13- visibleBoundaryBytes = 5
14 )6 )
15 7
16 // DecodeStats describes resources declared by one canonical VT blob.8 // DecodeStats describes resources declared by one canonical VT blob.
@@ -142,217 +134,3 @@ func NewScreenWithRecoveryTranscript(width, height int, config HistoryConfig, se
142 screen.history = history134 screen.history = history
143 return screen, nil135 return screen, nil
144 }136 }
145-
146-// MarshalVisible encodes the exact visible primary frame, including every
147-// blank, row width, and terminal cell attribute.
148-func MarshalVisible(frame renderer.Frame) ([]byte, error) {
149- if _, _, _, ok := visibleEncodingBudget(frame.Width, frame.Height); !ok {
150- return nil, fmt.Errorf("marshal visible: %w", errInvalidHistory)
151- }
152- boundaries := make([]LineBound, frame.Height)
153- for y := range boundaries {
154- for x := frame.Width - 1; x >= 0; x-- {
155- if !frame.At(x, y).Equal(renderer.BlankCell()) {
156- boundaries[y].End = x + 1
157- break
158- }
159- }
160- }
161- return marshalVisible(frame, boundaries)
162-}
163-
164-// PrimaryVisibleSnapshot is an owned copy of the primary buffer's visible
165-// state. Capture it while the screen owner is synchronized, then Marshal it
166-// after releasing that lock.
167-type PrimaryVisibleSnapshot struct {
168- frame renderer.Frame
169- boundaries []LineBound
170-}
171-
172-// PrimaryVisibleSnapshot copies the saved primary frame and its logical-line
173-// boundaries. When the alternate screen is active, the saved primary buffer is
174-// copied rather than the currently displayed alternate buffer.
175-func (s *Screen) PrimaryVisibleSnapshot() PrimaryVisibleSnapshot {
176- b := s.buffer
177- if s.alternate != nil {
178- b = s.alternate.buffer
179- }
180- if b == nil {
181- return PrimaryVisibleSnapshot{frame: s.PrimaryVisibleFrame()}
182- }
183- return PrimaryVisibleSnapshot{
184- frame: b.frame.Clone(),
185- boundaries: append([]LineBound(nil), b.boundaries...),
186- }
187-}
188-
189-// Marshal encodes an owned primary visible snapshot without accessing live VT
190-// state.
191-func (v PrimaryVisibleSnapshot) Marshal() ([]byte, error) {
192- if len(v.boundaries) == 0 && v.frame.Height > 0 {
193- return MarshalVisible(v.frame)
194- }
195- return marshalVisible(v.frame, v.boundaries)
196-}
197-
198-// MarshalPrimaryVisible preserves the primary buffer's logical-line boundaries
199-// alongside its cells so a restored viewport can subsequently reflow.
200-//
201-// Deprecated: callers holding a pane lock should use PrimaryVisibleSnapshot
202-// and marshal the result after unlocking.
203-func (s *Screen) MarshalPrimaryVisible() ([]byte, error) {
204- return s.PrimaryVisibleSnapshot().Marshal()
205-}
206-
207-func marshalVisible(frame renderer.Frame, boundaries []LineBound) ([]byte, error) {
208- _, bytes, boundaryBytes, ok := visibleEncodingBudget(frame.Width, frame.Height)
209- if !ok || len(boundaries) != frame.Height {
210- return nil, fmt.Errorf("marshal visible: %w", errInvalidHistory)
211- }
212- out := make([]byte, 0, int(13+bytes+boundaryBytes))
213- out = append(out, visibleMagic...)
214- out = append(out, historyVersion)
215- out = binary.BigEndian.AppendUint32(out, uint32(frame.Width))
216- out = binary.BigEndian.AppendUint32(out, uint32(frame.Height))
217- for y := range frame.Height {
218- for _, cell := range frame.Row(y) {
219- if !validHistoryCell(cell) {
220- return nil, fmt.Errorf("marshal visible: %w", errInvalidHistory)
221- }
222- out = appendHistoryCell(out, cell)
223- }
224- }
225- for _, boundary := range boundaries {
226- out = binary.BigEndian.AppendUint32(out, uint32(boundary.End))
227- if boundary.Soft {
228- out = append(out, 1)
229- } else {
230- out = append(out, 0)
231- }
232- }
233- return out, nil
234-}
235-
236-// PreflightVisibleBlob validates dimensions and cell declarations without
237-// allocating a frame.
238-func PreflightVisibleBlob(data []byte) (DecodeStats, error) {
239- if len(data) < 13 || string(data[:4]) != visibleMagic || data[4] != historyVersion {
240- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
241- }
242- width, height := uint64(binary.BigEndian.Uint32(data[5:9])), uint64(binary.BigEndian.Uint32(data[9:13]))
243- cells, bytes, boundaryBytes, ok := visibleEncodingBudget64(width, height)
244- if !ok || bytes > math.MaxUint64-boundaryBytes || uint64(len(data)-13) != bytes+boundaryBytes {
245- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
246- }
247- p := historyParser{data: data[13:]}
248- for range cells {
249- cell, ok := p.cell()
250- if !ok || !validHistoryCell(cell) {
251- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
252- }
253- }
254- for range height {
255- if uint64(binary.BigEndian.Uint32(p.data[:4])) > width || p.data[4] > 1 {
256- return DecodeStats{}, fmt.Errorf("preflight visible: %w", errInvalidHistory)
257- }
258- p.data = p.data[5:]
259- }
260- return DecodeStats{Rows: height, Cells: cells, Styles: cells, Bytes: bytes}, nil
261-}
262-
263-func visibleEncodingBudget(width, height int) (cells, cellBytes, boundaryBytes uint64, ok bool) {
264- if width < 0 || height < 0 {
265- return 0, 0, 0, false
266- }
267- return visibleEncodingBudget64(uint64(width), uint64(height))
268-}
269-
270-func visibleEncodingBudget64(width, height uint64) (cells, cellBytes, boundaryBytes uint64, ok bool) {
271- if width > math.MaxUint32 || height > math.MaxUint32 || (width != 0 && height > math.MaxUint64/width) || height > math.MaxUint64/visibleBoundaryBytes {
272- return 0, 0, 0, false
273- }
274- cells = width * height
275- cellBytes, ok = historyCellByteCount(cells)
276- if !ok || cells > maxHistoryCells || cellBytes > maxHistoryDecodedBytes {
277- return 0, 0, 0, false
278- }
279- boundaryBytes = height * visibleBoundaryBytes
280- if boundaryBytes > maxVisibleBoundaryBytes {
281- return 0, 0, 0, false
282- }
283- return cells, cellBytes, boundaryBytes, true
284-}
285-
286-// UnmarshalVisible decodes a preflighted exact visible frame.
287-func UnmarshalVisible(data []byte) (renderer.Frame, error) {
288- frame, _, err := unmarshalVisible(data)
289- return frame, err
290-}
291-
292-func unmarshalVisible(data []byte) (renderer.Frame, []LineBound, error) {
293- if _, err := PreflightVisibleBlob(data); err != nil {
294- return renderer.Frame{}, nil, err
295- }
296- width, height := int(binary.BigEndian.Uint32(data[5:9])), int(binary.BigEndian.Uint32(data[9:13]))
297- frame := renderer.NewFrame(width, height)
298- cellBytes, _ := historyCellByteCount(uint64(width) * uint64(height))
299- p := historyParser{data: data[13 : 13+cellBytes]}
300- for y := range height {
301- for x := range width {
302- cell, _ := p.cell()
303- frame.Set(x, y, cell)
304- }
305- }
306- boundaries := make([]LineBound, height)
307- offset := 13 + int(cellBytes)
308- for y := range boundaries {
309- boundaries[y] = LineBound{End: clamp(int(binary.BigEndian.Uint32(data[offset:offset+4])), 0, width), Soft: data[offset+4] == 1}
310- offset += 5
311- }
312- return frame, boundaries, nil
313-}
314-
315-// PrimaryVisibleFrame returns a deep copy of the exact visible primary frame.
316-func (s *Screen) PrimaryVisibleFrame() renderer.Frame {
317- if s.alternate != nil {
318- return s.alternate.frame.Clone()
319- }
320- return s.Frame.Clone()
321-}
322-
323-// PrimaryVisibleRows returns a deep copy of the visible primary-screen rows.
324-// When the alternate screen is active, it snapshots the saved primary screen
325-// rather than the currently displayed alternate frame.
326-func (s *Screen) PrimaryVisibleRows() [][]renderer.Cell {
327- frame := s.Frame
328- if s.alternate != nil {
329- frame = s.alternate.frame
330- }
331- rows := make([][]renderer.Cell, frame.Height)
332- for y := range frame.Height {
333- rows[y] = append([]renderer.Cell(nil), frame.Row(y)...)
334- }
335- return rows
336-}
337-
338-// RestorePrimaryVisible replaces the primary frame from an exact visible blob.
339-func (s *Screen) RestorePrimaryVisible(blob []byte) error {
340- frame, boundaries, err := unmarshalVisible(blob)
341- if err != nil {
342- return err
343- }
344- b := bufferFromFrame(frame)
345- b.boundaries = boundaries
346- // A persisted primary frame is authoritative: restoring it directly keeps
347- // exact row widths and height even when a collapsed layout chose a smaller
348- // initial PTY size. A subsequent resize reconciles it with live geometry.
349- if s.alternate != nil {
350- s.alternate.frame = frame
351- s.alternate.buffer = b
352- } else {
353- s.buffer = b
354- s.Frame = b.frame
355- }
356- s.fullRedraw()
357- return nil
358-}
D pkg/vt/snapshot_boundary_test.go +0 -43
@@ -1,43 +0,0 @@
1-package vt
2-
3-import (
4- "encoding/binary"
5- "testing"
6-
7- "github.com/stretchr/testify/require"
8-)
9-
10-func TestPreflightVisibleRejectsMalformedBoundaries(t *testing.T) {
11- s := NewScreen(4, 2)
12- s.Write([]byte("abcdef"))
13- blob, err := s.MarshalPrimaryVisible()
14- require.NoError(t, err)
15-
16- boundaryOffset := 13 + 2*4*historyCellBytes
17- tests := []struct {
18- name string
19- mutate func([]byte)
20- }{
21- {
22- name: "boundary end exceeds row width",
23- mutate: func(data []byte) {
24- binary.BigEndian.PutUint32(data[boundaryOffset:boundaryOffset+4], 5)
25- },
26- },
27- {
28- name: "soft flag is not canonical",
29- mutate: func(data []byte) {
30- data[boundaryOffset+4] = 2
31- },
32- },
33- }
34-
35- for _, tt := range tests {
36- t.Run(tt.name, func(t *testing.T) {
37- data := append([]byte(nil), blob...)
38- tt.mutate(data)
39- _, err := PreflightVisibleBlob(data)
40- require.Error(t, err)
41- })
42- }
43-}
@@ -1,43 +0,0 @@
1-package vt
2-
3-import (
4- "encoding/binary"
5- "testing"
6-
7- "github.com/stretchr/testify/require"
8-)
9-
10-func TestPreflightVisibleRejectsMalformedBoundaries(t *testing.T) {
11- s := NewScreen(4, 2)
12- s.Write([]byte("abcdef"))
13- blob, err := s.MarshalPrimaryVisible()
14- require.NoError(t, err)
15-
16- boundaryOffset := 13 + 2*4*historyCellBytes
17- tests := []struct {
18- name string
19- mutate func([]byte)
20- }{
21- {
22- name: "boundary end exceeds row width",
23- mutate: func(data []byte) {
24- binary.BigEndian.PutUint32(data[boundaryOffset:boundaryOffset+4], 5)
25- },
26- },
27- {
28- name: "soft flag is not canonical",
29- mutate: func(data []byte) {
30- data[boundaryOffset+4] = 2
31- },
32- },
33- }
34-
35- for _, tt := range tests {
36- t.Run(tt.name, func(t *testing.T) {
37- data := append([]byte(nil), blob...)
38- tt.mutate(data)
39- _, err := PreflightVisibleBlob(data)
40- require.Error(t, err)
41- })
42- }
43-}
M pkg/vt/snapshot_test.go +0 -46
@@ -1,7 +1,6 @@
11 package vt
22
33 import (
4- "strings"
54 "testing"
65
76 "github.com/bnema/vev/pkg/renderer"
@@ -79,48 +78,3 @@ func TestHistoryFromBlobsRestoresCellAccountingAndEvictsToBothBudgets(t *testing
7978 require.Equal(t, []string{"cc"}, historyViewTexts(restored.View()))
8079 require.Equal(t, 2, restored.Cells())
8180 }
82-
83-func TestPrimaryVisibleSnapshotRetainsReflowBoundaries(t *testing.T) {
84- s := NewScreen(5, 3)
85- s.Write([]byte("abcdefgh"))
86- blob, err := s.MarshalPrimaryVisible()
87- require.NoError(t, err)
88-
89- restored := NewScreen(1, 1)
90- require.NoError(t, restored.RestorePrimaryVisible(blob))
91- restored.Resize(3, 3)
92- require.Equal(t, "abc", rowString(restored.Frame.Row(0)))
93- require.Equal(t, "def", rowString(restored.Frame.Row(1)))
94- require.Equal(t, "gh ", rowString(restored.Frame.Row(2)))
95-}
96-
97-func TestPrimaryVisibleRowsCopiesActivePrimaryScreen(t *testing.T) {
98- s := NewScreen(6, 2)
99- s.Write([]byte("hello"))
100-
101- rows := s.PrimaryVisibleRows()
102- require.Len(t, rows, 2)
103- require.Equal(t, "hello ", rowString(rows[0]))
104-
105- rows[0][0].Rune = 'X'
106- rows[0] = nil
107-
108- require.Equal(t, "hello ", rowString(s.Frame.Row(0)))
109- require.Equal(t, "hello ", rowString(s.PrimaryVisibleRows()[0]))
110-}
111-
112-func TestPrimaryVisibleRowsUsesSavedPrimaryScreenWhenAlternateActive(t *testing.T) {
113- s := NewScreen(8, 2)
114- s.Write([]byte("primary"))
115- s.Write([]byte("\x1b[?1049h"))
116- s.Write([]byte("alt"))
117-
118- rows := s.PrimaryVisibleRows()
119- require.Len(t, rows, 2)
120- require.Equal(t, "primary ", rowString(rows[0]))
121- require.False(t, strings.Contains(rowString(rows[0]), "alt"))
122-
123- rows[0][0].Rune = 'X'
124- s.Write([]byte("\x1b[?1049l"))
125- require.Equal(t, "primary ", rowString(s.Frame.Row(0)))
126-}
@@ -1,7 +1,6 @@
1 package vt1 package vt
2 2
3 import (3 import (
4- "strings"
5 "testing"4 "testing"
6 5
7 "github.com/bnema/vev/pkg/renderer"6 "github.com/bnema/vev/pkg/renderer"
@@ -79,48 +78,3 @@ func TestHistoryFromBlobsRestoresCellAccountingAndEvictsToBothBudgets(t *testing
79 require.Equal(t, []string{"cc"}, historyViewTexts(restored.View()))78 require.Equal(t, []string{"cc"}, historyViewTexts(restored.View()))
80 require.Equal(t, 2, restored.Cells())79 require.Equal(t, 2, restored.Cells())
81 }80 }
82-
83-func TestPrimaryVisibleSnapshotRetainsReflowBoundaries(t *testing.T) {
84- s := NewScreen(5, 3)
85- s.Write([]byte("abcdefgh"))
86- blob, err := s.MarshalPrimaryVisible()
87- require.NoError(t, err)
88-
89- restored := NewScreen(1, 1)
90- require.NoError(t, restored.RestorePrimaryVisible(blob))
91- restored.Resize(3, 3)
92- require.Equal(t, "abc", rowString(restored.Frame.Row(0)))
93- require.Equal(t, "def", rowString(restored.Frame.Row(1)))
94- require.Equal(t, "gh ", rowString(restored.Frame.Row(2)))
95-}
96-
97-func TestPrimaryVisibleRowsCopiesActivePrimaryScreen(t *testing.T) {
98- s := NewScreen(6, 2)
99- s.Write([]byte("hello"))
100-
101- rows := s.PrimaryVisibleRows()
102- require.Len(t, rows, 2)
103- require.Equal(t, "hello ", rowString(rows[0]))
104-
105- rows[0][0].Rune = 'X'
106- rows[0] = nil
107-
108- require.Equal(t, "hello ", rowString(s.Frame.Row(0)))
109- require.Equal(t, "hello ", rowString(s.PrimaryVisibleRows()[0]))
110-}
111-
112-func TestPrimaryVisibleRowsUsesSavedPrimaryScreenWhenAlternateActive(t *testing.T) {
113- s := NewScreen(8, 2)
114- s.Write([]byte("primary"))
115- s.Write([]byte("\x1b[?1049h"))
116- s.Write([]byte("alt"))
117-
118- rows := s.PrimaryVisibleRows()
119- require.Len(t, rows, 2)
120- require.Equal(t, "primary ", rowString(rows[0]))
121- require.False(t, strings.Contains(rowString(rows[0]), "alt"))
122-
123- rows[0][0].Rune = 'X'
124- s.Write([]byte("\x1b[?1049l"))
125- require.Equal(t, "primary ", rowString(s.Frame.Row(0)))
126-}

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 12

Tags 11

↑↓ moveenter openesc close