v0.1.0 d9218d6

protocol_test.go RawHistoryFolder

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

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 23

Tags 10

↑↓ moveenter openesc close