main f2d166b

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	return surfaceMapperAt(t, c, events, 1)
631}
632
633// surfaceMapperAt is surfaceMapper with wl_compositor bound at compositorVersion
634// (damage_buffer needs 4).
635func surfaceMapperAt(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent, compositorVersion uint32) func() (ports.WindowMapped, uint32, uint32) {
636	t.Helper()
637	comp := bindVersion(t, c, "wl_compositor", compositorVersion)
638	wm := bindProtocol(t, c, "xdg_wm_base")
639	shm := bindProtocol(t, c, "wl_shm")
640	registerProtocol(t, c, shm)
641	fd, err := unix.MemfdCreate("held-key-buffer", 0)
642	if err != nil {
643		t.Fatal(err)
644	}
645	defer unix.Close(fd)
646	if err := unix.Ftruncate(fd, 4); err != nil {
647		t.Fatal(err)
648	}
649	pool, buffer := c.AllocateID(), c.AllocateID()
650	registerProtocol(t, c, pool)
651	registerProtocol(t, c, buffer)
652	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
653		t.Fatal(err)
654	}
655	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
656	return func() (ports.WindowMapped, uint32, uint32) {
657		surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
658		requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
659		registerProtocol(t, c, surf)
660		requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
661		serials := make(chan uint32, 1)
662		xp := &configureProxy{serial: serials}
663		xp.SetID(xdg)
664		registerWireProxy(c, xp)
665		registerProtocol(t, c, top)
666		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
667		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
668		if err := c.Roundtrip(); err != nil {
669			t.Fatal(err)
670		}
671		select {
672		case serial := <-serials:
673			requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
674		default:
675			t.Fatal("missing configure")
676		}
677		requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
678		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
679		if err := c.Roundtrip(); err != nil {
680			t.Fatal(err)
681		}
682		return mapped(t, events, 2*time.Second), surf, xdg
683	}
684}
685
686type popupDoneProxy struct {
687	wlturbo.BaseProxy
688	done chan struct{}
689}
690
691func (p *popupDoneProxy) Dispatch(e *wlturbo.Event) {
692	if e.Opcode == uint16(xdgshell.PopupEventPopupDone) {
693		p.done <- struct{}{}
694	}
695}
696
697// xdgWindow maps a 1×1 xdg_surface; role sets its role before the first commit.
698func xdgWindow(t *testing.T, c *wlturbo.Display, comp, wm, buffer uint32, role func(xdg uint32)) (surf, xdg uint32, serials chan uint32) {
699	t.Helper()
700	surf, xdg = c.AllocateID(), c.AllocateID()
701	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
702	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
703	registerProtocol(t, c, surf)
704	serials = make(chan uint32, 4)
705	proxy := &configureProxy{serial: serials}
706	proxy.SetID(xdg)
707	registerWireProxy(c, proxy)
708	role(xdg)
709	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
710	if err := c.Roundtrip(); err != nil {
711		t.Fatal(err)
712	}
713	return surf, xdg, serials
714}
715
716func ackAndAttach(t *testing.T, c *wlturbo.Display, surf, xdg, buffer uint32, serials chan uint32) {
717	t.Helper()
718	deadline := time.Now().Add(2 * time.Second)
719	for len(serials) == 0 && time.Now().Before(deadline) {
720		if err := c.Roundtrip(); err != nil {
721			t.Fatal(err)
722		}
723		time.Sleep(10 * time.Millisecond)
724	}
725	select {
726	case serial := <-serials:
727		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, serial)
728	default:
729		t.Fatal("configure timeout")
730	}
731	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
732	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
733	if err := c.Roundtrip(); err != nil {
734		t.Fatal(err)
735	}
736}
737
738func TestPopupAndFloatingLifecycle(t *testing.T) {
739	s, events, commands, dir := lifecycleServer(t)
740	c := protocolClient(t, s, dir)
741	comp := bindProtocol(t, c, "wl_compositor")
742	wm := bindProtocol(t, c, "xdg_wm_base")
743	shm := bindProtocol(t, c, "wl_shm")
744	registerProtocol(t, c, shm)
745	fd, err := unix.MemfdCreate("popup-buffer", 0)
746	if err != nil {
747		t.Fatal(err)
748	}
749	defer unix.Close(fd)
750	if err := unix.Ftruncate(fd, 4); err != nil {
751		t.Fatal(err)
752	}
753	pool, buffer := c.AllocateID(), c.AllocateID()
754	registerProtocol(t, c, pool)
755	registerProtocol(t, c, buffer)
756	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
757		t.Fatal(err)
758	}
759	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
760
761	// A fixed-size toplevel (min == max) floats, like a splash screen.
762	surf, xdg, serials := xdgWindow(t, c, comp, wm, buffer, func(xdg uint32) {
763		top := c.AllocateID()
764		registerProtocol(t, c, top)
765		requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
766		requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMinSize, int32(1), int32(1))
767		requestProtocol(t, c, top, xdgshell.ToplevelRequestSetMaxSize, int32(1), int32(1))
768	})
769	ackAndAttach(t, c, surf, xdg, buffer, serials)
770	top := mapped(t, events, 2*time.Second)
771	if !top.Floating {
772		t.Fatalf("splash not floating: %+v", top)
773	}
774	if ev, ok := waitEvent(t, events, 2*time.Second).(ports.WindowResized); !ok || ev.Width != 1 || ev.Height != 1 {
775		t.Fatalf("splash size %+v", ev)
776	}
777
778	positioner := c.AllocateID()
779	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
780	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
781	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
782	done := make(chan struct{}, 1)
783	psurf, pxdg, pserials := xdgWindow(t, c, comp, wm, buffer, func(pxdg uint32) {
784		popup := c.AllocateID()
785		proxy := &popupDoneProxy{done: done}
786		proxy.SetID(popup)
787		registerWireProxy(c, proxy)
788		requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, xdg, positioner)
789	})
790	req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
791	if !ok || req.Parent != top.ID || req.Positioner.Width != 1 {
792		t.Fatalf("popup request %+v", req)
793	}
794	commands <- ports.ConfigurePopup{ID: req.ID, Rect: ports.Rect{X: 2, Y: 3, W: 1, H: 1}}
795	ackAndAttach(t, c, psurf, pxdg, buffer, pserials)
796	if ev, ok := waitEvent(t, events, 2*time.Second).(ports.PopupMapped); !ok || ev.ID != req.ID {
797		t.Fatalf("popup mapped %+v", ev)
798	}
799	commands <- ports.ClosePopup{ID: req.ID}
800	for deadline := time.Now().Add(2 * time.Second); len(done) == 0; time.Sleep(10 * time.Millisecond) {
801		if time.Now().After(deadline) {
802			t.Fatal("popup_done timeout")
803		}
804		if err := c.Roundtrip(); err != nil {
805			t.Fatal(err)
806		}
807	}
808}
809
810func TestPopupIncompletePositioner(t *testing.T) {
811	s, _, _, dir := lifecycleServer(t)
812	c := protocolClient(t, s, dir)
813	comp := bindProtocol(t, c, "wl_compositor")
814	wm := bindProtocol(t, c, "xdg_wm_base")
815	surf, xdg := c.AllocateID(), c.AllocateID()
816	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
817	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
818	registerProtocol(t, c, xdg)
819	positioner := c.AllocateID()
820	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
821	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(10), int32(10))
822	requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetPopup, c.AllocateID(), uint32(0), positioner)
823	expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorInvalidPositioner))
824}

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