detached 1a6c6c9

wlr_extensions_test.go RawHistoryFolder

1//go:build linux
2
3package client_test
4
5import (
6	"bytes"
7	"encoding/binary"
8	"errors"
9	"os"
10	"reflect"
11	"testing"
12	"time"
13
14	"github.com/bnema/go-wayland-bindings/client/inputmethod"
15	kdeserverdecoration "github.com/bnema/go-wayland-bindings/client/serverdecoration"
16	"github.com/bnema/go-wayland-bindings/client/textinput"
17	"github.com/bnema/go-wayland-bindings/client/virtualkeyboard"
18	"github.com/bnema/go-wayland-bindings/client/wayland"
19	foreigntoplevel "github.com/bnema/go-wayland-bindings/client/wlrforeigntoplevelmanagement"
20	layershell "github.com/bnema/go-wayland-bindings/client/wlrlayershell"
21	outputmanagement "github.com/bnema/go-wayland-bindings/client/wlroutputmanagement"
22	outputpower "github.com/bnema/go-wayland-bindings/client/wlroutputpowermanagement"
23	screencopy "github.com/bnema/go-wayland-bindings/client/wlrscreencopy"
24	"github.com/bnema/go-wayland-bindings/client/xdgshell"
25	"github.com/bnema/wlturbo"
26	"golang.org/x/sys/unix"
27)
28
29// The output head, its modes and configuration objects are all created by
30// server events (new_id in an event) or nested client requests.
31func TestWlrOutputManagementHeadNewID(t *testing.T) {
32	s := newWireEnv(t)
33	mgr := outputmanagement.NewOutputManager(s.d.Context())
34	s.bind(1, outputmanagement.OutputManagerInterface, 4, 4, mgr)
35
36	var head *outputmanagement.OutputHead
37	mgr.OnHead(func(h *outputmanagement.OutputHead) { head = h })
38	var serial uint32
39	mgr.OnDone(func(v uint32) { serial = v })
40	s.event(mgr.ID(), 0, wordBytes(serverID+1)...)
41	if head == nil || head.ID() != serverID+1 || head.Version() != mgr.Version() || head.Version() != 4 {
42		t.Fatalf("head %+v", head)
43	}
44
45	var name string
46	var mode *outputmanagement.OutputMode
47	var current uint32
48	var scale wlturbo.Fixed
49	var enabled int32
50	head.OnName(func(v string) { name = v })
51	head.OnMode(func(m *outputmanagement.OutputMode) { mode = m })
52	head.OnCurrentMode(func(id uint32) { current = id })
53	head.OnScale(func(v wlturbo.Fixed) { scale = v })
54	head.OnEnabled(func(v int32) { enabled = v })
55	s.event(head.ID(), 0, str("HDMI-A-1")...)
56	s.event(head.ID(), 3, wordBytes(serverID+2)...)
57	if mode == nil || mode.ID() != serverID+2 || mode.Version() != 4 {
58		t.Fatalf("mode %+v", mode)
59	}
60	var w, h, refresh int32
61	var preferred bool
62	mode.OnSize(func(a, b int32) { w, h = a, b })
63	mode.OnRefresh(func(v int32) { refresh = v })
64	mode.OnPreferred(func() { preferred = true })
65	s.event(mode.ID(), 0, wordBytes(1920, 1080)...)
66	s.event(mode.ID(), 1, wordBytes(60000)...)
67	s.event(mode.ID(), 2)
68	s.event(head.ID(), 4, wordBytes(1)...)
69	s.event(head.ID(), 5, wordBytes(mode.ID())...)
70	s.event(head.ID(), 8, wordBytes(uint32(wlturbo.NewFixed(1.5)))...)
71	s.event(mgr.ID(), 1, wordBytes(41)...)
72	if name != "HDMI-A-1" || w != 1920 || h != 1080 || refresh != 60000 || !preferred ||
73		enabled != 1 || current != mode.ID() || scale.Float64() != 1.5 || serial != 41 {
74		t.Fatal(name, w, h, refresh, preferred, enabled, current, scale, serial)
75	}
76
77	// create_configuration -> enable_head (new_id + object) -> setters -> apply.
78	cfg, e := mgr.CreateConfiguration(serial)
79	noerr(t, e)
80	wantWords(t, "create_configuration", s.req(mgr.ID(), 0), cfg.ID(), 41)
81	ch, e := cfg.EnableHead(head)
82	noerr(t, e)
83	wantWords(t, "enable_head", s.req(cfg.ID(), 0), ch.ID(), head.ID())
84	noerr(t, ch.SetMode(mode))
85	wantWords(t, "set_mode", s.req(ch.ID(), 0), mode.ID())
86	noerr(t, ch.SetPosition(-10, 20))
87	wantWords(t, "set_position", s.req(ch.ID(), 2), 0xfffffff6, 20)
88	noerr(t, ch.SetTransform(wayland.TRANSFORM_90))
89	wantWords(t, "set_transform", s.req(ch.ID(), 3), wayland.TRANSFORM_90)
90	noerr(t, ch.SetScale(wlturbo.NewFixed(2)))
91	wantWords(t, "set_scale", s.req(ch.ID(), 4), uint32(wlturbo.NewFixed(2)))
92	noerr(t, ch.SetAdaptiveSync(outputmanagement.ADAPTIVE_SYNC_STATE_ENABLED))
93	wantWords(t, "set_adaptive_sync", s.req(ch.ID(), 5), 1)
94	noerr(t, cfg.Test())
95	s.req(cfg.ID(), 3)
96	noerr(t, cfg.Apply())
97	s.req(cfg.ID(), 2)
98	var results []string
99	cfg.OnSucceeded(func() { results = append(results, "succeeded") })
100	cfg.OnFailed(func() { results = append(results, "failed") })
101	cfg.OnCancelled(func() { results = append(results, "cancelled") })
102	s.event(cfg.ID(), 0)
103	s.event(cfg.ID(), 1)
104	s.event(cfg.ID(), 2)
105	if !reflect.DeepEqual(results, []string{"succeeded", "failed", "cancelled"}) {
106		t.Fatal(results)
107	}
108	noerr(t, cfg.Destroy())
109	s.req(cfg.ID(), 4)
110
111	// Release destructors (since 3) and manager finished (destructor event).
112	var headFinished bool
113	head.OnFinished(func() { headFinished = true })
114	s.event(head.ID(), 9)
115	if !headFinished {
116		t.Fatal("head finished not delivered")
117	}
118	noerr(t, mode.Release())
119	s.req(mode.ID(), 0)
120	noerr(t, head.Release())
121	s.req(head.ID(), 0)
122	noerr(t, mgr.Stop())
123	s.req(mgr.ID(), 1)
124	var finished bool
125	mgr.OnFinished(func() { finished = true })
126	s.event(mgr.ID(), 2)
127	if !finished {
128		t.Fatal("finished not delivered")
129	}
130	if e := mgr.Stop(); e == nil {
131		t.Fatal("request on a finished manager must fail")
132	}
133	s.noRequest()
134}
135
136func TestWlrOutputManagementVersionGate(t *testing.T) {
137	s := newWireEnv(t)
138	mgr := outputmanagement.NewOutputManager(s.d.Context())
139	s.bind(1, outputmanagement.OutputManagerInterface, 4, 2, mgr)
140	var head *outputmanagement.OutputHead
141	mgr.OnHead(func(h *outputmanagement.OutputHead) { head = h })
142	s.event(mgr.ID(), 0, wordBytes(serverID+1)...)
143	if head == nil || head.Version() != 2 {
144		t.Fatalf("head %+v", head)
145	}
146	if e := head.Release(); !errors.Is(e, wlturbo.ErrVersionTooLow) {
147		t.Fatalf("release on v2 head: %v", e)
148	}
149	s.noRequest()
150}
151
152func TestWlrScreencopyBufferFlow(t *testing.T) {
153	s := newWireEnv(t)
154	out := s.output()
155	shm := wayland.NewShm(s.d.Context())
156	s.bind(1, wayland.ShmInterface, 1, 1, shm)
157	mgr := screencopy.NewScreencopyManager(s.d.Context())
158	s.bind(2, screencopy.ScreencopyManagerInterface, 3, 3, mgr)
159
160	frame, e := mgr.CaptureOutput(1, out)
161	noerr(t, e)
162	wantWords(t, "capture_output", s.req(mgr.ID(), 0), frame.ID(), 1, out.ID())
163	if frame.Version() != 3 {
164		t.Fatalf("frame version %d", frame.Version())
165	}
166	var events []string
167	var shmW, shmH, shmStride, shmFormat uint32
168	frame.OnBuffer(func(f, w, h, st uint32) {
169		shmFormat, shmW, shmH, shmStride = f, w, h, st
170		events = append(events, "buffer")
171	})
172	frame.OnLinuxDmabuf(func(f, w, h uint32) { events = append(events, "dmabuf") })
173	frame.OnBufferDone(func() { events = append(events, "done") })
174	frame.OnFlags(func(f uint32) {
175		if f != screencopy.FLAGS_Y_INVERT {
176			t.Fatalf("flags %d", f)
177		}
178		events = append(events, "flags")
179	})
180	var sec uint64
181	var nsec uint32
182	frame.OnReady(func(hi, lo, ns uint32) { sec, nsec = uint64(hi)<<32|uint64(lo), ns; events = append(events, "ready") })
183	s.event(frame.ID(), 0, wordBytes(wayland.FORMAT_XRGB8888, 4, 2, 16)...)
184	s.event(frame.ID(), 5, wordBytes(0x34325258, 4, 2)...)
185	s.event(frame.ID(), 6)
186	if shmFormat != wayland.FORMAT_XRGB8888 || shmW != 4 || shmH != 2 || shmStride != 16 {
187		t.Fatal(shmFormat, shmW, shmH, shmStride)
188	}
189
190	// Build the wl_shm buffer the compositor asked for; the pool descriptor is
191	// duplicated by the kernel and the client's copy is closed by the binding.
192	memfd, e := unix.MemfdCreate("wlturbo-shm", unix.MFD_CLOEXEC)
193	noerr(t, e)
194	noerr(t, unix.Ftruncate(memfd, 32))
195	pool, e := shm.CreatePool(memfd, 32)
196	noerr(t, e)
197	requireFDClosed(t, memfd)
198	body, fd := s.reqFD(shm.ID(), 0)
199	unix.Close(fd)
200	wantWords(t, "create_pool", body, pool.ID(), 32)
201	buf, e := pool.CreateBuffer(0, 4, 2, 16, wayland.FORMAT_XRGB8888)
202	noerr(t, e)
203	wantWords(t, "create_buffer", s.req(pool.ID(), 0), buf.ID(), 0, 4, 2, 16, wayland.FORMAT_XRGB8888)
204
205	noerr(t, frame.Copy(buf))
206	wantWords(t, "copy", s.req(frame.ID(), 0), buf.ID())
207	s.event(frame.ID(), 1, wordBytes(screencopy.FLAGS_Y_INVERT)...)
208	s.event(frame.ID(), 2, wordBytes(1, 2, 999999999)...)
209	if sec != 1<<32|2 || nsec != 999999999 {
210		t.Fatal(sec, nsec)
211	}
212	want := []string{"buffer", "dmabuf", "done", "flags", "ready"}
213	if !reflect.DeepEqual(events, want) {
214		t.Fatalf("events %v, want %v", events, want)
215	}
216	noerr(t, frame.Destroy())
217	s.req(frame.ID(), 1)
218
219	// Region capture + damage + failure on a second frame.
220	f2, e := mgr.CaptureOutputRegion(0, out, -1, 2, 30, 40)
221	noerr(t, e)
222	wantWords(t, "capture_output_region", s.req(mgr.ID(), 1), f2.ID(), 0, out.ID(), 0xffffffff, 2, 30, 40)
223	noerr(t, f2.CopyWithDamage(buf))
224	wantWords(t, "copy_with_damage", s.req(f2.ID(), 2), buf.ID())
225	var damage []uint32
226	var failed bool
227	f2.OnDamage(func(x, y, w, h uint32) { damage = []uint32{x, y, w, h} })
228	f2.OnFailed(func() { failed = true })
229	s.event(f2.ID(), 4, wordBytes(1, 2, 3, 4)...)
230	s.event(f2.ID(), 3)
231	if !reflect.DeepEqual(damage, []uint32{1, 2, 3, 4}) || !failed {
232		t.Fatal(damage, failed)
233	}
234	noerr(t, f2.Destroy())
235	s.req(f2.ID(), 1)
236	noerr(t, buf.Destroy())
237	s.req(buf.ID(), 0)
238	noerr(t, mgr.Destroy())
239	s.req(mgr.ID(), 2)
240}
241
242func TestWlrScreencopyVersionGate(t *testing.T) {
243	s := newWireEnv(t)
244	out := s.output()
245	mgr := screencopy.NewScreencopyManager(s.d.Context())
246	s.bind(1, screencopy.ScreencopyManagerInterface, 3, 1, mgr)
247	frame, e := mgr.CaptureOutput(0, out)
248	noerr(t, e)
249	s.req(mgr.ID(), 0)
250	buf := wayland.NewBuffer(s.d.Context())
251	buf.SetID(s.d.AllocateID())
252	s.d.Context().Register(buf)
253	if e := frame.CopyWithDamage(buf); !errors.Is(e, wlturbo.ErrVersionTooLow) {
254		t.Fatalf("copy_with_damage on v1 frame: %v", e)
255	}
256	s.noRequest()
257	noerr(t, frame.Copy(buf))
258	s.req(frame.ID(), 0)
259}
260
261func TestWlrInputMethodFDAndCrossPackageEnums(t *testing.T) {
262	s := newWireEnv(t)
263	seat, surf := s.seat(), s.surface()
264	mgr := inputmethod.NewInputMethodManager(s.d.Context())
265	s.bind(1, inputmethod.InputMethodManagerInterface, 1, 1, mgr)
266	im, e := mgr.GetInputMethod(seat)
267	noerr(t, e)
268	wantWords(t, "get_input_method", s.req(mgr.ID(), 0), seat.ID(), im.ID())
269
270	var seq []string
271	var text string
272	var cursor, anchor, cause, hint, purpose uint32
273	im.OnActivate(func() { seq = append(seq, "activate") })
274	im.OnSurroundingText(func(tx string, c, a uint32) { text, cursor, anchor = tx, c, a; seq = append(seq, "surrounding") })
275	im.OnTextChangeCause(func(c uint32) { cause = c; seq = append(seq, "cause") })
276	im.OnContentType(func(h, p uint32) { hint, purpose = h, p; seq = append(seq, "content") })
277	im.OnDone(func() { seq = append(seq, "done") })
278	s.event(im.ID(), 0)
279	s.event(im.ID(), 2, cat(str("héllo"), wordBytes(3, 3))...)
280	s.event(im.ID(), 3, wordBytes(textinput.CHANGE_CAUSE_OTHER)...)
281	s.event(im.ID(), 4, wordBytes(textinput.CONTENT_HINT_SPELLCHECK, textinput.CONTENT_PURPOSE_EMAIL)...)
282	s.event(im.ID(), 5)
283	if !reflect.DeepEqual(seq, []string{"activate", "surrounding", "cause", "content", "done"}) ||
284		text != "héllo" || cursor != 3 || anchor != 3 || cause != textinput.CHANGE_CAUSE_OTHER ||
285		hint != textinput.CONTENT_HINT_SPELLCHECK || purpose != textinput.CONTENT_PURPOSE_EMAIL {
286		t.Fatal(seq, text, cursor, anchor, cause, hint, purpose)
287	}
288
289	noerr(t, im.CommitString("ok"))
290	if b := s.req(im.ID(), 0); !bytes.Equal(b, str("ok")) {
291		t.Fatalf("commit_string %x", b)
292	}
293	noerr(t, im.SetPreeditString("pre", -1, -1))
294	if b := s.req(im.ID(), 1); !bytes.Equal(b, cat(str("pre"), wordBytes(0xffffffff, 0xffffffff))) {
295		t.Fatalf("set_preedit_string %x", b)
296	}
297	noerr(t, im.DeleteSurroundingText(2, 1))
298	wantWords(t, "delete_surrounding_text", s.req(im.ID(), 2), 2, 1)
299	noerr(t, im.Commit(1))
300	wantWords(t, "commit", s.req(im.ID(), 3), 1)
301
302	popup, e := im.GetInputPopupSurface(surf)
303	noerr(t, e)
304	wantWords(t, "get_input_popup_surface", s.req(im.ID(), 4), popup.ID(), surf.ID())
305	var rect []int32
306	popup.OnTextInputRectangle(func(x, y, w, h int32) { rect = []int32{x, y, w, h} })
307	s.event(popup.ID(), 0, wordBytes(0xfffffffe, 5, 30, 12)...)
308	if !reflect.DeepEqual(rect, []int32{-2, 5, 30, 12}) {
309		t.Fatal(rect)
310	}
311
312	grab, e := im.GrabKeyboard()
313	noerr(t, e)
314	wantWords(t, "grab_keyboard", s.req(im.ID(), 5), grab.ID())
315
316	// keymap carries a descriptor: the first handler may take ownership, the
317	// second sees nil, and an unclaimed descriptor is closed by the transport.
318	var claim = true
319	var first, second []*wlturbo.OwnedFD
320	var format, size uint32
321	var got []byte
322	grab.OnKeymap(func(f uint32, fd *wlturbo.OwnedFD, sz uint32) {
323		first = append(first, fd)
324		format, size = f, sz
325		if !claim {
326			return
327		}
328		n, e := fd.Take()
329		noerr(t, e)
330		buf := make([]byte, 16)
331		m, e := unix.Read(n, buf)
332		noerr(t, e)
333		got = buf[:m]
334		unix.Close(n)
335	})
336	grab.OnKeymap(func(f uint32, fd *wlturbo.OwnedFD, sz uint32) { second = append(second, fd) })
337	pr, pw, e := os.Pipe()
338	noerr(t, e)
339	_, e = pw.Write([]byte("xkb-keymap"))
340	noerr(t, e)
341	send(t, s.p, payload(grab.ID(), 0, wordBytes(wayland.KEYMAP_FORMAT_XKB_V1, 10)...), int(pr.Fd()))
342	pr.Close()
343	pw.Close()
344	s.dispatch()
345	if string(got) != "xkb-keymap" || format != wayland.KEYMAP_FORMAT_XKB_V1 || size != 10 || len(second) != 1 || second[0] != nil {
346		t.Fatalf("keymap got=%q format=%d size=%d second=%v", got, format, size, second)
347	}
348
349	claim = false
350	pr2, pw2, e := os.Pipe()
351	noerr(t, e)
352	defer pr2.Close()
353	send(t, s.p, payload(grab.ID(), 0, wordBytes(wayland.KEYMAP_FORMAT_XKB_V1, 0)...), int(pw2.Fd()))
354	pw2.Close() // only the in-flight duplicate keeps the write end alive
355	s.dispatch()
356	pr2.SetReadDeadline(time.Now().Add(2 * time.Second))
357	if n, e := pr2.Read(make([]byte, 4)); n != 0 || e == nil {
358		t.Fatalf("unclaimed descriptor still open: n=%d err=%v", n, e)
359	}
360
361	var key, mods, repeat []uint32
362	grab.OnKey(func(serial, tm, k, st uint32) { key = []uint32{serial, tm, k, st} })
363	grab.OnModifiers(func(serial, d, l, lk, g uint32) { mods = []uint32{serial, d, l, lk, g} })
364	grab.OnRepeatInfo(func(rate, delay int32) { repeat = []uint32{uint32(rate), uint32(delay)} })
365	s.event(grab.ID(), 1, wordBytes(7, 100, 30, wayland.KEY_STATE_PRESSED)...)
366	s.event(grab.ID(), 2, wordBytes(8, 1, 2, 4, 1)...)
367	s.event(grab.ID(), 3, wordBytes(25, 600)...)
368	if !reflect.DeepEqual(key, []uint32{7, 100, 30, wayland.KEY_STATE_PRESSED}) ||
369		!reflect.DeepEqual(mods, []uint32{8, 1, 2, 4, 1}) || !reflect.DeepEqual(repeat, []uint32{25, 600}) {
370		t.Fatal(key, mods, repeat)
371	}
372
373	noerr(t, grab.Release())
374	s.req(grab.ID(), 0)
375	noerr(t, popup.Destroy())
376	s.req(popup.ID(), 0)
377	var unavailable bool
378	im.OnUnavailable(func() { unavailable = true })
379	s.event(im.ID(), 6)
380	if !unavailable {
381		t.Fatal("unavailable not delivered")
382	}
383	noerr(t, im.Destroy())
384	s.req(im.ID(), 6)
385	noerr(t, mgr.Destroy())
386	s.req(mgr.ID(), 1)
387}
388
389func TestWlrVirtualKeyboardKeymapFD(t *testing.T) {
390	s := newWireEnv(t)
391	seat := s.seat()
392	mgr := virtualkeyboard.NewVirtualKeyboardManager(s.d.Context())
393	s.bind(1, virtualkeyboard.VirtualKeyboardManagerInterface, 1, 1, mgr)
394	kb, e := mgr.CreateVirtualKeyboard(seat)
395	noerr(t, e)
396	wantWords(t, "create_virtual_keyboard", s.req(mgr.ID(), 0), seat.ID(), kb.ID())
397
398	r, w, e := os.Pipe()
399	noerr(t, e)
400	defer r.Close()
401	// Hand the write end to the binding as a plain descriptor.
402	fd, e := unix.Dup(int(w.Fd()))
403	noerr(t, e)
404	w.Close()
405	noerr(t, kb.Keymap(wayland.KEYMAP_FORMAT_XKB_V1, fd, 6))
406	requireFDClosed(t, fd)
407	body, got := s.reqFD(kb.ID(), 0)
408	wantWords(t, "keymap", body, wayland.KEYMAP_FORMAT_XKB_V1, 6)
409	_, e = unix.Write(got, []byte("keymap"))
410	noerr(t, e)
411	unix.Close(got)
412	buf := make([]byte, 16)
413	r.SetReadDeadline(time.Now().Add(2 * time.Second))
414	n, e := r.Read(buf)
415	noerr(t, e)
416	if string(buf[:n]) != "keymap" {
417		t.Fatalf("keymap payload %q", buf[:n])
418	}
419
420	noerr(t, kb.Key(1234, 30, wayland.KEY_STATE_PRESSED))
421	wantWords(t, "key", s.req(kb.ID(), 1), 1234, 30, wayland.KEY_STATE_PRESSED)
422	noerr(t, kb.Modifiers(1, 2, 4, 0))
423	wantWords(t, "modifiers", s.req(kb.ID(), 2), 1, 2, 4, 0)
424	noerr(t, kb.Destroy())
425	s.req(kb.ID(), 3)
426	if e := kb.Key(1, 1, 0); e == nil {
427		t.Fatal("request after destroy must fail")
428	}
429	s.noRequest()
430}
431
432// A layer surface is a wl_surface role defined in layershell, and its popup is
433// an xdg_popup from xdgshell; both packages must agree on the same objects.
434func TestWlrLayerShellPopupCrossPackage(t *testing.T) {
435	s := newWireEnv(t)
436	panel, popupSurf := s.surface(), s.surface()
437	out := s.output()
438
439	wm := xdgshell.NewXdgWmBase(s.d.Context())
440	s.bind(1, xdgshell.XdgWmBaseInterface, 6, 6, wm)
441	ls := layershell.NewLayerShell(s.d.Context())
442	s.bind(2, layershell.LayerShellInterface, 5, 5, ls)
443
444	surf, e := ls.GetLayerSurface(panel, out, layershell.LAYER_TOP, "panel")
445	noerr(t, e)
446	if b := s.req(ls.ID(), 0); !bytes.Equal(b, cat(wordBytes(surf.ID(), panel.ID(), out.ID(), layershell.LAYER_TOP), str("panel"))) {
447		t.Fatalf("get_layer_surface %x", b)
448	}
449	if surf.Version() != 5 {
450		t.Fatalf("layer surface version %d", surf.Version())
451	}
452	// A nullable output is encoded as object 0.
453	nullOut, e := ls.GetLayerSurface(s.surface(), nil, layershell.LAYER_BACKGROUND, "wallpaper")
454	noerr(t, e)
455	if b := s.req(ls.ID(), 0); binary.NativeEndian.Uint32(b[8:]) != 0 || binary.NativeEndian.Uint32(b[:4]) != nullOut.ID() {
456		t.Fatalf("null output %x", b)
457	}
458
459	anchors := uint32(layershell.ANCHOR_TOP | layershell.ANCHOR_LEFT | layershell.ANCHOR_RIGHT)
460	noerr(t, surf.SetSize(0, 30))
461	wantWords(t, "set_size", s.req(surf.ID(), 0), 0, 30)
462	noerr(t, surf.SetAnchor(anchors))
463	wantWords(t, "set_anchor", s.req(surf.ID(), 1), anchors)
464	noerr(t, surf.SetExclusiveZone(-1))
465	wantWords(t, "set_exclusive_zone", s.req(surf.ID(), 2), 0xffffffff)
466	noerr(t, surf.SetMargin(1, 2, 3, 4))
467	wantWords(t, "set_margin", s.req(surf.ID(), 3), 1, 2, 3, 4)
468	noerr(t, surf.SetKeyboardInteractivity(layershell.KEYBOARD_INTERACTIVITY_ON_DEMAND))
469	wantWords(t, "set_keyboard_interactivity", s.req(surf.ID(), 4), layershell.KEYBOARD_INTERACTIVITY_ON_DEMAND)
470	noerr(t, surf.SetLayer(layershell.LAYER_OVERLAY))
471	wantWords(t, "set_layer", s.req(surf.ID(), 8), layershell.LAYER_OVERLAY)
472	noerr(t, surf.SetExclusiveEdge(layershell.ANCHOR_TOP))
473	wantWords(t, "set_exclusive_edge", s.req(surf.ID(), 9), layershell.ANCHOR_TOP)
474
475	var cfgSerial, cfgW, cfgH uint32
476	surf.OnConfigure(func(serial, w, h uint32) {
477		cfgSerial, cfgW, cfgH = serial, w, h
478		noerr(t, surf.AckConfigure(serial))
479	})
480	s.event(surf.ID(), 0, wordBytes(77, 1920, 30)...)
481	if cfgSerial != 77 || cfgW != 1920 || cfgH != 30 {
482		t.Fatal(cfgSerial, cfgW, cfgH)
483	}
484	wantWords(t, "ack_configure", s.req(surf.ID(), 6), 77)
485
486	// The popup is created through xdg_shell with a NULL parent, then adopted
487	// by the layer surface.
488	xs, e := wm.GetXdgSurface(popupSurf)
489	noerr(t, e)
490	s.req(wm.ID(), 2)
491	pos, e := wm.CreatePositioner()
492	noerr(t, e)
493	s.req(wm.ID(), 1)
494	noerr(t, pos.SetSize(100, 50))
495	wantWords(t, "positioner size", s.req(pos.ID(), 1), 100, 50)
496	noerr(t, pos.SetAnchorRect(0, 0, 10, 30))
497	s.req(pos.ID(), 2)
498	popup, e := xs.GetPopup(nil, pos)
499	noerr(t, e)
500	wantWords(t, "get_popup", s.req(xs.ID(), 2), popup.ID(), 0, pos.ID())
501	noerr(t, surf.GetPopup(popup))
502	wantWords(t, "layer get_popup", s.req(surf.ID(), 5), popup.ID())
503
504	var popupCfg []int32
505	var done bool
506	popup.OnConfigure(func(x, y, w, h int32) { popupCfg = []int32{x, y, w, h} })
507	popup.OnPopupDone(func() { done = true })
508	s.event(popup.ID(), 0, wordBytes(4, 30, 100, 50)...)
509	s.event(popup.ID(), 1)
510	if !reflect.DeepEqual(popupCfg, []int32{4, 30, 100, 50}) || !done {
511		t.Fatal(popupCfg, done)
512	}
513
514	var closed bool
515	surf.OnClosed(func() { closed = true })
516	s.event(surf.ID(), 1)
517	if !closed {
518		t.Fatal("closed not delivered")
519	}
520	noerr(t, popup.Destroy())
521	s.req(popup.ID(), 0)
522	noerr(t, surf.Destroy())
523	s.req(surf.ID(), 7)
524	noerr(t, ls.Destroy())
525	s.req(ls.ID(), 1)
526	if e := surf.SetSize(1, 1); e == nil {
527		t.Fatal("request after destroy must fail")
528	}
529	s.noRequest()
530}
531
532func TestWlrLayerShellVersionGate(t *testing.T) {
533	s := newWireEnv(t)
534	ls := layershell.NewLayerShell(s.d.Context())
535	s.bind(1, layershell.LayerShellInterface, 5, 4, ls)
536	surf, e := ls.GetLayerSurface(s.surface(), nil, layershell.LAYER_TOP, "x")
537	noerr(t, e)
538	s.req(ls.ID(), 0)
539	if e := surf.SetExclusiveEdge(layershell.ANCHOR_TOP); !errors.Is(e, wlturbo.ErrVersionTooLow) {
540		t.Fatalf("set_exclusive_edge on v4: %v", e)
541	}
542	s.noRequest()
543	noerr(t, surf.SetLayer(layershell.LAYER_BOTTOM))
544	s.req(surf.ID(), 8)
545}
546
547func TestWlrForeignToplevelOutputPowerAndKDEDecoration(t *testing.T) {
548	s := newWireEnv(t)
549	seat, surf, out := s.seat(), s.surface(), s.output()
550
551	// foreign-toplevel: toplevel is a server new_id event; finished destroys.
552	tm := foreigntoplevel.NewForeignToplevelManager(s.d.Context())
553	s.bind(1, foreigntoplevel.ForeignToplevelManagerInterface, 3, 3, tm)
554	var top *foreigntoplevel.ForeignToplevelHandle
555	tm.OnToplevel(func(h *foreigntoplevel.ForeignToplevelHandle) { top = h })
556	s.event(tm.ID(), 0, wordBytes(serverID+1)...)
557	if top == nil || top.ID() != serverID+1 || top.Version() != 3 {
558		t.Fatalf("toplevel %+v", top)
559	}
560	var title, appID string
561	var states []byte
562	var enter, parent uint32
563	var done, closed bool
564	top.OnTitle(func(v string) { title = v })
565	top.OnAppId(func(v string) { appID = v })
566	top.OnState(func(v []byte) { states = append([]byte(nil), v...) })
567	top.OnOutputEnter(func(id uint32) { enter = id })
568	top.OnParent(func(id uint32) { parent = id })
569	top.OnDone(func() { done = true })
570	top.OnClosed(func() { closed = true })
571	s.event(top.ID(), 0, str("term")...)
572	s.event(top.ID(), 1, str("org.example.term")...)
573	s.event(top.ID(), 2, wordBytes(out.ID())...)
574	s.event(top.ID(), 4, arr(wordBytes(foreigntoplevel.STATE_MAXIMIZED, foreigntoplevel.STATE_ACTIVATED))...)
575	s.event(top.ID(), 7, wordBytes(0)...)
576	s.event(top.ID(), 5)
577	if title != "term" || appID != "org.example.term" || enter != out.ID() || parent != 0 || !done ||
578		!bytes.Equal(states, wordBytes(foreigntoplevel.STATE_MAXIMIZED, foreigntoplevel.STATE_ACTIVATED)) {
579		t.Fatal(title, appID, enter, parent, done, states)
580	}
581	noerr(t, top.SetMaximized())
582	s.req(top.ID(), 0)
583	noerr(t, top.Activate(seat))
584	wantWords(t, "activate", s.req(top.ID(), 4), seat.ID())
585	noerr(t, top.SetRectangle(surf, 1, 2, 3, 4))
586	wantWords(t, "set_rectangle", s.req(top.ID(), 6), surf.ID(), 1, 2, 3, 4)
587	noerr(t, top.SetFullscreen(nil))
588	wantWords(t, "set_fullscreen(nil)", s.req(top.ID(), 8), 0)
589	noerr(t, top.SetFullscreen(out))
590	wantWords(t, "set_fullscreen", s.req(top.ID(), 8), out.ID())
591	noerr(t, top.Close())
592	s.req(top.ID(), 5)
593	s.event(top.ID(), 6)
594	if !closed {
595		t.Fatal("closed not delivered")
596	}
597	noerr(t, top.Destroy())
598	s.req(top.ID(), 7)
599	noerr(t, tm.Stop())
600	s.req(tm.ID(), 0)
601	s.event(tm.ID(), 1)
602	if e := tm.Stop(); e == nil {
603		t.Fatal("request on a finished manager must fail")
604	}
605
606	// output-power: object argument + mode/failed events.
607	pm := outputpower.NewOutputPowerManager(s.d.Context())
608	s.bind(2, outputpower.OutputPowerManagerInterface, 1, 1, pm)
609	pw, e := pm.GetOutputPower(out)
610	noerr(t, e)
611	wantWords(t, "get_output_power", s.req(pm.ID(), 0), pw.ID(), out.ID())
612	var modes []uint32
613	var failed bool
614	pw.OnMode(func(m uint32) { modes = append(modes, m) })
615	pw.OnFailed(func() { failed = true })
616	s.event(pw.ID(), 0, wordBytes(outputpower.MODE_ON)...)
617	noerr(t, pw.SetMode(outputpower.MODE_OFF))
618	wantWords(t, "set_mode", s.req(pw.ID(), 0), outputpower.MODE_OFF)
619	s.event(pw.ID(), 0, wordBytes(outputpower.MODE_OFF)...)
620	s.event(pw.ID(), 1)
621	if !reflect.DeepEqual(modes, []uint32{outputpower.MODE_ON, outputpower.MODE_OFF}) || !failed {
622		t.Fatal(modes, failed)
623	}
624	noerr(t, pw.Destroy())
625	s.req(pw.ID(), 1)
626	noerr(t, pm.Destroy())
627	s.req(pm.ID(), 1)
628
629	// KDE server decoration: mode enums are per-interface constants.
630	dm := kdeserverdecoration.NewOrgKdeKwinServerDecorationManager(s.d.Context())
631	s.bind(3, kdeserverdecoration.OrgKdeKwinServerDecorationManagerInterface, 1, 1, dm)
632	var def uint32 = 99
633	dm.OnDefaultMode(func(m uint32) { def = m })
634	s.event(dm.ID(), 0, wordBytes(kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MANAGER_MODE_CLIENT)...)
635	if def != kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MANAGER_MODE_CLIENT {
636		t.Fatal(def)
637	}
638	dec, e := dm.Create(surf)
639	noerr(t, e)
640	wantWords(t, "create", s.req(dm.ID(), 0), dec.ID(), surf.ID())
641	var mode uint32 = 99
642	dec.OnMode(func(m uint32) { mode = m })
643	noerr(t, dec.RequestMode(kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER))
644	wantWords(t, "request_mode", s.req(dec.ID(), 1), kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER)
645	s.event(dec.ID(), 0, wordBytes(kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER)...)
646	if mode != kdeserverdecoration.ORG_KDE_KWIN_SERVER_DECORATION_MODE_SERVER {
647		t.Fatal(mode)
648	}
649	noerr(t, dec.Release())
650	s.req(dec.ID(), 0)
651	s.noRequest()
652}

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