refactor/kvconf 3462c76

layer_test.go RawHistoryFolder

1package wayland
2
3import (
4	"fmt"
5	"os"
6	"os/exec"
7	"testing"
8	"time"
9
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/go-wayland-bindings/server/wlrlayershell"
12	"github.com/bnema/go-wayland-bindings/server/xdgshell"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/bnema/wlturbo"
15	"golang.org/x/sys/unix"
16)
17
18type layerProxy struct {
19	wlturbo.BaseProxy
20	configured chan [3]uint32
21}
22
23func (p *layerProxy) Dispatch(e *wlturbo.Event) {
24	if e.Opcode == uint16(wlrlayershell.ZwlrLayerSurfaceV1EventConfigure) {
25		p.configured <- [3]uint32{e.Uint32(), e.Uint32(), e.Uint32()}
26	}
27}
28func layerProtocol(t *testing.T, c *wlturbo.Display) (uint32, uint32) {
29	t.Helper()
30	comp := bindProtocol(t, c, "wl_compositor")
31	shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
32	surf := c.AllocateID()
33	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
34	registerProtocol(t, c, surf)
35	layer := c.AllocateID()
36	requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layer, surf, uint32(0), uint32(ports.LayerTop), "test")
37	return surf, layer
38}
39func TestLayerInvalidSize(t *testing.T) {
40	s, _, _, dir := lifecycleServer(t)
41	c := protocolClient(t, s, dir)
42	surf, layer := layerProtocol(t, c)
43	registerProtocol(t, c, layer)
44	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30))
45	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(ports.AnchorTop))
46	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
47	expectProtocolError(t, c, layer, uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSize))
48}
49func TestLayerLifecycle(t *testing.T) {
50	s, events, _, dir := lifecycleServer(t)
51	c := protocolClient(t, s, dir)
52	surf, layer := layerProtocol(t, c)
53	proxy := &layerProxy{configured: make(chan [3]uint32, 2)}
54	proxy.SetID(layer)
55	registerWireProxy(c, proxy)
56	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(13))
57	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30))
58	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetExclusiveZone, int32(30))
59	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
60	if err := c.Roundtrip(); err != nil {
61		t.Fatal(err)
62	}
63	var config [3]uint32
64	select {
65	case config = <-proxy.configured:
66	case <-time.After(2 * time.Second):
67		t.Fatal("configure timeout")
68	}
69	if config[1] != 1920 || config[2] != 30 {
70		t.Fatalf("configure: %v", config)
71	}
72	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0])
73	shm := bindProtocol(t, c, "wl_shm")
74	registerProtocol(t, c, shm)
75	fd, err := unix.MemfdCreate("layer-buffer", 0)
76	if err != nil {
77		t.Fatal(err)
78	}
79	defer unix.Close(fd)
80	if err = unix.Ftruncate(fd, 1920*30*4); err != nil {
81		t.Fatal(err)
82	}
83	pool, buf := c.AllocateID(), c.AllocateID()
84	registerProtocol(t, c, pool)
85	registerProtocol(t, c, buf)
86	if err = wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(1920*30*4)); err != nil {
87		t.Fatal(err)
88	}
89	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1920), int32(30), int32(1920*4), uint32(wayland.ShmFormatArgb8888))
90	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
91	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
92	if err = c.Roundtrip(); err != nil {
93		t.Fatal(err)
94	}
95	select {
96	case ev := <-events:
97		v, ok := ev.(ports.LayerChanged)
98		if !ok || len(v.Layers) != 1 || v.Layers[0].Anchor != 13 || v.Layers[0].ExclusiveZone != 30 || v.Layers[0].Width != 1920 || v.Layers[0].Height != 30 {
99			t.Fatalf("mapped: %#v", ev)
100		}
101	case <-time.After(2 * time.Second):
102		t.Fatal("map timeout")
103	}
104	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
105	if err = c.Roundtrip(); err != nil {
106		t.Fatal(err)
107	}
108	select {
109	case ev := <-events:
110		v, ok := ev.(ports.LayerChanged)
111		if !ok || len(v.Layers) != 0 {
112			t.Fatalf("destroy: %#v", ev)
113		}
114	case <-time.After(2 * time.Second):
115		t.Fatal("destroy timeout")
116	}
117}
118func TestWaybarLayer(t *testing.T) {
119	tool, err := exec.LookPath("waybar")
120	if err != nil {
121		t.Skip("waybar missing")
122	}
123	s, events, _, dir := keyboardServer(t)
124	home := t.TempDir()
125	config := home + "/config.json"
126	style := home + "/style.css"
127	if err = os.WriteFile(config, []byte(`{"layer":"top","position":"top","height":30,"modules-left":["clock"]}`), 0600); err != nil {
128		t.Fatal(err)
129	}
130	if err = os.WriteFile(style, nil, 0600); err != nil {
131		t.Fatal(err)
132	}
133	// waybar (gio) aborts without a session bus; the user bus is the default.
134	bus := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
135	if bus == "" {
136		sock := os.Getenv("XDG_RUNTIME_DIR") + "/bus"
137		if _, err := os.Stat(sock); err != nil {
138			t.Skip("no D-Bus session bus")
139		}
140		bus = "unix:path=" + sock
141	}
142	_, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + home, "DBUS_SESSION_BUS_ADDRESS=" + bus}, "-c", config, "-s", style)
143	select {
144	case ev := <-events:
145		v, ok := ev.(ports.LayerChanged)
146		if !ok || len(v.Layers) != 1 || v.Layers[0].Height != 30 {
147			t.Fatalf("waybar event: %#v stderr: %s", ev, output.String())
148		}
149	case err := <-done:
150		t.Fatalf("waybar exited: %v stderr: %s", err, output.String())
151	case <-time.After(10 * time.Second):
152		t.Fatalf("waybar timeout stderr: %s", output.String())
153	}
154}
155
156func TestLayerAlreadyConstructed(t *testing.T) {
157	for _, scenario := range []string{"role"} {
158		t.Run(scenario, func(t *testing.T) {
159			s, _, _, dir := lifecycleServer(t)
160			c := protocolClient(t, s, dir)
161			comp := bindProtocol(t, c, "wl_compositor")
162			shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
163			surf := c.AllocateID()
164			requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
165			registerProtocol(t, c, surf)
166			switch scenario {
167			case "role":
168				sub := bindProtocol(t, c, "wl_subcompositor")
169				parent := c.AllocateID()
170				requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, parent)
171				registerProtocol(t, c, parent)
172				id := c.AllocateID()
173				requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, id, surf, parent)
174			}
175			id := c.AllocateID()
176			requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, id, surf, uint32(0), uint32(ports.LayerTop), "test")
177			expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed))
178		})
179	}
180}
181
182// A wl_surface keeps its layer role after its layer surface is destroyed,
183// so the client may give it a new one (swayosd does this on every OSD).
184func TestLayerRecreateOnSameSurface(t *testing.T) {
185	s, _, _, dir := lifecycleServer(t)
186	c := protocolClient(t, s, dir)
187	shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
188	surf, layer := layerProtocol(t, c)
189	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
190	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
191	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
192	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
193	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
194	again := c.AllocateID()
195	requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, again, surf, uint32(0), uint32(ports.LayerOverlay), "test")
196	requestProtocol(t, c, again, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(10), uint32(10))
197	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
198	if err := c.Roundtrip(); err != nil {
199		t.Fatalf("recreating the layer surface failed: %v", err)
200	}
201}
202
203type doneProxy struct {
204	wlturbo.BaseProxy
205	done   int
206	opcode uint16
207}
208
209func (p *doneProxy) Dispatch(e *wlturbo.Event) {
210	if e.Opcode == p.opcode {
211		p.done++
212	}
213}
214func TestXDGOutputDone(t *testing.T) {
215	for _, version := range []uint32{1, 2} {
216		t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) {
217			s, _, _, dir := lifecycleServer(t)
218			c := protocolClient(t, s, dir)
219			global, ok := c.Registry().FindGlobal("wl_output")
220			if !ok {
221				t.Fatal("missing wl_output")
222			}
223			output, err := bindWireID(c, global.Name, global.Interface, version)
224			if err != nil {
225				t.Fatal(err)
226			}
227			op := &doneProxy{opcode: uint16(wayland.OutputEventDone)}
228			op.SetID(output)
229			registerWireProxy(c, op)
230			global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1")
231			if !ok {
232				t.Fatal("missing xdg output manager")
233			}
234			manager, err := bindWireID(c, global.Name, global.Interface, 3)
235			if err != nil {
236				t.Fatal(err)
237			}
238			registerProtocol(t, c, manager)
239			if err := c.Roundtrip(); err != nil {
240				t.Fatal(err)
241			}
242			op.done = 0
243			xdg := c.AllocateID()
244			xp := &doneProxy{opcode: 2}
245			xp.SetID(xdg)
246			registerWireProxy(c, xp)
247			requestProtocol(t, c, manager, 1, xdg, output)
248			if err := c.Roundtrip(); err != nil {
249				t.Fatal(err)
250			}
251			if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) {
252				t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done)
253			}
254		})
255	}
256}
257
258type layerPointerProxy struct {
259	wlturbo.BaseProxy
260	enters  chan [2]float64
261	buttons chan uint32 // serials
262}
263
264func (p *layerPointerProxy) Dispatch(e *wlturbo.Event) {
265	switch e.Opcode {
266	case uint16(wayland.PointerEventEnter):
267		_, _ = e.Uint32(), e.Uint32()
268		p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()}
269	case uint16(wayland.PointerEventButton):
270		p.buttons <- e.Uint32()
271	}
272}
273
274// A bar gets the pointer, and a menu it opens on a click through
275// get_popup reaches core with the bar as its parent and a grab.
276func TestLayerPointerAndPopup(t *testing.T) {
277	s, events, commands, dir := lifecycleServer(t)
278	c := protocolClient(t, s, dir)
279	seat := bindVersion(t, c, "wl_seat", 8)
280	registerProtocol(t, c, seat)
281	pointer := c.AllocateID()
282	pp := &layerPointerProxy{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)}
283	pp.SetID(pointer)
284	registerWireProxy(c, pp)
285	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
286	comp := bindProtocol(t, c, "wl_compositor")
287	wm := bindProtocol(t, c, "xdg_wm_base")
288	shm := bindProtocol(t, c, "wl_shm")
289	registerProtocol(t, c, shm)
290	fd, err := unix.MemfdCreate("layer-popup-buffer", 0)
291	if err != nil {
292		t.Fatal(err)
293	}
294	defer unix.Close(fd)
295	if err = unix.Ftruncate(fd, 4); err != nil {
296		t.Fatal(err)
297	}
298	pool, buf := c.AllocateID(), c.AllocateID()
299	registerProtocol(t, c, pool)
300	registerProtocol(t, c, buf)
301	if err = wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
302		t.Fatal(err)
303	}
304	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
305
306	surf, layer := layerProtocol(t, c)
307	lp := &layerProxy{configured: make(chan [3]uint32, 2)}
308	lp.SetID(layer)
309	registerWireProxy(c, lp)
310	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
311	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
312	if err = c.Roundtrip(); err != nil {
313		t.Fatal(err)
314	}
315	var config [3]uint32
316	select {
317	case config = <-lp.configured:
318	case <-time.After(2 * time.Second):
319		t.Fatal("configure timeout")
320	}
321	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0])
322	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
323	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
324	if err = c.Roundtrip(); err != nil {
325		t.Fatal(err)
326	}
327	lc, ok := waitEvent(t, events, 2*time.Second).(ports.LayerChanged)
328	if !ok || len(lc.Layers) != 1 {
329		t.Fatalf("layer mapped %+v", lc)
330	}
331	id := lc.Layers[0].ID
332
333	commands <- ports.PointerFocus{ID: id, X: 0.5, Y: 0.25}
334	commands <- ports.PointerButtonTo{ID: id, Button: 0x110, Pressed: true}
335	var press uint32
336	for deadline := time.Now().Add(2 * time.Second); len(pp.buttons) == 0; time.Sleep(5 * time.Millisecond) {
337		if time.Now().After(deadline) {
338			t.Fatal("pointer events timeout")
339		}
340		if err = c.Roundtrip(); err != nil {
341			t.Fatal(err)
342		}
343	}
344	if at := <-pp.enters; at != [2]float64{0.5, 0.25} {
345		t.Fatalf("enter at %v", at)
346	}
347	press = <-pp.buttons
348
349	positioner := c.AllocateID()
350	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
351	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
352	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
353	done := make(chan struct{}, 1)
354	xdgWindow(t, c, comp, wm, buf, func(pxdg uint32) {
355		popup := c.AllocateID()
356		proxy := &popupDoneProxy{done: done}
357		proxy.SetID(popup)
358		registerWireProxy(c, proxy)
359		requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner)
360		requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestGetPopup, popup)
361		requestProtocol(t, c, popup, xdgshell.PopupRequestGrab, seat, press)
362	})
363	req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
364	if !ok || req.Parent != id || !req.Grab {
365		t.Fatalf("popup request %+v, want parent %d with a grab", req, id)
366	}
367	if len(done) != 0 {
368		t.Fatal("layer popup dismissed")
369	}
370}

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