v0.4.0 b6be3a8

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
182type doneProxy struct {
183	wlturbo.BaseProxy
184	done   int
185	opcode uint16
186}
187
188func (p *doneProxy) Dispatch(e *wlturbo.Event) {
189	if e.Opcode == p.opcode {
190		p.done++
191	}
192}
193func TestXDGOutputDone(t *testing.T) {
194	for _, version := range []uint32{1, 2} {
195		t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) {
196			s, _, _, dir := lifecycleServer(t)
197			c := protocolClient(t, s, dir)
198			global, ok := c.Registry().FindGlobal("wl_output")
199			if !ok {
200				t.Fatal("missing wl_output")
201			}
202			output, err := bindWireID(c, global.Name, global.Interface, version)
203			if err != nil {
204				t.Fatal(err)
205			}
206			op := &doneProxy{opcode: uint16(wayland.OutputEventDone)}
207			op.SetID(output)
208			registerWireProxy(c, op)
209			global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1")
210			if !ok {
211				t.Fatal("missing xdg output manager")
212			}
213			manager, err := bindWireID(c, global.Name, global.Interface, 3)
214			if err != nil {
215				t.Fatal(err)
216			}
217			registerProtocol(t, c, manager)
218			if err := c.Roundtrip(); err != nil {
219				t.Fatal(err)
220			}
221			op.done = 0
222			xdg := c.AllocateID()
223			xp := &doneProxy{opcode: 2}
224			xp.SetID(xdg)
225			registerWireProxy(c, xp)
226			requestProtocol(t, c, manager, 1, xdg, output)
227			if err := c.Roundtrip(); err != nil {
228				t.Fatal(err)
229			}
230			if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) {
231				t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done)
232			}
233		})
234	}
235}
236
237type layerPointerProxy struct {
238	wlturbo.BaseProxy
239	enters  chan [2]float64
240	buttons chan uint32 // serials
241}
242
243func (p *layerPointerProxy) Dispatch(e *wlturbo.Event) {
244	switch e.Opcode {
245	case uint16(wayland.PointerEventEnter):
246		_, _ = e.Uint32(), e.Uint32()
247		p.enters <- [2]float64{e.Fixed().Float64(), e.Fixed().Float64()}
248	case uint16(wayland.PointerEventButton):
249		p.buttons <- e.Uint32()
250	}
251}
252
253// A bar gets the pointer, and a menu it opens on a click through
254// get_popup reaches core with the bar as its parent and a grab.
255func TestLayerPointerAndPopup(t *testing.T) {
256	s, events, commands, dir := lifecycleServer(t)
257	c := protocolClient(t, s, dir)
258	seat := bindVersion(t, c, "wl_seat", 8)
259	registerProtocol(t, c, seat)
260	pointer := c.AllocateID()
261	pp := &layerPointerProxy{enters: make(chan [2]float64, 1), buttons: make(chan uint32, 1)}
262	pp.SetID(pointer)
263	registerWireProxy(c, pp)
264	requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
265	comp := bindProtocol(t, c, "wl_compositor")
266	wm := bindProtocol(t, c, "xdg_wm_base")
267	shm := bindProtocol(t, c, "wl_shm")
268	registerProtocol(t, c, shm)
269	fd, err := unix.MemfdCreate("layer-popup-buffer", 0)
270	if err != nil {
271		t.Fatal(err)
272	}
273	defer unix.Close(fd)
274	if err = unix.Ftruncate(fd, 4); err != nil {
275		t.Fatal(err)
276	}
277	pool, buf := c.AllocateID(), c.AllocateID()
278	registerProtocol(t, c, pool)
279	registerProtocol(t, c, buf)
280	if err = wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
281		t.Fatal(err)
282	}
283	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
284
285	surf, layer := layerProtocol(t, c)
286	lp := &layerProxy{configured: make(chan [3]uint32, 2)}
287	lp.SetID(layer)
288	registerWireProxy(c, lp)
289	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
290	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
291	if err = c.Roundtrip(); err != nil {
292		t.Fatal(err)
293	}
294	var config [3]uint32
295	select {
296	case config = <-lp.configured:
297	case <-time.After(2 * time.Second):
298		t.Fatal("configure timeout")
299	}
300	requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestAckConfigure, config[0])
301	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
302	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
303	if err = c.Roundtrip(); err != nil {
304		t.Fatal(err)
305	}
306	lc, ok := waitEvent(t, events, 2*time.Second).(ports.LayerChanged)
307	if !ok || len(lc.Layers) != 1 {
308		t.Fatalf("layer mapped %+v", lc)
309	}
310	id := lc.Layers[0].ID
311
312	commands <- ports.PointerFocus{ID: id, X: 0.5, Y: 0.25}
313	commands <- ports.PointerButtonTo{ID: id, Button: 0x110, Pressed: true}
314	var press uint32
315	for deadline := time.Now().Add(2 * time.Second); len(pp.buttons) == 0; time.Sleep(5 * time.Millisecond) {
316		if time.Now().After(deadline) {
317			t.Fatal("pointer events timeout")
318		}
319		if err = c.Roundtrip(); err != nil {
320			t.Fatal(err)
321		}
322	}
323	if at := <-pp.enters; at != [2]float64{0.5, 0.25} {
324		t.Fatalf("enter at %v", at)
325	}
326	press = <-pp.buttons
327
328	positioner := c.AllocateID()
329	requestProtocol(t, c, wm, xdgshell.WmBaseRequestCreatePositioner, positioner)
330	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetSize, int32(1), int32(1))
331	requestProtocol(t, c, positioner, xdgshell.PositionerRequestSetAnchorRect, int32(0), int32(0), int32(1), int32(1))
332	done := make(chan struct{}, 1)
333	xdgWindow(t, c, comp, wm, buf, func(pxdg uint32) {
334		popup := c.AllocateID()
335		proxy := &popupDoneProxy{done: done}
336		proxy.SetID(popup)
337		registerWireProxy(c, proxy)
338		requestProtocol(t, c, pxdg, xdgshell.SurfaceRequestGetPopup, popup, uint32(0), positioner)
339		requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestGetPopup, popup)
340		requestProtocol(t, c, popup, xdgshell.PopupRequestGrab, seat, press)
341	})
342	req, ok := waitEvent(t, events, 2*time.Second).(ports.PopupRequest)
343	if !ok || req.Parent != id || !req.Grab {
344		t.Fatalf("popup request %+v, want parent %d with a grab", req, id)
345	}
346	if len(done) != 0 {
347		t.Fatal("layer popup dismissed")
348	}
349}

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 25

Tags 10

↑↓ moveenter openesc close