main 602baa7

events_test.go RawHistoryFolder

1package xturbo_test
2
3import (
4	"bytes"
5	"encoding/binary"
6	"testing"
7
8	"bnema.dev/xturbo"
9	"bnema.dev/xturbo/internal/xtest"
10	"bnema.dev/xturbo/render"
11	"bnema.dev/xturbo/shape"
12	"bnema.dev/xturbo/xfixes"
13	"bnema.dev/xturbo/xproto"
14)
15
16// Xvfb negotiated XFIXES as major 138, first event 87, first error 140 when the
17// goldens were captured; the BadRegion and SelectionNotify goldens confirm it.
18const (
19	xfixesMajor, xfixesEvent, xfixesError = 138, 87, 140
20	geMajor                               = 0x83 // hand-written XGE golden
21)
22
23func loadEvent(t testing.TB, name string) []byte {
24	t.Helper()
25	raw, err := xtest.LoadHex("testdata/golden/" + name + ".event.hex")
26	if err != nil {
27		t.Fatal(err)
28	}
29	return raw
30}
31
32func goldenExt() *xturbo.ExtTable {
33	var ext xturbo.ExtTable
34	ext.Set(xturbo.ExtXFixes, xfixesMajor, xfixesEvent, xfixesError)
35	ext.Set(xturbo.ExtGenericEvent, geMajor, 0, 0)
36	ext.Set(xturbo.ExtShape, 129, 64, 0)
37	return &ext
38}
39
40func decode(t testing.TB, raw []byte, ext *xturbo.ExtTable) xturbo.Event {
41	t.Helper()
42	ev, n, ok := xturbo.DecodeEvent(raw, ext)
43	if !ok || n != len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) {
44		t.Fatalf("DecodeEvent(%x) = n %d ok %v", raw, n, ok)
45	}
46	return ev
47}
48
49// Expected values follow from the capture script (second client's window at
50// 10,20 300x200, ConfigureWindow x=-5 y=40 width=640, a format-32 ClientMessage
51// with words 0x01020304*(i+1), SetSelectionOwner PRIMARY) or from raw golden
52// offsets defined by the X11 protocol, not from generated code.
53func TestEventGoldens(t *testing.T) {
54	u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) }
55	ext := goldenExt()
56	core := func(name string, kind xturbo.EventKind) ([]byte, xturbo.Event) {
57		raw := loadEvent(t, "xproto/"+name)
58		ev := decode(t, raw, ext)
59		if ev.Kind != kind || ev.Ext != xturbo.ExtCore || ev.Seq != xturbo.Seq(binary.LittleEndian.Uint16(raw[2:])) || ev.Tail != nil {
60			t.Fatalf("%s: kind %d ext %d seq %d", name, ev.Kind, ev.Ext, ev.Seq)
61		}
62		return raw, ev
63	}
64	t.Run("MapRequest", func(t *testing.T) {
65		raw, ev := core("MapRequest", xproto.KindMapRequest)
66		e := xproto.MapRequestOf(&ev)
67		if ev.SendEvent || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window() != xproto.Window(u32(raw, 8)) || e.Window()&0xffff != 1 {
68			t.Fatal("fields")
69		}
70		d := xproto.MapRequestEventData{Parent: e.Parent(), Window: e.Window()}
71		var out [32]byte
72		d.Put(&out)
73		want := [32]byte(raw)
74		want[2], want[3] = 0, 0 // Put leaves the sequence to the server
75		if out != want {
76			t.Fatalf("Put %x want %x", out, want)
77		}
78	})
79	t.Run("ConfigureRequest", func(t *testing.T) {
80		raw, ev := core("ConfigureRequest", xproto.KindConfigureRequest)
81		e := xproto.ConfigureRequestOf(&ev)
82		if e.StackMode() != xproto.StackModeAbove || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window()&0xffff != 1 ||
83			e.Sibling() != xproto.WindowNone || e.X() != -5 || e.Y() != 40 || e.Width() != 640 || e.Height() != 200 ||
84			e.BorderWidth() != 0 || e.ValueMask() != xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth {
85			t.Fatalf("fields x=%d y=%d w=%d h=%d mask=%d", e.X(), e.Y(), e.Width(), e.Height(), e.ValueMask())
86		}
87		d := xproto.ConfigureRequestEventData{StackMode: e.StackMode(), Parent: e.Parent(), Window: e.Window(), Sibling: e.Sibling(),
88			X: e.X(), Y: e.Y(), Width: e.Width(), Height: e.Height(), BorderWidth: e.BorderWidth(), ValueMask: e.ValueMask()}
89		var out [32]byte
90		d.Put(&out)
91		if !bytes.Equal(out[4:], raw[4:]) || out[0] != 23 || out[1] != raw[1] {
92			t.Fatalf("Put %x", out)
93		}
94	})
95	t.Run("PropertyNotify", func(t *testing.T) {
96		raw, ev := core("PropertyNotify", xproto.KindPropertyNotify)
97		e := xproto.PropertyNotifyOf(&ev)
98		if e.Window() != xproto.Window(u32(raw, 4)) || e.Atom() != xproto.Atom(u32(raw, 8)) || e.Atom() < 69 ||
99			e.Time() != xproto.Timestamp(u32(raw, 12)) || e.State() != xproto.PropertyNewValue {
100			t.Fatal("fields")
101		}
102	})
103	t.Run("ClientMessage", func(t *testing.T) {
104		raw, ev := core("ClientMessage", xproto.KindClientMessage)
105		e := xproto.ClientMessageOf(&ev)
106		if !ev.SendEvent || e.Format() != 32 || e.Window()&0xffff != 1 || e.Type() != xproto.Atom(u32(raw, 8)) {
107			t.Fatal("fields")
108		}
109		words := e.Data().Data32()
110		for i, w := range words {
111			if w != uint32(0x01020304*(i+1)) {
112				t.Fatalf("data32[%d] = %x", i, w)
113			}
114		}
115		d := xproto.ClientMessageEventData{Format: 32, Window: e.Window(), Type: e.Type(), Data: e.Data()}
116		var out [32]byte
117		d.Put(&out)
118		want := [32]byte(raw)
119		want[0] &^= 0x80
120		want[2], want[3] = 0, 0
121		if out != want {
122			t.Fatalf("Put %x want %x", out, want)
123		}
124	})
125	t.Run("CreateNotify", func(t *testing.T) {
126		_, ev := core("CreateNotify", xproto.KindCreateNotify)
127		e := xproto.CreateNotifyOf(&ev)
128		if e.X() != 10 || e.Y() != 20 || e.Width() != 300 || e.Height() != 200 || e.OverrideRedirect() {
129			t.Fatal("fields")
130		}
131	})
132	t.Run("Map/Unmap/DestroyNotify", func(t *testing.T) {
133		_, ev := core("MapNotify", xproto.KindMapNotify)
134		if m := xproto.MapNotifyOf(&ev); m.Window()&0xffff != 1 || m.OverrideRedirect() {
135			t.Fatal("MapNotify")
136		}
137		_, ev = core("UnmapNotify", xproto.KindUnmapNotify)
138		if u := xproto.UnmapNotifyOf(&ev); u.Window()&0xffff != 1 || u.FromConfigure() {
139			t.Fatal("UnmapNotify")
140		}
141		_, ev = core("DestroyNotify", xproto.KindDestroyNotify)
142		if d := xproto.DestroyNotifyOf(&ev); d.Window()&0xffff != 1 {
143			t.Fatal("DestroyNotify")
144		}
145	})
146	t.Run("SelectionNotify", func(t *testing.T) {
147		_, ev := core("SelectionNotify", xproto.KindSelectionNotify)
148		e := xproto.SelectionNotifyOf(&ev)
149		if e.Requestor()&0xffff != 1 || e.Selection() != xproto.AtomSecondary || e.Target() != xproto.AtomString || e.Property() != xproto.AtomNone {
150			t.Fatal("fields")
151		}
152	})
153	t.Run("xfixes/SelectionNotify", func(t *testing.T) {
154		raw := loadEvent(t, "xfixes/SelectionNotify")
155		ev := decode(t, raw, ext)
156		if xfixes.KindSelectionNotify != xturbo.EventKind(xturbo.ExtXFixes)<<8 {
157			t.Fatal("KindSelectionNotify value")
158		}
159		if ev.Kind != xfixes.KindSelectionNotify || ev.Ext != xturbo.ExtXFixes {
160			t.Fatalf("kind %x", ev.Kind)
161		}
162		e := xfixes.SelectionNotifyOf(&ev)
163		if e.Subtype() != xfixes.SelectionEventSetSelectionOwner || e.Window() != xproto.Window(u32(raw, 4)) ||
164			e.Owner()&0xffff != 1 || e.Selection() != xproto.AtomPrimary || e.Timestamp() != e.SelectionTimestamp() {
165			t.Fatal("fields")
166		}
167		// Without XFIXES negotiated the same bytes are an unknown event.
168		if ev, _, ok := xturbo.DecodeEvent(raw, nil); !ok || ev.Kind != xturbo.KindUnknown {
169			t.Fatal("nil ext must decode as unknown")
170		}
171	})
172	t.Run("KeymapNotify", func(t *testing.T) {
173		raw := loadEvent(t, "xproto/KeymapNotify")
174		ev := decode(t, raw, ext)
175		if ev.Kind != xproto.KindKeymapNotify || ev.Seq != 0 {
176			t.Fatalf("kind %d seq %d", ev.Kind, ev.Seq)
177		}
178		e := xproto.KeymapNotifyOf(&ev)
179		if !bytes.Equal(e.Keys(), raw[1:]) {
180			t.Fatal("keys")
181		}
182		d := xproto.KeymapNotifyEventData{Keys: [31]uint8(raw[1:])}
183		var out [32]byte
184		d.Put(&out)
185		if !bytes.Equal(out[:], raw) {
186			t.Fatalf("Put %x", out)
187		}
188	})
189	t.Run("GeGeneric", func(t *testing.T) {
190		raw := loadEvent(t, "xproto/GeGeneric")
191		ev := decode(t, raw, ext)
192		if ev.Kind != xturbo.EventKind(xturbo.ExtGenericEvent)<<8|0x80|1 || ev.Ext != xturbo.ExtGenericEvent || ev.Seq != 5 ||
193			!bytes.Equal(ev.Tail, raw[32:]) {
194			t.Fatalf("kind %x tail %x", ev.Kind, ev.Tail)
195		}
196		g, ok := xproto.GeGenericOf(&ev)
197		if !ok || g.Extension() != geMajor || g.Length() != 2 || g.EventType() != 1 || !bytes.Equal(g.Tail(), raw[32:]) {
198			t.Fatal("view")
199		}
200		// Unknown major: still framed so Dispatch skips the tail.
201		ev, n, ok := xturbo.DecodeEvent(raw, nil)
202		if !ok || n != 40 || ev.Kind != xturbo.KindUnknown || len(ev.Tail) != 8 {
203			t.Fatal("unknown XGE framing")
204		}
205		ev.Tail = ev.Tail[:4]
206		if _, ok := xproto.GeGenericOf(&ev); ok {
207			t.Fatal("short Tail accepted")
208		}
209	})
210}
211
212// TestEventDataPutInPlace re-encodes events into the Event.Raw their fields
213// were read from. ClientMessage's Data is a union view over Raw; KeymapNotify's
214// Keys is a slice view over Raw. Put must read every field before writing dst.
215func TestEventDataPutInPlace(t *testing.T) {
216	raw := loadEvent(t, "xproto/ClientMessage")
217	ev := decode(t, raw, nil)
218	e := xproto.ClientMessageOf(&ev)
219	d := xproto.ClientMessageEventData{Format: e.Format(), Window: e.Window(), Type: e.Type(), Data: e.Data()}
220	d.Put(&ev.Raw)
221	want := [32]byte(raw)
222	want[0] &^= 0x80
223	want[2], want[3] = 0, 0
224	if ev.Raw != want {
225		t.Fatalf("ClientMessage in place %x want %x", ev.Raw, want)
226	}
227	if n := testing.AllocsPerRun(1000, func() { d.Data = e.Data(); d.Put(&ev.Raw) }); n != 0 {
228		t.Fatalf("in-place Put allocates %v times", n)
229	}
230	raw = loadEvent(t, "xproto/KeymapNotify")
231	ev = decode(t, raw, nil)
232	keys := xproto.KeymapNotifyOf(&ev).Keys()
233	k := xproto.KeymapNotifyEventData{Keys: [31]uint8(keys)}
234	k.Put(&ev.Raw)
235	if ev.Raw != [32]byte(raw) || !bytes.Equal(keys, raw[1:]) {
236		t.Fatalf("KeymapNotify in place %x", ev.Raw)
237	}
238}
239
240func TestEventCopyAliases(t *testing.T) {
241	if xproto.KindButtonRelease != 5 || xproto.KindKeyRelease != 3 || xproto.KindLeaveNotify != 8 || xproto.KindFocusOut != 10 || xproto.KindCirculateRequest != 27 {
242		t.Fatal("eventcopy kinds")
243	}
244	var raw [32]byte
245	raw[0], raw[1] = 5, 3 // ButtonRelease, button 3
246	binary.LittleEndian.PutUint16(raw[24:], 0x100)
247	ev := decode(t, raw[:], nil)
248	var r xproto.ButtonReleaseEvent = xproto.ButtonReleaseOf(&ev)
249	var p xproto.ButtonPressEvent = r // alias: same type
250	if p.Detail() != 3 || r.EventX() != 0x100 {
251		t.Fatal("fields")
252	}
253	d := xproto.ButtonReleaseEventData{Detail: 3, EventX: 0x100}
254	var out [32]byte
255	d.Put(&out)
256	raw[2], raw[3] = 0, 0
257	if out != raw {
258		t.Fatalf("Put %x", out)
259	}
260}
261
262func TestShapeEvent(t *testing.T) {
263	var raw [32]byte
264	raw[0], raw[1] = 64, 2 // first event 64, SK Input
265	binary.LittleEndian.PutUint32(raw[4:], 0x400001)
266	binary.LittleEndian.PutUint16(raw[8:], 0xfffe)
267	raw[20] = 1 // shaped: after window(4) extents(8) server_time(4)
268	ev := decode(t, raw[:], goldenExt())
269	if ev.Kind != shape.KindNotify || ev.Ext != xturbo.ExtShape {
270		t.Fatalf("kind %x", ev.Kind)
271	}
272	e := shape.NotifyOf(&ev)
273	if e.ShapeKind() != shape.SkInput || e.AffectedWindow() != 0x400001 || e.ExtentsX() != -2 || !e.Shaped() {
274		t.Fatal("fields")
275	}
276}
277
278func TestDecodeEventRejects(t *testing.T) {
279	raw := make([]byte, 64)
280	for _, code := range []byte{0, 1, 0x80, 0x81} {
281		raw[0] = code
282		if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
283			t.Fatalf("code %#x accepted", code)
284		}
285	}
286	raw[0] = 2
287	if _, _, ok := xturbo.DecodeEvent(raw[:31], nil); ok {
288		t.Fatal("short event accepted")
289	}
290	raw[0] = 35
291	binary.LittleEndian.PutUint32(raw[4:], 9) // 36 tail bytes > 32 available
292	if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
293		t.Fatal("truncated XGE accepted")
294	}
295	binary.LittleEndian.PutUint32(raw[4:], 0xffffffff)
296	if _, _, ok := xturbo.DecodeEvent(raw, nil); ok {
297		t.Fatal("huge XGE length accepted")
298	}
299	raw[0] = 100 // extension range, nothing negotiated
300	if ev, n, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || n != 32 || ev.Kind != xturbo.KindUnknown {
301		t.Fatal("unknown extension event")
302	}
303	raw[0] = 40 // core range gap 36..63
304	if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown {
305		t.Fatal("unassigned core code")
306	}
307	raw[0] = 35 // XGE evtype >= 128 is never a known kind
308	binary.LittleEndian.PutUint32(raw[4:], 0)
309	raw[1] = geMajor
310	binary.LittleEndian.PutUint16(raw[8:], 0x80)
311	if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown {
312		t.Fatal("XGE evtype >= 128")
313	}
314}
315
316func TestExtTableReverseMaps(t *testing.T) {
317	ext := goldenExt()
318	if id, rel, ok := ext.ByEvent(xfixesEvent + 1); !ok || id != xturbo.ExtXFixes || rel != 1 {
319		t.Fatal("ByEvent CursorNotify")
320	}
321	if _, _, ok := ext.ByEvent(xfixesEvent + 2); ok {
322		t.Fatal("XFIXES declares two events")
323	}
324	if id, rel, ok := ext.ByError(xfixesError); !ok || id != xturbo.ExtXFixes || rel != xfixes.BadRegion {
325		t.Fatal("ByError")
326	}
327	if id, ok := ext.ByMajor(xfixesMajor); !ok || id != xturbo.ExtXFixes {
328		t.Fatal("ByMajor")
329	}
330	ext.Set(xturbo.ExtXFixes, 140, 90, 150) // re-negotiation drops stale entries
331	if _, ok := ext.ByMajor(xfixesMajor); ok {
332		t.Fatal("stale major")
333	}
334	if _, _, ok := ext.ByEvent(xfixesEvent); ok {
335		t.Fatal("stale event")
336	}
337	if id, _, ok := ext.ByError(150); !ok || id != xturbo.ExtXFixes {
338		t.Fatal("new error base")
339	}
340	var render xturbo.ExtTable
341	render.Set(xturbo.ExtRender, 139, 0, 142)
342	for code := uint8(142); code < 147; code++ {
343		if id, rel, ok := render.ByError(code); !ok || id != xturbo.ExtRender || render.Info(id).FirstError+rel != code {
344			t.Fatalf("render error %d", code)
345		}
346	}
347	if _, _, ok := render.ByError(147); ok {
348		t.Fatal("RENDER declares five errors")
349	}
350}
351
352func TestErrorNames(t *testing.T) {
353	if xproto.BadWindow != 3 || xproto.BadAtom != 5 || xproto.BadImplementation != 17 ||
354		xproto.ErrorName(xproto.BadWindow) != "BadWindow" || xproto.ErrorName(0) != "" || xproto.ErrorName(200) != "" {
355		t.Fatal("xproto errors")
356	}
357	if xfixes.BadRegion != 0 || xfixes.ErrorName(0) != "BadRegion" {
358		t.Fatal("xfixes errors")
359	}
360	for code, name := range []string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} {
361		if render.ErrorName(uint8(code)) != name {
362			t.Fatalf("render %d", code)
363		}
364	}
365}
366
367// mixedEvents is 10k events: core, xfixes, XGE with a tail and unknown codes.
368func mixedEvents(t testing.TB) []byte {
369	var golden [][]byte
370	for _, name := range []string{"xproto/MapRequest", "xproto/ConfigureRequest", "xproto/PropertyNotify", "xproto/ClientMessage",
371		"xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} {
372		golden = append(golden, loadEvent(t, name))
373	}
374	unknown := make([]byte, 32)
375	unknown[0] = 100
376	golden = append(golden, unknown)
377	var stream []byte
378	for i := range 10000 {
379		stream = append(stream, golden[i%len(golden)]...)
380	}
381	return stream
382}
383
384// eventSink touches the per-kind view getters a window manager reads.
385func eventSink(ev *xturbo.Event) uint64 {
386	switch ev.Kind {
387	case xproto.KindMapRequest:
388		e := xproto.MapRequestOf(ev)
389		return uint64(e.Window()) + uint64(e.Parent())
390	case xproto.KindConfigureRequest:
391		e := xproto.ConfigureRequestOf(ev)
392		return uint64(e.Window()) + uint64(e.Width()) + uint64(e.ValueMask()) + uint64(int64(e.X()))
393	case xproto.KindPropertyNotify:
394		e := xproto.PropertyNotifyOf(ev)
395		return uint64(e.Atom()) + uint64(e.State())
396	case xproto.KindClientMessage:
397		e := xproto.ClientMessageOf(ev)
398		return uint64(e.Type()) + uint64(e.Data().Data32()[0])
399	case xproto.KindKeymapNotify:
400		return uint64(len(xproto.KeymapNotifyOf(ev).Keys()))
401	case xfixes.KindSelectionNotify:
402		e := xfixes.SelectionNotifyOf(ev)
403		return uint64(e.Owner()) + uint64(e.Selection())
404	case xturbo.EventKind(xturbo.ExtGenericEvent)<<8 | 0x80 | 1:
405		if g, ok := xproto.GeGenericOf(ev); ok {
406			return uint64(len(g.Tail()))
407		}
408	}
409	return 0
410}
411
412func decodeAll(t testing.TB, stream []byte, ext *xturbo.ExtTable) (sum uint64) {
413	for len(stream) > 0 {
414		ev, n, ok := xturbo.DecodeEvent(stream, ext)
415		if !ok {
416			t.Fatal("decode")
417		}
418		sum += eventSink(&ev)
419		stream = stream[n:]
420	}
421	return sum
422}
423
424func TestAllocEvents(t *testing.T) {
425	stream := mixedEvents(t)
426	ext := goldenExt()
427	var sink uint64
428	if n := testing.AllocsPerRun(10, func() { sink += decodeAll(t, stream, ext) }); n != 0 {
429		t.Fatalf("DecodeEvent x10k + views allocate %v times", n)
430	}
431	cm := xproto.ClientMessageEventData{Format: 32, Window: 1, Type: 2}
432	cfg := xproto.ConfigureNotifyEventData{Window: 1, Width: 640, Height: 480}
433	var out [32]byte
434	if n := testing.AllocsPerRun(1000, func() { cm.Put(&out); cfg.Put(&out) }); n != 0 {
435		t.Fatalf("EventData.Put allocates %v times", n)
436	}
437	b := xturbo.NewBuf(0)
438	if n := testing.AllocsPerRun(1000, func() {
439		b.Reset()
440		cm.Put(&out)
441		xproto.SendEvent(b, false, 1, xproto.EventMaskSubstructureRedirect, &out)
442	}); n != 0 {
443		t.Fatalf("SendEvent allocates %v times", n)
444	}
445	_ = sink
446}
447
448func BenchmarkDecodeEvents10k(b *testing.B) {
449	stream := mixedEvents(b)
450	ext := goldenExt()
451	var sink uint64
452	b.SetBytes(int64(len(stream)))
453	b.ReportAllocs()
454	for b.Loop() {
455		sink += decodeAll(b, stream, ext)
456	}
457	_ = sink
458}
459
460// FuzzDecodeEvent: no panic; n is 32 or the XGE length; Raw copies the first
461// 32 bytes; Tail stays within raw; Kind is consistent with Ext. extSeed picks
462// which extensions are negotiated and where their bases lie.
463func FuzzDecodeEvent(f *testing.F) {
464	for _, name := range []string{"xproto/MapRequest", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} {
465		f.Add(loadEvent(f, name), uint32(0x8a578c83))
466	}
467	f.Add(make([]byte, 32), uint32(0))
468	f.Fuzz(func(t *testing.T, raw []byte, extSeed uint32) {
469		var ext xturbo.ExtTable
470		ext.Set(xturbo.ExtXFixes, uint8(extSeed>>24), uint8(extSeed>>16), uint8(extSeed>>8))
471		ext.Set(xturbo.ExtGenericEvent, uint8(extSeed), 0, 0)
472		ext.Set(xturbo.ExtShape, uint8(extSeed>>4), uint8(extSeed>>12)|64, 0)
473		table := &ext
474		if extSeed&1 != 0 {
475			table = nil
476		}
477		ev, n, ok := xturbo.DecodeEvent(raw, table)
478		if !ok {
479			if n != 0 {
480				t.Fatal("n on failure")
481			}
482			return
483		}
484		if len(raw) < 32 || raw[0]&0x7f < 2 || n > len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) || ev.SendEvent != (raw[0]&0x80 != 0) {
485			t.Fatalf("n %d", n)
486		}
487		if raw[0]&0x7f == 35 {
488			if n != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) || len(ev.Tail) != n-32 {
489				t.Fatal("XGE framing")
490			}
491		} else if n != 32 || ev.Tail != nil {
492			t.Fatal("fixed framing")
493		}
494		if ev.Kind != xturbo.KindUnknown && ev.Kind>>8 != xturbo.EventKind(ev.Ext) {
495			t.Fatal("Kind/Ext mismatch")
496		}
497		_ = eventSink(&ev)
498	})
499}

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 1

Tags 1

↑↓ moveenter openesc close