1package vt
2
3import (
4 "encoding/base64"
5 "strconv"
6 "testing"
7
8 "github.com/bnema/vev-vt/graphics"
9 "github.com/stretchr/testify/require"
10)
11
12func screenKittyImageAPC(action string) []byte {
13 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
14 return []byte("\x1b_Ga=" + action + ",i=1,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")
15}
16
17func screenKittyImageAPCWithID(action string, id uint64) []byte {
18 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
19 return []byte("\x1b_Ga=" + action + ",i=" + strconv.FormatUint(id, 10) + ",f=32,s=1,v=1,C=1;" + payload + "\x1b\\")
20}
21
22func screenKittyPutAPC(id uint64) []byte {
23 return []byte("\x1b_Ga=p,i=" + strconv.FormatUint(id, 10) + ";\x1b\\")
24}
25
26func TestScreenKittyGraphicsFragmentedAPCAndResponse(t *testing.T) {
27 screen := NewScreen(16, 3)
28 var responses []string
29 screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) }
30
31 input := append([]byte("before"), screenKittyImageAPC("T")...)
32 input = append(input, []byte("after")...)
33 for _, b := range input {
34 screen.Write([]byte{b})
35 }
36
37 require.Equal(t, "beforeafter ", rowText(screen.Snapshot().Row(0)))
38 require.Equal(t, []string{"\x1b_Gi=1;OK\x1b\\"}, responses)
39 snapshot := screen.GraphicsSnapshot()
40 require.NotNil(t, snapshot)
41 require.Equal(t, uint64(1), snapshot.Usage().Assets)
42 require.Equal(t, uint64(1), snapshot.Usage().Placements)
43}
44
45func TestScreenKittyGraphicsPrimaryAndAlternateState(t *testing.T) {
46 screen := NewScreen(8, 3)
47 screen.Write(screenKittyImageAPC("T"))
48 primary := screen.GraphicsSnapshot()
49 require.NotNil(t, primary)
50 require.Equal(t, uint64(1), primary.Usage().Placements)
51
52 screen.Write([]byte("\x1b[?1049halt"))
53 require.True(t, screen.AltScreenActive())
54 require.Nil(t, screen.GraphicsSnapshot())
55 screen.Write(screenKittyImageAPCWithID("T", 2))
56 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
57
58 screen.Write([]byte("\x1b[?1049l"))
59 require.False(t, screen.AltScreenActive())
60 require.Equal(t, primary.Usage(), screen.GraphicsSnapshot().Usage())
61}
62
63func TestScreenReenteringActiveAlternateClearsGraphicsState(t *testing.T) {
64 screen := NewScreen(8, 3)
65 screen.Write([]byte("\x1b[?1049h"))
66 screen.Write(screenKittyImageAPC("T"))
67 before := screen.GraphicsSnapshot()
68 require.NotNil(t, before)
69 require.Equal(t, uint64(1), before.Usage().Placements)
70
71 screen.Write([]byte("\x1b[?1049h"))
72 after := screen.GraphicsSnapshot()
73 require.Nil(t, after)
74 require.Equal(t, uint64(1), before.Usage().Placements)
75}
76
77func TestScreenFullClearAbortsPendingUploadAndClearsPlacements(t *testing.T) {
78 screen := NewScreen(8, 3)
79 screen.Write(screenKittyImageAPC("T"))
80 screen.Write([]byte("\x1b_Ga=T,i=2,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
81 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
82
83 screen.Write([]byte("\x1b[2J"))
84 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
85 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
86 graphicsSnapshot := screen.GraphicsSnapshot()
87 require.NotNil(t, graphicsSnapshot)
88 require.Equal(t, uint64(1), graphicsSnapshot.Usage().Assets)
89 require.Equal(t, uint64(0), graphicsSnapshot.Usage().Placements)
90}
91
92func TestScreenPendingTransmitDisplayFailsClosedAcrossErrorAndResize(t *testing.T) {
93 pending := []byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\")
94 continuation := []byte("\x1b_Gm=0;IDBA\x1b\\")
95
96 screen := NewScreen(4, 2)
97 screen.Write(pending)
98 screen.Write([]byte("\x1b_Ga=p,i=1;\x1b\\"))
99 screen.Write(continuation)
100 require.NotNil(t, screen.GraphicsSnapshot())
101 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
102
103 screen.Write(pending)
104 screen.Resize(5, 2)
105 screen.Write(continuation)
106 require.NotNil(t, screen.GraphicsSnapshot())
107 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
108}
109
110func TestScreenPendingTransmitDisplayIsScopedToScreenBuffer(t *testing.T) {
111 screen := NewScreen(4, 2)
112 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
113 screen.Write([]byte("\x1b[?1049h"))
114 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
115 require.NotNil(t, screen.GraphicsSnapshot())
116 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
117 screen.Write([]byte("\x1b[?1049l"))
118 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
119 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
120}
121
122func TestScreenPartialPixelGeometryDoesNotInventPixelCoordinates(t *testing.T) {
123 screen := NewScreen(10, 4)
124 screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100})
125 screen.Write([]byte("\x1b[2;3H"))
126 screen.Write(screenKittyImageAPC("T"))
127 placement := screen.GraphicsSnapshot().Placements()[0]
128 require.Equal(t, graphics.PixelRect{X: 0, Y: 0, Width: 1, Height: 1}, placement.Destination())
129}
130
131func TestScreenKittyPlacementSeparatesCursorOriginCropAndPixelOffset(t *testing.T) {
132 screen := NewScreen(10, 4)
133 screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100, PixelHeight: 40})
134 screen.Write([]byte("\x1b[2;3H"))
135 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 64))
136 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=4,v=4,x=1,y=1,w=2,h=2,X=3,Y=4,C=1;" + payload + "\x1b\\"))
137
138 placement := screen.GraphicsSnapshot().Placements()[0]
139 require.Equal(t, graphics.PixelRect{X: 1, Y: 1, Width: 2, Height: 2}, placement.Source())
140 require.Equal(t, graphics.PixelRect{X: 23, Y: 14, Width: 2, Height: 2}, placement.Destination())
141}
142
143func TestScreenKittyGraphicsOrdinaryResetClearScrollAndResize(t *testing.T) {
144 screen := NewScreen(3, 2)
145 screen.Write(screenKittyImageAPC("T"))
146
147 // Printable text and a scroll do not guess at image anchoring. Existing
148 // placements remain available until a graphics-aware policy is supplied.
149 screen.Write([]byte("abcdefg"))
150 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
151 screen.Resize(4, 3)
152 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
153
154 // A full ordinary erase removes visible placements while retaining the
155 // protocol image mapping for a later explicit put.
156 screen.Write([]byte("\x1b[2J"))
157 graphicsSnapshot := screen.GraphicsSnapshot()
158 require.NotNil(t, graphicsSnapshot)
159 require.Equal(t, uint64(1), graphicsSnapshot.Usage().Assets)
160 require.Equal(t, uint64(0), graphicsSnapshot.Usage().Placements)
161
162 screen.Write(screenKittyPutAPC(1))
163 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
164 screen.Write([]byte("\x1bc"))
165 require.Nil(t, screen.GraphicsSnapshot())
166}
167
168func TestScreenSnapshotCapturesOwnedActiveGraphicsSnapshot(t *testing.T) {
169 screen := NewScreen(4, 2)
170 screen.Write(screenKittyImageAPC("T"))
171 captured := screen.Snapshot()
172 require.NotNil(t, captured.Graphics())
173 require.Equal(t, uint64(1), captured.Graphics().Usage().Placements)
174 require.Equal(t, captured.Graphics().Usage(), screen.GraphicsSnapshot().Usage(), "screen graphics snapshot returns the immutable active-scene reference")
175
176 screen.Write([]byte("\x1b[2J"))
177 require.Equal(t, uint64(1), captured.Graphics().Usage().Placements)
178 require.Equal(t, uint64(0), screen.Snapshot().Graphics().Usage().Placements)
179
180 // Graphics snapshots are independent of the text frame and have no Cell or
181 // VTH3 representation to invalidate.
182 require.Equal(t, graphics.Usage{Assets: 1, Placements: 1, EncodedBytes: 4, DecodedPixels: 1}, captured.Graphics().Usage())
183}
184
185func TestScreenOrdinaryTextPathDoesNotAllocateGraphicsState(t *testing.T) {
186 screen := NewScreen(4, 2)
187 screen.Write([]byte("text"))
188 require.Nil(t, screen.GraphicsSnapshot())
189 require.Nil(t, screen.Snapshot().Graphics())
190 require.Equal(t, "text", rowText(screen.Snapshot().Row(0)))
191}
192
193func TestScreenKittyDiscardRetainsTerminatorFraming(t *testing.T) {
194 screen := NewScreen(16, 2)
195 screen.kittyDiscard = true
196 screen.Write([]byte("discarded body"))
197 screen.Write([]byte("\x1b"))
198 screen.Write([]byte("\\after"))
199 require.Equal(t, "after ", rowText(screen.Snapshot().Row(0)))
200}
201
202func TestScreenKittyPlacementUsesCursorAndCControlsMovement(t *testing.T) {
203 screen := NewScreen(10, 4)
204 screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100, PixelHeight: 40})
205 screen.Write([]byte("\x1b[2;3H"))
206 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 16))
207 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=2,v=2,c=2,r=1,C=1;" + payload + "\x1b\\"))
208
209 snapshot := screen.GraphicsSnapshot()
210 require.NotNil(t, snapshot)
211 placements := snapshot.Placements()
212 require.Len(t, placements, 1)
213 require.Equal(t, graphics.PixelRect{X: 20, Y: 10, Width: 2, Height: 2}, placements[0].Destination())
214 require.Equal(t, 1, screen.CursorRow())
215 require.Equal(t, 2, screen.CursorCol())
216
217 screen.Write([]byte("\x1b_Ga=T,i=2,f=32,s=2,v=2,C=0;" + payload + "\x1b\\"))
218 require.Equal(t, 2, screen.CursorRow())
219 require.Equal(t, 3, screen.CursorCol())
220
221 screen.Write([]byte("\x1b[3;4H\x1b_Ga=T,i=3,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
222 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
223 placements = screen.GraphicsSnapshot().Placements()
224 require.Len(t, placements, 3)
225 require.Equal(t, graphics.PixelRect{X: 30, Y: 20, Width: 1, Height: 1}, placements[2].Destination())
226 require.Equal(t, 2, screen.CursorRow())
227 require.Equal(t, 3, screen.CursorCol())
228}