v0.7.1 ff50ee4

screen_graphics_test.go RawHistoryFolder

1package vt
2
3import (
4	"encoding/base64"
5	"math"
6	"runtime"
7	"strconv"
8	"strings"
9	"testing"
10
11	"github.com/bnema/vev-vt/graphics"
12	"github.com/stretchr/testify/require"
13)
14
15func screenKittyImageAPC(action string) []byte {
16	payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
17	return []byte("\x1b_Ga=" + action + ",i=1,f=32,s=1,v=1,C=1;" + payload + "\x1b\\")
18}
19
20func screenKittyImageAPCWithID(action string, id uint64) []byte {
21	payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
22	return []byte("\x1b_Ga=" + action + ",i=" + strconv.FormatUint(id, 10) + ",f=32,s=1,v=1,C=1;" + payload + "\x1b\\")
23}
24
25func screenKittyPutAPC(id uint64) []byte {
26	return []byte("\x1b_Ga=p,i=" + strconv.FormatUint(id, 10) + ";\x1b\\")
27}
28
29func TestScreenKittyGraphicsFragmentedAPCAndResponse(t *testing.T) {
30	screen := NewScreen(16, 3)
31	var responses []string
32	screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) }
33
34	input := append([]byte("before"), screenKittyImageAPC("T")...)
35	input = append(input, []byte("after")...)
36	for _, b := range input {
37		screen.Write([]byte{b})
38	}
39
40	require.Equal(t, "beforeafter     ", rowText(screen.Snapshot().Row(0)))
41	require.Equal(t, []string{"\x1b_Gi=1;OK\x1b\\"}, responses)
42	snapshot := screen.GraphicsSnapshot()
43	require.NotNil(t, snapshot)
44	require.Equal(t, uint64(1), snapshot.Usage().Assets)
45	require.Equal(t, uint64(1), snapshot.Usage().Placements)
46}
47
48func TestScreenKittyGraphicsWithoutImageIDDoesNotRespond(t *testing.T) {
49	screen := NewScreen(16, 3)
50	var responses []string
51	screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) }
52	payload := base64.RawStdEncoding.EncodeToString([]byte{1, 2, 3, 4})
53
54	screen.Write([]byte("\x1b_Ga=T,f=32,s=1,v=1,C=1;" + payload + "\x1b\\"))
55	screen.Write([]byte("\x1b_Ga=T,i=0,f=32,s=1,v=1,C=1;" + payload + "\x1b\\"))
56	screen.Write([]byte("\x1b_Ga=q,f=24,s=1,v=1;invalid\x1b\\"))
57
58	require.Empty(t, responses)
59	require.Equal(t, uint64(2), screen.GraphicsSnapshot().Usage().Placements)
60}
61
62func TestScreenKittyIcatDetectionResponses(t *testing.T) {
63	screen := NewScreen(80, 24)
64	var responses []string
65	screen.OnResponse = func(response []byte) { responses = append(responses, string(response)) }
66
67	screen.Write([]byte("\x1b_Ga=q,f=24,s=1,v=1,S=3,i=1;MTIz\x1b\\"))
68	screen.Write([]byte("\x1b_Ga=q,f=24,t=t,s=1,v=1,S=47,i=2;L2Rldi9zaG0va2l0dHktdHR5LWdyYXBoaWNzLXByb3RvY29sLTMzMTU3NTkxNjc\x1b\\"))
69	screen.Write([]byte("\x1b_Ga=q,f=24,t=s,s=1,v=1,S=18,i=3;aWNhdC1aQlJCWFdNQ0lIQ0ZD\x1b\\"))
70	screen.Write([]byte("\x1b[c"))
71
72	require.Equal(t, []string{
73		"\x1b_Gi=1;OK\x1b\\",
74		"\x1b_Gi=2;ENOTSUP\x1b\\",
75		"\x1b_Gi=3;ENOTSUP\x1b\\",
76		"\x1b[?62;22c",
77	}, responses)
78}
79
80func TestScreenKittyGraphicsPrimaryAndAlternateState(t *testing.T) {
81	screen := NewScreen(8, 3)
82	screen.Write(screenKittyImageAPC("T"))
83	primary := screen.GraphicsSnapshot()
84	require.NotNil(t, primary)
85	require.Equal(t, uint64(1), primary.Usage().Placements)
86
87	screen.Write([]byte("\x1b[?1049halt"))
88	require.True(t, screen.AltScreenActive())
89	require.Nil(t, screen.GraphicsSnapshot())
90	screen.Write(screenKittyImageAPCWithID("T", 2))
91	require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
92
93	screen.Write([]byte("\x1b[?1049l"))
94	require.False(t, screen.AltScreenActive())
95	require.Equal(t, primary.Usage(), screen.GraphicsSnapshot().Usage())
96}
97
98func TestScreenReenteringActiveAlternateClearsGraphicsState(t *testing.T) {
99	screen := NewScreen(8, 3)
100	screen.Write([]byte("\x1b[?1049h"))
101	screen.Write(screenKittyImageAPC("T"))
102	before := screen.GraphicsSnapshot()
103	require.NotNil(t, before)
104	require.Equal(t, uint64(1), before.Usage().Placements)
105
106	screen.Write([]byte("\x1b[?1049h"))
107	after := screen.GraphicsSnapshot()
108	require.Nil(t, after)
109	require.Equal(t, uint64(1), before.Usage().Placements)
110}
111
112func TestScreenFullClearAbortsPendingUploadAndClearsPlacements(t *testing.T) {
113	screen := NewScreen(8, 3)
114	screen.Write(screenKittyImageAPC("T"))
115	screen.Write([]byte("\x1b_Ga=T,i=2,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
116	require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
117
118	screen.Write([]byte("\x1b[2J"))
119	require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
120	screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
121	graphicsSnapshot := screen.GraphicsSnapshot()
122	require.NotNil(t, graphicsSnapshot)
123	require.Equal(t, uint64(1), graphicsSnapshot.Usage().Assets)
124	require.Equal(t, uint64(0), graphicsSnapshot.Usage().Placements)
125}
126
127func TestScreenPendingTransmitDisplayFailsClosedAcrossErrorAndResize(t *testing.T) {
128	pending := []byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\")
129	continuation := []byte("\x1b_Gm=0;IDBA\x1b\\")
130
131	screen := NewScreen(4, 2)
132	screen.Write(pending)
133	screen.Write([]byte("\x1b_Ga=p,i=1;\x1b\\"))
134	screen.Write(continuation)
135	require.NotNil(t, screen.GraphicsSnapshot())
136	require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
137
138	screen.Write(pending)
139	screen.Resize(5, 2)
140	screen.Write(continuation)
141	require.NotNil(t, screen.GraphicsSnapshot())
142	require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
143}
144
145func TestScreenPendingTransmitDisplayIsScopedToScreenBuffer(t *testing.T) {
146	screen := NewScreen(4, 2)
147	screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
148	screen.Write([]byte("\x1b[?1049h"))
149	screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
150	require.NotNil(t, screen.GraphicsSnapshot())
151	require.Equal(t, uint64(0), screen.GraphicsSnapshot().Usage().Placements)
152	screen.Write([]byte("\x1b[?1049l"))
153	screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
154	require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
155}
156
157func TestScreenPartialPixelGeometryDoesNotInventPixelCoordinates(t *testing.T) {
158	screen := NewScreen(10, 4)
159	screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100})
160	screen.Write([]byte("\x1b[2;3H"))
161	screen.Write(screenKittyImageAPC("T"))
162	placement := screen.GraphicsSnapshot().Placements()[0]
163	require.Equal(t, graphics.PixelRect{X: 0, Y: 0, Width: 1, Height: 1}, placement.Destination())
164}
165
166func TestScreenKittyPlacementSeparatesCursorOriginCropAndPixelOffset(t *testing.T) {
167	screen := NewScreen(10, 4)
168	screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100, PixelHeight: 40})
169	screen.Write([]byte("\x1b[2;3H"))
170	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 64))
171	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\\"))
172
173	placement := screen.GraphicsSnapshot().Placements()[0]
174	require.Equal(t, graphics.PixelRect{X: 1, Y: 1, Width: 2, Height: 2}, placement.Source())
175	require.Equal(t, graphics.PixelRect{X: 23, Y: 14, Width: 2, Height: 2}, placement.Destination())
176}
177
178func TestScreenKittyGraphicsOrdinaryResetClearScrollAndResize(t *testing.T) {
179	screen := NewScreen(3, 2)
180	screen.Write(screenKittyImageAPC("T"))
181
182	// Printable text and a scroll do not guess at image anchoring. Existing
183	// placements remain available until a graphics-aware policy is supplied.
184	screen.Write([]byte("abcdefg"))
185	require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
186	screen.Resize(4, 3)
187	require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
188
189	// A full ordinary erase removes visible placements while retaining the
190	// protocol image mapping for a later explicit put.
191	screen.Write([]byte("\x1b[2J"))
192	graphicsSnapshot := screen.GraphicsSnapshot()
193	require.NotNil(t, graphicsSnapshot)
194	require.Equal(t, uint64(1), graphicsSnapshot.Usage().Assets)
195	require.Equal(t, uint64(0), graphicsSnapshot.Usage().Placements)
196
197	screen.Write(screenKittyPutAPC(1))
198	require.Equal(t, uint64(1), screen.GraphicsSnapshot().Usage().Placements)
199	screen.Write([]byte("\x1bc"))
200	require.Nil(t, screen.GraphicsSnapshot())
201}
202
203func TestScreenSnapshotCapturesOwnedActiveGraphicsSnapshot(t *testing.T) {
204	screen := NewScreen(4, 2)
205	screen.Write(screenKittyImageAPC("T"))
206	captured := screen.Snapshot()
207	require.NotNil(t, captured.Graphics())
208	require.Equal(t, uint64(1), captured.Graphics().Usage().Placements)
209	require.Equal(t, captured.Graphics().Usage(), screen.GraphicsSnapshot().Usage(), "screen graphics snapshot returns the immutable active-scene reference")
210
211	screen.Write([]byte("\x1b[2J"))
212	require.Equal(t, uint64(1), captured.Graphics().Usage().Placements)
213	require.Equal(t, uint64(0), screen.Snapshot().Graphics().Usage().Placements)
214
215	// Graphics snapshots are independent of the text frame and have no Cell or
216	// VTH3 representation to invalidate.
217	require.Equal(t, graphics.Usage{Assets: 1, Placements: 1, EncodedBytes: 4, DecodedPixels: 1}, captured.Graphics().Usage())
218}
219
220func TestScreenOrdinaryTextPathDoesNotAllocateGraphicsState(t *testing.T) {
221	screen := NewScreen(4, 2)
222	screen.Write([]byte("text"))
223	require.Nil(t, screen.GraphicsSnapshot())
224	require.Nil(t, screen.Snapshot().Graphics())
225	require.Equal(t, "text", rowText(screen.Snapshot().Row(0)))
226}
227
228func TestScreenKittyDiscardRetainsTerminatorFraming(t *testing.T) {
229	screen := NewScreen(16, 2)
230	screen.kittyDiscard = true
231	screen.Write([]byte("discarded body"))
232	screen.Write([]byte("\x1b"))
233	screen.Write([]byte("\\after"))
234	require.Equal(t, "after           ", rowText(screen.Snapshot().Row(0)))
235}
236
237func TestScreenKittyPlacementUsesCursorAndCControlsMovement(t *testing.T) {
238	screen := NewScreen(10, 4)
239	screen.SetGeometry(Geometry{Cols: 10, Rows: 4, PixelWidth: 100, PixelHeight: 40})
240	screen.Write([]byte("\x1b[2;3H"))
241	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 16))
242	screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=2,v=2,c=2,r=1,C=1;" + payload + "\x1b\\"))
243
244	snapshot := screen.GraphicsSnapshot()
245	require.NotNil(t, snapshot)
246	placements := snapshot.Placements()
247	require.Len(t, placements, 1)
248	require.Equal(t, graphics.PixelRect{X: 20, Y: 10, Width: 2, Height: 2}, placements[0].Destination())
249	require.Equal(t, 1, screen.CursorRow())
250	require.Equal(t, 2, screen.CursorCol())
251
252	screen.Write([]byte("\x1b_Ga=T,i=2,f=32,s=2,v=2,C=0;" + payload + "\x1b\\"))
253	require.Equal(t, 1, screen.CursorRow())
254	require.Equal(t, 3, screen.CursorCol())
255
256	screen.Write([]byte("\x1b[3;4H\x1b_Ga=T,i=3,f=32,s=1,v=1,m=1,C=1;AQ\x1b\\"))
257	screen.Write([]byte("\x1b_Gm=0;IDBA\x1b\\"))
258	placements = screen.GraphicsSnapshot().Placements()
259	require.Len(t, placements, 3)
260	require.Equal(t, graphics.PixelRect{X: 30, Y: 20, Width: 1, Height: 1}, placements[2].Destination())
261	require.Equal(t, 2, screen.CursorRow())
262	require.Equal(t, 3, screen.CursorCol())
263}
264
265func TestScreenKittyPlacementNormalizesDeferredWrapCursor(t *testing.T) {
266	screen := NewScreen(3, 3)
267	screen.SetGeometry(Geometry{Cols: 3, Rows: 3, PixelWidth: 30, PixelHeight: 30})
268	screen.Write([]byte("abc"))
269	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 4))
270
271	screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=1,C=1;" + payload + "\x1b\\"))
272
273	placement := screen.GraphicsSnapshot().Placements()[0]
274	require.Equal(t, graphics.PixelRect{X: 20, Width: 1, Height: 1}, placement.Destination())
275}
276
277func TestScreenKittyNaturalImageHeightMovesCursorBelowPlacement(t *testing.T) {
278	screen := NewScreen(10, 5)
279	screen.SetGeometry(Geometry{Cols: 10, Rows: 5, PixelWidth: 100, PixelHeight: 50})
280	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 15*25*4))
281
282	screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=15,v=25;" + payload + "\x1b\\"))
283
284	// A 15x25 image occupies 2x3 cells. Kitty leaves the cursor immediately to
285	// the right of the placement and on its final occupied row.
286	require.Equal(t, 2, screen.CursorRow())
287	require.Equal(t, 2, screen.CursorCol())
288}
289
290func TestScreenKittyPlacementScrollsWithTallImageCursorAdvance(t *testing.T) {
291	screen := NewScreen(10, 5)
292	screen.SetGeometry(Geometry{Cols: 10, Rows: 5, PixelWidth: 100, PixelHeight: 50})
293	screen.Write([]byte("\x1b[2;1H"))
294	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 15*45*4))
295
296	screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=15,v=45;" + payload + "\x1b\\"))
297
298	placement := screen.GraphicsSnapshot().Placements()[0]
299	require.Equal(t, graphics.PixelRect{X: 0, Y: 0, Width: 15, Height: 45}, placement.Destination(), "the placement must scroll with the text buffer")
300	require.Equal(t, 4, screen.CursorRow())
301	require.Equal(t, 2, screen.CursorCol())
302}
303
304func TestScreenKittyPlacementScrollsOnlyWhenContainedInRegion(t *testing.T) {
305	screen := NewScreen(10, 6)
306	screen.SetGeometry(Geometry{Cols: 10, Rows: 6, PixelWidth: 100, PixelHeight: 60})
307	screen.Write([]byte("\x1b[2;5r"))
308
309	insidePayload := base64.RawStdEncoding.EncodeToString(make([]byte, 1*10*4))
310	screen.Write([]byte("\x1b[3;1H\x1b_Ga=T,i=1,f=32,s=1,v=10,C=1;" + insidePayload + "\x1b\\"))
311	straddlingPayload := base64.RawStdEncoding.EncodeToString(make([]byte, 1*20*4))
312	screen.Write([]byte("\x1b[1;1H\x1b_Ga=T,i=2,f=32,s=1,v=20,Y=5,C=1;" + straddlingPayload + "\x1b\\"))
313
314	screen.Write([]byte("\x1b[S"))
315
316	placements := screen.GraphicsSnapshot().Placements()
317	require.Len(t, placements, 2)
318	require.Equal(t, graphics.PixelRect{X: 0, Y: 10, Width: 1, Height: 10}, placements[0].Destination(), "a placement contained in the margin must scroll")
319	require.Equal(t, graphics.PixelRect{X: 0, Y: 5, Width: 1, Height: 20}, placements[1].Destination(), "a placement crossing the margin must remain fixed")
320}
321
322func TestScreenKittyPlacementScrollSkipsBottomEdgeOverflow(t *testing.T) {
323	screen := NewScreen(1, 2)
324	screen.SetGeometry(Geometry{Cols: 1, Rows: 2, PixelWidth: 10, PixelHeight: 20})
325	payload := base64.RawStdEncoding.EncodeToString(make([]byte, 1*10*4))
326	screen.Write([]byte("\x1b_Ga=T,i=1,f=32,s=1,v=10,C=1;" + payload + "\x1b\\"))
327	placement := screen.GraphicsSnapshot().Placements()[0]
328	nearLimit := graphics.PixelRect{X: 0, Y: math.MaxInt64 - 11, Width: 1, Height: 10}
329	require.NoError(t, screen.graphics.scene.UpdatePlacement(placement.ID(), graphics.PlacementSpec{Destination: nearLimit}))
330
331	require.NotPanics(t, func() { screen.Write([]byte("\x1b[T")) })
332
333	placement = screen.GraphicsSnapshot().Placements()[0]
334	require.Equal(t, nearLimit, placement.Destination())
335}
336
337// Fragmented APCs reuse the Screen's escape buffer, so retained graphics state
338// (assets and pending chunked uploads) must not alias it.
339func TestScreenKittyFragmentedAPCsDoNotAliasEscapeBuffer(t *testing.T) {
340	screen := NewScreen(16, 3)
341	writeByteWise := func(data []byte) {
342		for i := range data {
343			screen.Write(data[i : i+1])
344		}
345	}
346	enc := func(b ...byte) string { return base64.StdEncoding.EncodeToString(b) }
347	writeByteWise([]byte("\x1b_Ga=t,i=1,f=32,s=1,v=1;" + enc(1, 2, 3, 4) + "\x1b\\"))
348	// Chunked upload interleaved with other escape sequences reusing the buffer.
349	writeByteWise([]byte("\x1b_Ga=t,i=2,f=32,s=1,v=1,m=1;" + enc(9, 9, 9, 9)[:4] + "\x1b\\"))
350	writeByteWise([]byte("\x1b[1mtext\x1b[0m"))
351	writeByteWise([]byte("\x1b_Gm=0;" + enc(9, 9, 9, 9)[4:] + "\x1b\\"))
352
353	snapshot := screen.GraphicsSnapshot()
354	require.NotNil(t, snapshot)
355	var got [][]byte
356	for _, asset := range snapshot.Assets() {
357		got = append(got, asset.Bytes())
358	}
359	require.ElementsMatch(t, [][]byte{{1, 2, 3, 4}, {9, 9, 9, 9}}, got)
360}
361
362func screenAssetBytes(t *testing.T, screen *Screen) [][]byte {
363	t.Helper()
364	snapshot := screen.GraphicsSnapshot()
365	require.NotNil(t, snapshot)
366	var got [][]byte
367	for _, asset := range snapshot.Assets() {
368		got = append(got, asset.Bytes())
369	}
370	return got
371}
372
373// A callback may call Write re-entrantly; the inner Write must not clobber the
374// reusable combine buffer the outer Write is still parsing.
375func TestScreenWriteReentrantFromCallbackKeepsOuterStreamIntact(t *testing.T) {
376	t.Run("split CSI then DA reply writes text", func(t *testing.T) {
377		screen := NewScreen(40, 3)
378		fired := 0
379		screen.OnResponse = func([]byte) {
380			fired++
381			// Leave a partial escape, then complete it: the second inner
382			// Write takes the combine path while the outer Write is mid-parse.
383			screen.Write([]byte("\x1b["))
384			screen.Write([]byte("1mZZZZZZZZZZ"))
385		}
386		screen.Write([]byte("\x1b[1"))
387		screen.Write([]byte("m\x1b[cBCDEFGH"))
388		require.Equal(t, 1, fired)
389		require.Equal(t, "ZZZZZZZZZZBCDEFGH", strings.TrimRight(rowText(screen.Snapshot().Row(0)), " "))
390	})
391	t.Run("split APC response writes upload", func(t *testing.T) {
392		screen := NewScreen(40, 3)
393		inner := false
394		screen.OnResponse = func([]byte) {
395			if inner {
396				return
397			}
398			inner = true
399			for _, b := range []byte("\x1b_Ga=t,f=32,s=1,v=1;CQkJCQ==\x1b\\") {
400				screen.Write([]byte{b})
401			}
402		}
403		screen.Write([]byte("\x1b_Ga=t,i=1,f=32,s=1,v=1;AQID"))
404		screen.Write([]byte("BA==\x1b\\XYZ"))
405		require.Equal(t, "XYZ", strings.TrimRight(rowText(screen.Snapshot().Row(0)), " "))
406		require.ElementsMatch(t, [][]byte{{1, 2, 3, 4}, {9, 9, 9, 9}}, screenAssetBytes(t, screen))
407	})
408}
409
410// Base64 payloads padded with CR/LF decode into a buffer sized for the raw
411// input. The scene must not retain that slack beyond its accounted bytes,
412// otherwise untrusted output could hold memory past MaxEncodedBytes.
413func TestScreenKittyNewlinePaddedPayloadRetainsOnlyAccountedBytes(t *testing.T) {
414	const uploads, padding = 16, 1 << 20
415	screen := NewScreen(16, 3)
416	pad := strings.Repeat("\n", padding)
417	var before, after runtime.MemStats
418	runtime.GC()
419	runtime.ReadMemStats(&before)
420	for i := range uploads {
421		screen.Write([]byte("\x1b_Ga=t,i=" + strconv.Itoa(i+1) + ",f=32,s=1,v=1;AQID" + pad + "BA==\x1b\\"))
422	}
423	runtime.GC()
424	runtime.ReadMemStats(&after)
425	require.Equal(t, uint64(4*uploads), screen.graphics.scene.Usage().EncodedBytes)
426	for _, asset := range screenAssetBytes(t, screen) {
427		require.Equal(t, []byte{1, 2, 3, 4}, asset)
428	}
429	// Untrimmed decode buffers would retain about 0.75 MiB per upload.
430	growth := int64(after.HeapAlloc) - int64(before.HeapAlloc)
431	require.Less(t, growth, int64(4<<20), "heap retained by %d padded uploads", uploads)
432	runtime.KeepAlive(screen)
433}
434
435// The Screen borrows the caller's Write buffer while parsing; nothing it
436// retains may alias that buffer once Write returns.
437func TestScreenKittyGraphicsDoesNotAliasCallerWriteBuffer(t *testing.T) {
438	scribble := func(b []byte) {
439		for i := range b {
440			b[i] = 0xff
441		}
442	}
443	t.Run("single-shot transmit and display", func(t *testing.T) {
444		screen := NewScreen(16, 3)
445		buf := []byte("\x1b_Ga=T,i=1,f=32,s=1,v=1;AQIDBA==\x1b\\")
446		screen.Write(buf)
447		scribble(buf)
448		require.Equal(t, [][]byte{{1, 2, 3, 4}}, screenAssetBytes(t, screen))
449	})
450	t.Run("first and final chunk in one write", func(t *testing.T) {
451		screen := NewScreen(16, 3)
452		buf := []byte("\x1b_Ga=t,i=1,f=32,s=1,v=1,m=1;AQID\x1b\\\x1b_Gm=0;BA==\x1b\\")
453		screen.Write(buf)
454		scribble(buf)
455		require.Equal(t, [][]byte{{1, 2, 3, 4}}, screenAssetBytes(t, screen))
456	})
457	t.Run("first chunk write then buffer reuse before final chunk", func(t *testing.T) {
458		screen := NewScreen(16, 3)
459		buf := []byte("\x1b_Ga=t,i=1,f=32,s=1,v=1,m=1;AQID\x1b\\")
460		screen.Write(buf)
461		scribble(buf)
462		screen.Write([]byte("\x1b_Gm=0;BA==\x1b\\"))
463		require.Equal(t, [][]byte{{1, 2, 3, 4}}, screenAssetBytes(t, screen))
464	})
465}

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 2

Tags 13

↑↓ moveenter openesc close