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