v0.5.1 8e4df5c

screen_graphics_test.go RawHistoryFolder

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}

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