1package vt
2
3import (
4 "encoding/base64"
5 "math"
6 "strconv"
7 "testing"
8
9 "github.com/bnema/vev-vt/graphics"
10 "github.com/stretchr/testify/require"
11)
12
13func screenKittyImageAPC(action string) []byte {
14 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
15 return []byte("\x1b_Ga=" + action + ",i=1,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")
16}
17
18func screenKittyImageAPCWithID(action string, id uint64) []byte {
19 payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
20 return []byte("\x1b_Ga=" + action + ",i=" + strconv.FormatUint(id, 10) + ",f=32,s=1,v=1,C=1;" + payload + "\x1b\\")
21}
22
23func screenKittyPutAPC(id uint64) []byte {
24 return []byte("\x1b_Ga=p,i=" + strconv.FormatUint(id, 10) + ";\x1b\\")
25}
26
27func TestScreenKittyGraphicsFragmentedAPCAndResponse(t *testing.T) {
28 screen := NewScreen(16, 3)
29 var responses []string
30 screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) }
31
32 input := append([]byte("before"), screenKittyImageAPC("T")...)
33 input = append(input, []byte("after")...)
34 for _, b := range input {
35 screen.Write([]byte{b})
36 }
37
38 require.Equal(t, "beforeafter ", rowText(screen.Snapshot().Row(0)))
39 require.Equal(t, []string{"\x1b_Gi=1;OK\x1b\\"}, responses)
40 snapshot := screen.GraphicsSnapshot()
41 require.NotNil(t, snapshot)
42 require.Equal(t, uint64(1), snapshot.Usage().Assets)
43 require.Equal(t, uint64(1), snapshot.Usage().Placements)
44}
45
46func TestScreenKittyIcatDetectionResponses(t *testing.T) {
47 screen := NewScreen(80, 24)
48 var responses []string
49 screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) }
50
51 screen.Write([]byte("\x1b_Ga=q,f=24,s=1,v=1,S=3,i=1;MTIz\x1b\\"))
52 screen.Write([]byte("\x1b_Ga=q,f=24,t=t,s=1,v=1,S=47,i=2;L2Rldi9zaG0va2l0dHktdHR5LWdyYXBoaWNzLXByb3RvY29sLTMzMTU3NTkxNjc\x1b\\"))
53 screen.Write([]byte("\x1b_Ga=q,f=24,t=s,s=1,v=1,S=18,i=3;aWNhdC1aQlJCWFdNQ0lIQ0ZD\x1b\\"))
54 screen.Write([]byte("\x1b[c"))
55
56 require.Equal(t, []string{
57 "\x1b_Gi=1;OK\x1b\\",
58 "\x1b_Gi=2;ENOTSUP\x1b\\",
59 "\x1b_Gi=3;ENOTSUP\x1b\\",
60 "\x1b[?62;22c",
61 }, responses)
62}
63
64func TestScreenKittyGraphicsPrimaryAndAlternateState(t *testing.T) {
65 screen := NewScreen(8, 3)
66 screen.Write(screenKittyImageAPC("T"))
67 primary := screen.GraphicsSnapshot()
68 require.NotNil(t, primary)
69 require.Equal(t, uint64(1), primary.Usage().Placements)
70
71 screen.Write([]byte("\x1b[?1049halt"))
72 require.True(t, screen.AltScreenActive())
73 require.Nil(t, screen.GraphicsSnapshot())
74 screen.Write(screenKittyImageAPCWithID("T", 2))
75 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
76
77 screen.Write([]byte("\x1b[?1049l"))
78 require.False(t, screen.AltScreenActive())
79 require.Equal(t, primary.Usage(), screen.GraphicsSnapshot().Usage())
80}
81
82func TestScreenReenteringActiveAlternateClearsGraphicsState(t *testing.T) {
83 screen := NewScreen(8, 3)
84 screen.Write([]byte("\x1b[?1049h"))
85 screen.Write(screenKittyImageAPC("T"))
86 before := screen.GraphicsSnapshot()
87 require.NotNil(t, before)
88 require.Equal(t, uint64(1), before.Usage().Placements)
89
90 screen.Write([]byte("\x1b[?1049h"))
91 after := screen.GraphicsSnapshot()
92 require.Nil(t, after)
93 require.Equal(t, uint64(1), before.Usage().Placements)
94}
95
96func TestScreenFullClearAbortsPendingUploadAndClearsPlacements(t *testing.T) {
97 screen := NewScreen(8, 3)
98 screen.Write(screenKittyImageAPC("T"))
99 screen.Write([]byte("\x1b_Ga=T,i=2,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
100 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
101
102 screen.Write([]byte("\x1b[2J"))
103 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
104 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
105 graphicsSnapshot := screen.GraphicsSnapshot()
106 require.NotNil(t, graphicsSnapshot)
107 require.Equal(t, uint64(1), graphicsSnapshot.Usage().Assets)
108 require.Equal(t, uint64(0), graphicsSnapshot.Usage().Placements)
109}
110
111func TestScreenPendingTransmitDisplayFailsClosedAcrossErrorAndResize(t *testing.T) {
112 pending := []byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\")
113 continuation := []byte("\x1b_Gm=0;IDBA\x1b\\")
114
115 screen := NewScreen(4, 2)
116 screen.Write(pending)
117 screen.Write([]byte("\x1b_Ga=p,i=1;\x1b\\"))
118 screen.Write(continuation)
119 require.NotNil(t, screen.GraphicsSnapshot())
120 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
121
122 screen.Write(pending)
123 screen.Resize(5, 2)
124 screen.Write(continuation)
125 require.NotNil(t, screen.GraphicsSnapshot())
126 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
127}
128
129func TestScreenPendingTransmitDisplayIsScopedToScreenBuffer(t *testing.T) {
130 screen := NewScreen(4, 2)
131 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
132 screen.Write([]byte("\x1b[?1049h"))
133 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
134 require.NotNil(t, screen.GraphicsSnapshot())
135 require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
136 screen.Write([]byte("\x1b[?1049l"))
137 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
138 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
139}
140
141func TestScreenPartialPixelGeometryDoesNotInventPixelCoordinates(t *testing.T) {
142 screen := NewScreen(10, 4)
143 screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100})
144 screen.Write([]byte("\x1b[2;3H"))
145 screen.Write(screenKittyImageAPC("T"))
146 placement := screen.GraphicsSnapshot().Placements()[0]
147 require.Equal(t, graphics.PixelRect{X: 0, Y: 0, Width: 1, Height: 1}, placement.Destination())
148}
149
150func TestScreenKittyPlacementSeparatesCursorOriginCropAndPixelOffset(t *testing.T) {
151 screen := NewScreen(10, 4)
152 screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100, PixelHeight: 40})
153 screen.Write([]byte("\x1b[2;3H"))
154 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 64))
155 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\\"))
156
157 placement := screen.GraphicsSnapshot().Placements()[0]
158 require.Equal(t, graphics.PixelRect{X: 1, Y: 1, Width: 2, Height: 2}, placement.Source())
159 require.Equal(t, graphics.PixelRect{X: 23, Y: 14, Width: 2, Height: 2}, placement.Destination())
160}
161
162func TestScreenKittyGraphicsOrdinaryResetClearScrollAndResize(t *testing.T) {
163 screen := NewScreen(3, 2)
164 screen.Write(screenKittyImageAPC("T"))
165
166 // Printable text and a scroll do not guess at image anchoring. Existing
167 // placements remain available until a graphics-aware policy is supplied.
168 screen.Write([]byte("abcdefg"))
169 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
170 screen.Resize(4, 3)
171 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
172
173 // A full ordinary erase removes visible placements while retaining the
174 // protocol image mapping for a later explicit put.
175 screen.Write([]byte("\x1b[2J"))
176 graphicsSnapshot := screen.GraphicsSnapshot()
177 require.NotNil(t, graphicsSnapshot)
178 require.Equal(t, uint64(1), graphicsSnapshot.Usage().Assets)
179 require.Equal(t, uint64(0), graphicsSnapshot.Usage().Placements)
180
181 screen.Write(screenKittyPutAPC(1))
182 require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
183 screen.Write([]byte("\x1bc"))
184 require.Nil(t, screen.GraphicsSnapshot())
185}
186
187func TestScreenSnapshotCapturesOwnedActiveGraphicsSnapshot(t *testing.T) {
188 screen := NewScreen(4, 2)
189 screen.Write(screenKittyImageAPC("T"))
190 captured := screen.Snapshot()
191 require.NotNil(t, captured.Graphics())
192 require.Equal(t, uint64(1), captured.Graphics().Usage().Placements)
193 require.Equal(t, captured.Graphics().Usage(), screen.GraphicsSnapshot().Usage(), "screen graphics snapshot returns the immutable active-scene reference")
194
195 screen.Write([]byte("\x1b[2J"))
196 require.Equal(t, uint64(1), captured.Graphics().Usage().Placements)
197 require.Equal(t, uint64(0), screen.Snapshot().Graphics().Usage().Placements)
198
199 // Graphics snapshots are independent of the text frame and have no Cell or
200 // VTH3 representation to invalidate.
201 require.Equal(t, graphics.Usage{Assets: 1, Placements: 1, EncodedBytes: 4, DecodedPixels: 1}, captured.Graphics().Usage())
202}
203
204func TestScreenOrdinaryTextPathDoesNotAllocateGraphicsState(t *testing.T) {
205 screen := NewScreen(4, 2)
206 screen.Write([]byte("text"))
207 require.Nil(t, screen.GraphicsSnapshot())
208 require.Nil(t, screen.Snapshot().Graphics())
209 require.Equal(t, "text", rowText(screen.Snapshot().Row(0)))
210}
211
212func TestScreenKittyDiscardRetainsTerminatorFraming(t *testing.T) {
213 screen := NewScreen(16, 2)
214 screen.kittyDiscard = true
215 screen.Write([]byte("discarded body"))
216 screen.Write([]byte("\x1b"))
217 screen.Write([]byte("\\after"))
218 require.Equal(t, "after ", rowText(screen.Snapshot().Row(0)))
219}
220
221func TestScreenKittyPlacementUsesCursorAndCControlsMovement(t *testing.T) {
222 screen := NewScreen(10, 4)
223 screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100, PixelHeight: 40})
224 screen.Write([]byte("\x1b[2;3H"))
225 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 16))
226 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=2,v=2,c=2,r=1,C=1;" + payload + "\x1b\\"))
227
228 snapshot := screen.GraphicsSnapshot()
229 require.NotNil(t, snapshot)
230 placements := snapshot.Placements()
231 require.Len(t, placements, 1)
232 require.Equal(t, graphics.PixelRect{X: 20, Y: 10, Width: 2, Height: 2}, placements[0].Destination())
233 require.Equal(t, 1, screen.CursorRow())
234 require.Equal(t, 2, screen.CursorCol())
235
236 screen.Write([]byte("\x1b_Ga=T,i=2,f=32,s=2,v=2,C=0;" + payload + "\x1b\\"))
237 require.Equal(t, 1, screen.CursorRow())
238 require.Equal(t, 3, screen.CursorCol())
239
240 screen.Write([]byte("\x1b[3;4H\x1b_Ga=T,i=3,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
241 screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
242 placements = screen.GraphicsSnapshot().Placements()
243 require.Len(t, placements, 3)
244 require.Equal(t, graphics.PixelRect{X: 30, Y: 20, Width: 1, Height: 1}, placements[2].Destination())
245 require.Equal(t, 2, screen.CursorRow())
246 require.Equal(t, 3, screen.CursorCol())
247}
248
249func TestScreenKittyPlacementNormalizesDeferredWrapCursor(t *testing.T) {
250 screen := NewScreen(3, 3)
251 screen.SetGeometry(Geometry{Cols: 3, Rows: 3, PixelWidth: 30, PixelHeight: 30})
252 screen.Write([]byte("abc"))
253 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 4))
254
255 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,C=1;" + payload + "\x1b\\"))
256
257 placement := screen.GraphicsSnapshot().Placements()[0]
258 require.Equal(t, graphics.PixelRect{X: 20, Width: 1, Height: 1}, placement.Destination())
259}
260
261func TestScreenKittyNaturalImageHeightMovesCursorBelowPlacement(t *testing.T) {
262 screen := NewScreen(10, 5)
263 screen.SetGeometry(Geometry{Cols: 10, Rows: 5, PixelWidth: 100, PixelHeight: 50})
264 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 15*25*4))
265
266 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=15,v=25;" + payload + "\x1b\\"))
267
268 // A 15x25 image occupies 2x3 cells. Kitty leaves the cursor immediately to
269 // the right of the placement and on its final occupied row.
270 require.Equal(t, 2, screen.CursorRow())
271 require.Equal(t, 2, screen.CursorCol())
272}
273
274func TestScreenKittyPlacementScrollsWithTallImageCursorAdvance(t *testing.T) {
275 screen := NewScreen(10, 5)
276 screen.SetGeometry(Geometry{Cols: 10, Rows: 5, PixelWidth: 100, PixelHeight: 50})
277 screen.Write([]byte("\x1b[2;1H"))
278 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 15*45*4))
279
280 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=15,v=45;" + payload + "\x1b\\"))
281
282 placement := screen.GraphicsSnapshot().Placements()[0]
283 require.Equal(t, graphics.PixelRect{X: 0, Y: 0, Width: 15, Height: 45}, placement.Destination(), "the placement must scroll with the text buffer")
284 require.Equal(t, 4, screen.CursorRow())
285 require.Equal(t, 2, screen.CursorCol())
286}
287
288func TestScreenKittyPlacementScrollsOnlyWhenContainedInRegion(t *testing.T) {
289 screen := NewScreen(10, 6)
290 screen.SetGeometry(Geometry{Cols: 10, Rows: 6, PixelWidth: 100, PixelHeight: 60})
291 screen.Write([]byte("\x1b[2;5r"))
292
293 insidePayload := base64.RawStdEncoding.EncodeToString(make([]byte, 1*10*4))
294 screen.Write([]byte("\x1b[3;1H\x1b_Ga=T,i=1,f=32,s=1,v=10,C=1;" + insidePayload + "\x1b\\"))
295 straddlingPayload := base64.RawStdEncoding.EncodeToString(make([]byte, 1*20*4))
296 screen.Write([]byte("\x1b[1;1H\x1b_Ga=T,i=2,f=32,s=1,v=20,Y=5,C=1;" + straddlingPayload + "\x1b\\"))
297
298 screen.Write([]byte("\x1b[S"))
299
300 placements := screen.GraphicsSnapshot().Placements()
301 require.Len(t, placements, 2)
302 require.Equal(t, graphics.PixelRect{X: 0, Y: 10, Width: 1, Height: 10}, placements[0].Destination(), "a placement contained in the margin must scroll")
303 require.Equal(t, graphics.PixelRect{X: 0, Y: 5, Width: 1, Height: 20}, placements[1].Destination(), "a placement crossing the margin must remain fixed")
304}
305
306func TestScreenKittyPlacementScrollSkipsBottomEdgeOverflow(t *testing.T) {
307 screen := NewScreen(1, 2)
308 screen.SetGeometry(Geometry{Cols: 1, Rows: 2, PixelWidth: 10, PixelHeight: 20})
309 payload := base64.RawStdEncoding.EncodeToString(make([]byte, 1*10*4))
310 screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=10,C=1;" + payload + "\x1b\\"))
311 placement := screen.GraphicsSnapshot().Placements()[0]
312 nearLimit := graphics.PixelRect{X: 0, Y: math.MaxInt64 - 11, Width: 1, Height: 10}
313 require.NoError(t, screen.graphics.scene.UpdatePlacement(placement.ID(), graphics.PlacementSpec{Destination: nearLimit}))
314
315 require.NotPanics(t, func() { screen.Write([]byte("\x1b[T")) })
316
317 placement = screen.GraphicsSnapshot().Placements()[0]
318 require.Equal(t, nearLimit, placement.Destination())
319}