v0.4.0 0b9cfda

screen_graphics_test.go RawHistoryFolder

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}

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