v0.4.0 b6be3a8

protocol_test.go RawHistoryFolder

1package wayland
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"os"
8	"path/filepath"
9	"testing"
10	"time"
11
12	"github.com/bnema/neferwl/internal/ports"
13
14	"github.com/bnema/go-wayland-bindings/server/wayland"
15	"github.com/bnema/go-wayland-bindings/server/xdgshell"
16	"github.com/bnema/wlturbo"
17	"golang.org/x/sys/unix"
18)
19
20func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display {
21	t.Helper()
22	c, err := wlturbo.Connect(filepath.Join(dir, s.SocketName()))
23	if err != nil {
24		t.Fatal(err)
25	}
26	t.Cleanup(func() { _ = c.Close(); forgetWireClient(c) })
27	if err := c.Roundtrip(); err != nil {
28		t.Fatal(err)
29	}
30	return c
31}
32
33func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {
34	t.Helper()
35	return bindVersion(t, c, iface, 1)
36}
37
38func bindVersion(t *testing.T, c *wlturbo.Display, iface string, version uint32) uint32 {
39	t.Helper()
40	g, ok := c.Registry().FindGlobal(iface)
41	if !ok {
42		t.Fatalf("missing %s", iface)
43	}
44	id, err := bindWireID(c, g.Name, g.Interface, version)
45	if err != nil {
46		t.Fatal(err)
47	}
48	return id
49}
50
51func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, args ...any) {
52	t.Helper()
53	if err := wireRequest(c, id, uint16(op), nil, args...); err != nil {
54		t.Fatal(err)
55	}
56}
57
58func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) {
59	t.Helper()
60	done := make(chan error, 1)
61	go func() {
62		for {
63			if err := c.Dispatch(); err != nil {
64				done <- err
65				return
66			}
67		}
68	}()
69	select {
70	case err := <-done:
71		var displayErr *wlturbo.DisplayError
72		if !errors.As(err, &displayErr) {
73			t.Fatalf("expected display error, got %v", err)
74		}
75		if displayErr.ObjectID != object || displayErr.Code != code {
76			t.Fatalf("protocol error = %v, want object=%d code=%d", displayErr, object, code)
77		}
78	case <-time.After(2 * time.Second):
79		_ = c.Close()
80		t.Fatal("protocol error timeout")
81	}
82}
83
84type protocolProxy struct{ wlturbo.BaseProxy }
85
86func (*protocolProxy) Dispatch(*wlturbo.Event) {}
87
88func registerProtocol(t *testing.T, c *wlturbo.Display, id uint32) {
89	t.Helper()
90	p := &protocolProxy{}
91	p.SetID(id)
92	registerWireProxy(c, p)
93}
94
95func TestXDGProtocolErrors(t *testing.T) {
96	for _, scenario := range []string{"bogus ack", "buffer before ack", "subsurface role", "defunct toplevel"} {
97		t.Run(scenario, func(t *testing.T) {
98			s, _, _, dir := lifecycleServer(t)
99			c := protocolClient(t, s, dir)
100			compositor := bindProtocol(t, c, "wl_compositor")
101			wm := bindProtocol(t, c, "xdg_wm_base")
102			surface := c.AllocateID()
103			requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, surface)
104			registerProtocol(t, c, surface)
105			if scenario == "subsurface role" {
106				parent := c.AllocateID()
107				requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, parent)
108				registerProtocol(t, c, parent)
109				subcomp := bindProtocol(t, c, "wl_subcompositor")
110				requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, c.AllocateID(), surface, parent)
111				requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surface)
112				expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole))
113				return
114			}
115			xdg := c.AllocateID()
116			requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surface)
117			top := c.AllocateID()
118			registerProtocol(t, c, xdg)
119			registerProtocol(t, c, top)
120			requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
121			switch scenario {
122			case "bogus ack":
123				requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0))
124				expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial))
125			case "buffer before ack":
126				shm := bindProtocol(t, c, "wl_shm")
127				fd, err := unix.MemfdCreate("protocol-buffer", 0)
128				if err != nil {
129					t.Fatal(err)
130				}
131				defer unix.Close(fd)
132				if err := unix.Ftruncate(fd, 4); err != nil {
133					t.Fatal(err)
134				}
135				pool := c.AllocateID()
136				registerProtocol(t, c, shm)
137				if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
138					t.Fatal(err)
139				}
140				buffer := c.AllocateID()
141				registerProtocol(t, c, pool)
142				registerProtocol(t, c, buffer)
143				requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
144				requestProtocol(t, c, surface, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
145				requestProtocol(t, c, surface, wayland.SurfaceRequestCommit)
146				expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorUnconfiguredBuffer))
147			case "defunct toplevel":
148				requestProtocol(t, c, xdg, xdgshell.SurfaceRequestDestroy)
149				expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorDefunctRoleObject))
150			}
151		})
152	}
153}
154
155func TestPopupBlocksSecondRole(t *testing.T) {
156	s, _, _, dir := lifecycleServer(t)
157	c := protocolClient(t, s, dir)
158	comp := bindProtocol(t, c, "wl_compositor")
159	wm := bindProtocol(t, c, "xdg_wm_base")
160	surf, xdg := c.AllocateID(), c.AllocateID()
161	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
162	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
163	registerProtocol(t, c, xdg)
164	registerProtocol(t, c, surf)
165	positioner := c.AllocateID()
166	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
167	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(10), int32(10))
168	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
169	if err := c.Roundtrip(); err != nil {
170		t.Fatal(err)
171	}
172	popup := c.AllocateID()
173	registerProtocol(t, c, popup)
174	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner)
175	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, c.AllocateID())
176	expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorAlreadyConstructed))
177}
178
179type configureProxy struct {
180	wlturbo.BaseProxy
181	serial chan uint32
182}
183
184func (p *configureProxy) Dispatch(e *wlturbo.Event) {
185	if e.Opcode == uint16(xdgshell.SurfaceEventConfigure) {
186		p.serial <- e.Uint32()
187	}
188}
189
190func TestRecreatedToplevelRequiresNewBuffer(t *testing.T) {
191	s, events, _, dir := lifecycleServer(t)
192	c := protocolClient(t, s, dir)
193	comp := bindProtocol(t, c, "wl_compositor")
194	wm := bindProtocol(t, c, "xdg_wm_base")
195	shm := bindProtocol(t, c, "wl_shm")
196	surf, xdg := c.AllocateID(), c.AllocateID()
197	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
198	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
199	serials := make(chan uint32, 4)
200	proxy := &configureProxy{serial: serials}
201	proxy.SetID(xdg)
202	registerWireProxy(c, proxy)
203	fd, err := unix.MemfdCreate("recreated-buffer", 0)
204	if err != nil {
205		t.Fatal(err)
206	}
207	defer unix.Close(fd)
208	if err := unix.Ftruncate(fd, 4); err != nil {
209		t.Fatal(err)
210	}
211	registerProtocol(t, c, shm)
212	pool, buffer := c.AllocateID(), c.AllocateID()
213	registerProtocol(t, c, pool)
214	registerProtocol(t, c, buffer)
215	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
216		t.Fatal(err)
217	}
218	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
219	top := c.AllocateID()
220	registerProtocol(t, c, top)
221	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
222	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
223	if err := c.Roundtrip(); err != nil {
224		t.Fatal(err)
225	}
226	select {
227	case serial := <-serials:
228		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
229	case <-time.After(2 * time.Second):
230		t.Fatal("initial configure timeout")
231	}
232	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
233	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
234	if err := c.Roundtrip(); err != nil {
235		t.Fatal(err)
236	}
237	mapped(t, events, 2*time.Second)
238	requestProtocol(t, c, top, xdgshell.ToplevelRequestDestroy)
239	if err := c.Roundtrip(); err != nil {
240		t.Fatal(err)
241	}
242	select {
243	case ev := <-events:
244		if _, ok := ev.(ports.WindowUnmapped); !ok {
245			t.Fatalf("expected unmap, got %T", ev)
246		}
247	case <-time.After(2 * time.Second):
248		t.Fatal("unmap timeout")
249	}
250	top = c.AllocateID()
251	registerProtocol(t, c, top)
252	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
253	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
254	if err := c.Roundtrip(); err != nil {
255		t.Fatal(err)
256	}
257	select {
258	case serial := <-serials:
259		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
260	case <-time.After(2 * time.Second):
261		t.Fatal("recreated configure timeout")
262	}
263	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
264	if err := c.Roundtrip(); err != nil {
265		t.Fatal(err)
266	}
267	select {
268	case ev := <-events:
269		t.Fatalf("mapped without new buffer: %T", ev)
270	case <-time.After(300 * time.Millisecond):
271	}
272	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
273	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
274	if err := c.Roundtrip(); err != nil {
275		t.Fatal(err)
276	}
277	mapped(t, events, 2*time.Second)
278}
279
280func TestSHMInvalidStride(t *testing.T) {
281	s, _, _, dir := lifecycleServer(t)
282	c := protocolClient(t, s, dir)
283	shm := bindProtocol(t, c, "wl_shm")
284	fd, err := unix.MemfdCreate("invalid-stride", 0)
285	if err != nil {
286		t.Fatal(err)
287	}
288	defer unix.Close(fd)
289	if err := unix.Ftruncate(fd, 16); err != nil {
290		t.Fatal(err)
291	}
292	pool := c.AllocateID()
293	registerProtocol(t, c, shm)
294	registerProtocol(t, c, pool)
295	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
296		t.Fatal(err)
297	}
298	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, c.AllocateID(), int32(0), int32(2), int32(1), int32(4), uint32(wayland.ShmFormatArgb8888))
299	expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride))
300}
301
302func TestSHMUnpaddedLastRow(t *testing.T) {
303	for _, tc := range []struct {
304		name  string
305		size  int32
306		valid bool
307	}{{"exact", 28, true}, {"short", 27, false}} {
308		t.Run(tc.name, func(t *testing.T) {
309			s, _, _, dir := lifecycleServer(t)
310			c := protocolClient(t, s, dir)
311			shm := bindProtocol(t, c, "wl_shm")
312			fd, err := unix.MemfdCreate("unpadded-last-row", 0)
313			if err != nil {
314				t.Fatal(err)
315			}
316			defer unix.Close(fd)
317			if err := unix.Ftruncate(fd, int64(tc.size)); err != nil {
318				t.Fatal(err)
319			}
320			pool := c.AllocateID()
321			registerProtocol(t, c, shm)
322			registerProtocol(t, c, pool)
323			if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, tc.size); err != nil {
324				t.Fatal(err)
325			}
326			buffer := c.AllocateID()
327			registerProtocol(t, c, buffer)
328			requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(4), int32(2), int32(2), int32(16), uint32(wayland.ShmFormatXrgb8888))
329			if !tc.valid {
330				expectProtocolError(t, c, shm, uint32(wayland.ShmErrorInvalidStride))
331				return
332			}
333			if err := c.Roundtrip(); err != nil {
334				t.Fatal(err)
335			}
336		})
337	}
338}
339
340// A buffer whose pool file was shrunk below it has no content.
341func TestBufferContentTruncatedPool(t *testing.T) {
342	fd, err := unix.MemfdCreate("truncated-pool", unix.MFD_CLOEXEC)
343	if err != nil {
344		t.Fatal(err)
345	}
346	f := os.NewFile(uintptr(fd), "truncated-pool")
347	defer f.Close()
348	if err := f.Truncate(28); err != nil {
349		t.Fatal(err)
350	}
351	b := &buffer{pool: &pool{id: 3, file: f, size: 28}, offset: 4, width: 2, height: 2, stride: 16}
352	c, ok := b.content(1)
353	if !ok || c.SHM == nil || c.SHM.Pool != 3 || c.SHM.Offset != 4 || c.SHM.Stride != 16 {
354		t.Fatalf("content ok=%v %+v", ok, c.SHM)
355	}
356	if err := f.Truncate(27); err != nil {
357		t.Fatal(err)
358	}
359	if _, ok := b.content(1); ok {
360		t.Fatal("content from a truncated pool")
361	}
362}
363
364type releaseProxy struct {
365	wlturbo.BaseProxy
366	released chan uint32
367}
368
369func (p *releaseProxy) Dispatch(e *wlturbo.Event) {
370	if e.Opcode == uint16(wayland.BufferEventRelease) {
371		p.released <- p.ID()
372	}
373}
374
375// wl_shm buffers are read in place: released when replaced or when their
376// surface is destroyed, never on commit.
377func TestSHMBufferReleasedOnReplaceAndDestroy(t *testing.T) {
378	s, _, _, dir := lifecycleServer(t)
379	c := protocolClient(t, s, dir)
380	comp := bindProtocol(t, c, "wl_compositor")
381	shm := bindProtocol(t, c, "wl_shm")
382	fd, err := unix.MemfdCreate("release", unix.MFD_CLOEXEC)
383	if err != nil {
384		t.Fatal(err)
385	}
386	defer unix.Close(fd)
387	if err := unix.Ftruncate(fd, 64); err != nil {
388		t.Fatal(err)
389	}
390	pool := c.AllocateID()
391	registerProtocol(t, c, shm)
392	registerProtocol(t, c, pool)
393	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(64)); err != nil {
394		t.Fatal(err)
395	}
396	released := make(chan uint32, 4)
397	bufs := make([]uint32, 2)
398	for i := range bufs {
399		bufs[i] = c.AllocateID()
400		p := &releaseProxy{released: released}
401		p.SetID(bufs[i])
402		registerWireProxy(c, p)
403		requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, bufs[i], int32(i*32), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
404	}
405	surf := c.AllocateID()
406	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
407	registerProtocol(t, c, surf)
408	expect := func(want []uint32) {
409		t.Helper()
410		if err := c.Roundtrip(); err != nil {
411			t.Fatal(err)
412		}
413		var got []uint32
414		for len(released) > 0 {
415			got = append(got, <-released)
416		}
417		if len(got) != len(want) || (len(want) == 1 && got[0] != want[0]) {
418			t.Fatalf("released %v, want %v", got, want)
419		}
420	}
421	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[0], int32(0), int32(0))
422	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
423	expect(nil)
424	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, bufs[1], int32(0), int32(0))
425	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
426	expect([]uint32{bufs[0]})
427	requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
428	expect([]uint32{bufs[1]})
429}
430
431type pointerEvents struct {
432	wlturbo.BaseProxy
433	enters  chan [2]float64
434	buttons chan uint32
435	axes    chan string
436}
437
438func (p *pointerEvents) Dispatch(e *wlturbo.Event) {
439	switch e.Opcode {
440	case uint16(wayland.PointerEventEnter):
441		_ = e.Uint32()
442		_ = e.Uint32()
443		p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()}
444	case uint16(wayland.PointerEventButton):
445		_ = e.Uint32()
446		_ = e.Uint32()
447		p.buttons <- e.Uint32()
448	case uint16(wayland.PointerEventAxisSource):
449		p.axes <- fmt.Sprint("source ", e.Uint32())
450	case uint16(wayland.PointerEventAxisValue120):
451		p.axes <- fmt.Sprint("v120 ", e.Uint32(), " ", e.Int32())
452	case uint16(wayland.PointerEventAxis):
453		_ = e.Uint32()
454		p.axes <- fmt.Sprint("axis ", e.Uint32(), " ", e.Fixed().Float64())
455	case uint16(wayland.PointerEventAxisStop):
456		_ = e.Uint32()
457		p.axes <- fmt.Sprint("stop ", e.Uint32())
458	}
459}
460
461func TestPointerProtocol(t *testing.T) {
462	s, events, commands, dir := lifecycleServer(t)
463	c := protocolClient(t, s, dir)
464	seat := bindVersion(t, c, "wl_seat", 8)
465	registerProtocol(t, c, seat)
466	pointer := c.AllocateID()
467	p := &pointerEvents{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 8), axes: make(chan string, 16)}
468	p.SetID(pointer)
469	registerWireProxy(c, p)
470	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
471	comp := bindProtocol(t, c, "wl_compositor")
472	wm := bindProtocol(t, c, "xdg_wm_base")
473	shm := bindProtocol(t, c, "wl_shm")
474	registerProtocol(t, c, shm)
475	surf, xdg := c.AllocateID(), c.AllocateID()
476	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
477	registerProtocol(t, c, surf)
478	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
479	serials := make(chan uint32, 1)
480	xp := &configureProxy{serial: serials}
481	xp.SetID(xdg)
482	registerWireProxy(c, xp)
483	top := c.AllocateID()
484	registerProtocol(t, c, top)
485	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
486	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
487	if err := c.Roundtrip(); err != nil {
488		t.Fatal(err)
489	}
490	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
491	fd, err := unix.MemfdCreate("pointer-buffer", 0)
492	if err != nil {
493		t.Fatal(err)
494	}
495	defer unix.Close(fd)
496	if err := unix.Ftruncate(fd, 4); err != nil {
497		t.Fatal(err)
498	}
499	pool, buffer := c.AllocateID(), c.AllocateID()
500	registerProtocol(t, c, pool)
501	registerProtocol(t, c, buffer)
502	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
503		t.Fatal(err)
504	}
505	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
506	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
507	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
508	if err := c.Roundtrip(); err != nil {
509		t.Fatal(err)
510	}
511	w := mapped(t, events, 2*time.Second)
512	commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}
513	commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond}
514	// Middle, side and extra buttons pass through as evdev codes.
515	for _, b := range []uint32{0x112, 0x113, 0x114} {
516		commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, Time: 1 * time.Millisecond}
517	}
518	commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, Time: 2 * time.Millisecond}}
519	commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 3 * time.Millisecond}}
520	deadline := time.After(2 * time.Second)
521	dispatched := make(chan error, 1)
522	go func() {
523		for {
524			if err := c.Dispatch(); err != nil {
525				dispatched <- err
526				return
527			}
528		}
529	}()
530	for len(p.enters) == 0 || len(p.buttons) < 4 || len(p.axes) < 5 {
531		select {
532		case err := <-dispatched:
533			t.Fatal(err)
534		case <-deadline:
535			t.Fatal("pointer events timeout")
536		case <-time.After(time.Millisecond):
537		}
538	}
539	select {
540	case coords := <-p.enters:
541		if coords != [2]float64{10, 20} {
542			t.Fatalf("enter: %v", coords)
543		}
544	default:
545		t.Fatal("missing pointer enter")
546	}
547	for _, want := range []uint32{0x110, 0x112, 0x113, 0x114} {
548		if b := <-p.buttons; b != want {
549			t.Fatalf("button: %#x, want %#x", b, want)
550		}
551	}
552	for _, want := range []string{"source 0", "v120 0 120", "axis 0 15", "source 1", "stop 1"} {
553		if got := <-p.axes; got != want {
554			t.Fatalf("axis event %q, want %q", got, want)
555		}
556	}
557}
558
559type heldKeyProxy struct {
560	wlturbo.BaseProxy
561	enters chan []byte
562}
563
564func (p *heldKeyProxy) Dispatch(e *wlturbo.Event) {
565	if e.Opcode == uint16(wayland.KeyboardEventEnter) {
566		_ = e.Uint32()
567		_ = e.Uint32()
568		p.enters <- e.Array()
569	}
570}
571
572func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) {
573	s, events, commands, dir := keyboardServer(t)
574	c := protocolClient(t, s, dir)
575	seat := bindProtocol(t, c, "wl_seat")
576	registerProtocol(t, c, seat)
577	keyboard := c.AllocateID()
578	kp := &heldKeyProxy{enters: make(chan []byte, 4)}
579	kp.SetID(keyboard)
580	registerWireProxy(c, kp)
581	requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboard)
582	mapWindow := toplevelMapper(t, c, events)
583	a := mapWindow().ID
584	b := mapWindow().ID
585	// Dispatch without sending a new request after focus changes.
586	dispatched := make(chan error, 1)
587	go func() {
588		for {
589			if err := c.Dispatch(); err != nil {
590				dispatched <- err
591				return
592			}
593		}
594	}()
595	enter := func(want []byte) {
596		t.Helper()
597		select {
598		case got := <-kp.enters:
599			if !bytes.Equal(got, want) {
600				t.Fatalf("keyboard enter keys = %v, want %v", got, want)
601			}
602		case err := <-dispatched:
603			t.Fatal(err)
604		case <-time.After(2 * time.Second):
605			t.Fatal("keyboard enter timeout")
606		}
607	}
608	commands <- ports.FocusWindow{ID: a}
609	enter(nil)
610	commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}
611	commands <- ports.FocusWindow{ID: b}
612	enter([]byte{30, 0, 0, 0})
613	commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, Time: 2 * time.Millisecond}}
614	commands <- ports.FocusWindow{ID: a}
615	enter(nil)
616}
617
618// toplevelMapper returns a function that maps a new 1x1 xdg toplevel on c and
619// returns the WindowMapped event it produced.
620func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() ports.WindowMapped {
621	t.Helper()
622	m := surfaceMapper(t, c, events)
623	return func() ports.WindowMapped { w, _, _ := m(); return w }
624}
625
626// surfaceMapper is toplevelMapper also returning the wl_surface and
627// xdg_surface IDs.
628func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) {
629	t.Helper()
630	comp := bindProtocol(t, c, "wl_compositor")
631	wm := bindProtocol(t, c, "xdg_wm_base")
632	shm := bindProtocol(t, c, "wl_shm")
633	registerProtocol(t, c, shm)
634	fd, err := unix.MemfdCreate("held-key-buffer", 0)
635	if err != nil {
636		t.Fatal(err)
637	}
638	defer unix.Close(fd)
639	if err := unix.Ftruncate(fd, 4); err != nil {
640		t.Fatal(err)
641	}
642	pool, buffer := c.AllocateID(), c.AllocateID()
643	registerProtocol(t, c, pool)
644	registerProtocol(t, c, buffer)
645	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
646		t.Fatal(err)
647	}
648	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
649	return func() (ports.WindowMapped, uint32, uint32) {
650		surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
651		requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
652		registerProtocol(t, c, surf)
653		requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
654		serials := make(chan uint32, 1)
655		xp := &configureProxy{serial: serials}
656		xp.SetID(xdg)
657		registerWireProxy(c, xp)
658		registerProtocol(t, c, top)
659		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
660		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
661		if err := c.Roundtrip(); err != nil {
662			t.Fatal(err)
663		}
664		select {
665		case serial := <-serials:
666			requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
667		default:
668			t.Fatal("missing configure")
669		}
670		requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
671		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
672		if err := c.Roundtrip(); err != nil {
673			t.Fatal(err)
674		}
675		return mapped(t, events, 2*time.Second), surf, xdg
676	}
677}
678
679type popupDoneProxy struct {
680	wlturbo.BaseProxy
681	done chan struct{}
682}
683
684func (p *popupDoneProxy) Dispatch(e *wlturbo.Event) {
685	if e.Opcode == uint16(xdgshell.PopupEventPopupDone) {
686		p.done <- struct{}{}
687	}
688}
689
690// xdgWindow maps a 1×1 xdg_surface; role sets its role before the first commit.
691func xdgWindow(t *testing.T, c *wlturbo.Display, comp, wm, buffer uint32, role func(xdg uint32)) (surf, xdg uint32, serials chan uint32) {
692	t.Helper()
693	surf, xdg = c.AllocateID(), c.AllocateID()
694	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
695	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
696	registerProtocol(t, c, surf)
697	serials = make(chan uint32, 4)
698	proxy := &configureProxy{serial: serials}
699	proxy.SetID(xdg)
700	registerWireProxy(c, proxy)
701	role(xdg)
702	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
703	if err := c.Roundtrip(); err != nil {
704		t.Fatal(err)
705	}
706	return surf, xdg, serials
707}
708
709func ackAndAttach(t *testing.T, c *wlturbo.Display, surf, xdg, buffer uint32, serials chan uint32) {
710	t.Helper()
711	deadline := time.Now().Add(2 * time.Second)
712	for len(serials) == 0 && time.Now().Before(deadline) {
713		if err := c.Roundtrip(); err != nil {
714			t.Fatal(err)
715		}
716		time.Sleep(10 * time.Millisecond)
717	}
718	select {
719	case serial := <-serials:
720		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
721	default:
722		t.Fatal("configure timeout")
723	}
724	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
725	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
726	if err := c.Roundtrip(); err != nil {
727		t.Fatal(err)
728	}
729}
730
731func TestPopupAndFloatingLifecycle(t *testing.T) {
732	s, events, commands, dir := lifecycleServer(t)
733	c := protocolClient(t, s, dir)
734	comp := bindProtocol(t, c, "wl_compositor")
735	wm := bindProtocol(t, c, "xdg_wm_base")
736	shm := bindProtocol(t, c, "wl_shm")
737	registerProtocol(t, c, shm)
738	fd, err := unix.MemfdCreate("popup-buffer", 0)
739	if err != nil {
740		t.Fatal(err)
741	}
742	defer unix.Close(fd)
743	if err := unix.Ftruncate(fd, 4); err != nil {
744		t.Fatal(err)
745	}
746	pool, buffer := c.AllocateID(), c.AllocateID()
747	registerProtocol(t, c, pool)
748	registerProtocol(t, c, buffer)
749	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
750		t.Fatal(err)
751	}
752	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
753
754	// A fixed-size toplevel (min == max) floats, like a splash screen.
755	surf, xdg, serials := xdgWindow(t, c, comp, wm, buffer, func(xdg uint32) {
756		top := c.AllocateID()
757		registerProtocol(t, c, top)
758		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
759		requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMinSize, int32(1), int32(1))
760		requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMaxSize, int32(1), int32(1))
761	})
762	ackAndAttach(t, c, surf, xdg, buffer, serials)
763	top := mapped(t, events, 2*time.Second)
764	if !top.Floating {
765		t.Fatalf("splash not floating: %+v", top)
766	}
767	if ev, ok := waitEvent(t, events, 2*time.Second).(ports.WindowResized); !ok || ev.Width != 1 || ev.Height != 1 {
768		t.Fatalf("splash size %+v", ev)
769	}
770
771	positioner := c.AllocateID()
772	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
773	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
774	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
775	done := make(chan struct{}, 1)
776	psurf, pxdg, pserials := xdgWindow(t, c, comp, wm, buffer, func(pxdg uint32) {
777		popup := c.AllocateID()
778		proxy := &popupDoneProxy{done: done}
779		proxy.SetID(popup)
780		registerWireProxy(c, proxy)
781		requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, xdg, positioner)
782	})
783	req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
784	if !ok || req.Parent != top.ID || req.Positioner.Width != 1 {
785		t.Fatalf("popup request %+v", req)
786	}
787	commands <- ports.ConfigurePopup{ID: req.ID, Rect: ports.Rect{X: 2, Y: 3, W: 1, H: 1}}
788	ackAndAttach(t, c, psurf, pxdg, buffer, pserials)
789	if ev, ok := waitEvent(t, events, 2*time.Second).(ports.PopupMapped); !ok || ev.ID != req.ID {
790		t.Fatalf("popup mapped %+v", ev)
791	}
792	commands <- ports.ClosePopup{ID: req.ID}
793	for deadline := time.Now().Add(2 * time.Second); len(done) == 0; time.Sleep(10 * time.Millisecond) {
794		if time.Now().After(deadline) {
795			t.Fatal("popup_done timeout")
796		}
797		if err := c.Roundtrip(); err != nil {
798			t.Fatal(err)
799		}
800	}
801}
802
803func TestPopupIncompletePositioner(t *testing.T) {
804	s, _, _, dir := lifecycleServer(t)
805	c := protocolClient(t, s, dir)
806	comp := bindProtocol(t, c, "wl_compositor")
807	wm := bindProtocol(t, c, "xdg_wm_base")
808	surf, xdg := c.AllocateID(), c.AllocateID()
809	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
810	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
811	registerProtocol(t, c, xdg)
812	positioner := c.AllocateID()
813	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
814	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(10), int32(10))
815	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, c.AllocateID(), uint32(0), positioner)
816	expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorInvalidPositioner))
817}

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 25

Tags 10

↑↓ moveenter openesc close