detached 1a6c6c9

extensions_test.go RawHistoryFolder

1//go:build linux
2
3package client_test
4
5import (
6	"encoding/binary"
7	"errors"
8	"net"
9	"os"
10	"slices"
11	"strings"
12	"testing"
13	"time"
14
15	"github.com/bnema/go-wayland-bindings/client/cursorshape"
16	"github.com/bnema/go-wayland-bindings/client/fractionalscale"
17	"github.com/bnema/go-wayland-bindings/client/linuxdmabuf"
18	drmsyncobj "github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
19	"github.com/bnema/go-wayland-bindings/client/textinput"
20	"github.com/bnema/go-wayland-bindings/client/viewporter"
21	"github.com/bnema/go-wayland-bindings/client/wayland"
22	"github.com/bnema/go-wayland-bindings/client/xdgshell"
23	"github.com/bnema/wlturbo"
24	"golang.org/x/sys/unix"
25)
26
27func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
28	t.Helper()
29	fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
30	if e != nil {
31		t.Fatal(e)
32	}
33	a := os.NewFile(uintptr(fds[0]), "a")
34	b := os.NewFile(uintptr(fds[1]), "b")
35	ca, e := net.FileConn(a)
36	a.Close()
37	if e != nil {
38		t.Fatal(e)
39	}
40	cb, e := net.FileConn(b)
41	b.Close()
42	if e != nil {
43		t.Fatal(e)
44	}
45	t.Cleanup(func() { ca.Close(); cb.Close() })
46	return ca.(*net.UnixConn), cb.(*net.UnixConn)
47}
48func msg(id uint32, op uint16, body ...uint32) []byte {
49	b := make([]byte, 8+4*len(body))
50	binary.NativeEndian.PutUint32(b, id)
51	binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
52	for i, v := range body {
53		binary.NativeEndian.PutUint32(b[8+i*4:], v)
54	}
55	return b
56}
57func payload(id uint32, op uint16, b ...byte) []byte {
58	p := make([]byte, 8+len(b))
59	copy(p, msg(id, op))
60	copy(p[8:], b)
61	binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
62	return p
63}
64func arr(b []byte) []byte {
65	out := make([]byte, 4+(len(b)+3)&^3)
66	binary.NativeEndian.PutUint32(out, uint32(len(b)))
67	copy(out[4:], b)
68	return out
69}
70func str(s string) []byte { return arr(append([]byte(s), 0)) }
71func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) {
72	t.Helper()
73	var rights []byte
74	if len(fd) > 0 {
75		rights = unix.UnixRights(fd...)
76	}
77	if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil {
78		t.Fatal(e)
79	}
80}
81func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
82	t.Helper()
83	p.SetReadDeadline(time.Now().Add(2 * time.Second))
84	b := make([]byte, 4096)
85	n, e := p.Read(b)
86	if e != nil {
87		t.Fatal(e)
88	}
89	if n < 8 {
90		t.Fatal("short request")
91	}
92	return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n]
93}
94func noerr(t *testing.T, e error) {
95	t.Helper()
96	if e != nil {
97		t.Fatal(e)
98	}
99}
100func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, announced uint32, supported uint32, proxy wlturbo.Proxy) {
101	t.Helper()
102	// A fake server must advertise a global before clients can negotiate it.
103	announcement := append(msg(0, 0, name)[8:], str(iface)...)
104	announcement = append(announcement, msg(0, 0, announced)[8:]...)
105	send(t, p, payload(d.Registry().ID(), 0, announcement...))
106	noerr(t, d.Dispatch())
107	v, err := d.Registry().BindNegotiated(iface, supported, proxy)
108	noerr(t, err)
109	want := min(announced, supported)
110	_, op, b := request(t, p)
111	if op != 0 || v != want || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != want || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
112		t.Fatalf("bad bind %s announced=%d supported=%d negotiated=%d wire=%x", iface, announced, supported, v, b)
113	}
114}
115
116// serverID is the first server-allocated object ID on the wire.
117const serverID = 0xff000000
118
119// wordBytes encodes 32-bit words as a message body in native byte order.
120func wordBytes(v ...uint32) []byte { return msg(0, 0, v...)[8:] }
121
122// cat concatenates message body fragments.
123func cat(parts ...[]byte) []byte {
124	var out []byte
125	for _, p := range parts {
126		out = append(out, p...)
127	}
128	return out
129}
130
131// decodeWords splits a message body into 32-bit words.
132func decodeWords(b []byte) []uint32 {
133	w := make([]uint32, len(b)/4)
134	for i := range w {
135		w[i] = binary.NativeEndian.Uint32(b[i*4:])
136	}
137	return w
138}
139
140// wantWords requires a request body to be exactly the given 32-bit words.
141func wantWords(t *testing.T, what string, b []byte, want ...uint32) {
142	t.Helper()
143	if got := decodeWords(b); !slices.Equal(got, want) { // nil and empty are equal
144		t.Fatalf("%s wire = %v, want %v", what, got, want)
145	}
146}
147
148// wantRequestWords reads one request and requires its exact object, opcode and
149// 32-bit word arguments.
150func wantRequestWords(t *testing.T, p *net.UnixConn, id uint32, op uint16, want ...uint32) {
151	t.Helper()
152	gid, gop, body := request(t, p)
153	if gid != id || gop != op || len(body) != 4*len(want) {
154		t.Fatalf("request object=%d op=%d body=%x, want object=%d op=%d words=%v", gid, gop, body, id, op, want)
155	}
156	wantWords(t, "request", body, want...)
157}
158
159// requireFDClosed fails if fd is still open in this process: the binding must
160// close its copy of a descriptor once the kernel took it with a request. Call
161// it before reading the request, because the received duplicate may reuse the
162// same descriptor number.
163func requireFDClosed(t *testing.T, fd int) {
164	t.Helper()
165	if _, e := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); e == nil {
166		t.Fatalf("descriptor %d sent by the client was not closed", fd)
167	}
168}
169
170// requireNoWrite fails unless the client wrote nothing within a short deadline.
171func requireNoWrite(t *testing.T, p *net.UnixConn) {
172	t.Helper()
173	p.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
174	var b [64]byte
175	n, e := p.Read(b[:])
176	if e == nil {
177		t.Fatalf("unexpected bytes on the wire: %x", b[:n])
178	}
179	if !errors.Is(e, os.ErrDeadlineExceeded) {
180		t.Fatal(e)
181	}
182}
183
184// recvRequestFDs reads one client message and the descriptors that came with
185// it. Every received descriptor is closed before a fatal failure, so a failed
186// check cannot leak them; on success the caller owns fds.
187func recvRequestFDs(t *testing.T, p *net.UnixConn) (id uint32, op uint16, body []byte, fds []int) {
188	t.Helper()
189	fail := func(format string, args ...any) {
190		t.Helper()
191		for _, fd := range fds {
192			unix.Close(fd)
193		}
194		t.Fatalf(format, args...)
195	}
196	p.SetReadDeadline(time.Now().Add(2 * time.Second))
197	b := make([]byte, 4096)
198	oob := make([]byte, 256)
199	n, oobn, _, _, e := p.ReadMsgUnix(b, oob)
200	if oobn > 0 {
201		msgs, pe := unix.ParseSocketControlMessage(oob[:oobn])
202		for i := range msgs {
203			r, re := unix.ParseUnixRights(&msgs[i])
204			fds = append(fds, r...)
205			if re != nil && pe == nil {
206				pe = re
207			}
208		}
209		if pe != nil {
210			fail("control message: %v", pe)
211		}
212	}
213	if e != nil {
214		fail("read request: %v", e)
215	}
216	if n < 8 {
217		fail("short request (%d bytes)", n)
218	}
219	if size := int(binary.NativeEndian.Uint32(b[4:]) >> 16); size != n {
220		fail("header size %d != %d bytes read", size, n)
221	}
222	return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n], fds
223}
224
225// requestWithFD reads the next request, requires its object and opcode, and
226// requires exactly one descriptor, which the caller then owns.
227func requestWithFD(t *testing.T, p *net.UnixConn, id uint32, op uint16) ([]byte, int) {
228	t.Helper()
229	gid, gop, body, fds := recvRequestFDs(t, p)
230	if gid != id || gop != op || len(fds) != 1 {
231		for _, fd := range fds {
232			unix.Close(fd)
233		}
234		t.Fatalf("request object=%d op=%d fds=%d, want object=%d op=%d with 1 descriptor (body %x)", gid, gop, len(fds), id, op, body)
235	}
236	return body, fds[0]
237}
238
239// register gives a proxy a client-allocated ID and registers it with d.
240func register(d *wlturbo.Display, o wlturbo.Proxy) {
241	o.SetID(d.AllocateID())
242	d.Context().Register(o)
243}
244
245// wireEnv is one client connection against a fake compositor on a socketpair.
246type wireEnv struct {
247	t *testing.T
248	d *wlturbo.Display
249	p *net.UnixConn
250}
251
252func newWireEnv(t *testing.T) *wireEnv {
253	t.Helper()
254	c, p := pair(t)
255	d, e := wlturbo.ConnectFromConn(c)
256	noerr(t, e)
257	t.Cleanup(func() { d.Close() })
258	request(t, p) // wl_display.get_registry
259	return &wireEnv{t: t, d: d, p: p}
260}
261
262func (s *wireEnv) dispatch() { s.t.Helper(); noerr(s.t, s.d.Dispatch()) }
263
264// bind advertises and binds a global; see the package-level bind.
265func (s *wireEnv) bind(name uint32, iface string, announced, supported uint32, proxy wlturbo.Proxy) {
266	s.t.Helper()
267	bind(s.t, s.d, s.p, name, iface, announced, supported, proxy)
268}
269
270// event sends one server event and dispatches it on the client.
271func (s *wireEnv) event(id uint32, op uint16, body ...byte) {
272	s.t.Helper()
273	s.eventFD(id, op, body)
274}
275
276// eventFD is event for an event that carries descriptors (SCM_RIGHTS).
277func (s *wireEnv) eventFD(id uint32, op uint16, body []byte, fd ...int) {
278	s.t.Helper()
279	send(s.t, s.p, payload(id, op, body...), fd...)
280	s.dispatch()
281}
282
283// req reads the next client request and checks its object and opcode.
284func (s *wireEnv) req(id uint32, op uint16) []byte {
285	s.t.Helper()
286	gid, gop, b := request(s.t, s.p)
287	if gid != id || gop != op {
288		s.t.Fatalf("request object=%d op=%d, want object=%d op=%d (body %x)", gid, gop, id, op, b)
289	}
290	return b
291}
292
293// reqFD is req for a request that carries exactly one descriptor.
294func (s *wireEnv) reqFD(id uint32, op uint16) ([]byte, int) {
295	s.t.Helper()
296	return requestWithFD(s.t, s.p, id, op)
297}
298
299// noRequest fails if the client wrote anything.
300func (s *wireEnv) noRequest() { s.t.Helper(); requireNoWrite(s.t, s.p) }
301
302func (s *wireEnv) surface() *wayland.Surface {
303	o := wayland.NewSurface(s.d.Context())
304	register(s.d, o)
305	return o
306}
307func (s *wireEnv) seat() *wayland.Seat {
308	o := wayland.NewSeat(s.d.Context())
309	register(s.d, o)
310	return o
311}
312func (s *wireEnv) output() *wayland.Output {
313	o := wayland.NewOutput(s.d.Context())
314	register(s.d, o)
315	return o
316}
317func (s *wireEnv) pointer() *wayland.Pointer {
318	o := wayland.NewPointer(s.d.Context())
319	register(s.d, o)
320	return o
321}
322func (s *wireEnv) region() *wayland.Region {
323	o := wayland.NewRegion(s.d.Context())
324	register(s.d, o)
325	return o
326}
327func (s *wireEnv) buffer() *wayland.Buffer {
328	o := wayland.NewBuffer(s.d.Context())
329	register(s.d, o)
330	return o
331}
332
333func TestExtensionsOverSocketpair(t *testing.T) {
334	c, p := pair(t)
335	d, e := wlturbo.ConnectFromConn(c)
336	noerr(t, e)
337	defer d.Close()
338	request(t, p)
339	// A registry announcement is the only input to optional-global discovery.
340	send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...))
341	noerr(t, d.Dispatch())
342	if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 {
343		t.Fatal("announced fractional scale missing", g, ok)
344	}
345	if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
346		t.Fatal("absent text input")
347	}
348	// Remove the discovery-only announcement before the bind matrix assigns
349	// its own global name to this interface.
350	send(t, p, msg(d.Registry().ID(), 1, 11))
351	noerr(t, d.Dispatch())
352	surface := wayland.NewSurface(d.Context())
353	surface.SetID(d.AllocateID())
354	d.Context().Register(surface)
355	seat := wayland.NewSeat(d.Context())
356	seat.SetID(d.AllocateID())
357	d.Context().Register(seat)
358	wm := xdgshell.NewXdgWmBase(d.Context())
359	bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 6, 7, wm)
360	xs, e := wm.GetXdgSurface(surface)
361	noerr(t, e)
362	request(t, p)
363	top, e := xs.GetToplevel()
364	noerr(t, e)
365	request(t, p)
366	var sequence []string
367	top.OnConfigure(func(w, h int32, states []byte) {
368		if w != 800 || h != 600 {
369			t.Fatalf("size %d,%d", w, h)
370		}
371		sequence = append(sequence, "top")
372	})
373	xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) })
374	send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...))
375	noerr(t, d.Dispatch())
376	send(t, p, msg(xs.ID(), 0, 9))
377	noerr(t, d.Dispatch())
378	_, op, b := request(t, p)
379	if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" {
380		t.Fatalf("xdg %d %x %v", op, b, sequence)
381	}
382	noerr(t, top.Destroy())
383	request(t, p)
384	noerr(t, xs.Destroy())
385	request(t, p)
386	noerr(t, wm.Destroy())
387	request(t, p)
388	dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
389	bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, 4, dm)
390	fb, e := dm.GetDefaultFeedback()
391	noerr(t, e)
392	request(t, p)
393	table, e := os.CreateTemp(t.TempDir(), "table")
394	noerr(t, e)
395	var bytes [32]byte
396	binary.NativeEndian.PutUint32(bytes[:], 0x34325258)
397	binary.NativeEndian.PutUint64(bytes[8:], 19)
398	binary.NativeEndian.PutUint32(bytes[16:], 0x34325241)
399	binary.NativeEndian.PutUint64(bytes[24:], 42)
400	_, e = table.Write(bytes[:])
401	noerr(t, e)
402	defer table.Close()
403	var entries []linuxdmabuf.FormatEntry
404	var indices []uint16
405	var feedback []string
406	fb.OnMainDevice(func(dev []byte) {
407		if len(dev) != 8 {
408			t.Fatalf("device %x", dev)
409		}
410		feedback = append(feedback, "main")
411	})
412	fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
413		entries, e = linuxdmabuf.ReadFormatTable(fd, size)
414		noerr(t, e)
415		feedback = append(feedback, "table")
416	})
417	fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") })
418	fb.OnTrancheFormats(func(data []byte) {
419		indices, e = linuxdmabuf.TrancheIndices(data, len(entries))
420		noerr(t, e)
421		feedback = append(feedback, "formats")
422	})
423	fb.OnTrancheFlags(func(flags uint32) {
424		if flags != 1 {
425			t.Fatal(flags)
426		}
427		feedback = append(feedback, "flags")
428	})
429	fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") })
430	fb.OnDone(func() { feedback = append(feedback, "done") })
431	send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...))
432	noerr(t, d.Dispatch())
433	send(t, p, msg(fb.ID(), 1, 32), int(table.Fd()))
434	noerr(t, d.Dispatch())
435	send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...))
436	noerr(t, d.Dispatch())
437	send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...))
438	noerr(t, d.Dispatch())
439	send(t, p, msg(fb.ID(), 6, 1))
440	noerr(t, d.Dispatch())
441	send(t, p, msg(fb.ID(), 3))
442	noerr(t, d.Dispatch())
443	send(t, p, msg(fb.ID(), 0))
444	noerr(t, d.Dispatch())
445	if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" {
446		t.Fatalf("feedback %v %v %v", entries, indices, feedback)
447	}
448	noerr(t, fb.Destroy())
449	request(t, p)
450	params, e := dm.CreateParams()
451	noerr(t, e)
452	request(t, p)
453	f, e := os.Open("/dev/null")
454	noerr(t, e)
455	noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0))
456	request(t, p)
457	if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
458		t.Fatal("sent fd not closed")
459	}
460	f.Close()
461	var buffer *wayland.Buffer
462	params.OnCreated(func(b *wayland.Buffer) { buffer = b })
463	params.OnFailed(func() { sequence = append(sequence, "failed") })
464	noerr(t, params.Create(1, 1, 0x34325258, 0))
465	request(t, p)
466	send(t, p, msg(params.ID(), 0, 999))
467	noerr(t, d.Dispatch())
468	if buffer == nil || buffer.ID() != 999 {
469		t.Fatal("typed buffer missing")
470	}
471	noerr(t, buffer.Destroy())
472	request(t, p)
473	noerr(t, params.Destroy())
474	request(t, p)
475	failedParams, e := dm.CreateParams()
476	noerr(t, e)
477	request(t, p)
478	failedParams.OnFailed(func() { sequence = append(sequence, "failed") })
479	noerr(t, failedParams.Create(1, 1, 0x34325258, 0))
480	request(t, p)
481	send(t, p, msg(failedParams.ID(), 1))
482	noerr(t, d.Dispatch())
483	if sequence[len(sequence)-1] != "failed" {
484		t.Fatal(sequence)
485	}
486	noerr(t, failedParams.Destroy())
487	request(t, p)
488	immedParams, e := dm.CreateParams()
489	noerr(t, e)
490	request(t, p)
491	immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0)
492	noerr(t, e)
493	request(t, p)
494	noerr(t, immediate.Destroy())
495	request(t, p)
496	noerr(t, immedParams.Destroy())
497	request(t, p)
498	noerr(t, dm.Destroy())
499	request(t, p)
500	sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
501	bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, sync)
502	ss, e := sync.GetSurface(surface)
503	noerr(t, e)
504	request(t, p)
505	f, e = os.Open("/dev/null")
506	noerr(t, e)
507	tl, e := sync.ImportTimeline(int(f.Fd()))
508	noerr(t, e)
509	request(t, p)
510	if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil {
511		t.Fatal("timeline fd not closed")
512	}
513	f.Close()
514	noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456))
515	_, op, b = request(t, p)
516	if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 {
517		t.Fatalf("acquire %d %x", op, b)
518	}
519	noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc))
520	_, op, b = request(t, p)
521	if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 {
522		t.Fatalf("release %d %x", op, b)
523	}
524	noerr(t, ss.Destroy())
525	request(t, p)
526	noerr(t, tl.Destroy())
527	request(t, p)
528	noerr(t, sync.Destroy())
529	request(t, p)
530	vp := viewporter.NewWpViewporter(d.Context())
531	bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, 1, vp)
532	view, e := vp.GetViewport(surface)
533	noerr(t, e)
534	request(t, p)
535	noerr(t, view.Destroy())
536	request(t, p)
537	noerr(t, vp.Destroy())
538	request(t, p)
539	fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
540	bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, 1, fs)
541	scale, e := fs.GetFractionalScale(surface)
542	noerr(t, e)
543	request(t, p)
544	var preferred uint32
545	scale.OnPreferredScale(func(v uint32) { preferred = v })
546	send(t, p, msg(scale.ID(), 0, 150))
547	noerr(t, d.Dispatch())
548	if preferred != 150 {
549		t.Fatal(preferred)
550	}
551	noerr(t, scale.Destroy())
552	request(t, p)
553	noerr(t, fs.Destroy())
554	request(t, p)
555	ti := textinput.NewTextInputManagerV3(d.Context())
556	bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 2, 1, ti)
557	input, e := ti.GetTextInput(seat)
558	noerr(t, e)
559	request(t, p)
560	noerr(t, input.Enable())
561	request(t, p)
562	noerr(t, input.SetSurroundingText("hello", 5, 5))
563	request(t, p)
564	noerr(t, input.Commit())
565	request(t, p)
566	var events []string
567	input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) })
568	input.OnCommitString(func(s string) { events = append(events, "commit:"+s) })
569	input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") })
570	input.OnDone(func(uint32) { events = append(events, "done") })
571	send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...))
572	noerr(t, d.Dispatch())
573	send(t, p, payload(input.ID(), 3, str("hi")...))
574	noerr(t, d.Dispatch())
575	send(t, p, msg(input.ID(), 4, 1, 2))
576	noerr(t, d.Dispatch())
577	send(t, p, msg(input.ID(), 5, 1))
578	noerr(t, d.Dispatch())
579	if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" {
580		t.Fatal(events)
581	}
582	noerr(t, input.Disable())
583	request(t, p)
584	noerr(t, input.Commit())
585	request(t, p)
586	noerr(t, input.Destroy())
587	request(t, p)
588	noerr(t, ti.Destroy())
589	request(t, p)
590	// Core data-device remains in client/wayland; verify its typed child and
591	// both directions of clipboard descriptor ownership.
592	mgr := wayland.NewDataDeviceManager(d.Context())
593	bind(t, d, p, 7, wayland.DataDeviceManagerInterface, 3, 4, mgr)
594	device, e := mgr.GetDataDevice(seat)
595	noerr(t, e)
596	request(t, p)
597	source, e := mgr.CreateDataSource()
598	noerr(t, e)
599	request(t, p)
600	var offer *wayland.DataOffer
601	device.OnDataOffer(func(o *wayland.DataOffer) { offer = o })
602	send(t, p, msg(device.ID(), 0, 998))
603	noerr(t, d.Dispatch())
604	if offer == nil || offer.ID() != 998 {
605		t.Fatal("offer not typed")
606	}
607	read, write, e := os.Pipe()
608	noerr(t, e)
609	defer read.Close()
610	noerr(t, offer.Receive("text/plain", int(write.Fd())))
611	request(t, p)
612	if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil {
613		t.Fatal("clipboard sent fd retained")
614	}
615	write.Close()
616	source.OnSend(func(mime string, fd *wlturbo.OwnedFD) {
617		if mime != "text/plain" {
618			t.Fatal(mime)
619		}
620		n, e := fd.Take()
621		noerr(t, e)
622		unix.Close(n)
623	})
624	received, e := os.Open("/dev/null")
625	noerr(t, e)
626	send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd()))
627	noerr(t, d.Dispatch())
628	received.Close()
629	noerr(t, offer.Destroy())
630	request(t, p)
631	noerr(t, source.Destroy())
632	request(t, p)
633	noerr(t, device.Release())
634	request(t, p)
635	// release is a version 4 request; the manager was negotiated at 3, so it
636	// must be refused locally instead of reaching the compositor.
637	if err := mgr.Release(); !errors.Is(err, wlturbo.ErrVersionTooLow) {
638		t.Fatalf("release at v3 = %v, want ErrVersionTooLow", err)
639	}
640	// Cursor shape: get a device for a wl_pointer and set a shape at an
641	// enter serial; check the exact wire arguments.
642	pointer := wayland.NewPointer(d.Context())
643	pointer.SetID(d.AllocateID())
644	d.Context().Register(pointer)
645	shapes := cursorshape.NewWpCursorShapeManager(d.Context())
646	bind(t, d, p, 8, cursorshape.WpCursorShapeManagerInterface, 2, 1, shapes)
647	shape, e := shapes.GetPointer(pointer)
648	noerr(t, e)
649	if id, op, b := request(t, p); id != shapes.ID() || op != 1 || binary.NativeEndian.Uint32(b) != shape.ID() || binary.NativeEndian.Uint32(b[4:]) != pointer.ID() {
650		t.Fatalf("get_pointer wire id=%d op=%d %x", id, op, b)
651	}
652	noerr(t, shape.SetShape(42, cursorshape.SHAPE_POINTER))
653	if id, op, b := request(t, p); id != shape.ID() || op != 1 || binary.NativeEndian.Uint32(b) != 42 || binary.NativeEndian.Uint32(b[4:]) != 4 {
654		t.Fatalf("set_shape wire id=%d op=%d %x", id, op, b)
655	}
656	noerr(t, shape.Destroy())
657	request(t, p)
658	noerr(t, shapes.Destroy())
659	request(t, p)
660
661}
662
663func TestLiveFeedback(t *testing.T) {
664	if os.Getenv("WLTURBO_LIVE") != "1" {
665		t.Skip("set WLTURBO_LIVE=1")
666	}
667	d, e := wlturbo.Connect("")
668	noerr(t, e)
669	defer d.Close()
670	noerr(t, d.Roundtrip())
671	for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface, cursorshape.WpCursorShapeManagerInterface} {
672		g, ok := d.Registry().FindGlobal(iface)
673		if !ok {
674			t.Logf("%s absent", iface)
675			continue
676		}
677		v := g.Version
678		cap := uint32(1)
679		if iface == xdgshell.XdgWmBaseInterface {
680			cap = 7
681		}
682		if iface == linuxdmabuf.LinuxDmabufInterface {
683			cap = 4
684		}
685		if v > cap {
686			v = cap
687		}
688		t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version)
689		if iface != linuxdmabuf.LinuxDmabufInterface {
690			continue
691		}
692		if v < 4 {
693			t.Fatal("dmabuf feedback requires v4")
694		}
695		dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
696		negotiated, err := d.Registry().BindNegotiated(iface, 4, dm)
697		noerr(t, err)
698		if negotiated != v {
699			t.Fatalf("dmabuf negotiated=%d want=%d", negotiated, v)
700		}
701		fb, e := dm.GetDefaultFeedback()
702		noerr(t, e)
703		done := false
704		fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) })
705		fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) {
706			entries, err := linuxdmabuf.ReadFormatTable(fd, size)
707			if err != nil {
708				t.Errorf("table: %v", err)
709			}
710			t.Logf("format-table entries=%d", len(entries))
711		})
712		fb.OnDone(func() { done = true })
713		for i := 0; i < 100 && !done; i++ {
714			noerr(t, d.Dispatch())
715		}
716		if !done {
717			t.Fatal("no feedback done")
718		}
719		noerr(t, fb.Destroy())
720		noerr(t, dm.Destroy())
721	}
722}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close