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}