v0.5.0 089fa12

input_desktop_extensions_test.go RawHistoryFolder

1//go:build linux
2
3package protocol_test
4
5import (
6	"encoding/binary"
7	"errors"
8	"os"
9	"reflect"
10	"testing"
11	"time"
12
13	"github.com/bnema/wlturbo"
14	"github.com/bnema/wlturbo/protocol/datacontrol"
15	"github.com/bnema/wlturbo/protocol/extforeigntoplevel"
16	"github.com/bnema/wlturbo/protocol/idleinhibit"
17	"github.com/bnema/wlturbo/protocol/idlenotify"
18	"github.com/bnema/wlturbo/protocol/imagecapturesource"
19	"github.com/bnema/wlturbo/protocol/imagecopycapture"
20	"github.com/bnema/wlturbo/protocol/pointerconstraints"
21	"github.com/bnema/wlturbo/protocol/primaryselection"
22	"github.com/bnema/wlturbo/protocol/relativepointer"
23	"github.com/bnema/wlturbo/protocol/shortcutsinhibit"
24	"github.com/bnema/wlturbo/protocol/workspace"
25	"github.com/bnema/wlturbo/protocol/xdgdecoration"
26	"github.com/bnema/wlturbo/protocol/xdgoutput"
27	"github.com/bnema/wlturbo/protocol/xdgshell"
28	"golang.org/x/sys/unix"
29)
30
31func TestRelativePointerMotionDecode(t *testing.T) {
32	s := newWireEnv(t)
33	ptr := s.pointer()
34	mgr := relativepointer.NewRelativePointerManager(s.d.Context())
35	s.bind(1, relativepointer.RelativePointerManagerInterface, 1, 1, mgr)
36	rp, e := mgr.GetRelativePointer(ptr)
37	noerr(t, e)
38	wantWords(t, "get_relative_pointer", s.req(mgr.ID(), 1), rp.ID(), ptr.ID())
39
40	type motion struct {
41		hi, lo         uint32
42		dx, dy, ux, uy wlturbo.Fixed
43	}
44	var got []motion
45	rp.OnRelativeMotion(func(hi, lo uint32, dx, dy, ux, uy wlturbo.Fixed) {
46		got = append(got, motion{hi, lo, dx, dy, ux, uy})
47	})
48	// 64-bit microsecond timestamp split in two words; negative deltas must
49	// keep their sign through the 24.8 fixed-point decode.
50	want := motion{1, 0x89abcdef, wlturbo.NewFixed(-1.5), wlturbo.NewFixed(2.25), wlturbo.NewFixed(-3), wlturbo.NewFixed(0.5)}
51	s.event(rp.ID(), 0, wordBytes(want.hi, want.lo, uint32(want.dx), uint32(want.dy), uint32(want.ux), uint32(want.uy))...)
52	if len(got) != 1 || got[0] != want {
53		t.Fatalf("motion %+v, want %+v", got, want)
54	}
55	if got[0].dx.Float64() != -1.5 || got[0].dy.Float64() != 2.25 {
56		t.Fatalf("fixed decode %v %v", got[0].dx.Float64(), got[0].dy.Float64())
57	}
58	// Two handlers are both called, in registration order.
59	var order []int
60	rp.OnRelativeMotion(func(_, _ uint32, _, _, _, _ wlturbo.Fixed) { order = append(order, 1) })
61	rp.OnRelativeMotion(func(_, _ uint32, _, _, _, _ wlturbo.Fixed) { order = append(order, 2) })
62	s.event(rp.ID(), 0, wordBytes(0, 1, 0, 0, 0, 0)...)
63	if !reflect.DeepEqual(order, []int{1, 2}) || len(got) != 2 {
64		t.Fatalf("handlers order=%v motions=%d", order, len(got))
65	}
66	noerr(t, rp.Destroy())
67	s.req(rp.ID(), 0)
68	noerr(t, mgr.Destroy())
69	s.req(mgr.ID(), 0)
70}
71
72func TestPointerConstraintsNullableRegion(t *testing.T) {
73	s := newWireEnv(t)
74	surf, ptr := s.surface(), s.pointer()
75	pc := pointerconstraints.NewPointerConstraints(s.d.Context())
76	s.bind(1, pointerconstraints.PointerConstraintsInterface, 1, 1, pc)
77
78	// A null region means "use the surface input region" and is wire id 0.
79	lock, e := pc.LockPointer(surf, ptr, nil, pointerconstraints.LIFETIME_PERSISTENT)
80	noerr(t, e)
81	wantWords(t, "lock_pointer(nil)", s.req(pc.ID(), 1), lock.ID(), surf.ID(), ptr.ID(), 0, pointerconstraints.LIFETIME_PERSISTENT)
82
83	reg := s.region()
84	conf, e := pc.ConfinePointer(surf, ptr, reg, pointerconstraints.LIFETIME_ONESHOT)
85	noerr(t, e)
86	wantWords(t, "confine_pointer(region)", s.req(pc.ID(), 2), conf.ID(), surf.ID(), ptr.ID(), reg.ID(), pointerconstraints.LIFETIME_ONESHOT)
87
88	noerr(t, lock.SetCursorPositionHint(wlturbo.NewFixed(10.5), wlturbo.NewFixed(-2)))
89	wantWords(t, "set_cursor_position_hint", s.req(lock.ID(), 1), uint32(wlturbo.NewFixed(10.5)), uint32(wlturbo.NewFixed(-2)))
90	noerr(t, lock.SetRegion(nil))
91	wantWords(t, "locked.set_region(nil)", s.req(lock.ID(), 2), 0)
92	noerr(t, lock.SetRegion(reg))
93	wantWords(t, "locked.set_region", s.req(lock.ID(), 2), reg.ID())
94	noerr(t, conf.SetRegion(nil))
95	wantWords(t, "confined.set_region(nil)", s.req(conf.ID(), 1), 0)
96
97	var seq []string
98	lock.OnLocked(func() { seq = append(seq, "locked") })
99	lock.OnUnlocked(func() { seq = append(seq, "unlocked") })
100	conf.OnConfined(func() { seq = append(seq, "confined") })
101	conf.OnUnconfined(func() { seq = append(seq, "unconfined") })
102	s.event(lock.ID(), 0)
103	s.event(lock.ID(), 1)
104	s.event(conf.ID(), 0)
105	s.event(conf.ID(), 1)
106	if !reflect.DeepEqual(seq, []string{"locked", "unlocked", "confined", "unconfined"}) {
107		t.Fatal(seq)
108	}
109	noerr(t, lock.Destroy())
110	s.req(lock.ID(), 0)
111	noerr(t, conf.Destroy())
112	s.req(conf.ID(), 0)
113	noerr(t, pc.Destroy())
114	s.req(pc.ID(), 0)
115}
116
117// rawPipe returns a pipe as plain descriptors. An *os.File would close a reused
118// descriptor number on Close after the binding already closed the original.
119func rawPipe(t *testing.T) (*os.File, int) {
120	t.Helper()
121	var fds [2]int
122	noerr(t, unix.Pipe2(fds[:], unix.O_CLOEXEC))
123	r := os.NewFile(uintptr(fds[0]), "pipe-r")
124	t.Cleanup(func() { r.Close() })
125	return r, fds[1]
126}
127
128func TestPrimarySelectionNewIDAndFDOwnership(t *testing.T) {
129	s := newWireEnv(t)
130	seat := s.seat()
131	mgr := primaryselection.NewPrimarySelectionDeviceManager(s.d.Context())
132	s.bind(1, primaryselection.PrimarySelectionDeviceManagerInterface, 1, 1, mgr)
133	dev, e := mgr.GetDevice(seat)
134	noerr(t, e)
135	wantWords(t, "get_device", s.req(mgr.ID(), 1), dev.ID(), seat.ID())
136	src, e := mgr.CreateSource()
137	noerr(t, e)
138	wantWords(t, "create_source", s.req(mgr.ID(), 0), src.ID())
139	noerr(t, src.Offer("text/plain"))
140	s.req(src.ID(), 0)
141	noerr(t, dev.SetSelection(src, 77))
142	wantWords(t, "set_selection", s.req(dev.ID(), 0), src.ID(), 77)
143	noerr(t, dev.SetSelection(nil, 78))
144	wantWords(t, "set_selection(nil)", s.req(dev.ID(), 0), 0, 78)
145
146	// data_offer carries a server-allocated new_id which must come back as a
147	// typed, registered child inheriting the device version.
148	var offer *primaryselection.PrimarySelectionOffer
149	var mimes []string
150	dev.OnDataOffer(func(o *primaryselection.PrimarySelectionOffer) {
151		offer = o
152		o.OnOffer(func(m string) { mimes = append(mimes, m) })
153	})
154	var selected []uint32
155	dev.OnSelection(func(id uint32) { selected = append(selected, id) })
156	s.event(dev.ID(), 0, wordBytes(serverID+1)...)
157	if offer == nil || offer.ID() != serverID+1 || offer.Version() != dev.Version() {
158		t.Fatalf("offer %+v", offer)
159	}
160	s.event(offer.ID(), 0, str("text/plain")...)
161	s.event(dev.ID(), 1, wordBytes(offer.ID())...)
162	s.event(dev.ID(), 1, wordBytes(0)...) // null selection
163	if !reflect.DeepEqual(mimes, []string{"text/plain"}) || !reflect.DeepEqual(selected, []uint32{offer.ID(), 0}) {
164		t.Fatalf("mimes=%v selected=%v", mimes, selected)
165	}
166
167	// Client -> compositor: the sent descriptor is duplicated by the kernel and
168	// the client's copy is closed by the binding.
169	r, w := rawPipe(t)
170	noerr(t, offer.Receive("text/plain", w))
171	// Check before reading the request: the received duplicate may reuse the
172	// number of the descriptor the client just closed.
173	requireFDClosed(t, w)
174	body, fd := s.reqFD(offer.ID(), 0)
175	// Closed explicitly below; the guard covers early t.Fatal paths only.
176	defer func() {
177		if fd >= 0 {
178			unix.Close(fd)
179		}
180	}()
181	if len(body) < 4 {
182		t.Fatalf("receive body %x", body)
183	}
184	_, e = unix.Write(fd, []byte("payload"))
185	noerr(t, e)
186	noerr(t, unix.Close(fd))
187	fd = -1
188	buf := make([]byte, 16)
189	n, e := r.Read(buf)
190	noerr(t, e)
191	if string(buf[:n]) != "payload" {
192		t.Fatalf("payload %q", buf[:n])
193	}
194
195	// Compositor -> client: source.send hands over an owned descriptor.
196	var taken, ignored *wlturbo.OwnedFD
197	var sendMimes []string
198	src.OnSend(func(m string, f *wlturbo.OwnedFD) {
199		sendMimes = append(sendMimes, m)
200		if taken == nil {
201			taken = f
202			n, e := f.Take()
203			noerr(t, e)
204			_, e = unix.Write(n, []byte("from-client"))
205			noerr(t, e)
206			unix.Close(n)
207			return
208		}
209		ignored = f // deliberately neither taken nor closed
210	})
211	sr, sw, e := os.Pipe()
212	noerr(t, e)
213	send(t, s.p, payload(src.ID(), 0, str("text/plain")...), int(sw.Fd()))
214	s.dispatch()
215	sw.Close()
216	n, e = sr.Read(buf)
217	noerr(t, e)
218	if string(buf[:n]) != "from-client" {
219		t.Fatalf("source payload %q", buf[:n])
220	}
221	if _, e := taken.Take(); e == nil {
222		t.Fatal("second Take on a taken descriptor must fail")
223	}
224	sr.Close()
225	// An unclaimed descriptor is closed by the transport when dispatch ends,
226	// so the pipe reader sees EOF although the handler never closed it.
227	pr, pw, e := os.Pipe()
228	noerr(t, e)
229	defer pr.Close()
230	send(t, s.p, payload(src.ID(), 0, str("text/html")...), int(pw.Fd()))
231	pw.Close() // only the in-flight duplicate keeps the write end alive
232	s.dispatch()
233	if ignored == nil || len(sendMimes) != 2 {
234		t.Fatalf("send events %v", sendMimes)
235	}
236	if _, e := ignored.Take(); e == nil {
237		t.Fatal("unclaimed descriptor must be closed after dispatch, but Take succeeded")
238	}
239	pr.SetReadDeadline(time.Now().Add(2 * time.Second))
240	if n, e := pr.Read(buf); n != 0 || e == nil {
241		t.Fatalf("unclaimed descriptor still open: n=%d err=%v", n, e)
242	}
243	var cancelled bool
244	src.OnCancelled(func() { cancelled = true })
245	s.event(src.ID(), 1)
246	if !cancelled {
247		t.Fatal("cancelled not delivered")
248	}
249	noerr(t, offer.Destroy())
250	s.req(offer.ID(), 1)
251	noerr(t, src.Destroy())
252	s.req(src.ID(), 1)
253	noerr(t, dev.Destroy())
254	s.req(dev.ID(), 1)
255	noerr(t, mgr.Destroy())
256	s.req(mgr.ID(), 2)
257}
258
259func TestDataControlNewIDAndFDOwnership(t *testing.T) {
260	s := newWireEnv(t)
261	seat := s.seat()
262	mgr := datacontrol.NewExtDataControlManager(s.d.Context())
263	s.bind(1, datacontrol.ExtDataControlManagerInterface, 1, 1, mgr)
264	dev, e := mgr.GetDataDevice(seat)
265	noerr(t, e)
266	wantWords(t, "get_data_device", s.req(mgr.ID(), 1), dev.ID(), seat.ID())
267	src, e := mgr.CreateDataSource()
268	noerr(t, e)
269	wantWords(t, "create_data_source", s.req(mgr.ID(), 0), src.ID())
270	noerr(t, dev.SetSelection(src))
271	wantWords(t, "set_selection", s.req(dev.ID(), 0), src.ID())
272	noerr(t, dev.SetPrimarySelection(nil))
273	wantWords(t, "set_primary_selection(nil)", s.req(dev.ID(), 2), 0)
274
275	var offers []*datacontrol.ExtDataControlOffer
276	dev.OnDataOffer(func(o *datacontrol.ExtDataControlOffer) { offers = append(offers, o) })
277	var sel, primary []uint32
278	dev.OnSelection(func(id uint32) { sel = append(sel, id) })
279	dev.OnPrimarySelection(func(id uint32) { primary = append(primary, id) })
280	var finished bool
281	dev.OnFinished(func() { finished = true })
282	s.event(dev.ID(), 0, wordBytes(serverID+1)...)
283	s.event(dev.ID(), 0, wordBytes(serverID+2)...)
284	if len(offers) != 2 || offers[0].ID() != serverID+1 || offers[1].ID() != serverID+2 {
285		t.Fatalf("offers %+v", offers)
286	}
287	s.event(dev.ID(), 1, wordBytes(serverID+1)...)
288	s.event(dev.ID(), 3, wordBytes(serverID+2)...)
289	s.event(dev.ID(), 3, wordBytes(0)...)
290	if !reflect.DeepEqual(sel, []uint32{serverID + 1}) || !reflect.DeepEqual(primary, []uint32{serverID + 2, 0}) {
291		t.Fatalf("sel=%v primary=%v", sel, primary)
292	}
293	var mime string
294	offers[0].OnOffer(func(m string) { mime = m })
295	s.event(offers[0].ID(), 0, str("text/uri-list")...)
296	if mime != "text/uri-list" {
297		t.Fatal(mime)
298	}
299
300	r, w := rawPipe(t)
301	noerr(t, offers[0].Receive("text/uri-list", w))
302	requireFDClosed(t, w)
303	_, fd := s.reqFD(offers[0].ID(), 0)
304	unix.Close(fd)
305	buf := make([]byte, 8)
306	if n, e := r.Read(buf); n != 0 || e == nil {
307		t.Fatalf("expected EOF once both write ends closed, n=%d err=%v", n, e)
308	}
309
310	// source.send: the handler owns the descriptor and can Close it early.
311	var closedByHandler bool
312	src.OnSend(func(m string, f *wlturbo.OwnedFD) {
313		noerr(t, f.Close())
314		noerr(t, f.Close()) // idempotent
315		if _, e := f.Take(); e == nil {
316			t.Error("Take after Close must fail")
317		}
318		closedByHandler = true
319	})
320	pr, pw, e := os.Pipe()
321	noerr(t, e)
322	defer pr.Close()
323	send(t, s.p, payload(src.ID(), 0, str("text/plain")...), int(pw.Fd()))
324	pw.Close()
325	s.dispatch()
326	pr.SetReadDeadline(time.Now().Add(2 * time.Second))
327	if n, e := pr.Read(buf); !closedByHandler || n != 0 || e == nil {
328		t.Fatalf("closed=%v n=%d err=%v", closedByHandler, n, e)
329	}
330	s.event(dev.ID(), 2)
331	if !finished {
332		t.Fatal("finished not delivered")
333	}
334	noerr(t, src.Offer("text/plain"))
335	s.req(src.ID(), 0)
336	noerr(t, offers[0].Destroy())
337	s.req(offers[0].ID(), 1)
338	noerr(t, src.Destroy())
339	s.req(src.ID(), 1)
340	noerr(t, dev.Destroy())
341	s.req(dev.ID(), 1)
342	noerr(t, mgr.Destroy())
343	s.req(mgr.ID(), 2)
344}
345
346func TestWorkspaceLifecycle(t *testing.T) {
347	s := newWireEnv(t)
348	out := s.output()
349	mgr := workspace.NewExtWorkspaceManager(s.d.Context())
350	s.bind(1, workspace.ExtWorkspaceManagerInterface, 1, 1, mgr)
351
352	var groups []*workspace.ExtWorkspaceGroupHandle
353	var spaces []*workspace.ExtWorkspaceHandle
354	var log []string
355	mgr.OnWorkspaceGroup(func(g *workspace.ExtWorkspaceGroupHandle) { groups = append(groups, g) })
356	mgr.OnWorkspace(func(w *workspace.ExtWorkspaceHandle) { spaces = append(spaces, w) })
357	mgr.OnDone(func() { log = append(log, "done") })
358	mgr.OnFinished(func() { log = append(log, "finished") })
359
360	s.event(mgr.ID(), 0, wordBytes(serverID+1)...)
361	s.event(mgr.ID(), 1, wordBytes(serverID+2)...)
362	if len(groups) != 1 || len(spaces) != 1 || groups[0].ID() != serverID+1 || spaces[0].ID() != serverID+2 {
363		t.Fatal("typed children missing")
364	}
365	g, w := groups[0], spaces[0]
366	var gcaps, wcaps, state uint32
367	var outs, entered []uint32
368	var id, name string
369	var coords []byte
370	g.OnCapabilities(func(c uint32) { gcaps = c })
371	g.OnOutputEnter(func(o uint32) { outs = append(outs, o) })
372	g.OnWorkspaceEnter(func(x uint32) { entered = append(entered, x) })
373	g.OnRemoved(func() { log = append(log, "group-removed") })
374	w.OnId(func(v string) { id = v })
375	w.OnName(func(v string) { name = v })
376	w.OnCoordinates(func(c []byte) { coords = c })
377	w.OnState(func(v uint32) { state = v })
378	w.OnCapabilities(func(v uint32) { wcaps = v })
379	w.OnRemoved(func() { log = append(log, "workspace-removed") })
380
381	s.event(g.ID(), 0, wordBytes(workspace.GROUP_CAPABILITIES_CREATE_WORKSPACE)...)
382	s.event(g.ID(), 1, wordBytes(out.ID())...)
383	s.event(g.ID(), 3, wordBytes(w.ID())...)
384	s.event(w.ID(), 0, str("ws-1")...)
385	s.event(w.ID(), 1, str("Workspace é")...)
386	s.event(w.ID(), 2, arr(wordBytes(1, 2))...)
387	s.event(w.ID(), 3, wordBytes(workspace.STATE_ACTIVE|workspace.STATE_URGENT)...)
388	s.event(w.ID(), 4, wordBytes(workspace.WORKSPACE_CAPABILITIES_ACTIVATE|workspace.WORKSPACE_CAPABILITIES_ASSIGN)...)
389	s.event(mgr.ID(), 2)
390	if gcaps != workspace.GROUP_CAPABILITIES_CREATE_WORKSPACE || !reflect.DeepEqual(outs, []uint32{out.ID()}) || !reflect.DeepEqual(entered, []uint32{w.ID()}) ||
391		id != "ws-1" || name != "Workspace é" || !reflect.DeepEqual(decodeWords(coords), []uint32{1, 2}) ||
392		state != 3 || wcaps != 9 || !reflect.DeepEqual(log, []string{"done"}) {
393		t.Fatalf("state gcaps=%d outs=%v entered=%v id=%q name=%q coords=%v state=%d wcaps=%d log=%v", gcaps, outs, entered, id, name, coords, state, wcaps, log)
394	}
395
396	// Requests are only transactional after commit, so the wire order is
397	// request, request, commit.
398	noerr(t, w.Activate())
399	s.req(w.ID(), 1)
400	noerr(t, w.Assign(g))
401	wantWords(t, "assign", s.req(w.ID(), 3), g.ID())
402	noerr(t, g.CreateWorkspace("new"))
403	if b := s.req(g.ID(), 0); binary.NativeEndian.Uint32(b) != 4 || string(b[4:7]) != "new" {
404		t.Fatalf("create_workspace %x", b)
405	}
406	noerr(t, w.Deactivate())
407	s.req(w.ID(), 2)
408	noerr(t, w.Remove())
409	s.req(w.ID(), 4)
410	noerr(t, mgr.Commit())
411	s.req(mgr.ID(), 0)
412
413	// Removal: the compositor sends removed, the client answers with destroy.
414	s.event(w.ID(), 5)
415	s.event(g.ID(), 5)
416	noerr(t, w.Destroy())
417	s.req(w.ID(), 0)
418	noerr(t, g.Destroy())
419	s.req(g.ID(), 1)
420	// stop is answered by finished, which is the final event.
421	noerr(t, mgr.Stop())
422	s.req(mgr.ID(), 1)
423	s.event(mgr.ID(), 3)
424	if !reflect.DeepEqual(log, []string{"done", "workspace-removed", "group-removed", "finished"}) {
425		t.Fatal(log)
426	}
427	// finished is a destructor event: the manager is dead and must not
428	// reach the compositor any more.
429	if e := mgr.Commit(); e == nil {
430		t.Fatal("Commit after finished must fail")
431	}
432	s.noRequest()
433}
434
435func TestIdleNotifyVersionGate(t *testing.T) {
436	s := newWireEnv(t)
437	seat := s.seat()
438	old := idlenotify.NewExtIdleNotifier(s.d.Context())
439	s.bind(1, idlenotify.ExtIdleNotifierInterface, 2, 1, old) // negotiated v1
440	if _, e := old.GetInputIdleNotification(1000, seat); !errors.Is(e, wlturbo.ErrVersionTooLow) {
441		t.Fatalf("v1 get_input_idle_notification = %v, want ErrVersionTooLow", e)
442	}
443	s.noRequest() // refused locally, nothing reaches the compositor
444	n, e := old.GetIdleNotification(1000, seat)
445	noerr(t, e)
446	wantWords(t, "get_idle_notification", s.req(old.ID(), 1), n.ID(), 1000, seat.ID())
447
448	send(t, s.p, msg(s.d.Registry().ID(), 1, 1)) // global_remove for the v1 bind
449	s.dispatch()
450	cur := idlenotify.NewExtIdleNotifier(s.d.Context())
451	s.bind(2, idlenotify.ExtIdleNotifierInterface, 2, 2, cur)
452	in, e := cur.GetInputIdleNotification(0, seat) // zero timeout is valid
453	noerr(t, e)
454	wantWords(t, "get_input_idle_notification", s.req(cur.ID(), 2), in.ID(), 0, seat.ID())
455	var seq []string
456	in.OnIdled(func() { seq = append(seq, "idled") })
457	in.OnResumed(func() { seq = append(seq, "resumed") })
458	s.event(in.ID(), 0)
459	s.event(in.ID(), 1)
460	if !reflect.DeepEqual(seq, []string{"idled", "resumed"}) {
461		t.Fatal(seq)
462	}
463	noerr(t, in.Destroy())
464	s.req(in.ID(), 0)
465	noerr(t, n.Destroy())
466	s.req(n.ID(), 0)
467}
468
469func TestInhibitAndDesktopHintsWire(t *testing.T) {
470	s := newWireEnv(t)
471	surf, seat, out := s.surface(), s.seat(), s.output()
472
473	ii := idleinhibit.NewIdleInhibitManager(s.d.Context())
474	s.bind(1, idleinhibit.IdleInhibitManagerInterface, 1, 1, ii)
475	inh, e := ii.CreateInhibitor(surf)
476	noerr(t, e)
477	wantWords(t, "create_inhibitor", s.req(ii.ID(), 1), inh.ID(), surf.ID())
478	noerr(t, inh.Destroy())
479	s.req(inh.ID(), 0)
480	noerr(t, ii.Destroy())
481	s.req(ii.ID(), 0)
482
483	ks := shortcutsinhibit.NewKeyboardShortcutsInhibitManager(s.d.Context())
484	s.bind(2, shortcutsinhibit.KeyboardShortcutsInhibitManagerInterface, 1, 1, ks)
485	k, e := ks.InhibitShortcuts(surf, seat)
486	noerr(t, e)
487	wantWords(t, "inhibit_shortcuts", s.req(ks.ID(), 1), k.ID(), surf.ID(), seat.ID())
488	var seq []string
489	k.OnActive(func() { seq = append(seq, "active") })
490	k.OnInactive(func() { seq = append(seq, "inactive") })
491	s.event(k.ID(), 0)
492	s.event(k.ID(), 1)
493	if !reflect.DeepEqual(seq, []string{"active", "inactive"}) {
494		t.Fatal(seq)
495	}
496	noerr(t, k.Destroy())
497	s.req(k.ID(), 0)
498	noerr(t, ks.Destroy())
499	s.req(ks.ID(), 0)
500
501	xo := xdgoutput.NewZxdgOutputManager(s.d.Context())
502	s.bind(3, xdgoutput.ZxdgOutputManagerInterface, 3, 3, xo)
503	o, e := xo.GetXdgOutput(out)
504	noerr(t, e)
505	wantWords(t, "get_xdg_output", s.req(xo.ID(), 1), o.ID(), out.ID())
506	var pos, size [2]int32
507	var name string
508	o.OnLogicalPosition(func(x, y int32) { pos = [2]int32{x, y} })
509	o.OnLogicalSize(func(w, h int32) { size = [2]int32{w, h} })
510	o.OnName(func(n string) { name = n })
511	s.event(o.ID(), 0, wordBytes(uint32(0xfffffff6), 20)...) // -10, 20
512	s.event(o.ID(), 1, wordBytes(1920, 1080)...)
513	s.event(o.ID(), 3, str("DP-1")...)
514	if pos != [2]int32{-10, 20} || size != [2]int32{1920, 1080} || name != "DP-1" {
515		t.Fatal(pos, size, name)
516	}
517	noerr(t, o.Destroy())
518	s.req(o.ID(), 0)
519	noerr(t, xo.Destroy())
520	s.req(xo.ID(), 0)
521}
522
523func TestXdgDecorationCrossPackageToplevel(t *testing.T) {
524	s := newWireEnv(t)
525	surf := s.surface()
526	wm := xdgshell.NewXdgWmBase(s.d.Context())
527	s.bind(1, xdgshell.XdgWmBaseInterface, 6, 6, wm)
528	xs, e := wm.GetXdgSurface(surf)
529	noerr(t, e)
530	s.req(wm.ID(), 2)
531	top, e := xs.GetToplevel()
532	noerr(t, e)
533	s.req(xs.ID(), 1)
534
535	dm := xdgdecoration.NewZxdgDecorationManager(s.d.Context())
536	s.bind(2, xdgdecoration.ZxdgDecorationManagerInterface, 2, 2, dm)
537	dec, e := dm.GetToplevelDecoration(top)
538	noerr(t, e)
539	wantWords(t, "get_toplevel_decoration", s.req(dm.ID(), 1), dec.ID(), top.ID())
540	noerr(t, dec.SetMode(xdgdecoration.MODE_SERVER_SIDE))
541	wantWords(t, "set_mode", s.req(dec.ID(), 1), xdgdecoration.MODE_SERVER_SIDE)
542	var modes []uint32
543	dec.OnConfigure(func(m uint32) { modes = append(modes, m) })
544	s.event(dec.ID(), 0, wordBytes(xdgdecoration.MODE_CLIENT_SIDE)...)
545	if !reflect.DeepEqual(modes, []uint32{xdgdecoration.MODE_CLIENT_SIDE}) {
546		t.Fatal(modes)
547	}
548	noerr(t, dec.UnsetMode())
549	s.req(dec.ID(), 2)
550	noerr(t, dec.Destroy())
551	s.req(dec.ID(), 0)
552	noerr(t, dm.Destroy())
553	s.req(dm.ID(), 0)
554}
555
556func TestImageCaptureCrossPackageChildren(t *testing.T) {
557	s := newWireEnv(t)
558	out, ptr := s.output(), s.pointer()
559
560	// Foreign toplevel handles come from ext-foreign-toplevel-list and are
561	// consumed by a different package (imagecapturesource).
562	list := extforeigntoplevel.NewExtForeignToplevelList(s.d.Context())
563	s.bind(1, extforeigntoplevel.ExtForeignToplevelListInterface, 1, 1, list)
564	var handle *extforeigntoplevel.ExtForeignToplevelHandle
565	list.OnToplevel(func(h *extforeigntoplevel.ExtForeignToplevelHandle) { handle = h })
566	s.event(list.ID(), 0, wordBytes(serverID+1)...)
567	if handle == nil || handle.ID() != serverID+1 {
568		t.Fatal("toplevel handle missing")
569	}
570	var title, appID, ident string
571	handle.OnTitle(func(v string) { title = v })
572	handle.OnAppId(func(v string) { appID = v })
573	handle.OnIdentifier(func(v string) { ident = v })
574	s.event(handle.ID(), 2, str("term")...)
575	s.event(handle.ID(), 3, str("org.example.term")...)
576	s.event(handle.ID(), 4, str("id-1")...)
577	if title != "term" || appID != "org.example.term" || ident != "id-1" {
578		t.Fatal(title, appID, ident)
579	}
580
581	om := imagecapturesource.NewExtOutputImageCaptureSourceManager(s.d.Context())
582	s.bind(2, imagecapturesource.ExtOutputImageCaptureSourceManagerInterface, 1, 1, om)
583	fm := imagecapturesource.NewExtForeignToplevelImageCaptureSourceManager(s.d.Context())
584	s.bind(3, imagecapturesource.ExtForeignToplevelImageCaptureSourceManagerInterface, 1, 1, fm)
585	outSrc, e := om.CreateSource(out)
586	noerr(t, e)
587	wantWords(t, "output create_source", s.req(om.ID(), 0), outSrc.ID(), out.ID())
588	topSrc, e := fm.CreateSource(handle)
589	noerr(t, e)
590	wantWords(t, "toplevel create_source", s.req(fm.ID(), 0), topSrc.ID(), handle.ID())
591
592	cm := imagecopycapture.NewExtImageCopyCaptureManager(s.d.Context())
593	s.bind(4, imagecopycapture.ExtImageCopyCaptureManagerInterface, 1, 1, cm)
594	sess, e := cm.CreateSession(topSrc, imagecopycapture.OPTIONS_PAINT_CURSORS)
595	noerr(t, e)
596	wantWords(t, "create_session", s.req(cm.ID(), 0), sess.ID(), topSrc.ID(), imagecopycapture.OPTIONS_PAINT_CURSORS)
597	cur, e := cm.CreatePointerCursorSession(outSrc, ptr)
598	noerr(t, e)
599	wantWords(t, "create_pointer_cursor_session", s.req(cm.ID(), 1), cur.ID(), outSrc.ID(), ptr.ID())
600
601	// Session constraints: advertised before done, arrays decoded.
602	var log []string
603	var size [2]uint32
604	var dev []byte
605	var dmaFormat uint32
606	var mods []byte
607	sess.OnBufferSize(func(w, h uint32) { size = [2]uint32{w, h}; log = append(log, "size") })
608	var shm []uint32
609	sess.OnShmFormat(func(f uint32) { shm = append(shm, f); log = append(log, "shm") })
610	sess.OnDmabufDevice(func(d []byte) { dev = d; log = append(log, "dev") })
611	sess.OnDmabufFormat(func(f uint32, m []byte) { dmaFormat, mods = f, m; log = append(log, "dmabuf") })
612	sess.OnDone(func() { log = append(log, "done") })
613	sess.OnStopped(func() { log = append(log, "stopped") })
614	s.event(sess.ID(), 0, wordBytes(1280, 720)...)
615	s.event(sess.ID(), 1, wordBytes(1)...) // wl_shm.format xrgb8888
616	s.event(sess.ID(), 2, arr(make([]byte, 8))...)
617	s.event(sess.ID(), 3, cat(wordBytes(0x34325258), arr(wordBytes(0, 0, 1, 0)))...)
618	s.event(sess.ID(), 4)
619	if !reflect.DeepEqual(shm, []uint32{1}) {
620		t.Fatalf("shm formats %v", shm)
621	}
622	if size != [2]uint32{1280, 720} || len(dev) != 8 || dmaFormat != 0x34325258 || len(mods) != 16 ||
623		!reflect.DeepEqual(log, []string{"size", "shm", "dev", "dmabuf", "done"}) {
624		t.Fatal(size, dev, dmaFormat, mods, log)
625	}
626
627	frame, e := sess.CreateFrame()
628	noerr(t, e)
629	wantWords(t, "create_frame", s.req(sess.ID(), 0), frame.ID())
630	buf := s.buffer()
631	noerr(t, frame.AttachBuffer(buf))
632	wantWords(t, "attach_buffer", s.req(frame.ID(), 1), buf.ID())
633	noerr(t, frame.DamageBuffer(0, 0, 1280, 720))
634	wantWords(t, "damage_buffer", s.req(frame.ID(), 2), 0, 0, 1280, 720)
635	noerr(t, frame.Capture())
636	s.req(frame.ID(), 3)
637
638	var fl []string
639	var damage [4]int32
640	var tv [3]uint32
641	frame.OnTransform(func(v uint32) { fl = append(fl, "transform") })
642	frame.OnDamage(func(x, y, w, h int32) { damage = [4]int32{x, y, w, h}; fl = append(fl, "damage") })
643	frame.OnPresentationTime(func(hi, lo, ns uint32) { tv = [3]uint32{hi, lo, ns}; fl = append(fl, "time") })
644	frame.OnReady(func() { fl = append(fl, "ready") })
645	frame.OnFailed(func(uint32) { fl = append(fl, "failed") })
646	s.event(frame.ID(), 0, wordBytes(0)...)
647	s.event(frame.ID(), 1, wordBytes(1, 2, 3, 4)...)
648	s.event(frame.ID(), 2, wordBytes(0, 1700000000, 999)...)
649	s.event(frame.ID(), 3)
650	if damage != [4]int32{1, 2, 3, 4} || tv != [3]uint32{0, 1700000000, 999} ||
651		!reflect.DeepEqual(fl, []string{"transform", "damage", "time", "ready"}) {
652		t.Fatal(damage, tv, fl)
653	}
654	// ready is terminal for a frame, and a session allows one frame at a time
655	// even after ready, so destroy it before creating the frame that fails.
656	noerr(t, frame.Destroy())
657	s.req(frame.ID(), 0)
658	failing, e := sess.CreateFrame()
659	noerr(t, e)
660	wantWords(t, "create_frame(2)", s.req(sess.ID(), 0), failing.ID())
661	noerr(t, failing.AttachBuffer(buf))
662	s.req(failing.ID(), 1)
663	noerr(t, failing.Capture())
664	s.req(failing.ID(), 3)
665	var reasons []uint32
666	failing.OnReady(func() { t.Error("failed frame must not become ready") })
667	failing.OnFailed(func(r uint32) { reasons = append(reasons, r) })
668	s.event(failing.ID(), 4, wordBytes(imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS)...)
669	if !reflect.DeepEqual(reasons, []uint32{imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS}) {
670		t.Fatal(reasons)
671	}
672
673	// Cursor session: get_capture_session is a typed child too.
674	var cl []string
675	var pos, hot [2]int32
676	cur.OnEnter(func() { cl = append(cl, "enter") })
677	cur.OnLeave(func() { cl = append(cl, "leave") })
678	cur.OnPosition(func(x, y int32) { pos = [2]int32{x, y} })
679	cur.OnHotspot(func(x, y int32) { hot = [2]int32{x, y} })
680	s.event(cur.ID(), 0)
681	s.event(cur.ID(), 2, wordBytes(uint32(0xfffffffb), 9)...) // -5, 9
682	s.event(cur.ID(), 3, wordBytes(4, 6)...)
683	s.event(cur.ID(), 1)
684	if pos != [2]int32{-5, 9} || hot != [2]int32{4, 6} || !reflect.DeepEqual(cl, []string{"enter", "leave"}) {
685		t.Fatal(pos, hot, cl)
686	}
687	csess, e := cur.GetCaptureSession()
688	noerr(t, e)
689	wantWords(t, "get_capture_session", s.req(cur.ID(), 1), csess.ID())
690	if csess.Version() != cur.Version() {
691		t.Fatalf("child version %d, parent %d", csess.Version(), cur.Version())
692	}
693
694	// destroy order: children, sources, managers, toplevel handle.
695	for _, c := range []struct {
696		name string
697		id   uint32
698		op   uint16
699		fn   func() error
700	}{
701		{"failed frame", failing.ID(), 0, failing.Destroy},
702		{"capture session", csess.ID(), 1, csess.Destroy},
703		{"session", sess.ID(), 1, sess.Destroy},
704		{"cursor", cur.ID(), 0, cur.Destroy},
705		{"output source", outSrc.ID(), 0, outSrc.Destroy},
706		{"toplevel source", topSrc.ID(), 0, topSrc.Destroy},
707		{"copy manager", cm.ID(), 2, cm.Destroy},
708		{"output manager", om.ID(), 1, om.Destroy},
709		{"toplevel manager", fm.ID(), 1, fm.Destroy},
710		{"handle", handle.ID(), 0, handle.Destroy},
711	} {
712		// Subtests run serially (no t.Parallel): the wire order of destroy
713		// requests on the shared socketpair is part of what is asserted.
714		ok := t.Run(c.name, func(t *testing.T) {
715			ss := *s
716			ss.t = t
717			noerr(t, c.fn())
718			ss.req(c.id, c.op)
719		})
720		if !ok {
721			// A failed step leaves unread bytes on the socket; carrying on
722			// would misattribute them to later destroys.
723			t.FailNow()
724		}
725	}
726}

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 8

↑↓ moveenter openesc close